Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ Thumbs.db
/dist/
/out/

# Idris2 build artefacts (nested ABI package build dirs)
**/build/
*.ttc
*.ttm

# Dependencies
/node_modules/
/vendor/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module Oblibeniser.ABI.Layout
import Oblibeniser.ABI.Types
import Data.Vect
import Data.So
import Data.Nat
import Decidable.Equality

%default total

Expand All @@ -27,25 +29,42 @@ paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat
paddingFor offset alignment =
if offset `mod` alignment == 0
then 0
else alignment - (offset `mod` alignment)

||| Proof that alignment divides aligned size
public export
data Divides : Nat -> Nat -> Type where
DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m
else minus alignment (offset `mod` alignment)

||| Round up to next alignment boundary
public export
alignUp : (size : Nat) -> (alignment : Nat) -> Nat
alignUp size alignment =
size + paddingFor size alignment

||| Proof that alignUp produces aligned result
||| Proof that alignment divides aligned size: `m = k * n`.
public export
data Divides : Nat -> Nat -> Type where
DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m

||| Sound decision procedure for divisibility. Returns a genuine
||| `Divides n m` witness when `n` evenly divides `m`, otherwise Nothing.
||| Division by zero is undecidable here and yields Nothing.
public export
decDivides : (n : Nat) -> (m : Nat) -> Maybe (Divides n m)
decDivides Z _ = Nothing
decDivides (S k) m =
let q = m `div` (S k) in
case decEq m (q * (S k)) of
Yes prf => Just (DivideBy q prf)
No _ => Nothing

||| Sound divisibility check for an aligned size. The general theorem
||| "alignUp size align is always divisible by align" needs div/mod lemmas
||| from Data.Nat and is tracked as residual proof work; here we *decide* it
||| via `decDivides`, which returns a genuine witness when it holds. For the
||| concrete ABI layouts below, divisibility is proven outright (`DivideBy`).
||| (Previously `alignUpCorrect … = DivideBy … Refl`, whose `Refl` cannot
||| typecheck for symbolic inputs.)
public export
alignUpCorrect : (size : Nat) -> (align : Nat) -> (align > 0) -> Divides align (alignUp size align)
alignUpCorrect size align prf =
-- Proof that (size + padding) is divisible by align
DivideBy ((size + paddingFor size align) `div` align) Refl
alignUpDivides : (size : Nat) -> (align : Nat) ->
Maybe (Divides align (alignUp size align))
alignUpDivides size align = decDivides align (alignUp size align)

--------------------------------------------------------------------------------
-- Struct Field Layout
Expand Down Expand Up @@ -77,7 +96,7 @@ record StructLayout where

||| Calculate total struct size with padding
public export
calcStructSize : Vect n Field -> Nat -> Nat
calcStructSize : Vect k Field -> Nat -> Nat
calcStructSize [] align = 0
calcStructSize (f :: fs) align =
let lastOffset = foldl (\acc, field => nextFieldOffset field) f.offset fs
Expand All @@ -86,23 +105,26 @@ calcStructSize (f :: fs) align =

||| Proof that field offsets are correctly aligned
public export
data FieldsAligned : Vect n Field -> Type where
data FieldsAligned : Vect k Field -> Type where
NoFields : FieldsAligned []
ConsField :
(f : Field) ->
(rest : Vect n Field) ->
(rest : Vect k Field) ->
Divides f.alignment f.offset ->
FieldsAligned rest ->
FieldsAligned (f :: rest)

||| Verify a struct layout is valid
||| Decide field alignment for every field, building a real `FieldsAligned`
||| witness from per-field divisibility proofs.
public export
verifyLayout : (fields : Vect n Field) -> (align : Nat) -> Either String StructLayout
verifyLayout fields align =
let size = calcStructSize fields align
in case decSo (size >= sum (map (\f => f.size) fields)) of
Yes prf => Right (MkStructLayout fields size align)
No _ => Left "Invalid struct size"
decFieldsAligned : (fs : Vect k Field) -> Maybe (FieldsAligned fs)
decFieldsAligned [] = Just NoFields
decFieldsAligned (f :: fs) =
case decDivides f.alignment f.offset of
Nothing => Nothing
Just dvd => case decFieldsAligned fs of
Nothing => Nothing
Just rest => Just (ConsField f fs dvd rest)

--------------------------------------------------------------------------------
-- Platform-Specific Layouts
Expand All @@ -113,14 +135,8 @@ public export
PlatformLayout : Platform -> Type -> Type
PlatformLayout p t = StructLayout

||| Verify layout is correct for all platforms
public export
verifyAllPlatforms :
(layouts : (p : Platform) -> PlatformLayout p t) ->
Either String ()
verifyAllPlatforms layouts =
-- Check that layout is valid on all platforms
Right ()
-- `verifyAllLayouts` is defined at the end of this module, after the concrete
-- layouts and `checkCABI` it depends on.

