From fd9962c44f99a01ec5881b61bd127b485aff4cd8 Mon Sep 17 00:00:00 2001 From: Andre Maroneze <andre.maroneze@cea.fr> Date: Wed, 2 Dec 2020 14:34:49 +0100 Subject: [PATCH] [Json] add JSON cache and merge functions --- src/kernel_internals/runtime/special_hooks.ml | 9 ++ src/libraries/utils/json.mli | 64 +++++++++++++ src/libraries/utils/json.mll | 90 +++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/kernel_internals/runtime/special_hooks.ml b/src/kernel_internals/runtime/special_hooks.ml index 9161ca6e6d6..de0cf6026b6 100644 --- a/src/kernel_internals/runtime/special_hooks.ml +++ b/src/kernel_internals/runtime/special_hooks.ml @@ -221,6 +221,15 @@ let run_list_all_plugin_options () = else Cmdline.nop let () = Cmdline.run_after_exiting_stage run_list_all_plugin_options +(* Write JSON files to disk if required *) +let flush_json_files () = + let written = Json.flush_cache () in + List.iter (fun fp -> + Kernel.feedback "Wrote: %a" Filepath.Normalized.pretty fp) + written + +let () = Cmdline.at_normal_exit (fun () -> flush_json_files ()) + (* Local Variables: compile-command: "make -C ../../.." diff --git a/src/libraries/utils/json.mli b/src/libraries/utils/json.mli index f374ebf853b..80ffaf64513 100644 --- a/src/libraries/utils/json.mli +++ b/src/libraries/utils/json.mli @@ -125,3 +125,67 @@ val field : string -> t -> t [Null] is considered an empty object. @raise Not_found if the field is absent from the object. @raise Invalid_argument if the object is not an [Assoc] or [Null] object. *) + +(** The functions below read and write to JSON files asynchronously; they are + intended to be called at different times during execution. + To avoid reopening, re-reading and rewriting the same file several times, + they instead operate as following: + - Read the file on first use, and store it in memory; + - Perform merge operations using the in-memory representations; + - Flush the final form to disk before quitting Frama-C. + The latter is done via function [json_flush_cache] below, which is setup + to run with an [at_exit] trigger. + + Note: no other functions should modify the contents of [path]; any + modifications will be overwritten when the cache is flushed. + + @since Frama-C+dev +*) + +(** Exception raised by the functions below when incompatible types are + merged. *) +exception CannotMerge of string + +(** + [merge_object path json_obj] recursively merges the object [json_obj] into the + JSON file [path]. If [path] does not exist, it is created. + Merge consists in combining values with the same key, e.g. if [path] + already contains an object {"kernel": {"options": ["a"]}}, and + [json_obj] is {"kernel": {"options": ["b"]}}, the result will be + {"kernel": {"options": ["a", "b"]}}. Cannot merge heterogeneous + objects, i.e. in the previous example, if "options" were associated + with a string in [path], trying to merge an array into it would + raise [JsonCannotMerge]. + The merged object is updated in the memory cache. + + @raise [CannotMerge] if the objects have conflicting types for + the same keys, or if the root JSON element is not an object. + @since Frama-C+dev +*) +val merge_object : Filepath.Normalized.t -> Yojson.Basic.t -> unit + +(** + [merge_list path json_array] merges the array [json_array] into the + JSON file [path]. See [merge_object] for more details. + Unlike objects, arrays are merged by simply concatenating their list + of elements. + + @raise [CannotMerge] if the root JSON element is not an array. + @since Frama-C+dev +*) +val merge_array : Filepath.Normalized.t -> Yojson.Basic.t -> unit + +(** + [from_file path] opens [path] and stores its JSON object in + a memory cache, to be used by the other related functions. + @since Frama-C+dev +*) +val from_file: Filepath.Normalized.t -> Yojson.Basic.t + +(** + Flushes the JSON objects in the cache. Returns the names of the written + files. + + @since Frama-C+dev +*) +val flush_cache : unit -> Filepath.Normalized.t list diff --git a/src/libraries/utils/json.mll b/src/libraries/utils/json.mll index f17c82ac25d..7368c65f3e4 100644 --- a/src/libraries/utils/json.mll +++ b/src/libraries/utils/json.mll @@ -296,4 +296,94 @@ let of_list xs = `List xs let of_array xs = `List (Array.to_list xs) let of_fields m = `Assoc m + +(* JSON file cache and merging *) + +exception CannotMerge of string + +(* Table of filepaths to JSON arrays, to be written at the end of a run *) +let json_tbl = Hashtbl.create 3 + +let rec merge_assoc_lists la lb = + let cmp = fun (k1, _) (k2, _) -> String.compare k1 k2 in + let rec aux acc l1 l2 = + match l1, l2 with + | [], [] -> acc + | [], l | l, [] -> List.rev_append l acc + | e1 :: r1, e2 :: r2 -> + let c = cmp e1 e2 in + if c < 0 then aux (e1 :: acc) r1 l2 + else if c > 0 then aux (e2 :: acc) l1 r2 + else (* c = 0 *) + let (k1, v1) = e1 in + let (_, v2) = e2 in + match v1, v2 with + | `Assoc a1, `Assoc a2 -> + let v = `Assoc (merge_assoc_lists a1 a2) in + aux ((k1, v) :: acc) r1 r2 + | `List l1, `List l2 -> + let v = `List (l1 @ l2) in + aux ((k1, v) :: acc) r1 r2 + | o1, o2 -> + let pp = Yojson.Basic.pretty_to_string in + raise (CannotMerge + ("cannot merge heterogeneous objects '" + ^ pp o1 ^ "' and '" ^ pp o2 ^ "'")) + in + let r = aux [] (List.sort cmp la) (List.sort cmp lb) in + List.rev r + +let merge_object path json_root_obj = + let open Yojson.Basic.Util in + let existing_root_obj = + try + match Hashtbl.find json_tbl path with + | `Assoc _ as root -> root + | _ -> raise (CannotMerge "JSON root element should be an object") + with Not_found -> + `Assoc [] + in + let existing_assoc = existing_root_obj |> to_assoc in + let new_assoc = json_root_obj |> to_assoc in + let merged = merge_assoc_lists existing_assoc new_assoc in + let merged_obj = `Assoc merged in + Hashtbl.replace json_tbl path merged_obj + +let merge_array path json_root_array = + let open Yojson.Basic.Util in + let existing_root_array = + try + match Hashtbl.find json_tbl path with + | `List _ as root -> root + | _ -> raise (CannotMerge "JSON root element should be an array") + with Not_found -> + `List [] + in + let existing_list = existing_root_array |> to_list in + let new_list = json_root_array |> to_list in + let merged_list = `List (existing_list @ new_list) in + Hashtbl.replace json_tbl path merged_list + +let flush_cache () = + let written = + Hashtbl.fold (fun (path : Filepath.Normalized.t) json acc -> + let oc = open_out (path:>string) in + Yojson.Basic.pretty_to_channel oc json; + output_char oc '\n'; (* ensure JSON file terminates with a newline *) + path :: acc + ) json_tbl [] + in + Hashtbl.clear json_tbl; + List.rev written + +let json_cache = Hashtbl.create 3 + +let from_file (path : Filepath.Normalized.t) = + try + Hashtbl.find json_cache path + with Not_found -> + let json = Yojson.Basic.from_file (path:>string) in + Hashtbl.replace json_cache path json; + json + } -- GitLab