Skip to content

Commit 6f9bdc2

Browse files
hyperpolymathJonathan D.A. Jewellclaude
authored
ABI: make Idris2 proofs genuinely compile + add machine-checked theorems (#164)
## Idris2 ABI proofs — genuinely compile + machine-checked theorems Part of the family-wide ABI-proofs review. The hand-written ABI was never compiler-checked and did **not** typecheck under Idris2 0.7.0; this makes it real and verified, following the merged **iseriser** reference. **Systemic fixes:** `decEq _ _ = No absurd` → explicit off-diagonal cases; `{auto 0 nonNull}` smart constructors → `choose`-based; `paddingFor` `-` → `minus`; real `decDivides`/`decFieldsAligned` replacing `?fieldsAlignedProof`; honest `offsetInBounds`; `thisPlatform` de-`%runElab`'d; buildable nested layout + ipkg; new `Proofs.idr` with real theorems. **Verified:** `idris2 --build` clean (0/0); adversarial re-verify found no `believe_me`/`postulate`/holes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01DF9CcCuL4YJoqs26eHsYiA)_ --------- Co-authored-by: Jonathan D.A. Jewell <paraordinate@yahoo.co.uk> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fe51a4a commit 6f9bdc2

6 files changed

Lines changed: 402 additions & 51 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,8 @@ deps/
117117
.cache/
118118
build/
119119
dist/
120+
121+
# Idris2 build artefacts
122+
**/build/
123+
*.ttc
124+
*.ttm
File renamed without changes.

src/interface/abi/Layout.idr renamed to src/interface/abi/Verisimiser/ABI/Layout.idr

Lines changed: 107 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ module Verisimiser.ABI.Layout
1919
import Verisimiser.ABI.Types
2020
import Data.Vect
2121
import Data.So
22+
import Data.Nat
23+
import Decidable.Equality
2224

2325
%default total
2426

@@ -32,24 +34,40 @@ paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat
3234
paddingFor offset alignment =
3335
if offset `mod` alignment == 0
3436
then 0
35-
else alignment - (offset `mod` alignment)
37+
else minus alignment (offset `mod` alignment)
3638

37-
||| Proof that alignment divides aligned size.
39+
||| Proof that alignment divides aligned size: `m = k * n`.
3840
public export
3941
data Divides : Nat -> Nat -> Type where
4042
DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m
4143

44+
||| Sound decision procedure for divisibility. Returns a genuine
45+
||| `Divides n m` witness when `n` evenly divides `m`, otherwise Nothing.
46+
||| Division by zero is undecidable here and yields Nothing.
47+
public export
48+
decDivides : (n : Nat) -> (m : Nat) -> Maybe (Divides n m)
49+
decDivides Z _ = Nothing
50+
decDivides (S k) m =
51+
let q = m `div` (S k) in
52+
case decEq m (q * (S k)) of
53+
Yes prf => Just (DivideBy q prf)
54+
No _ => Nothing
55+
4256
||| Round up to next alignment boundary.
4357
public export
4458
alignUp : (size : Nat) -> (alignment : Nat) -> Nat
4559
alignUp size alignment =
4660
size + paddingFor size alignment
4761

48-
||| Proof that alignUp produces aligned result.
62+
||| Sound divisibility check for an aligned size. The general theorem
63+
||| "alignUp size align is always divisible by align" needs div/mod lemmas
64+
||| and is tracked as residual proof work; here we *decide* it via
65+
||| `decDivides`, which returns a genuine witness when it holds. For the
66+
||| concrete ABI layouts below, divisibility is proven outright (`DivideBy`).
4967
public export
50-
alignUpCorrect : (size : Nat) -> (align : Nat) -> (align > 0) -> Divides align (alignUp size align)
51-
alignUpCorrect size align prf =
52-
DivideBy ((size + paddingFor size align) `div` align) Refl
68+
alignUpDivides : (size : Nat) -> (align : Nat) ->
69+
Maybe (Divides align (alignUp size align))
70+
alignUpDivides size align = decDivides align (alignUp size align)
5371

5472
--------------------------------------------------------------------------------
5573
-- Struct Field Layout
@@ -81,7 +99,7 @@ record StructLayout where
8199

82100
||| Calculate total struct size with padding.
83101
public export
84-
calcStructSize : Vect n Field -> Nat -> Nat
102+
calcStructSize : Vect k Field -> Nat -> Nat
85103
calcStructSize [] align = 0
86104
calcStructSize (f :: fs) align =
87105
let lastOffset = foldl (\acc, field => nextFieldOffset field) f.offset fs
@@ -90,23 +108,26 @@ calcStructSize (f :: fs) align =
90108

91109
||| Proof that field offsets are correctly aligned.
92110
public export
93-
data FieldsAligned : Vect n Field -> Type where
111+
data FieldsAligned : Vect k Field -> Type where
94112
NoFields : FieldsAligned []
95113
ConsField :
96114
(f : Field) ->
97-
(rest : Vect n Field) ->
115+
(rest : Vect k Field) ->
98116
Divides f.alignment f.offset ->
99117
FieldsAligned rest ->
100118
FieldsAligned (f :: rest)
101119

102-
||| Verify a struct layout is valid.
120+
||| Decide field alignment for every field, building a real `FieldsAligned`
121+
||| witness from per-field divisibility proofs.
103122
public export
104-
verifyLayout : (fields : Vect n Field) -> (align : Nat) -> Either String StructLayout
105-
verifyLayout fields align =
106-
let size = calcStructSize fields align
107-
in case decSo (size >= sum (map (\f => f.size) fields)) of
108-
Yes prf => Right (MkStructLayout fields size align)
109-
No _ => Left "Invalid struct size"
123+
decFieldsAligned : (fs : Vect k Field) -> Maybe (FieldsAligned fs)
124+
decFieldsAligned [] = Just NoFields
125+
decFieldsAligned (f :: fs) =
126+
case decDivides f.alignment f.offset of
127+
Nothing => Nothing
128+
Just dvd => case decFieldsAligned fs of
129+
Nothing => Nothing
130+
Just rest => Just (ConsField f fs dvd rest)
110131

111132
--------------------------------------------------------------------------------
112133
-- Platform-Specific Layouts
@@ -137,11 +158,15 @@ data CABICompliant : StructLayout -> Type where
137158
FieldsAligned layout.fields ->
138159
CABICompliant layout
139160

140-
||| Check if layout follows C ABI.
161+
||| Verify a layout against the C ABI alignment rules, returning a genuine
162+
||| `CABICompliant` proof (built from real per-field divisibility witnesses)
163+
||| or an error when some field offset is misaligned.
141164
public export
142165
checkCABI : (layout : StructLayout) -> Either String (CABICompliant layout)
143166
checkCABI layout =
144-
Right (CABIOk layout ?fieldsAlignedProof)
167+
case decFieldsAligned layout.fields of
168+
Just prf => Right (CABIOk layout prf)
169+
Nothing => Left "Field offsets are not correctly aligned for the C ABI"
145170

146171
--------------------------------------------------------------------------------
147172
-- OctadRecord Layout
@@ -183,11 +208,26 @@ octadRecordLayout =
183208
]
184209
80 -- Total size: 80 bytes
185210
8 -- Alignment: 8 bytes
211+
{sizeCorrect = Oh}
212+
{aligned = DivideBy 10 Refl}
186213

