Skip to content

Commit 07fda92

Browse files
proof(abi): machine-check layout properties for all 8 ABI structs (Idris2 0.7.0) (#37)
The Idris2 ABI had never compiled (no .ipkg; modules declared `module Types/Layout/Foreign` but cross-imported as `GSA.ABI.*`). This makes the package build and proves the previously-declared-but-unpopulated layout predicates. Verified end-to-end with Idris2 0.7.0: cd src/interface/abi && idris2 --typecheck gsa-abi.ipkg Build fixes (package compiles for the first time): - Add gsa-abi.ipkg; fix imports GSA.ABI.* -> flat module names. - Types.idr: %hide Data.List.NonEmpty (name clash); nonEmptyId via `with` (case on `unpack s` did not refine the dependent goal); rename a shadowing pattern variable. - Layout.idr: alignUpCeilIsMultiple did NOT typecheck as written; rewritten via an in-module total `ceilDiv` so the multiple-witness is a direct Refl. - Foreign.idr: `total` is a reserved word (renamed param); fix linear ServerHandle misuse in extractConfig/applyConfig/serverAction/getLogs/ closeHandle (consume the handle by pattern match, rebuild for the borrow-return). Proofs (32 = 8 structs x 4 properties), all machine-checked: - NoOverlap, AllFieldsAligned, SizeAligned, SizeCoversFields for ServerHandle, ProbeResult, ConfigField, A2MLConfig, GameProfile, ServerOctad, Fingerprint, DriftReport. - Discharged by decision procedures extracted via getYes; each proof typechecks only because its Dec reduces to Yes for that concrete layout. PROOF-NEEDS.md updated to the verified state (corrects the stale alignUpEquiv claim; records the Zig<->Idris cross-check as the main remaining obligation). No postulates / believe_me / assert_total; all modules %default total. Claude-Session: https://claude.ai/code/session_01LvsZgNxFbeqfRmrVFNhJ8G Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7372315 commit 07fda92

5 files changed

Lines changed: 307 additions & 74 deletions

File tree

PROOF-NEEDS.md

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,72 @@
11
# Proof Requirements
22

3-
## Current State (2026-04-03 blitz)
3+
## Current State (2026-06-21 — verified with Idris2 0.7.0)
44

5-
- ABI exists in `src/interface/abi/Layout.idr`
6-
- **RESOLVED**: `postulate alignUpProducesAligned` replaced with constructive proof
7-
- Remaining: one weaker equivalence postulate (`alignUpEquiv`)
5+
The ABI package typechecks end-to-end (all modules `%default total`, no
6+
`postulate` / `believe_me` / `assert_total`):
87

9-
## What Was Done
8+
```bash
9+
cd src/interface/abi && idris2 --typecheck gsa-abi.ipkg
10+
# 1/3: Building Types (Types.idr)
11+
# 2/3: Building Layout (Layout.idr)
12+
# 3/3: Building Foreign (Foreign.idr)
13+
```
1014

11-
### Alignment Proof (CLOSED)
15+
Toolchain: Idris2 0.7.0, Chez backend (`.tool-versions` pins `idris2 0.7.0`).
1216

13-
The original `postulate alignUpProducesAligned` asserted that `alignUp` produces
14-
aligned results but could not be proven because Idris2 0.7.x's `modNatNZ` lacks
15-
composition lemmas.
17+
## What is proven (machine-checked)
1618

17-
**Solution**: Introduced `alignUpCeil` — an alternative alignment function that
18-
computes the next multiple via ceiling division. The result is `k * alignment`
19-
by construction, so the alignment property is trivially witnessed:
19+
### Memory layout (`Layout.idr`)
20+
For every one of the 8 ABI structs — ServerHandle, ProbeResult, ConfigField,
21+
A2MLConfig, GameProfile, ServerOctad, Fingerprint, DriftReport — all four
22+
layout properties are proven:
2023

21-
- `alignUpCeil` : computes next multiple via `ceilDiv(n, a) * a`
22-
- `alignUpCeilIsMultiple` : constructive proof returning `MkMultiple k Refl`
24+
| Property | Meaning |
25+
|----------|---------|
26+
| `NoOverlap` | no two fields' `[offset, offset+size)` ranges overlap |
27+
| `AllFieldsAligned` | every field offset is a multiple of its alignment |
28+
| `SizeAligned` | total struct size is a multiple of the struct alignment |
29+
| `SizeCoversFields` | total size ≥ sum of field sizes (padding accounted) |
2330

24-
No postulates, no `believe_me`. Both cases (`remainder = 0` and `remainder > 0`)
25-
return a direct `Refl` witness.
31+
Discharged by decision procedures (`decNoOverlap`, `decAllAligned`,
32+
`decSizeAligned`, `decSizeCovers`) extracted via `getYes`; each proof
33+
typechecks only because its `Dec` reduces to `Yes` for that concrete layout —
34+
i.e. a malformed layout would fail to compile.
2635

27-
**Remaining postulate**: `alignUpEquiv` asserts that `alignUp` and `alignUpCeil`
28-
produce the same result. This is mathematically trivial but opaque `modNatNZ`
29-
prevents structural proof. Strictly weaker than the original postulate.
36+
Also still proven: the per-type size/alignment `Refl` proofs, the
37+
`SizeOf`↔layout agreement (`LayoutMatchesSizeOf`), and cross-platform
38+
x86_64 ≡ aarch64 equivalence.
3039

31-
## What Still Needs Proving
40+
### Alignment (`Layout.idr`)
41+
- `alignUpCeil offset a = ceilDiv offset a * a` (via an in-module total
42+
`ceilDiv`).
43+
- `alignUpCeilIsMultiple` — constructive `Refl` proof that the result is
44+
`k * a`.
3245

33-
- **Server probe safety**: Prove that probes do not cause side effects on targets
34-
- **Configuration drift detection**: Prove drift dashboard identifies all deviations
35-
- **Access control for admin panels**: Prove panel-level authorization
46+
> Correction to the prior version of this file: the earlier
47+
> `alignUpCeilIsMultiple` did **not** typecheck (the `case` form left the
48+
> goal stuck). It was rewritten via `ceilDiv` so the witness is a direct
49+
> `Refl`. There is no remaining `alignUpEquiv` postulate — the proven path
50+
> uses `alignUpCeil`/`ceilDiv` directly, so that obligation is moot.
3651
37-
## When alignUpEquiv Can Be Closed
52+
### Domain validators (`Types.idr`)
53+
Decidable `portInRange`, `nonEmptyId`, `configFieldCountPositive`,
54+
`allPortsValid`, the `Valid*` smart constructors, and the linear
55+
`ServerHandle` with its erased non-null witness.
3856

39-
When Idris2 stdlib gains either:
40-
- `modNatNZ_zero : modNatNZ (k * d) d ok = 0`
41-
- `modNatNZ_spec : n = divNatNZ n d ok * d + modNatNZ n d ok`
57+
## What still needs proving
4258

43-
## Priority
44-
- **DONE** — Alignment proof is constructive
45-
- **MEDIUM** — Server probe safety (production concern)
46-
- **LOW** — Drift detection, access control (need richer specifications)
59+
- **Zig ↔ Idris layout cross-check (HIGH).** `Layout.idr` proves the Idris
60+
model is internally consistent, but nothing yet checks the hand-written
61+
field offsets/sizes against Zig's `@sizeOf`/`@offsetOf`. A generated test
62+
emitting Zig's numbers and comparing them to the `Layout` constants would
63+
close the actual cross-language ABI guarantee.
64+
- **Server probe safety (MEDIUM).** Prove probes cause no side effects on
65+
targets. Needs a specification first.
66+
- **Configuration drift-detection completeness (LOW).** Needs a richer spec.
67+
- **Access control for admin panels (LOW).** Needs a specification.
68+
69+
## Notes
70+
- `Foreign.idr`'s linear `ServerHandle` wrappers now typecheck: each handle is
71+
consumed by a pattern match and rebuilt for the borrow-return, so linearity
72+
is respected (previously they used the handle twice and did not compile).

src/interface/abi/Foreign.idr

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323

2424
module Foreign
2525

26-
import GSA.ABI.Types
27-
import GSA.ABI.Layout
26+
import Types
27+
import Layout
2828

2929
import Data.List
3030
import Data.Maybe
@@ -233,12 +233,12 @@ readStringArray ptr = do
233233
where
234234
covering
235235
go : Int -> Int -> List String -> IO (List String)
236-
go idx total acc =
237-
if idx >= total
236+
go idx tot acc =
237+
if idx >= tot
238238
then pure (reverse acc)
239239
else do
240240
s <- primIO (prim__arrayGetString ptr idx)
241-
go (idx + 1) total (s :: acc)
241+
go (idx + 1) tot (s :: acc)
242242

243243
--------------------------------------------------------------------------------
244244
-- Safe Wrappers: Server Lifecycle
@@ -302,10 +302,10 @@ fingerprint host ports = do
302302
export
303303
covering
304304
extractConfig : (1 handle : ServerHandle) -> GameProfile -> IO (Either Result A2MLConfig, ServerHandle)
305-
extractConfig handle profile = do
306-
ptr <- primIO (prim__extractConfig handle.rawPtr profile.id)
305+
extractConfig (MkServerHandle p sid v) profile = do
306+
ptr <- primIO (prim__extractConfig p profile.id)
307307
case prim__isNull ptr /= 0 of
308-
True => pure (Left NullPointer, handle)
308+
True => pure (Left NullPointer, MkServerHandle p sid v)
309309
False => do
310310
-- Deserialise the A2MLConfig from the returned pointer
311311
serverId <- primIO (prim__readString ptr)
@@ -314,7 +314,7 @@ extractConfig handle profile = do
314314
configPath <- primIO (prim__arrayGetString ptr 3)
315315
primIO (prim__free ptr)
316316
let format = fromMaybe KeyValue (parseConfigFormat formatCode)
317-
pure (Right (MkA2MLConfig serverId gameId format configPath []), handle)
317+
pure (Right (MkA2MLConfig serverId gameId format configPath []), MkServerHandle p sid v)
318318
where
319319
parseConfigFormat : Int -> Maybe ConfigFormat
320320
parseConfigFormat 0 = Just XML
@@ -337,15 +337,15 @@ extractConfig handle profile = do
337337
export
338338
covering
339339
applyConfig : (1 handle : ServerHandle) -> A2MLConfig -> IO (Either Result (), ServerHandle)
340-
applyConfig handle config = do
340+
applyConfig (MkServerHandle p sid v) config = do
341341
-- The Zig layer accepts a serialised config struct.
342342
-- For now we pass the config path and let the Zig layer handle serialisation.
343343
-- A proper implementation would use prim__applyConfig with a packed struct.
344-
result <- primIO (prim__serverAction handle.rawPtr ("apply:" ++ config.configPath))
344+
result <- primIO (prim__serverAction p ("apply:" ++ config.configPath))
345345
let parsed = if result == 0
346346
then Right ()
347347
else Left (fromMaybe Error (resultFromInt (negate result)))
348-
pure (parsed, handle)
348+
pure (parsed, MkServerHandle p sid v)
349349

350350
||| Send a named action to the server (start, stop, restart, status, etc.),
351351
||| BORROWING the handle. The action string must match one of the actions
@@ -357,11 +357,11 @@ applyConfig handle config = do
357357
export
358358
covering
359359
serverAction : (1 handle : ServerHandle) -> String -> IO (Either Result String, ServerHandle)
360-
serverAction handle action = do
361-
result <- primIO (prim__serverAction handle.rawPtr action)
360+
serverAction (MkServerHandle p sid v) action = do
361+
result <- primIO (prim__serverAction p action)
362362
if result >= 0
363-
then pure (Right ("Action '" ++ action ++ "' completed (code " ++ show result ++ ")"), handle)
364-
else pure (Left (fromMaybe Error (resultFromInt (negate result))), handle)
363+
then pure (Right ("Action '" ++ action ++ "' completed (code " ++ show result ++ ")"), MkServerHandle p sid v)
364+
else pure (Left (fromMaybe Error (resultFromInt (negate result))), MkServerHandle p sid v)
365365

366366
||| Retrieve the last N lines of server log output, BORROWING the handle.
367367
||| Log lines are returned in chronological order (oldest first).
@@ -372,14 +372,14 @@ serverAction handle action = do
372372
export
373373
covering
374374
getLogs : (1 handle : ServerHandle) -> Nat -> IO (Either Result (List String), ServerHandle)
375-
getLogs handle lineCount = do
376-
ptr <- primIO (prim__getLogs handle.rawPtr (cast lineCount))
375+
getLogs (MkServerHandle p sid v) lineCount = do
376+
ptr <- primIO (prim__getLogs p (cast lineCount))
377377
case prim__isNull ptr /= 0 of
378-
True => pure (Left NullPointer, handle)
378+
True => pure (Left NullPointer, MkServerHandle p sid v)
379379
False => do
380380
lines <- readStringArray ptr
381381
primIO (prim__free ptr)
382-
pure (Right lines, handle)
382+
pure (Right lines, MkServerHandle p sid v)
383383

384384
||| Close and release a server handle, CONSUMING it.
385385
||| After this call, the handle is no longer valid. The linear type system
@@ -391,8 +391,8 @@ getLogs handle lineCount = do
391391
export
392392
covering
393393
closeHandle : (1 handle : ServerHandle) -> IO Result
394-
closeHandle handle = do
395-
result <- primIO (prim__closeHandle handle.rawPtr)
394+
closeHandle (MkServerHandle p _ _) = do
395+
result <- primIO (prim__closeHandle p)
396396
pure (fromMaybe Error (resultFromInt result))
397397

398398
--------------------------------------------------------------------------------

0 commit comments

Comments
 (0)