--------------------------------------------------------------------------------
-- C ABI Compatibility
Expand All @@ -134,12 +150,15 @@ data CABICompliant : StructLayout -> Type where
FieldsAligned layout.fields ->
CABICompliant layout

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

--------------------------------------------------------------------------------
-- AuditEntry Layout (oblibeniser-specific)
Expand Down Expand Up @@ -171,11 +190,24 @@ auditEntryLayout =
]
64 -- Total size: 64 bytes (with 4 bytes trailing padding)
8 -- Alignment: 8 bytes
{sizeCorrect = Oh}
{aligned = DivideBy 8 Refl}

||| Proof that AuditEntry layout is C-ABI compliant
||| Proof that AuditEntry layout is C-ABI compliant. Each field offset
||| divides its alignment: 0|8, 8|8, 16|8, 24|8, 32|8, 40|8, 48|8, 56|4.
export
auditEntryLayoutValid : CABICompliant auditEntryLayout
auditEntryLayoutValid = CABIOk auditEntryLayout ?auditEntryFieldsAligned
auditEntryLayoutValid : CABICompliant Layout.auditEntryLayout
auditEntryLayoutValid =
CABIOk auditEntryLayout
(ConsField _ _ (DivideBy 0 Refl)
(ConsField _ _ (DivideBy 1 Refl)
(ConsField _ _ (DivideBy 2 Refl)
(ConsField _ _ (DivideBy 3 Refl)
(ConsField _ _ (DivideBy 4 Refl)
(ConsField _ _ (DivideBy 5 Refl)
(ConsField _ _ (DivideBy 6 Refl)
(ConsField _ _ (DivideBy 14 Refl)
NoFields))))))))

--------------------------------------------------------------------------------
-- StateSnapshot Layout (oblibeniser-specific)
Expand All @@ -201,11 +233,21 @@ stateSnapshotLayout =
]
40 -- Total size: 40 bytes (with 4 bytes internal padding after stateSize)
8 -- Alignment: 8 bytes
{sizeCorrect = Oh}
{aligned = DivideBy 5 Refl}

||| Proof that StateSnapshot layout is C-ABI compliant
||| Proof that StateSnapshot layout is C-ABI compliant. Each field offset
||| divides its alignment: 0|8, 8|8, 16|8, 24|4, 32|8.
export
stateSnapshotLayoutValid : CABICompliant stateSnapshotLayout
stateSnapshotLayoutValid = CABIOk stateSnapshotLayout ?snapshotFieldsAligned
stateSnapshotLayoutValid : CABICompliant Layout.stateSnapshotLayout
stateSnapshotLayoutValid =
CABIOk stateSnapshotLayout
(ConsField _ _ (DivideBy 0 Refl)
(ConsField _ _ (DivideBy 1 Refl)
(ConsField _ _ (DivideBy 2 Refl)
(ConsField _ _ (DivideBy 6 Refl)
(ConsField _ _ (DivideBy 4 Refl)
NoFields)))))

--------------------------------------------------------------------------------
-- UndoStack Layout (oblibeniser-specific)
Expand All @@ -228,25 +270,53 @@ undoStackLayout =
]
24 -- Total size: 24 bytes
8 -- Alignment: 8 bytes
{sizeCorrect = Oh}
{aligned = DivideBy 3 Refl}

||| Proof that UndoStack layout is C-ABI compliant
||| Proof that UndoStack layout is C-ABI compliant. Each field offset
||| divides its alignment: 0|8, 8|4, 12|4, 16|8.
export
undoStackLayoutValid : CABICompliant undoStackLayout
undoStackLayoutValid = CABIOk undoStackLayout ?undoStackFieldsAligned
undoStackLayoutValid : CABICompliant Layout.undoStackLayout
undoStackLayoutValid =
CABIOk undoStackLayout
(ConsField _ _ (DivideBy 0 Refl)
(ConsField _ _ (DivideBy 2 Refl)
(ConsField _ _ (DivideBy 3 Refl)
(ConsField _ _ (DivideBy 2 Refl)
NoFields))))

--------------------------------------------------------------------------------
-- Offset Calculation
--------------------------------------------------------------------------------

||| Calculate field offset with proof of correctness
||| Look up a field's offset by name in a layout.
public export
fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (n : Nat ** Field)
fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (Nat, Field)
fieldOffset layout name =
case findIndex (\f => f.name == name) layout.fields of
Just idx => Just (finToNat idx ** index idx layout.fields)
Just idx => Just (finToNat idx, index idx layout.fields)
Nothing => Nothing