187214
||| Proof that the OctadRecord layout is C-ABI compliant.
188215
export
189-
octadRecordValid : CABICompliant octadRecordLayout
190-
octadRecordValid = CABIOk octadRecordLayout ?octadFieldsAligned
216+
octadRecordValid : CABICompliant Layout.octadRecordLayout
217+
octadRecordValid =
218+
CABIOk octadRecordLayout
219+
(ConsField _ _ (DivideBy 0 Refl) -- entity_id 0 / 8
220+
(ConsField _ _ (DivideBy 2 Refl) -- backend 8 / 4
221+
(ConsField _ _ (DivideBy 3 Refl) -- active_dims 12 / 4
222+
(ConsField _ _ (DivideBy 2 Refl) -- provenance 16 / 8
223+
(ConsField _ _ (DivideBy 3 Refl) -- temporal 24 / 8
224+
(ConsField _ _ (DivideBy 4 Refl) -- drift_score 32 / 8
225+
(ConsField _ _ (DivideBy 5 Refl) -- lineage 40 / 8
226+
(ConsField _ _ (DivideBy 6 Refl) -- constraints 48 / 8
227+
(ConsField _ _ (DivideBy 7 Refl) -- acl 56 / 8
228+
(ConsField _ _ (DivideBy 8 Refl) -- simulation 64 / 8
229+
(ConsField _ _ (DivideBy 9 Refl) -- metadata 72 / 8
230+
NoFields)))))))))))
191231

