Skip to content

Commit 32bf567

Browse files
hyperpolymathJonathan D.A. Jewellclaude
authored
ABI: make Idris2 proofs genuinely compile + add machine-checked theorems (#29)
## Idris2 ABI proofs — genuinely compile + machine-checked theorems Part of the family-wide ABI-proofs review. The hand-written ABI 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 5b796ef commit 32bf567

6 files changed

Lines changed: 322 additions & 20 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ Thumbs.db
1818
/dist/
1919
/out/
2020

21+
# Idris2 build artifacts
22+
**/build/
23+
*.ttc
24+
*.ttm
25+
2126
# Dependencies
2227
/node_modules/
2328
/vendor/
Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ module Bqniser.ABI.Layout
1717
import Bqniser.ABI.Types
1818
import Data.Vect
1919
import Data.So
20+
import Data.Nat
21+
import Decidable.Equality
2022

2123
%default total
2224

@@ -55,11 +57,21 @@ arrayByteSize layout =
5557
dataSize = layout.elemCount * layout.elemSize
5658
in headerSize + shapeSize + dataSize
5759

58-
||| Proof that array byte size is always >= 8 (at minimum the header)
60+
||| `8 + n` is at least 8 for every `n`. `(>=)` on Nat routes through
61+
||| `compare`, which does not reduce for a symbolic tail, so we case on `n`:
62+
||| both `compare Z Z = EQ` and `compare (S k) Z = GT` reduce, never `LT`.
63+
geEight : (n : Nat) -> So ((8 + n) >= 8)
64+
geEight Z = Oh
65+
geEight (S _) = Oh
66+
67+
||| Proof that array byte size is always >= 8 (at minimum the header).
68+
||| `arrayByteSize = (8 + rank*8) + dataSize`; reassociate to `8 + (...)`
69+
||| and discharge with `geEight`.
5970
public export
6071
arraySizeMinHeader : {rank : Nat} -> (layout : BQNArrayLayout rank) ->
6172
So (arrayByteSize layout >= 8)
62-
arraySizeMinHeader layout = ?arraySizeMinHeaderProof
73+
arraySizeMinHeader (MkBQNArrayLayout _ _ _ elemSize elemCount) =
74+
geEight (rank * 8 + elemCount * elemSize)
6375

6476
||| Calculate element count from shape (product of extents)
6577
public export
@@ -82,24 +94,40 @@ paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat
8294
paddingFor offset alignment =
8395
if offset `mod` alignment == 0
8496
then 0
85-
else alignment - (offset `mod` alignment)
97+
else minus alignment (offset `mod` alignment)
8698

87-
||| Proof that alignment divides aligned size
99+
||| Proof that alignment divides aligned size: `m = k * n`.
88100
public export
89101
data Divides : Nat -> Nat -> Type where
90102
DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m
91103

104+
||| Sound decision procedure for divisibility. Returns a genuine
105+
||| `Divides n m` witness when `n` evenly divides `m`, otherwise Nothing.
106+
||| Division by zero is undecidable here and yields Nothing.
107+
public export
108+
decDivides : (n : Nat) -> (m : Nat) -> Maybe (Divides n m)
109+
decDivides Z _ = Nothing
110+
decDivides (S k) m =
111+
let q = m `div` (S k) in
112+
case decEq m (q * (S k)) of
113+
Yes prf => Just (DivideBy q prf)
114+
No _ => Nothing
115+
92116
||| Round up to next alignment boundary
93117
public export
94118
alignUp : (size : Nat) -> (alignment : Nat) -> Nat
95119
alignUp size alignment =
96120
size + paddingFor size alignment
97121

98-
||| Proof that alignUp produces aligned result
122+
||| Sound divisibility check for an aligned size. The general theorem
123+
||| "alignUp size align is always divisible by align" needs div/mod lemmas;
124+
||| here we *decide* it via `decDivides`, which returns a genuine witness when
125+
||| it holds. (Previously `DivideBy (… div …) Refl`, whose `Refl` cannot
126+
||| typecheck for symbolic inputs.)
99127
public export
100-
alignUpCorrect : (size : Nat) -> (align : Nat) -> (align > 0) -> Divides align (alignUp size align)
101-
alignUpCorrect size align prf =
102-
DivideBy ((size + paddingFor size align) `div` align) Refl
128+
alignUpDivides : (size : Nat) -> (align : Nat) ->
129+
Maybe (Divides align (alignUp size align))
130+
alignUpDivides size align = decDivides align (alignUp size align)
103131

104132
--------------------------------------------------------------------------------
105133
-- BQN Value Header Layout
@@ -208,6 +236,8 @@ cbqnArrayDescLayout =
208236
]
209237
16 -- Total size: 16 bytes
210238
8 -- Alignment: 8 bytes
239+
{sizeCorrect = Oh}
240+
{aligned = DivideBy 2 Refl}
211241