||| Proof that field offset is within struct bounds
||| Decide whether a field lies within a struct's byte bounds, returning a
||| genuine proof when `offset + size <= totalSize`. The previous signature
||| asserted this for *every* field unconditionally, which is false (a field
||| need not belong to the layout); this honest version decides it.
public export
offsetInBounds : (layout : StructLayout) -> (f : Field) ->
Maybe (So (f.offset + f.size <= layout.totalSize))
offsetInBounds layout f =
case choose (f.offset + f.size <= layout.totalSize) of
Left ok => Just ok
Right _ => Nothing

||| Verify that all oblibeniser concrete layouts are C-ABI compliant. This
||| fails (Left) if any concrete layout is misaligned, rather than asserting
||| it. Each layout's `FieldsAligned` witness is built by `checkCABI`'s sound
||| `decFieldsAligned`.
public export
offsetInBounds : (layout : StructLayout) -> (f : Field) -> So (f.offset + f.size <= layout.totalSize)
offsetInBounds layout f = ?offsetInBoundsProof
verifyAllLayouts : Either String ()
verifyAllLayouts = do
_ <- checkCABI auditEntryLayout
_ <- checkCABI stateSnapshotLayout
_ <- checkCABI undoStackLayout
Right ()
91 changes: 91 additions & 0 deletions src/interface/abi/Oblibeniser/ABI/Proofs.idr
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
-- SPDX-License-Identifier: MPL-2.0
-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
--
||| Machine-checked proofs over the oblibeniser ABI.
|||
||| These are not runtime tests — they are propositional statements the Idris2
||| type checker must discharge at compile time. If any concrete ABI struct
||| layout were misaligned, the result-code encoding wrong, or a decision
||| procedure mis-defined, this module would fail to typecheck and the proof
||| build would go red.
|||
||| The C-ABI compliance witnesses are built directly from per-field
||| divisibility proofs (`DivideBy k Refl`, where `offset = k * alignment`).
||| Multiplication reduces during type checking, so these are fully verified
||| by the compiler; we avoid routing them through `Nat` division, which is a
||| primitive that does not reduce at the type level.

module Oblibeniser.ABI.Proofs

import Oblibeniser.ABI.Types
import Oblibeniser.ABI.Layout
import Data.So
import Data.Vect

%default total

--------------------------------------------------------------------------------
-- The concrete reversible-computing FFI struct layouts are C-ABI compliant.
--------------------------------------------------------------------------------

||| Every field offset in the AuditEntry layout divides its alignment:
||| 0|8, 8|8, 16|8, 24|8, 32|8, 40|8, 48|8, 56|4.
export
auditEntryCompliant : CABICompliant Layout.auditEntryLayout
auditEntryCompliant =
CABIOk auditEntryLayout
(ConsField _ _ (DivideBy 0 Refl)
(ConsField _ _ (DivideBy 1 Refl)
(ConsField _ _ (DivideBy 2 Refl)
(ConsField _ _ (DivideBy 3 Refl)
(ConsField _ _ (DivideBy 4 Refl)
(ConsField _ _ (DivideBy 5 Refl)
(ConsField _ _ (DivideBy 6 Refl)
(ConsField _ _ (DivideBy 14 Refl)
NoFields))))))))

||| Every field offset in the StateSnapshot layout divides its alignment:
||| 0|8, 8|8, 16|8, 24|4, 32|8.
export
stateSnapshotCompliant : CABICompliant Layout.stateSnapshotLayout
stateSnapshotCompliant =
CABIOk stateSnapshotLayout
(ConsField _ _ (DivideBy 0 Refl)
(ConsField _ _ (DivideBy 1 Refl)
(ConsField _ _ (DivideBy 2 Refl)
(ConsField _ _ (DivideBy 6 Refl)
(ConsField _ _ (DivideBy 4 Refl)
NoFields)))))

||| Every field offset in the UndoStack layout divides its alignment:
||| 0|8, 8|4, 12|4, 16|8.
export
undoStackCompliant : CABICompliant Layout.undoStackLayout
undoStackCompliant =
CABIOk undoStackLayout
(ConsField _ _ (DivideBy 0 Refl)
(ConsField _ _ (DivideBy 2 Refl)
(ConsField _ _ (DivideBy 3 Refl)
(ConsField _ _ (DivideBy 2 Refl)
NoFields))))

--------------------------------------------------------------------------------
-- Result-code encoding: the integer contract the Zig FFI depends on.
--------------------------------------------------------------------------------

||| Success is encoded as 0, matching the C convention.
export
okIsZero : resultToInt Ok = 0
okIsZero = Refl

||| NotReversible is encoded as 5 — the inverse-computation FFI returns this
||| code and the safe wrappers decode it back to `NotReversible`.
export
notReversibleIsFive : resultToInt NotReversible = 5
notReversibleIsFive = Refl

||| InverseProofFailed is encoded as 7 — the central correctness failure code
||| for oblibeniser's reversibility guarantee.
export
inverseProofFailedIsSeven : resultToInt InverseProofFailed = 7
inverseProofFailedIsSeven = Refl
Loading
Loading