192232
--------------------------------------------------------------------------------
193233
-- ProvenanceEntry Layout
@@ -218,11 +258,21 @@ provenanceEntryLayout =
218258
]
219259
88 -- Total size: 88 bytes
220260
8 -- Alignment: 8 bytes
261+
{sizeCorrect = Oh}
262+
{aligned = DivideBy 11 Refl}
221263

222264
||| Proof that the ProvenanceEntry layout is C-ABI compliant.
223265
export
224-
provenanceEntryValid : CABICompliant provenanceEntryLayout
225-
provenanceEntryValid = CABIOk provenanceEntryLayout ?provenanceFieldsAligned
266+
provenanceEntryValid : CABICompliant Layout.provenanceEntryLayout
267+
provenanceEntryValid =
268+
CABIOk provenanceEntryLayout
269+
(ConsField _ _ (DivideBy 0 Refl) -- hash 0 / 1
270+
(ConsField _ _ (DivideBy 32 Refl) -- previous_hash 32 / 1
271+
(ConsField _ _ (DivideBy 8 Refl) -- entity_id 64 / 8
272+
(ConsField _ _ (DivideBy 18 Refl) -- operation 72 / 4
273+
(ConsField _ _ (DivideBy 19 Refl) -- _padding 76 / 4
274+
(ConsField _ _ (DivideBy 10 Refl) -- timestamp 80 / 8
275+
NoFields))))))
226276

227277
--------------------------------------------------------------------------------
228278
-- DriftMeasurement Layout
@@ -248,11 +298,19 @@ driftMeasurementLayout =
248298
]
249299
88 -- Total size: 88 bytes
250300
8 -- Alignment: 8 bytes
301+
{sizeCorrect = Oh}
302+
{aligned = DivideBy 11 Refl}
251303

252304
||| Proof that the DriftMeasurement layout is C-ABI compliant.
253305
export
254-
driftMeasurementValid : CABICompliant driftMeasurementLayout
255-
driftMeasurementValid = CABIOk driftMeasurementLayout ?driftFieldsAligned
306+
driftMeasurementValid : CABICompliant Layout.driftMeasurementLayout
307+
driftMeasurementValid =
308+
CABIOk driftMeasurementLayout
309+
(ConsField _ _ (DivideBy 0 Refl) -- entity_id 0 / 8
310+
(ConsField _ _ (DivideBy 1 Refl) -- overall_score 8 / 8
311+
(ConsField _ _ (DivideBy 2 Refl) -- scores 16 / 8
312+
(ConsField _ _ (DivideBy 10 Refl) -- measured_at 80 / 8
313+
NoFields))))
256314

257315
--------------------------------------------------------------------------------
258316
-- TemporalSnapshot Layout
@@ -284,25 +342,43 @@ temporalSnapshotLayout =
284342
]
285343
48 -- Total size: 48 bytes
286344
8 -- Alignment: 8 bytes
345+
{sizeCorrect = Oh}
346+
{aligned = DivideBy 6 Refl}
287347

