From f073186af51fc52993a0d9f059f3189a8a81672c Mon Sep 17 00:00:00 2001
From: Valentin Perrelle <valentin.perrelle@cea.fr>
Date: Wed, 22 Sep 2021 15:13:44 +0200
Subject: [PATCH] [Eva] export computation state through server API

---
 .../generated/plugins/eva/general/index.ts    | 50 ++++++++++++++++--
 ivette/src/frama-c/kernel/Globals.tsx         |  4 +-
 ivette/src/frama-c/plugins/eva/Summary.tsx    | 46 +++++++++++------
 ivette/src/frama-c/plugins/eva/summary.css    |  9 ++++
 src/plugins/value/api/general_requests.ml     | 47 ++++++++++++-----
 src/plugins/value/api/values_request.ml       |  3 +-
 src/plugins/value/engine/analysis.ml          | 51 ++++++++++++++++---
 src/plugins/value/engine/analysis.mli         | 15 ++++--
 8 files changed, 177 insertions(+), 48 deletions(-)

diff --git a/ivette/src/frama-c/api/generated/plugins/eva/general/index.ts b/ivette/src/frama-c/api/generated/plugins/eva/general/index.ts
index 05370709479..86688a25b9d 100644
--- a/ivette/src/frama-c/api/generated/plugins/eva/general/index.ts
+++ b/ivette/src/frama-c/api/generated/plugins/eva/general/index.ts
@@ -54,15 +54,53 @@ import { jTagSafe } from 'frama-c/api/kernel/data';
 //@ts-ignore
 import { tag } from 'frama-c/api/kernel/data';
 
