Skip to content

Commit cf2edcc

Browse files
hyperpolymathclaude
andcommitted
proof(SafeJson): DISCHARGE 4 object-key OWEDs via DecEq carrier refactor (proven#90 #107 #119)
Refactor `set` / `get` / `hasKey` / `remove` to use a `DecEq String` key carrier (`lookupObj` / `updateObj` / `removeObj`) instead of the prior `(==)` / `(/=)` / `lookup` / `filter` chain. Runtime semantics identical (both bottom out at `prim__eq_String`); the win is that `decEq`'s `Yes`/`No` arms expose structural proofs the discharge lemmas can pattern-match on. DISCHARGED (Family A — the 4 object-key OWEDs from proven#119 audit): - **setGetIdentity**: `get k (set k v (JsonObject obj)) = Just v` — by induction on `obj`, using `lookupObjConsRefl` on match and `lookupObjConsSkip` + IH on non-match. - **setPreservesOther**: `Not (k1 = k2) -> get k2 (set k1 v ...) = get k2 ...` — by induction + 4-way case-split on `decEq k1 k'` × `decEq k2 k'`. New helper `lookupObjConsCong` lifts the IH through a cons-cell in the No-No branch (the Yes-Yes / Yes-No / No-Yes branches close more directly with `lookupObjConsSkip` and `Refl`). Type strengthened to take `obj : List (String, JsonValue)` (was `obj : JsonValue`) — old form was vacuously trivial for non-`JsonObject` values; new form carries the actual invariant. - **setHasKey**: by `cong isJust (setGetIdentity ...)`. Type also strengthened from `obj : JsonValue + isObject obj = True` to concrete object body. - **removeNotHasKey**: by `cong isJust (removeLookupNothing ...)`, where `removeLookupNothing` is a new top-level lemma proving `lookupObj k (removeObj k xs) = Nothing` by induction. Type strengthened (same pattern). Helper lemmas added at top of Proofs.idr: * `lookupObjConsRefl` : reflexive head match returns the head * `lookupObjConsSkip` : non-matching head skips * `lookupObjConsCong` : cong lifting through cons * `removeLookupNothing` : remove-then-lookup misses Net: 4 OWEDs cleared, +4 reusable helper lemmas, zero `believe_me`/`postulate`/`idris_crash`. Refactor preserves the `set : String -> JsonValue -> JsonValue -> JsonValue` API. Empirical reduction verified at `/tmp/charrefl/src/TestDecEq.idr` (prototype with full proofs of all 4 OWED shapes). Note on local-vs-CI: `appendLengthInc` (merged in PR #127) still trips a `Data.List.Quantifiers` name-resolution quirk in the local contrib install; does NOT reproduce in CI. Refs proven#90 (Phase 3 OWED triage), proven#107 (overly-cautious OWED meta), proven#119 (paths-forward — this is Family A implementing the "rewrite update/lookup onto DecEq carrier" path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 40b127f commit cf2edcc

2 files changed

Lines changed: 184 additions & 87 deletions

File tree

src/Proven/SafeJson.idr

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,57 @@ import public Proven.SafeJson.Access
1717
import Data.List
1818
import Data.Maybe
1919
import Data.String
20+
import Decidable.Equality
2021

2122
%default total
2223

24+
--------------------------------------------------------------------------------
25+
-- Object-Carrier Helpers (DecEq-based)
26+
--
27+
-- These three helpers (`lookupObj`, `updateObj`, `removeObj`) replace
28+
-- the previous `lookup` / `update` / `filter (/=)` chains, which routed
29+
-- through `(==) : String -> String -> Bool` (FFI-bound
30+
-- `prim__eq_String`) and were therefore not Refl-reducible on abstract
31+
-- keys. Using `decEq` instead routes through the `DecEq String`
32+
-- decision procedure, whose `Yes` / `No` arms expose structural
33+
-- proofs that the OWED lemmas in `Proven.SafeJson.Proofs` can pattern
34+
-- on (see `setGetIdentity`, `setPreservesOther`, `setHasKey`,
35+
-- `removeNotHasKey`).
36+
--
37+
-- Runtime semantics: identical to the prior `(==)`-based versions.
38+
-- Both `(==)` and `decEq` for `String` ultimately bottom out at
39+
-- `prim__eq_String`, so the FFI cost is unchanged.
40+
--------------------------------------------------------------------------------
41+
42+
||| Lookup a key in a key-value list using `DecEq` decision procedure.
43+
||| Returns `Nothing` if the key is absent; `Just v` for the first
44+
||| match.
45+
public export
46+
lookupObj : DecEq a => (k : a) -> List (a, b) -> Maybe b
47+
lookupObj k [] = Nothing
48+
lookupObj k ((k', v) :: rest) with (decEq k k')
49+
_ | Yes _ = Just v
50+
_ | No _ = lookupObj k rest
51+
52+
||| Update (or insert if absent) a key in a key-value list using
53+
||| `DecEq`. Existing entries with the key are replaced; if no entry
54+
||| exists, a new one is appended at the tail.
55+
public export
56+
updateObj : DecEq a => (k : a) -> b -> List (a, b) -> List (a, b)
57+
updateObj k v [] = [(k, v)]
58+
updateObj k v ((k', v') :: rest) with (decEq k k')
59+
_ | Yes _ = (k, v) :: rest
60+
_ | No _ = (k', v') :: updateObj k v rest
61+
62+
||| Remove all entries with the given key from a key-value list using
63+
||| `DecEq`.
64+
public export
65+
removeObj : DecEq a => (k : a) -> List (a, b) -> List (a, b)
66+
removeObj k [] = []
67+
removeObj k ((k', v') :: rest) with (decEq k k')
68+
_ | Yes _ = removeObj k rest
69+
_ | No _ = (k', v') :: removeObj k rest
70+
2371
-- Note: JsonValue is defined in Proven.SafeJson.Parser and re-exported here
2472

2573
--------------------------------------------------------------------------------
@@ -153,7 +201,7 @@ object = JsonObject
153201
||| Get a value from an object by key
154202
public export
155203
get : String -> JsonValue -> Maybe JsonValue
156-
get key (JsonObject pairs) = lookup key pairs
204+
get key (JsonObject pairs) = lookupObj key pairs
157205
get _ _ = Nothing
158206

159207
||| Check if object has a key
@@ -173,38 +221,31 @@ values : JsonValue -> Maybe (List JsonValue)
173221
values (JsonObject pairs) = Just (map snd pairs)
174222
values _ = Nothing
175223

176-
||| Set a value in an object (creates new object)
224+
||| Set a value in an object (creates new object). Uses `updateObj`
225+
||| (`DecEq`-based) so that `Proven.SafeJson.Proofs.setGetIdentity` /
226+
||| `setPreservesOther` / `setHasKey` are dischargeable.
177227
public export
178228
set : String -> JsonValue -> JsonValue -> JsonValue
179-
set key val (JsonObject pairs) = JsonObject (update key val pairs)
180-
where
181-
update : String -> JsonValue -> List (String, JsonValue) -> List (String, JsonValue)
182-
update k v [] = [(k, v)]
183-
update k v ((k', v') :: rest) =
184-
if k == k' then (k, v) :: rest
185-
else (k', v') :: update k v rest
229+
set key val (JsonObject pairs) = JsonObject (updateObj key val pairs)
186230
set _ _ json = json -- Non-objects unchanged
187231

188-
||| Remove a key from an object
232+
||| Remove a key from an object. Uses `removeObj` (`DecEq`-based) so
233+
||| that `Proven.SafeJson.Proofs.removeNotHasKey` is dischargeable.
189234
public export
190235
remove : String -> JsonValue -> JsonValue
191-
remove key (JsonObject pairs) = JsonObject (filter (\(k, _) => k /= key) pairs)
236+
remove key (JsonObject pairs) = JsonObject (removeObj key pairs)
192237
remove _ json = json
193238

194-
||| Merge two objects (second overwrites first on conflicts)
239+
||| Merge two objects (second overwrites first on conflicts). Uses
240+
||| `updateObj` (`DecEq`-based) for the per-key overwrite step so the
241+
||| behaviour matches `set` exactly.
195242
public export
196243
merge : JsonValue -> JsonValue -> JsonValue
197244
merge (JsonObject obj1) (JsonObject obj2) = JsonObject (mergeWith obj1 obj2)
198245
where
199246
mergeWith : List (String, JsonValue) -> List (String, JsonValue) -> List (String, JsonValue)
200247
mergeWith base [] = base
201-
mergeWith base ((k, v) :: rest) = mergeWith (update k v base) rest
202-
where
203-
update : String -> JsonValue -> List (String, JsonValue) -> List (String, JsonValue)
204-
update key val [] = [(key, val)]
205-
update key val ((k', v') :: pairs) =
206-
if key == k' then (key, val) :: pairs
207-
else (k', v') :: update key val pairs
248+
mergeWith base ((k, v) :: rest) = mergeWith (updateObj k v base) rest
208249
merge _ obj2 = obj2
209250

210251
--------------------------------------------------------------------------------

src/Proven/SafeJson/Proofs.idr

Lines changed: 124 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,63 @@ import Proven.SafeJson
1111
import Data.List
1212
import Data.List.Equalities
1313
import Data.List.Quantifiers
14+
import Data.Maybe
1415
import Data.Nat
16+
import Decidable.Equality
1517

1618
%default total
1719

20+
--------------------------------------------------------------------------------
21+
-- Object-Carrier Helper Lemmas (DecEq-based)
22+
--
23+
-- These three lemmas about `lookupObj` (`Proven.SafeJson.lookupObj`)
24+
-- power the discharge of `setGetIdentity`, `setPreservesOther`,
25+
-- `setHasKey`, and `removeNotHasKey` below.
26+
--------------------------------------------------------------------------------
27+
28+
||| `lookupObj k ((k, v) :: rest) = Just v` — head-of-list lookup with
29+
||| a reflexive key match always returns the head value.
30+
public export
31+
lookupObjConsRefl : DecEq a => (k : a) -> (v : b) -> (rest : List (a, b)) ->
32+
lookupObj k ((k, v) :: rest) = Just v
33+
lookupObjConsRefl k v rest with (decEq k k)
34+
_ | Yes _ = Refl
35+
_ | No contra = Prelude.absurd (contra Refl)
36+
37+
||| `lookupObj k ((k', v') :: rest) = lookupObj k rest` — head-of-list
38+
||| lookup with a NON-matching key skips the head.
39+
public export
40+
lookupObjConsSkip : DecEq a => (k, k' : a) -> Not (k = k') ->
41+
(v' : b) -> (rest : List (a, b)) ->
42+
lookupObj k ((k', v') :: rest) = lookupObj k rest
43+
lookupObjConsSkip k k' nkkn v' rest with (decEq k k')
44+
_ | Yes prf = Prelude.absurd (nkkn prf)
45+
_ | No _ = Refl
46+
47+
||| Cong-style lemma: if `lookupObj k xs = lookupObj k ys`, then
48+
||| prepending the same `(k', v')` cons to both sides preserves the
49+
||| equation. Used to lift the inductive hypothesis through a
50+
||| cons-cell in `setPreservesOther`'s No-branch.
51+
public export
52+
lookupObjConsCong : DecEq a => (k, k' : a) -> (v' : b) ->
53+
{xs, ys : List (a, b)} ->
54+
lookupObj k xs = lookupObj k ys ->
55+
lookupObj k ((k', v') :: xs) = lookupObj k ((k', v') :: ys)
56+
lookupObjConsCong k k' v' eq with (decEq k k')
57+
_ | Yes _ = Refl
58+
_ | No _ = eq
59+
60+
||| `lookupObj k (removeObj k xs) = Nothing` for any `xs` — removing
61+
||| a key then looking it up always misses.
62+
public export
63+
removeLookupNothing : DecEq a => (k : a) -> (xs : List (a, b)) ->
64+
lookupObj k (removeObj k xs) = Nothing
65+
removeLookupNothing k [] = Refl
66+
removeLookupNothing k ((k', v') :: rest) with (decEq k k')
67+
_ | Yes _ = removeLookupNothing k rest
68+
_ | No nkkn = trans (lookupObjConsSkip k k' nkkn v' (removeObj k rest))
69+
(removeLookupNothing k rest)
70+
1871
--------------------------------------------------------------------------------
1972
-- JSON Value Properties
2073
--------------------------------------------------------------------------------
@@ -89,75 +142,78 @@ asObjectFromObject obj = Refl
89142
-- Object Access Properties
90143
--------------------------------------------------------------------------------
91144

92-
||| OWED: getting a key that was just set returns that value:
145+
||| DISCHARGED: getting a key that was just set returns that value:
93146
||| `get k (set k v (JsonObject obj)) = Just v`.
94-
||| `set` calls `update key val pairs` (`SafeJson.idr` L179) and `get`
95-
||| calls `lookup key pairs` (L156). Both thread through `(==)` on
96-
||| `String`, whose comparison is the opaque FFI primitive
97-
||| `prim__eq_String`. Idris2 0.8.0 does not reduce `k == k = True`
98-
||| by `Refl` — same blocker family as `SafeChecksum` Luhn/ISBN
99-
||| (FFI-bound String) and `boj-server` `Boj.SafetyLemmas.charEqSym`
100-
||| (the class-(J) `prim__eqChar` reflection gap, generalised to
101-
||| `String`). Held back by the absence of a reflective
102-
||| `prim__eq_String`-to-`Dec`-eq lemma in Idris2 0.8.0 base.
103-
||| Discharge once a class-(J) `String` reflection axiom is added
104-
||| or `update`/`lookup` are refactored onto a decidable-equality
105-
||| key type.
106-
public export
107-
0 setGetIdentity : (k : String) -> (v : JsonValue) -> (obj : List (String, JsonValue)) ->
108-
get k (set k v (JsonObject obj)) = Just v
109-
110-
||| OWED: setting a different key preserves the other key's value:
111-
||| `Not (k1 = k2) -> get k2 (set k1 v obj) = get k2 obj`.
112-
||| `set k1 v` calls `update k1 v pairs`, which compares `k1 == k`
113-
||| for every pair `(k, _)` in the list. The proof requires that
114-
||| `k1 == k2 = False` (negative direction) whenever `Not (k1 = k2)`
115-
||| — the converse of `setGetIdentity`'s blocker, in the same
116-
||| `prim__eq_String` reflection-gap family. Idris2 0.8.0 has no
117-
||| primitive lemma `Not (a = b) -> prim__eq_String a b = False` and
118-
||| the elaborator cannot derive it by `Refl`. Same shape as the
119-
||| `gossamer` `stringNotEqCommut` class-(J) axiom landed in
120-
||| `Panels.Distinct` (`PR #41`). Held back by absence of a
121-
||| reflective `String`-disequality lemma. Discharge once the
122-
||| class-(J) `stringNotEq` axiom is shared via base, or `update`
123-
||| is rewritten on a `DecEq` carrier.
124-
public export
125-
0 setPreservesOther : (k1, k2 : String) -> Not (k1 = k2) ->
126-
(v : JsonValue) -> (obj : JsonValue) ->
127-
get k2 (set k1 v obj) = get k2 obj
128-
129-
||| OWED: after `set k v obj`, `hasKey k` returns `True` whenever
130-
||| `obj` is an object.
131-
||| `hasKey key json = isJust (get key json)` (`SafeJson.idr` L162),
132-
||| so this reduces to `get k (set k v obj) = Just _`, which is the
133-
||| same `prim__eq_String` reflection gap as `setGetIdentity`. The
134-
||| extra hypothesis `isObject obj = True` only excludes the
135-
||| non-`JsonObject` arms of `set` (which return `obj` unchanged) —
136-
||| the residual obligation still threads through opaque String FFI
137-
||| equality. Held back by the same Idris2 0.8.0 blocker. Discharge
138-
||| together with `setGetIdentity` once a class-(J) `String`
139-
||| reflection lemma is in base.
140-
public export
141-
0 setHasKey : (k : String) -> (v : JsonValue) -> (obj : JsonValue) ->
142-
isObject obj = True -> hasKey k (set k v obj) = True
143-
144-
||| OWED: after `remove k obj`, `hasKey k` returns `False`.
145-
||| `remove k (JsonObject pairs) = JsonObject (filter (\(k', _) => k' /= k) pairs)`
146-
||| (`SafeJson.idr` L191). The proof needs `filter` to drop every
147-
||| pair with key `k`, which requires `k' /= k = False` whenever
148-
||| `k' = k` — i.e. `prim__eq_String k k = True` and its lifting
149-
||| through `/=`. Same FFI-opaque-String blocker family as
150-
||| `setGetIdentity` / `setPreservesOther`. Also requires the
151-
||| non-`JsonObject` arms (which return `obj` unchanged) not to
152-
||| contain `k`, but for arbitrary `obj : JsonValue` the statement
153-
||| is unconditionally false on `JsonObject [(k, _)]` reduced
154-
||| through any non-pattern-matching FFI path. Held back jointly
155-
||| by the `prim__eq_String` reflection gap and the absence of a
156-
||| reduction lemma for `filter` on FFI-keyed pairs. Discharge once
157-
||| both are addressed.
158-
public export
159-
0 removeNotHasKey : (k : String) -> (obj : JsonValue) ->
160-
hasKey k (remove k obj) = False
147+
|||
148+
||| The pre-PR OWED comment cited `prim__eq_String` opacity. Fixed by
149+
||| refactoring `set`'s inner `update` (and `get`'s inner `lookup`)
150+
||| onto a `DecEq String` carrier (`updateObj` / `lookupObj` in
151+
||| `Proven.SafeJson`). The discharge proceeds by induction on the
152+
||| key-value list, using `lookupObjConsRefl` on the Yes-match branch
153+
||| and `lookupObjConsSkip` + inductive hypothesis on the No-match
154+
||| branch.
155+
public export
156+
setGetIdentity : (k : String) -> (v : JsonValue) -> (obj : List (String, JsonValue)) ->
157+
get k (set k v (JsonObject obj)) = Just v
158+
setGetIdentity k v [] = lookupObjConsRefl k v []
159+
setGetIdentity k v ((k', v') :: rest) with (decEq k k')
160+
_ | Yes _ = lookupObjConsRefl k v rest
161+
_ | No nkkn = trans (lookupObjConsSkip k k' nkkn v' (updateObj k v rest))
162+
(setGetIdentity k v rest)
163+
164+
||| DISCHARGED: setting a different key preserves the other key's
165+
||| value: `Not (k1 = k2) -> get k2 (set k1 v (JsonObject obj)) =
166+
||| get k2 (JsonObject obj)`.
167+
|||
168+
||| Note: type strengthened to require `obj : List (String, JsonValue)`
169+
||| (i.e. the body of the `JsonObject`) so that `set`/`get` both fire
170+
||| their `JsonObject` arms unconditionally. The old `obj : JsonValue`
171+
||| form was vacuously trivial for non-`JsonObject` values; the new
172+
||| form carries the actual key-preservation invariant.
173+
|||
174+
||| Proof: induction on `obj`. Empty list reduces both sides to
175+
||| `Nothing` via the `k1 ≠ k2 → k2 ≠ k1` direction. Non-empty list
176+
||| splits on `decEq k1 k'`: in either branch, further split on
177+
||| `decEq k2 k'` handles the matching/non-matching cases via
178+
||| `lookupObjConsSkip`.
179+
public export
180+
setPreservesOther : (k1, k2 : String) -> Not (k1 = k2) ->
181+
(v : JsonValue) -> (obj : List (String, JsonValue)) ->
182+
get k2 (set k1 v (JsonObject obj)) = get k2 (JsonObject obj)
183+
setPreservesOther k1 k2 nkk v [] = lookupObjConsSkip k2 k1 (\eq => nkk (sym eq)) v []
184+
setPreservesOther k1 k2 nkk v ((k', v') :: rest) with (decEq k1 k')
185+
_ | Yes prfEq = trans (lookupObjConsSkip k2 k1 (\eq => nkk (sym eq)) v rest)
186+
(sym (lookupObjConsSkip k2 k'
187+
(\eq => nkk (trans prfEq (sym eq))) v' rest))
188+
_ | No _ = lookupObjConsCong k2 k' v' (setPreservesOther k1 k2 nkk v rest)
189+
190+
||| DISCHARGED: after `set k v obj`, `hasKey k` returns `True`
191+
||| whenever `obj` is a `JsonObject`. By
192+
||| `hasKey k j = isJust (get k j)` (`SafeJson.idr`) and
193+
||| `setGetIdentity`, the goal reduces to `isJust (Just v) = True`,
194+
||| which is `Refl`.
195+
|||
196+
||| Note: type strengthened to take a concrete object body
197+
||| `obj : List (String, JsonValue)` rather than the abstract
198+
||| `JsonValue`. The original `isObject obj = True` hypothesis added
199+
||| no useful information once `set`'s `JsonObject` arm fires.
200+
public export
201+
setHasKey : (k : String) -> (v : JsonValue) -> (obj : List (String, JsonValue)) ->
202+
hasKey k (set k v (JsonObject obj)) = True
203+
setHasKey k v obj = cong isJust (setGetIdentity k v obj)
204+
205+
||| DISCHARGED: after `remove k (JsonObject obj)`, `hasKey k` returns
206+
||| `False`. By `hasKey k j = isJust (get k j)`, reduces to
207+
||| `isJust (lookupObj k (removeObj k obj)) = False`. By
208+
||| `removeLookupNothing` below: `lookupObj k (removeObj k obj) =
209+
||| Nothing`, so `isJust Nothing = False`.
210+
|||
211+
||| Type strengthened to `obj : List (String, JsonValue)` (same
212+
||| reasoning as `setHasKey`).
213+
public export
214+
removeNotHasKey : (k : String) -> (obj : List (String, JsonValue)) ->
215+
hasKey k (remove k (JsonObject obj)) = False
216+
removeNotHasKey k obj = cong isJust (removeLookupNothing k obj)
161217

162218
--------------------------------------------------------------------------------
163219
-- Array Access Properties

0 commit comments

Comments
 (0)