288348
||| Proof that the TemporalSnapshot layout is C-ABI compliant.
289349
export
290-
temporalSnapshotValid : CABICompliant temporalSnapshotLayout
291-
temporalSnapshotValid = CABIOk temporalSnapshotLayout ?temporalFieldsAligned
350+
temporalSnapshotValid : CABICompliant Layout.temporalSnapshotLayout
351+
temporalSnapshotValid =
352+
CABIOk temporalSnapshotLayout
353+
(ConsField _ _ (DivideBy 0 Refl) -- entity_id 0 / 8
354+
(ConsField _ _ (DivideBy 1 Refl) -- version 8 / 8
355+
(ConsField _ _ (DivideBy 2 Refl) -- valid_from 16 / 8
356+
(ConsField _ _ (DivideBy 3 Refl) -- valid_to 24 / 8
357+
(ConsField _ _ (DivideBy 4 Refl) -- snapshot_ptr 32 / 8
358+
(ConsField _ _ (DivideBy 10 Refl) -- snapshot_len 40 / 4
359+
(ConsField _ _ (DivideBy 11 Refl) -- operation 44 / 4
360+
NoFields)))))))
292361

293362
--------------------------------------------------------------------------------
294363
-- Offset Calculation
295364
--------------------------------------------------------------------------------
296365

297366
||| Calculate field offset with proof of correctness.
298367
public export
299-
fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (n : Nat ** Field)
368+
fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (Nat, Field)
300369
fieldOffset layout name =
301370
case findIndex (\f => f.name == name) layout.fields of
302-
Just idx => Just (finToNat idx ** index idx layout.fields)
371+
Just idx => Just (finToNat idx, index idx layout.fields)
303372
Nothing => Nothing
304373

