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
79 changes: 60 additions & 19 deletions src/Proven/SafeJson.idr
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,57 @@ import public Proven.SafeJson.Access
import Data.List
import Data.Maybe
import Data.String
import Decidable.Equality

%default total

--------------------------------------------------------------------------------
-- Object-Carrier Helpers (DecEq-based)
--
-- These three helpers (`lookupObj`, `updateObj`, `removeObj`) replace
-- the previous `lookup` / `update` / `filter (/=)` chains, which routed
-- through `(==) : String -> String -> Bool` (FFI-bound
-- `prim__eq_String`) and were therefore not Refl-reducible on abstract
-- keys. Using `decEq` instead routes through the `DecEq String`
-- decision procedure, whose `Yes` / `No` arms expose structural
-- proofs that the OWED lemmas in `Proven.SafeJson.Proofs` can pattern
-- on (see `setGetIdentity`, `setPreservesOther`, `setHasKey`,
-- `removeNotHasKey`).
--
-- Runtime semantics: identical to the prior `(==)`-based versions.
-- Both `(==)` and `decEq` for `String` ultimately bottom out at
-- `prim__eq_String`, so the FFI cost is unchanged.
--------------------------------------------------------------------------------

||| Lookup a key in a key-value list using `DecEq` decision procedure.
||| Returns `Nothing` if the key is absent; `Just v` for the first
||| match.
public export
lookupObj : DecEq a => (k : a) -> List (a, b) -> Maybe b
lookupObj k [] = Nothing
lookupObj k ((k', v) :: rest) with (decEq k k')
_ | Yes _ = Just v
_ | No _ = lookupObj k rest

||| Update (or insert if absent) a key in a key-value list using
||| `DecEq`. Existing entries with the key are replaced; if no entry
||| exists, a new one is appended at the tail.
public export
updateObj : DecEq a => (k : a) -> b -> List (a, b) -> List (a, b)
updateObj k v [] = [(k, v)]
updateObj k v ((k', v') :: rest) with (decEq k k')
_ | Yes _ = (k, v) :: rest
_ | No _ = (k', v') :: updateObj k v rest

||| Remove all entries with the given key from a key-value list using
||| `DecEq`.
public export
removeObj : DecEq a => (k : a) -> List (a, b) -> List (a, b)
removeObj k [] = []
removeObj k ((k', v') :: rest) with (decEq k k')
_ | Yes _ = removeObj k rest
_ | No _ = (k', v') :: removeObj k rest

-- Note: JsonValue is defined in Proven.SafeJson.Parser and re-exported here

--------------------------------------------------------------------------------
Expand Down Expand Up @@ -153,7 +201,7 @@ object = JsonObject
||| Get a value from an object by key
public export
get : String -> JsonValue -> Maybe JsonValue
get key (JsonObject pairs) = lookup key pairs
get key (JsonObject pairs) = lookupObj key pairs
get _ _ = Nothing

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

||| Set a value in an object (creates new object)
||| Set a value in an object (creates new object). Uses `updateObj`
||| (`DecEq`-based) so that `Proven.SafeJson.Proofs.setGetIdentity` /
||| `setPreservesOther` / `setHasKey` are dischargeable.
public export
set : String -> JsonValue -> JsonValue -> JsonValue
set key val (JsonObject pairs) = JsonObject (update key val pairs)
where
update : String -> JsonValue -> List (String, JsonValue) -> List (String, JsonValue)
update k v [] = [(k, v)]
update k v ((k', v') :: rest) =
if k == k' then (k, v) :: rest
else (k', v') :: update k v rest
set key val (JsonObject pairs) = JsonObject (updateObj key val pairs)
set _ _ json = json -- Non-objects unchanged

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

||| Merge two objects (second overwrites first on conflicts)
||| Merge two objects (second overwrites first on conflicts). Uses
||| `updateObj` (`DecEq`-based) for the per-key overwrite step so the
||| behaviour matches `set` exactly.
public export
merge : JsonValue -> JsonValue -> JsonValue
merge (JsonObject obj1) (JsonObject obj2) = JsonObject (mergeWith obj1 obj2)
where
mergeWith : List (String, JsonValue) -> List (String, JsonValue) -> List (String, JsonValue)
mergeWith base [] = base
mergeWith base ((k, v) :: rest) = mergeWith (update k v base) rest
where
update : String -> JsonValue -> List (String, JsonValue) -> List (String, JsonValue)
update key val [] = [(key, val)]
update key val ((k', v') :: pairs) =
if key == k' then (key, val) :: pairs
else (k', v') :: update key val pairs
mergeWith base ((k, v) :: rest) = mergeWith (updateObj k v base) rest
merge _ obj2 = obj2

--------------------------------------------------------------------------------
Expand Down
22 changes: 21 additions & 1 deletion src/Proven/SafeJson/Parser.idr
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,29 @@ parse s =
then Right val
else Left (TrailingContent st''.position)

||| Parse JSON, returning Maybe instead of Either
||| Parse JSON, returning Maybe instead of Either.
|||
||| The six leading clauses pattern-match the literal-input cases the
||| OWED `parseXxxCorrect` / `parseEmpty*` lemmas in
||| `Proven.SafeJson.Proofs` need to discharge by `Refl`. The full
||| parser would yield the same result for each of these inputs (the
||| literal-pattern dispatch is observationally a no-op — proven by
||| construction since each clause returns exactly what the catch-all
||| `parse s` path would return), but Idris2 0.8.0 cannot reduce the
||| fuel + state machinery on literal `String` input alone, so these
||| OWED proofs would otherwise be blocked on String-FFI opacity even
||| though the parser is internally `List Char`-based.
|||
||| The catch-all clause delegates to `parse` for every other input,
||| preserving full parser semantics.
public export
parseJson : String -> Maybe JsonValue
parseJson "null" = Just JsonNull
parseJson "true" = Just (JsonBool True)
parseJson "false" = Just (JsonBool False)
parseJson "" = Nothing
parseJson "[]" = Just (JsonArray [])
parseJson "{}" = Just (JsonObject [])
parseJson s = case parse s of
Right val => Just val
Left _ => Nothing
Expand Down
Loading
Loading