Skip to content

Commit 321d97f

Browse files
hyperpolymathclaude
andcommitted
proof(SafeFile): DISCHARGE readTrackingMonotonic + writeTrackingMonotonic via inlined-constructor + Nat induction (proven#90 #107 #119)
The two OWED tracking-monotonic theorems were blocked by Idris2 0.8.0 not reducing record-update field projection through a function call. The fix is two-part: 1. **Operations.idr**: Flip `updateAfterRead`/`updateAfterWrite` from `export` to `public export`, AND replace the record-update sugar (`{ bytesRead := ... } h`) with the explicit `MkSafeHandle` constructor. Runtime behaviour is identical, but the explicit constructor allows `(updateAfterRead h bytes).bytesRead` to reduce definitionally to `h.bytesRead + bytes` at type-check time — confirmed by /tmp/charrefl test harness. 2. **Proofs.idr**: Add helper `plusGteOriginal : (n, m : Nat) -> (n + m) >= n = True` (induction on `n`, case-split on `m` in the `Z` branch only). Discharge both theorems by applying the helper. Empirical reduction map confirms `(n + m) >= n = True` reduces via the `compare`-based Bool `>=` for Nat after pattern-matching on `n` and `m` in the `Z` cases (`reference_idris2_0_8_0_reduction_map.md`). Zero `believe_me`/`postulate`/`idris_crash`. No external API change — `updateAfterRead`/`updateAfterWrite` already had no non-test callers (grep'd whole tree). Visibility flip is monotonic (`export` → `public export`). Refs proven#90 (Phase 3 OWED triage), proven#107 (overly-cautious OWED meta-issue), proven#119 (paths-forward proposal). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 477172b commit 321d97f

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)