212242
--------------------------------------------------------------------------------
213243
-- Platform-Specific Layouts
@@ -229,17 +259,56 @@ verifyAllPlatforms layouts = Right ()
229259
-- C ABI Compatibility
230260
--------------------------------------------------------------------------------
231261

232-
||| Proof that a struct follows C ABI rules
262+
||| Proof that every field offset in a layout is correctly aligned.
263+
public export
264+
data FieldsAligned : Vect k Field -> Type where
265+
NoFields : FieldsAligned []
266+
ConsField :
267+
(f : Field) ->
268+
(rest : Vect k Field) ->
269+
Divides f.alignment f.offset ->
270+
FieldsAligned rest ->
271+
FieldsAligned (f :: rest)
272+
273+
||| Decide field alignment for every field, building a real `FieldsAligned`
274+
||| witness from per-field divisibility proofs.
275+
public export
276+
decFieldsAligned : (fs : Vect k Field) -> Maybe (FieldsAligned fs)
277+
decFieldsAligned [] = Just NoFields
278+
decFieldsAligned (f :: fs) =
279+
case decDivides f.alignment f.offset of
280+
Nothing => Nothing
281+
Just dvd => case decFieldsAligned fs of
282+
Nothing => Nothing
283+
Just rest => Just (ConsField f fs dvd rest)
284+
285+
||| Proof that a struct layout follows C ABI alignment rules.
233286
public export
234287
data CABICompliant : StructLayout -> Type where
235288
CABIOk :
236289
(layout : StructLayout) ->
290+
FieldsAligned layout.fields ->
237291
CABICompliant layout
238292

239-
||| Proof that CBQN array descriptor is C-ABI compliant
293+
||| Verify a layout against the C ABI alignment rules, returning a genuine
294+
||| `CABICompliant` proof (built from real per-field divisibility witnesses)
295+
||| or an error when some field offset is misaligned.
296+
public export
297+
checkCABI : (layout : StructLayout) -> Either String (CABICompliant layout)
298+
checkCABI layout =
299+
case decFieldsAligned layout.fields of
300+
Just prf => Right (CABIOk layout prf)
301+
Nothing => Left "Field offsets are not correctly aligned for the C ABI"
302+
303+
||| Proof that CBQN array descriptor is C-ABI compliant.
304+
||| Offsets 0 and 8 are both divisible by alignment 8.
240305
public export
241-
cbqnArrayDescCABI : CABICompliant cbqnArrayDescLayout
242-
cbqnArrayDescCABI = CABIOk cbqnArrayDescLayout
306+
cbqnArrayDescCABI : CABICompliant Layout.cbqnArrayDescLayout
307+
cbqnArrayDescCABI =
308+
CABIOk cbqnArrayDescLayout
309+
(ConsField _ _ (DivideBy 0 Refl)
310+
(ConsField _ _ (DivideBy 1 Refl)
311+
NoFields))
243312

