Skip to content

Commit 8e0badc

Browse files
ABI Layer 4: prove the FFI result-code seam is injective + faithful (#38)
## Summary Layer 4 (seal the ABI↔FFI seam): proves the FFI result-code encoding (`resultToInt`) is a **sound, lossless wire encoding** — decoder `intToResult` with `resultRoundTrip`, from which `resultToIntInjective` is derived. Distinct ABI outcomes never collide on the C wire. Complements the structural `abi-ffi-gate.py`. New module `*.ABI.FfiSeam` (imports `Types`). Positive + non-vacuity controls. ## Testing Idris2 0.7.0 `--build` → exit 0, zero warnings. Adversarial rejection confirmed. `build/` removed. No `believe_me`/`postulate`/`sorry`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01A6PSzJWpRxtzGDjUCEh7Mx --- _Generated by [Claude Code](https://claude.ai/code/session_01A6PSzJWpRxtzGDjUCEh7Mx)_ --------- Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent d6ec80c commit 8e0badc

2 files changed

Lines changed: 126 additions & 1 deletion

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 4 — ABI<->FFI seam soundness proofs for Nimiser.
5+
|||
6+
||| The structural gate (scripts/abi-ffi-gate.py) checks that the Idris
7+
||| `Result` enum and the Zig FFI enum agree by name+value. This module
8+
||| supplies the PROOF-SIDE guarantee that the encoding itself is sound:
9+
|||
10+
||| (a) `resultToInt` is injective — distinct ABI outcomes never collide
11+
||| on the wire (`resultToIntInjective`).
12+
||| (b) there is a total decoder `intToResult` and a round-trip law
13+
||| `resultRoundTrip` showing the C integer faithfully reconstructs
14+
||| the ABI value (lossless / faithful encoding).
15+
|||
16+
||| Injectivity is then DERIVED from the round-trip via `justInjective . cong`,
17+
||| which is the cleanest route and depends only on the round-trip Refls
18+
||| reducing. The decoder is written with boolean `==` on Bits32 so that
19+
||| `intToResult (resultToInt r)` reduces definitionally for each concrete
20+
||| constructor, making every round-trip clause `Refl`.
21+
|||
22+
||| There is no `ProofStatus`/`statusToInt` (or any other FFI enum encoder)
23+
||| in this repo's ABI, so `Result` is the sole seam to seal here.
24+
25+
module Nimiser.ABI.FfiSeam
26+
27+
import Nimiser.ABI.Types
28+
29+
%default total
30+
31+
--------------------------------------------------------------------------------
32+
-- Local helper
33+
--------------------------------------------------------------------------------
34+
35+
||| `Just` is injective: equal wrapped values come from equal payloads.
36+
||| (Defined locally; the base library in this toolchain does not export it.)
37+
private
38+
justInjective : {0 x, y : a} -> Just x = Just y -> x = y
39+
justInjective Refl = Refl
40+
41+
--------------------------------------------------------------------------------
42+
-- Decoder: C integer -> ABI Result
43+
--------------------------------------------------------------------------------
44+
45+
||| Decode a C integer result code back into the ABI `Result`.
46+
||| Uses boolean `==` on Bits32 (which reduces on concrete literals) so the
47+
||| round-trip law below holds definitionally. Unknown codes decode to Nothing.
48+
public export
49+
intToResult : Bits32 -> Maybe Result
50+
intToResult x =
51+
if x == 0 then Just Ok
52+
else if x == 1 then Just Error
53+
else if x == 2 then Just InvalidParam
54+
else if x == 3 then Just OutOfMemory
55+
else if x == 4 then Just NullPointer
56+
else if x == 5 then Just CompilationFailed
57+
else if x == 6 then Just TemplateError
58+
else if x == 7 then Just MacroError
59+
else Nothing
60+
61+
--------------------------------------------------------------------------------
62+
-- Round-trip: faithful / lossless encoding
63+
--------------------------------------------------------------------------------
64+
65+
||| Every ABI result survives a round trip through its C integer encoding:
66+
||| decoding the encoded value reconstructs exactly the original `Result`.
67+
||| Each clause reduces to `Refl` because the boolean `==` comparisons on the
68+
||| concrete literal produced by `resultToInt` evaluate at type-check time.
69+
export
70+
resultRoundTrip : (r : Result) -> intToResult (resultToInt r) = Just r
71+
resultRoundTrip Ok = Refl
72+
resultRoundTrip Error = Refl
73+
resultRoundTrip InvalidParam = Refl
74+
resultRoundTrip OutOfMemory = Refl
75+
resultRoundTrip NullPointer = Refl
76+
resultRoundTrip CompilationFailed = Refl
77+
resultRoundTrip TemplateError = Refl
78+
resultRoundTrip MacroError = Refl
79+
80+
--------------------------------------------------------------------------------
81+
-- Injectivity, derived from the round trip
82+
--------------------------------------------------------------------------------
83+
84+
||| The encoding is unambiguous: distinct ABI outcomes never collide on the
85+
||| wire. Derived cleanly from the round-trip law — if two results encode to
86+
||| the same integer, decoding that integer gives `Just a` and `Just b`, so
87+
||| `Just a = Just b`, hence `a = b`.
88+
export
89+
resultToIntInjective : (a, b : Result) -> resultToInt a = resultToInt b -> a = b
90+
resultToIntInjective a b prf =
91+
justInjective $
92+
rewrite sym (resultRoundTrip a) in
93+
rewrite sym (resultRoundTrip b) in
94+
cong intToResult prf
95+
96+
--------------------------------------------------------------------------------
97+
-- Positive controls (concrete decodes, machine-checked)
98+
--------------------------------------------------------------------------------
99+
100+
||| Positive control: the integer 0 decodes to `Ok`.
101+
export
102+
decodeZeroIsOk : intToResult 0 = Just Ok
103+
decodeZeroIsOk = Refl
104+
105+
||| Positive control: the integer 7 (the largest code) decodes to `MacroError`.
106+
export
107+
decodeSevenIsMacroError : intToResult 7 = Just MacroError
108+
decodeSevenIsMacroError = Refl
109+
110+
||| Positive control: an out-of-range code (8) decodes to `Nothing`.
111+
export
112+
decodeEightIsNothing : intToResult 8 = Nothing
113+
decodeEightIsNothing = Refl
114+
115+
--------------------------------------------------------------------------------
116+
-- Negative / non-vacuity control (machine-checked)
117+
--------------------------------------------------------------------------------
118+
119+
||| Non-vacuity control: two DISTINCT result codes have DISTINCT integers.
120+
||| If the encoding were trivial/collapsing, this proof would be impossible.
121+
||| Distinct primitive Bits32 literals are provably unequal, discharged by the
122+
||| coverage checker via `Refl impossible`.
123+
export
124+
okIntDistinctFromErrorInt : Not (resultToInt Ok = resultToInt Error)
125+
okIntDistinctFromErrorInt = \case Refl impossible

src/interface/abi/nimiser-abi.ipkg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
-- SPDX-License-Identifier: MPL-2.0
22
package nimiser-abi
33
sourcedir = "."
4-
modules = Nimiser.ABI.Types, Nimiser.ABI.Layout, Nimiser.ABI.Foreign, Nimiser.ABI.Proofs, Nimiser.ABI.Semantics, Nimiser.ABI.Invariants
4+
modules = Nimiser.ABI.Types, Nimiser.ABI.Layout, Nimiser.ABI.Foreign, Nimiser.ABI.Proofs, Nimiser.ABI.Semantics, Nimiser.ABI.Invariants, Nimiser.ABI.FfiSeam

0 commit comments

Comments
 (0)