Skip to content

Commit 01c7c07

Browse files
proof(SafeFile): DISCHARGE readTrackingMonotonic + writeTrackingMonotonic (#136)
## Summary Discharge the last two OWED handle-tracking theorems in `Proven.SafeFile.Proofs`: - `readTrackingMonotonic : (Operations.updateAfterRead h bytes).bytesRead >= h.bytesRead = True` - `writeTrackingMonotonic : (Operations.updateAfterWrite h bytes).bytesWritten >= h.bytesWritten = True` The blocker per the OWED comment was "Idris2 0.8.0 doesn't reduce record-update field projection through a function call". Verified that's accurate (test harness at `/tmp/charrefl/src/TestNatGte.idr`), but the fix is two-part: 1. **`Operations.idr`** — Flip both `updateAfterRead` / `updateAfterWrite` from `export` to `public export` AND inline the explicit `MkSafeHandle` constructor instead of the record-update sugar (`{ bytesRead := ... } h`). Runtime semantics identical, but the explicit constructor reduces definitionally under field projection at type-check time. 2. **`Proofs.idr`** — Add helper `plusGteOriginal : (n, m : Nat) -> (n + m) >= n = True` (induction on `n`, case-split on `m` only in the `Z` branch). Discharge both theorems trivially. ## Verification - `idris2 --check src/Proven/SafeFile/Operations.idr` → Exit 0 - `idris2 --check src/Proven/SafeFile/Proofs.idr` → Exit 0 - Empirical confirmation: `/tmp/charrefl/src/TestNatGte.idr` shows record-update DOES NOT reduce; explicit-constructor DOES. Zero `believe_me` / `postulate` / `idris_crash`. No external-caller breakage (no uses outside `SafeFile/Proofs.idr` per estate grep). Refs proven#90 (Phase 3 OWED triage), proven#107 (overly-cautious OWED meta), proven#119 (paths-forward). ## Test plan - [x] Local type-check `Operations.idr` clean - [x] Local type-check `Proofs.idr` clean - [ ] CI `idris2 --check proven.ipkg` green - [ ] CI test suite green 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 48cc38c commit 01c7c07

2 files changed

Lines changed: 43 additions & 33 deletions

File tree

src/Proven/SafeFile/Operations.idr

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,25 @@ checkWritable h =
156156
else Err (InvalidOperation "write" h.mode)
157157

158158
||| Update handle after read
159-
export
159+
|||
160+
||| Inlined as explicit `MkSafeHandle` constructor (rather than
161+
||| record-update syntax) so that the field projection
162+
||| `(updateAfterRead h bytes).bytesRead` reduces definitionally to
163+
||| `h.bytesRead + bytes` at type-check time. Record-update sugar does
164+
||| not unfold under projection in Idris2 0.8.0; explicit constructor
165+
||| does. Runtime behaviour is identical.
166+
public export
160167
updateAfterRead : SafeHandle -> Nat -> SafeHandle
161-
updateAfterRead h bytes = { bytesRead := h.bytesRead + bytes } h
168+
updateAfterRead h bytes =
169+
MkSafeHandle h.handleId h.mode h.path (h.bytesRead + bytes) h.bytesWritten
162170

163171
||| Update handle after write
164-
export
172+
|||
173+
||| See `updateAfterRead` for why this uses explicit-constructor form.
174+
public export
165175
updateAfterWrite : SafeHandle -> Nat -> SafeHandle
166-
updateAfterWrite h bytes = { bytesWritten := h.bytesWritten + bytes } h
176+
updateAfterWrite h bytes =
177+
MkSafeHandle h.handleId h.mode h.path h.bytesRead (h.bytesWritten + bytes)
167178

168179
||| Create new handle (for simulation/testing)
169180
export

src/Proven/SafeFile/Proofs.idr

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Proven.Core
1313
import Proven.SafeFile.Types
1414
import Proven.SafeFile.Operations
1515
import Data.List
16+
import Data.Nat
1617
import Data.String
1718

1819
%default total
@@ -210,35 +211,33 @@ fileSizeCheckPrevents path size limit tooLarge = ()
210211
-- Handle Tracking Proofs
211212
--------------------------------------------------------------------------------
212213

213-
||| OWED: `updateAfterRead h bytes` produces a handle whose `bytesRead`
214-
||| field equals `h.bytesRead + bytes`, hence is `>=` the prior value.
215-
||| Operationally true by direct unfolding of
216-
||| `updateAfterRead h bytes = { bytesRead := h.bytesRead + bytes } h`
217-
||| in `Proven.SafeFile.Operations`.
218-
||| Held back by Idris2 0.8.0 not reducing record-update field projection
219-
||| through a function call during type-checking — `(updateAfterRead h
220-
||| bytes).bytesRead` does not normalise to `h.bytesRead + bytes` by Refl
221-
||| (same blocker as the deleted `newHandleZeroCounters` documented at
222-
||| L218-220). Discharge once Idris2 fixes the record-update reduction
223-
||| (tracked upstream) or via a manual rewrite + `lteAddRight` proof on
224-
||| `Nat` once both sides are forced into normal form.
225-
export
226-
0 readTrackingMonotonic : (h : SafeHandle) -> (bytes : Nat) ->
227-
(Operations.updateAfterRead h bytes).bytesRead >= h.bytesRead = True
228-
229-
||| OWED: `updateAfterWrite h bytes` produces a handle whose
230-
||| `bytesWritten` field equals `h.bytesWritten + bytes`, hence is `>=`
231-
||| the prior value. Operationally true by direct unfolding of
232-
||| `updateAfterWrite h bytes = { bytesWritten := h.bytesWritten + bytes } h`
233-
||| in `Proven.SafeFile.Operations`.
234-
||| Held back by Idris2 0.8.0 not reducing record-update field projection
235-
||| through a function call during type-checking (same blocker family as
236-
||| `readTrackingMonotonic` above). Discharge once Idris2 fixes the
237-
||| record-update reduction, or via a manual rewrite + `lteAddRight`
238-
||| proof on `Nat` once both sides are forced into normal form.
239-
export
240-
0 writeTrackingMonotonic : (h : SafeHandle) -> (bytes : Nat) ->
241-
(Operations.updateAfterWrite h bytes).bytesWritten >= h.bytesWritten = True
214+
||| Helper: `n + m >= n = True` for all `Nat`. Used to discharge the
215+
||| tracking-monotonic theorems below. Induction on `n` (case-split on
216+
||| `m` is only needed in the `Z` branch because `m >= 0` reduces only
217+
||| after pattern-matching on whether `m` is `Z` or `S _`).
218+
plusGteOriginal : (n, m : Nat) -> (n + m) >= n = True
219+
plusGteOriginal Z Z = Refl
220+
plusGteOriginal Z (S _) = Refl
221+
plusGteOriginal (S k) m = plusGteOriginal k m
222+
223+
||| DISCHARGED: `updateAfterRead h bytes` produces a handle whose
224+
||| `bytesRead` is `>=` the prior value. By inlined-constructor
225+
||| definition in `Operations.updateAfterRead`, the field projection
226+
||| reduces definitionally to `h.bytesRead + bytes`; the remaining
227+
||| Nat inequality `(n + m) >= n = True` is closed by induction
228+
||| (`plusGteOriginal`).
229+
public export
230+
readTrackingMonotonic : (h : SafeHandle) -> (bytes : Nat) ->
231+
(Operations.updateAfterRead h bytes).bytesRead >= h.bytesRead = True
232+
readTrackingMonotonic h bytes = plusGteOriginal h.bytesRead bytes
233+
234+
||| DISCHARGED: `updateAfterWrite h bytes` produces a handle whose
235+
||| `bytesWritten` is `>=` the prior value. Same shape of proof as
236+
||| `readTrackingMonotonic` above.
237+
public export
238+
writeTrackingMonotonic : (h : SafeHandle) -> (bytes : Nat) ->
239+
(Operations.updateAfterWrite h bytes).bytesWritten >= h.bytesWritten = True
240+
writeTrackingMonotonic h bytes = plusGteOriginal h.bytesWritten bytes
242241

243242
||| Theorem: New handle has zero counters
244243
-- newHandleZeroCounters removed: newHandle constructs a record but

0 commit comments

Comments
 (0)