244313
--------------------------------------------------------------------------------
245314
-- Offset Calculation
@@ -252,3 +321,15 @@ fieldOffset layout name =
252321
case findIndex (\f => f.name == name) layout.fields of
253322
Just idx => Just (finToNat idx ** index idx layout.fields)
254323
Nothing => Nothing
324+
325+
||| Decide whether a field lies within a struct's byte bounds, returning a
326+
||| genuine proof when `offset + size <= totalSize`. A previous template
327+
||| version asserted this for *every* field unconditionally, which is false
328+
||| (a field need not belong to the layout); this honest version decides it.
329+
public export
330+
offsetInBounds : (layout : StructLayout) -> (f : Field) ->
331+
Maybe (So (f.offset + f.size <= layout.totalSize))
332+
offsetInBounds layout f =
333+
case choose (f.offset + f.size <= layout.totalSize) of
334+
Left ok => Just ok
335+
Right _ => Nothing
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 bqniser ABI.
5+
|||
6+
||| These are not runtime tests — they are propositional statements the Idris2
7+
||| type checker must discharge at compile time. If the concrete CBQN array
8+
||| descriptor layout were misaligned, the FFI result-code encoding wrong, or
9+
||| the BQN type-tag encoding wrong, this module would fail to typecheck and
10+
||| the proof build would go red.
11+
|||
12+
||| The C-ABI compliance witness is built directly from per-field divisibility
13+
||| proofs (`DivideBy k Refl`, where `offset = k * alignment`). Multiplication
14+
||| reduces during type checking, so these are fully verified by the compiler;
15+
||| we never route them through `Nat` division, which is a primitive that does
16+
||| not reduce at the type level.
17+
18+
module Bqniser.ABI.Proofs
19+
20+
import Bqniser.ABI.Types
21+
import Bqniser.ABI.Layout
22+
import Data.So
23+
import Data.Vect
24+
25+
%default total
26+
27+
--------------------------------------------------------------------------------
28+
-- The concrete CBQN array-descriptor layout is provably C-ABI compliant.
29+
--------------------------------------------------------------------------------
30+
31+
||| Every field offset in the CBQN array descriptor divides its alignment:
32+
||| 0|8 (= 0 * 8) and 8|8 (= 1 * 8).
33+
export
34+
cbqnArrayDescCompliant : CABICompliant Layout.cbqnArrayDescLayout
35+
cbqnArrayDescCompliant =
36+
CABIOk cbqnArrayDescLayout
37+
(ConsField _ _ (DivideBy 0 Refl)
38+
(ConsField _ _ (DivideBy 1 Refl)
39+
NoFields))
40+
41+
--------------------------------------------------------------------------------
42+
-- Result-code round-trip: the encoding the Zig FFI depends on.
43+
--------------------------------------------------------------------------------
44+
45+
||| Success is encoded as 0 (the C convention the FFI bridge relies on).
46+
export
47+
okIsZero : resultToInt Ok = 0
48+
okIsZero = Refl
49+
50+
||| Null-pointer errors are encoded as 4, matching errorDescription/the bridge.
51+
export
52+
nullPointerIsFour : resultToInt NullPointer = 4
53+
nullPointerIsFour = Refl
54+
55+
--------------------------------------------------------------------------------
56+
-- BQN type-tag encoding: must match CBQN's internal tags 0..6.
57+
--------------------------------------------------------------------------------
58+
59+
||| Numbers carry tag 0.
60+
export
61+
numberTagIsZero : bqnTypeToInt BQNNumber = 0
62+
numberTagIsZero = Refl
63+
64+
||| Arrays — the fundamental compound type — carry the top tag 6.
65+
export
66+
arrayTagIsSix : bqnTypeToInt BQNArray = 6
67+
arrayTagIsSix = Refl
68+
69+
--------------------------------------------------------------------------------
70+
-- A scalar (rank 0) holds exactly one element.
71+
--------------------------------------------------------------------------------
72+
73+
||| The empty shape vector has product 1, so a rank-0 value is a single cell.
74+
export
75+
scalarSingleElement : shapeProduct (the (Vect 0 Bits64) []) = 1
76+
scalarSingleElement = Refl

0 commit comments

Comments
 (0)