305-
||| Proof that field offset is within struct bounds.
374+
||| Decide whether a field lies within a struct's byte bounds, returning a
375+
||| genuine proof when `offset + size <= totalSize`. The previous signature
376+
||| asserted this for *every* field unconditionally, which is false (a field
377+
||| need not belong to the layout); this honest version decides it.
306378
public export
307-
offsetInBounds : (layout : StructLayout) -> (f : Field) -> So (f.offset + f.size <= layout.totalSize)
308-
offsetInBounds layout f = ?offsetInBoundsProof
379+
offsetInBounds : (layout : StructLayout) -> (f : Field) ->
380+
Maybe (So (f.offset + f.size <= layout.totalSize))
381+
offsetInBounds layout f =
382+
case choose (f.offset + f.size <= layout.totalSize) of
383+
Left ok => Just ok
384+
Right _ => Nothing
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
||| Machine-checked proofs over the VeriSimiser ABI.
5+
|||
6+
||| These are not runtime tests — they are propositional statements the Idris2
7+
||| type checker must discharge at compile time. If any concrete ABI layout
8+
||| were misaligned, the result-code encoding wrong, or a decision procedure
9+
||| mis-defined, this module would fail to typecheck and the proof build would
10+
||| go red.
11+
|||
12+
||| The C-ABI compliance witnesses are built directly from per-field
13+
||| divisibility proofs (`DivideBy k Refl`, where `offset = k * alignment`).
14+
||| Multiplication reduces during type checking, so these are fully verified
15+
||| by the compiler; we avoid routing them through `Nat` division, which is a
16+
||| primitive that does not reduce at the type level.
17+
18+
module Verisimiser.ABI.Proofs
19+
20+
import Verisimiser.ABI.Types
21+
import Verisimiser.ABI.Layout
22+
import Data.So
23+
import Data.Vect
24+
25+
%default total
26+
27+
--------------------------------------------------------------------------------
28+
-- The concrete FFI struct layouts are provably C-ABI compliant.
29+
--------------------------------------------------------------------------------
30+
31+
||| Every field offset in the OctadRecord layout divides its alignment:
32+
||| 0|8, 8|4, 12|4, 16|8, 24|8, 32|8, 40|8, 48|8, 56|8, 64|8, 72|8.
33+
export
34+
octadRecordCompliant : CABICompliant Layout.octadRecordLayout
35+
octadRecordCompliant =
36+
CABIOk octadRecordLayout
37+
(ConsField _ _ (DivideBy 0 Refl)
38+
(ConsField _ _ (DivideBy 2 Refl)
39+
(ConsField _ _ (DivideBy 3 Refl)
40+
(ConsField _ _ (DivideBy 2 Refl)
41+
(ConsField _ _ (DivideBy 3 Refl)
42+
(ConsField _ _ (DivideBy 4 Refl)
43+
(ConsField _ _ (DivideBy 5 Refl)
44+
(ConsField _ _ (DivideBy 6 Refl)
45+
(ConsField _ _ (DivideBy 7 Refl)
46+
(ConsField _ _ (DivideBy 8 Refl)
47+
(ConsField _ _ (DivideBy 9 Refl)
48+
NoFields)))))))))))
49+
50+
||| Every field offset in the ProvenanceEntry layout is aligned:
51+
||| 0|1, 32|1, 64|8, 72|4, 76|4, 80|8.
52+
export
53+
provenanceEntryCompliant : CABICompliant Layout.provenanceEntryLayout
54+
provenanceEntryCompliant =
55+
CABIOk provenanceEntryLayout
56+
(ConsField _ _ (DivideBy 0 Refl)
57+
(ConsField _ _ (DivideBy 32 Refl)
58+
(ConsField _ _ (DivideBy 8 Refl)
59+
(ConsField _ _ (DivideBy 18 Refl)
60+
(ConsField _ _ (DivideBy 19 Refl)
61+
(ConsField _ _ (DivideBy 10 Refl)
62+
NoFields))))))
63+
64+
||| Every field offset in the DriftMeasurement layout is aligned:
65+
||| 0|8, 8|8, 16|8, 80|8.
66+
export
67+
driftMeasurementCompliant : CABICompliant Layout.driftMeasurementLayout
68+
driftMeasurementCompliant =
69+
CABIOk driftMeasurementLayout
70+
(ConsField _ _ (DivideBy 0 Refl)
71+
(ConsField _ _ (DivideBy 1 Refl)
72+
(ConsField _ _ (DivideBy 2 Refl)
73+
(ConsField _ _ (DivideBy 10 Refl)
74+
NoFields))))
75+
76+
||| Every field offset in the TemporalSnapshot layout is aligned:
77+
||| 0|8, 8|8, 16|8, 24|8, 32|8, 40|4, 44|4.
78+
export
79+
temporalSnapshotCompliant : CABICompliant Layout.temporalSnapshotLayout
80+
temporalSnapshotCompliant =
81+
CABIOk temporalSnapshotLayout
82+
(ConsField _ _ (DivideBy 0 Refl)
83+
(ConsField _ _ (DivideBy 1 Refl)
84+
(ConsField _ _ (DivideBy 2 Refl)
85+
(ConsField _ _ (DivideBy 3 Refl)
86+
(ConsField _ _ (DivideBy 4 Refl)
87+
(ConsField _ _ (DivideBy 10 Refl)
88+
(ConsField _ _ (DivideBy 11 Refl)
89+
NoFields)))))))
90+
91+
--------------------------------------------------------------------------------
92+
-- Result-code round-trip: the encoding the Zig FFI depends on.
93+
--------------------------------------------------------------------------------
94+
95+
export
96+
okIsZero : resultToInt Ok = 0
97+
okIsZero = Refl
98+
99+
export
100+
nullPointerIsFour : resultToInt NullPointer = 4
101+
nullPointerIsFour = Refl
102+
103+
--------------------------------------------------------------------------------
104+
-- Octad model invariants.
105+
--------------------------------------------------------------------------------
106+
107+
||| The octad is exactly eight dimensions — the defining cardinality of the
108+
||| VeriSimDB model.
109+
export
110+
octadCardinality : length Types.allDimensions = 8
111+
octadCardinality = Refl
112+
113+
||| The octad tag encoding the Zig FFI depends on: Simulation is the eighth
114+
||| (index 7) dimension.
115+
export
116+
simulationIsSeven : octadToInt Simulation = 7
117+
simulationIsSeven = Refl
118+
119+
||| Tier 1 (piggyback) capabilities carry a static proof that they never write
120+
||| to the target database — the core safety guarantee of VeriSimiser.
121+
export
122+
provenanceIsPiggyback : dimensionTier Provenance = Tier1
123+
provenanceIsPiggyback = Refl

0 commit comments

Comments
 (0)