-const isComputed_internal: Server.GetRequest<null,boolean> = {
+/** State of the computation of Eva Analysis. */
+export type computationStateType = "not_computed" | "computing" | "computed";
+
+/** Loose decoder for `computationStateType` */
+export const jComputationStateType: Json.Loose<computationStateType> =
+  Json.jUnion<"not_computed" | "computing" | "computed">(
+    Json.jTag("not_computed"),
+    Json.jTag("computing"),
+    Json.jTag("computed"),
+  );
+
+/** Safe decoder for `computationStateType` */
+export const jComputationStateTypeSafe: Json.Safe<computationStateType> =
+  Json.jFail(jComputationStateType,'ComputationStateType expected');
+
+/** Natural order for `computationStateType` */
+export const byComputationStateType: Compare.Order<computationStateType> =
+  Compare.structural;
+
+/** Signal for state [`computationState`](#computationstate)  */
+export const signalComputationState: Server.Signal = {
+  name: 'plugins.eva.general.signalComputationState',
+};
+
+const getComputationState_internal: Server.GetRequest<
+  null,
+  computationStateType
+  > = {
   kind: Server.RqKind.GET,
-  name:   'plugins.eva.general.isComputed',
+  name:   'plugins.eva.general.getComputationState',
   input:  Json.jNull,
-  output: Json.jBoolean,
+  output: jComputationStateType,
   signals: [],
 };
-/** True if the Eva analysis has been done */
-export const isComputed: Server.GetRequest<null,boolean>= isComputed_internal;
+/** Getter for state [`computationState`](#computationstate)  */
+export const getComputationState: Server.GetRequest<
+  null,
+  computationStateType
+  >= getComputationState_internal;
+
+const computationState_internal: State.Value<computationStateType> = {
+  name: 'plugins.eva.general.computationState',
+  signal: signalComputationState,
+  getter: getComputationState,
+};
+/** The current computation state of the analysis. */
+export const computationState: State.Value<computationStateType> = computationState_internal;
 
 const getCallers_internal: Server.GetRequest<
   Json.key<'#fct'>,
@@ -286,6 +324,7 @@ const alarmCategoryTags_internal: Server.GetRequest<null,tag[]> = {
   name:   'plugins.eva.general.alarmCategoryTags',
   input:  Json.jNull,
   output: Json.jList(jTag),
+  signals: [],
 };
 /** Registered tags for the above type. */
 export const alarmCategoryTags: Server.GetRequest<null,tag[]>= alarmCategoryTags_internal;
@@ -435,6 +474,7 @@ const getStats_internal: Server.GetRequest<null,statistics> = {
   name:   'plugins.eva.general.getStats',
   input:  Json.jNull,
   output: jStatistics,
+  signals: [],
 };
 /** Getter for state [`stats`](#stats)  */
 export const getStats: Server.GetRequest<null,statistics>= getStats_internal;
diff --git a/ivette/src/frama-c/kernel/Globals.tsx b/ivette/src/frama-c/kernel/Globals.tsx
index 7705b8ee816..6c94aedd859 100644
--- a/ivette/src/frama-c/kernel/Globals.tsx
+++ b/ivette/src/frama-c/kernel/Globals.tsx
@@ -33,7 +33,7 @@ import * as Ivette from 'ivette';
 
 import * as States from 'frama-c/states';
 import { functions, functionsData } from 'frama-c/api/kernel/ast';
-import { isComputed } from 'frama-c/api/plugins/eva/general';
+import { computationState } from 'frama-c/api/plugins/eva/general';
 
 // --------------------------------------------------------------------------
 // --- Global Search Hints
@@ -115,7 +115,7 @@ export default () => {
     useFlipSettings('ivette.globals.evaonly', false);
   const multipleSelection = selection?.multiple;
   const multipleSelectionActive = multipleSelection?.allSelections.length > 0;
-  const evaComputed = States.useRequest(isComputed, null);
+  const evaComputed = States.useSyncValue(computationState) === 'computed';
 
   function isSelected(fct: functionsData) {
     return multipleSelection?.allSelections.some(
diff --git a/ivette/src/frama-c/plugins/eva/Summary.tsx b/ivette/src/frama-c/plugins/eva/Summary.tsx
index 9aea5f55144..f9beeefda29 100644
--- a/ivette/src/frama-c/plugins/eva/Summary.tsx
+++ b/ivette/src/frama-c/plugins/eva/Summary.tsx
@@ -27,6 +27,8 @@ import { Vfill } from 'dome/layout/boxes';
 import * as States from 'frama-c/states';
 import * as Eva from 'frama-c/api/plugins/eva/general';
 
+import { LED } from 'dome/controls/buttons';
+
 import './summary.css';
 
 function percent(reachable: number, total: number): string {
@@ -200,22 +202,36 @@ function Statuses(data: Eva.statistics): JSX.Element {
 export function EvaSummary(): JSX.Element {
   const alarmCategories = States.useTags(Eva.alarmCategoryTags);
   const data = States.useSyncValue(Eva.stats);
+  const state = States.useSyncValue(Eva.computationState);
 
-  return (data && alarmCategories ? (
-    <div className="eva-summary">
-      <h1>Analysis Summary</h1>
-      <h2>Coverage</h2>
-      {CoverageTable(data)}
-      <h2>Errors</h2>
-      {Errors(data)}
-      <h2>Alarms</h2>
-      {Alarms(data, alarmCategories)}
-      <h2>Statuses</h2>
-      {Statuses(data)}
-    </div>
-  ) :
-    <></>
-  );
+  if (state === 'not_computed')
+    return (
+      <div className="eva-summary-status">
+        Eva analysis not run yet.
+      </div>);
+
+  if (state === 'computing')
+    return (
+      <div className="eva-summary-status">
+        Eva analysis running.
+        <LED status="active" blink />
+      </div>);
+
+  if (state === 'computed' && data && alarmCategories)
+    return (
+      <div className="eva-summary computed">
+        <h1>Analysis Summary</h1>
+        <h2>Coverage</h2>
+        {CoverageTable(data)}
+        <h2>Errors</h2>
+        {Errors(data)}
+        <h2>Alarms</h2>
+        {Alarms(data, alarmCategories)}
+        <h2>Statuses</h2>
+        {Statuses(data)}
+      </div>);
+
+  return (<></>);
 }
 
 function EvaSummaryComponent(): JSX.Element {
diff --git a/ivette/src/frama-c/plugins/eva/summary.css b/ivette/src/frama-c/plugins/eva/summary.css
index 82aca9ded08..1b67fd4d29d 100644
--- a/ivette/src/frama-c/plugins/eva/summary.css
+++ b/ivette/src/frama-c/plugins/eva/summary.css
@@ -1,3 +1,12 @@
+.eva-summary-status {
+    margin: 6px;
+}
+
+.eva-summary-status .dome-xButton-led {
+    display: inline-block;
+    vertical-align: baseline;
+}
+
 .eva-summary {
     padding: 10px;
     background-color: white;
diff --git a/src/plugins/value/api/general_requests.ml b/src/plugins/value/api/general_requests.ml
index 78a7ba5c09f..789a5b7494a 100644
--- a/src/plugins/value/api/general_requests.ml
+++ b/src/plugins/value/api/general_requests.ml
@@ -31,11 +31,31 @@ let package =
     ~readme:"eva.md"
     ()
 
-let () = Request.register ~package
-    ~kind:`GET ~name:"isComputed"
-    ~descr:(Markdown.plain "True if the Eva analysis has been done")
-    ~input:(module Data.Junit) ~output:(module Data.Jbool)
-    Db.Value.is_computed
+module ComputationState = struct
+  open Analysis
+  type t = computation_state
+  let jtype =
+    Data.declare ~package
+      ~name:"computationStateType"
+      ~descr:(Markdown.plain "State of the computation of Eva Analysis.")
+      Package.(Junion [
+          Jtag "not_computed" ;
+          Jtag "computing" ;
+          Jtag "computed"])
+  let to_json = function
+    | Analysis.NotComputed -> `String "not_computed"
+    | Computing -> `String "computing"
+    | Computed -> `String "computed"
+end
+
+let _computation_signal =
+  States.register_value ~package
+    ~name:"computationState"
+    ~descr:(Markdown.plain "The current computation state of the analysis.")
+    ~output:(module ComputationState)
+    ~get:Analysis.current_computation_state
+    ~add_hook:Analysis.register_computation_hook
+    ()
 
 let is_computed kf =
   Db.Value.is_computed () &&
@@ -380,15 +400,14 @@ module Statistics = struct
           "count", `Int c ]) (AlarmsStats.bindings x.alarms))]
 end
 
-let () =
-  let signal = States.register_value ~package
-      ~name:"stats"
-      ~descr:(Markdown.plain "Statistics about the last Eva analysis")
-      ~output:(module Statistics)
-      ~get:Value_results.compute_stats
-      ()
-  in
-  Analysis.register_computed_hook (fun () -> Request.emit signal)
+let _computed_signal =
+  States.register_value ~package
+    ~name:"stats"
+    ~descr:(Markdown.plain "Statistics about the last Eva analysis")
+    ~output:(module Statistics)
+    ~get:Value_results.compute_stats
+    ~add_hook:(Analysis.register_computation_hook ~on:Computed)
+    ()
 
 
 (**************************************************************************)
diff --git a/src/plugins/value/api/values_request.ml b/src/plugins/value/api/values_request.ml
index 5255b073cb6..1aae152c818 100644
--- a/src/plugins/value/api/values_request.ml
+++ b/src/plugins/value/api/values_request.ml
@@ -60,7 +60,8 @@ type domain = {
 let signal = Request.signal ~package ~name:"changed"
     ~descr:(Md.plain "Emitted when EVA results has changed")
 
-let () = Analysis.register_computed_hook (fun () -> Request.emit signal)
+let () = Analysis.register_computation_hook ~on:Computed
+    (fun _ -> Request.emit signal)
 
 (* -------------------------------------------------------------------------- *)
 (* --- Marker Utilities                                                   --- *)
diff --git a/src/plugins/value/engine/analysis.ml b/src/plugins/value/engine/analysis.ml
index 278bc97910d..7fde8c2e3dd 100644
--- a/src/plugins/value/engine/analysis.ml
+++ b/src/plugins/value/engine/analysis.ml
@@ -114,6 +114,35 @@ module Default =
     : Analyzer)
 
 
+(* Current state of the analysis *)
+type computation_state = NotComputed | Computing | Computed
+
+module ComputationState =
+struct
+  let to_string = function
+    | NotComputed -> "NotComputed"
+    | Computing -> "Computing"
+    | Computed -> "Computed"
+
+  module Prototype =
+  struct
+    include Datatype.Serializable_undefined
+    type t = computation_state
+    let name = "Eva.Analysis.ComputationState"
+    let pretty fmt s = Format.pp_print_string fmt (to_string s)
+    let reprs = [ NotComputed ; Computing ; Computed ]
+    let dependencies = [ Db.Value.self ]
+    let default () = NotComputed
+  end
+
+  module Datatype' = Datatype.Make (Prototype)
+  module Hook = Hook.Build (Prototype)
+  include (State_builder.Ref (Datatype') (Prototype))
+
+  let set s = set s; Hook.apply s
+  let () = add_hook_on_update (fun r -> Hook.apply !r)
+end
+
 (* Reference to the current configuration (built by Abstractions.configure from
    the parameters of Eva regarding the abstractions used in the analysis) and
    the current Analyzer module. *)
@@ -127,22 +156,27 @@ let current_analyzer () = (module (val (snd !ref_analyzer)): S)
    Useful for the GUI parts that depend on it. *)
 module Analyzer_Hook = Hook.Build (struct type t = (module S) end)
 
-(* Set of hooks called whenever the current Analyzer is computed.
-   Useful for the GUI parts that depend on it. *)
-module Computed_Hook = Hook.Build (struct type t = unit end)
-
 (* Register a new hook. *)
 let register_hook = Analyzer_Hook.extend
 
-(* Register a new computed hook. *)
-let register_computed_hook = Computed_Hook.extend
-
 (* Sets the current Analyzer module for a given configuration.
    Calls the hooks above. *)
 let set_current_analyzer config (analyzer: (module Analyzer)) =
   Analyzer_Hook.apply (module (val analyzer): S);
   ref_analyzer := (config, analyzer)
 
+(* Get the current computation state. *)
+let current_computation_state () =
+  ComputationState.get ()
+
+(* Register a hook on current computation state *)
+let register_computation_hook ?on f =
+  let f' = match on with
+    | None -> f
+    | Some s -> fun s' -> if s = s' then f s
+  in
+  ComputationState.Hook.extend f'
+
 let cvalue_initial_state () =
   let module A = (val snd !ref_analyzer) in
   let _, lib_entry = Globals.entry_point () in
@@ -177,9 +211,10 @@ let force_compute () =
     Eva_audit.check_configuration (Kernel.AuditCheck.get ());
   let kf, lib_entry = Globals.entry_point () in
   reset_analyzer ();
+  ComputationState.set Computing; (* The new analyzer can be accesed through hooks *)
   let module Analyzer = (val snd !ref_analyzer) in
   Analyzer.compute_from_entry_point ~lib_entry kf ;
-  Computed_Hook.apply ()
+  ComputationState.set Computed
 
 (* Resets the Analyzer when the current project is changed. *)
 let () =
diff --git a/src/plugins/value/engine/analysis.mli b/src/plugins/value/engine/analysis.mli
index 732d307bd55..055917d6cd4 100644
--- a/src/plugins/value/engine/analysis.mli
+++ b/src/plugins/value/engine/analysis.mli
@@ -70,9 +70,18 @@ val register_hook: ((module S) -> unit) -> unit
     is changed. This happens when a new analysis is run with different
     abstractions than before, or when the current project is changed. *)
 
-val register_computed_hook: (unit -> unit) -> unit
-(** Registers a hook that will be called each time the [current] analyzer
-    has been computed. *)
+type computation_state = NotComputed | Computing | Computed
+(** Computation state of the analysis. *)
+
+val current_computation_state : unit -> computation_state
+(** Get the current computation state of the analysis, updated by
+    [force_compute] and states updates. *)
+
+val register_computation_hook: ?on:computation_state ->
+  (computation_state -> unit) -> unit
+(** Registers a hook that will be called each time the analysis starts or
+    finishes. If [on] is given, the hook will only be called when the
+    analysis switches to this specific state. *)
 
 val force_compute : unit -> unit
 (** Perform a full analysis, starting from the [main] function. *)
-- 
GitLab