From e6e0cdc8cf07cc83daa4801840e29a26e36e2ee0 Mon Sep 17 00:00:00 2001
From: Valentin Perrelle <valentin.perrelle@cea.fr>
Date: Fri, 31 Mar 2023 16:17:43 +0200
Subject: [PATCH] [Ivette] Displays the focused callstack

---
 .../frama-c/plugins/callgraph/api/index.ts    | 36 +++++------
 .../plugins/callgraph/graph-style.json        |  8 +++
 .../src/frama-c/plugins/callgraph/index.tsx   | 60 +++++++++++++++----
 ivette/src/frama-c/plugins/eva/valuetable.tsx |  2 +-
 src/plugins/callgraph/callgraph_api.ml        |  5 +-
 src/plugins/callgraph/cg.ml                   |  6 +-
 src/plugins/callgraph/requests.ml             | 50 +++++++++-------
 7 files changed, 110 insertions(+), 57 deletions(-)

diff --git a/ivette/src/frama-c/plugins/callgraph/api/index.ts b/ivette/src/frama-c/plugins/callgraph/api/index.ts
index af0fd6b1f54..e231a66fbba 100644
--- a/ivette/src/frama-c/plugins/callgraph/api/index.ts
+++ b/ivette/src/frama-c/plugins/callgraph/api/index.ts
@@ -55,34 +55,38 @@ import { tag } from 'frama-c/kernel/api/data';
 import { tagDefault } from 'frama-c/kernel/api/data';
 
 export interface vertex {
-  /** kf */
+  /** The function represented by the node */
   kf: fct;
-  /** is_root */
+  /** whether this node is the root of a service */
   is_root: boolean;
+  /** the root of this node's service */
+  root: fct;
 }
 
 /** Decoder for `vertex` */
 export const jVertex: Json.Decoder<vertex> =
-  Json.jObject({ kf: jFct, is_root: Json.jBoolean,});
+  Json.jObject({ kf: jFct, is_root: Json.jBoolean, root: jFct,});
 
 /** Natural order for `vertex` */
 export const byVertex: Compare.Order<vertex> =
   Compare.byFields
-    <{ kf: fct, is_root: boolean }>({
+    <{ kf: fct, is_root: boolean, root: fct }>({
     kf: byFct,
     is_root: Compare.boolean,
+    root: byFct,
   });
 
 /** Default value for `vertex` */
-export const vertexDefault: vertex = { kf: fctDefault, is_root: false };
+export const vertexDefault: vertex =
+  { kf: fctDefault, is_root: false, root: fctDefault };
 
-/** Whether The nature of a node */
+/** Whether the call goes through services or not */
 export enum edgeKind {
-  /**  */
+  /** a call between two services */
   inter_services = 'inter_services',
-  /**  */
+  /** a call inside a service */
   inter_functions = 'inter_functions',
-  /**  */
+  /** both cases above */
   both = 'both',
 }
 
@@ -137,30 +141,22 @@ export interface graph {
   vertices: vertex[];
   /** edges */
   edges: edge[];
-  /** entry_point */
-  entry_point?: fct;
 }
 
 /** Decoder for `graph` */
 export const jGraph: Json.Decoder<graph> =
-  Json.jObject({
-    vertices: Json.jArray(jVertex),
-    edges: Json.jArray(jEdge),
-    entry_point: Json.jOption(jFct),
-  });
+  Json.jObject({ vertices: Json.jArray(jVertex), edges: Json.jArray(jEdge),});
 
 /** Natural order for `graph` */
 export const byGraph: Compare.Order<graph> =
   Compare.byFields
-    <{ vertices: vertex[], edges: edge[], entry_point?: fct }>({
+    <{ vertices: vertex[], edges: edge[] }>({
     vertices: Compare.array(byVertex),
     edges: Compare.array(byEdge),
-    entry_point: Compare.defined(byFct),
   });
 
 /** Default value for `graph` */
-export const graphDefault: graph =
-  { vertices: [], edges: [], entry_point: undefined };
+export const graphDefault: graph = { vertices: [], edges: [] };
 
 /** Signal for state [`callgraph`](#callgraph)  */
 export const signalCallgraph: Server.Signal = {
diff --git a/ivette/src/frama-c/plugins/callgraph/graph-style.json b/ivette/src/frama-c/plugins/callgraph/graph-style.json
index 6a10b6d0882..e1aff869e93 100644
--- a/ivette/src/frama-c/plugins/callgraph/graph-style.json
+++ b/ivette/src/frama-c/plugins/callgraph/graph-style.json
@@ -30,5 +30,13 @@
       "target-arrow-color": "#888",
       "arrow-scale": 2.0
     }
+  },
+  {
+    "selector": ".callstack-selected",
+    "style": {
+      "overlay-color": "#8bf",
+      "overlay-padding": "8px",
+      "overlay-opacity": 0.4
+    }
   }
 ]
diff --git a/ivette/src/frama-c/plugins/callgraph/index.tsx b/ivette/src/frama-c/plugins/callgraph/index.tsx
index e5c8ccfc9d9..73cddb54911 100644
--- a/ivette/src/frama-c/plugins/callgraph/index.tsx
+++ b/ivette/src/frama-c/plugins/callgraph/index.tsx
@@ -20,22 +20,27 @@
 /*                                                                          */
 /* ************************************************************************ */
 
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import _ from 'lodash';
 import * as Ivette from 'ivette';
 import * as Server from 'frama-c/server';
 
-import * as API from './api';
+import * as AstAPI from 'frama-c/kernel/api/ast';
+import * as CgAPI from './api';
+import * as ValuesAPI from 'frama-c/plugins/eva/api/values';
 
-import Cytoscape from 'cytoscape';
+import Cy from 'cytoscape';
 import CytoscapeComponent from 'react-cytoscapejs';
 import 'frama-c/plugins/dive/cytoscape_libs';
 import 'cytoscape-panzoom/cytoscape.js-panzoom.css';
-
 import style from './graph-style.json';
-import { useSyncValue } from 'frama-c/states';
+
+import { useGlobalState } from 'dome/data/states';
+import { useRequest, useSyncValue } from 'frama-c/states';
 
 import gearsIcon from 'frama-c/plugins/eva/images/gears.svg';
+import { CallstackState } from 'frama-c/plugins/eva/valuetable';
+
 import './callgraph.css';
 
 
@@ -73,27 +78,56 @@ function getWidth(node: any): string {
 // --- Graph
 // --------------------------------------------------------------------------
 
-function convertGraph(graph: API.graph): object[] {
+function edgeId(source: AstAPI.fct, target: AstAPI.fct): string {
+  return `${source}-${target}`;
+}
+
+function convertGraph(graph: CgAPI.graph): object[] {
   const elements = [];
   for (const v of graph.vertices) {
-    elements.push({data: {...v, id:v.kf}});
+    elements.push({data: {...v, id: v.kf}});
   }
   for (const e of graph.edges) {
-    elements.push({data: {source: e.src, target: e.dst}});
+    const id = edgeId(e.src, e.dst);
+    elements.push({data: {...e, id, source: e.src, target: e.dst}});
   }
-  console.log(elements);
   return elements;
 }
 
+type callstack = {
+  callee: AstAPI.fct,
+  caller?: AstAPI.fct,
+  stmt?: AstAPI.marker,
+  rank?: number
+}[]
+
+function selectCallstack(cy: Cy.Core, callstack: callstack | undefined): void {
+  const className = 'callstack-selected';
+  cy.$(`.${className}`).removeClass(className);
+  callstack?.forEach((call) => {
+    cy.$(`node[id='${call.callee}']`).addClass(className);
+    if (call.caller) {
+      const id = edgeId(call.caller, call.callee);
+      cy.$(`edge[id='${id}']`).addClass(className);
+    }
+  });
+}
 
 function Callgraph() : JSX.Element { 
-  const isComputed = useSyncValue(API.isComputed);
-  const graph = useSyncValue(API.callgraph);
-  const [cy, setCy] = useState<Cytoscape.Core>();
+  const isComputed = useSyncValue(CgAPI.isComputed);
+  const graph = useSyncValue(CgAPI.callgraph);
+  const [cy, setCy] = useState<Cy.Core>();
+  const [cs] = useGlobalState(CallstackState);
+  const callstack = useRequest(ValuesAPI.getCallstackInfo, cs);
+
   const layout = {name: 'cola', nodeSpacing: 32};
 
+  useEffect(() => {
+    cy && selectCallstack(cy, callstack);
+  }, [cy, callstack]);
+
   if (isComputed === false) {
-    Server.send(API.compute, null);
+    Server.send(CgAPI.compute, null);
     return (<img src={gearsIcon} className="callgraph-computing" />);
   }
   else if (graph !== undefined) {
diff --git a/ivette/src/frama-c/plugins/eva/valuetable.tsx b/ivette/src/frama-c/plugins/eva/valuetable.tsx
index 2b2490d51ad..95a83acbc70 100644
--- a/ivette/src/frama-c/plugins/eva/valuetable.tsx
+++ b/ivette/src/frama-c/plugins/eva/valuetable.tsx
@@ -972,7 +972,7 @@ function useEvaluationMode(props: EvaluationModeProps): void {
 /* -------------------------------------------------------------------------- */
 
 /* Table's state. It is global for when the user changes the view. */
-const CallstackState = new GlobalState<callstack>('Summary');
+export const CallstackState = new GlobalState<callstack>('Summary');
 const FunctionsManagerState = new GlobalState(new FunctionsManager());
 const FocusState = new GlobalState<Probe | undefined>(undefined);
 
diff --git a/src/plugins/callgraph/callgraph_api.ml b/src/plugins/callgraph/callgraph_api.ml
index a2d67c27747..8ae2e7c7fca 100644
--- a/src/plugins/callgraph/callgraph_api.ml
+++ b/src/plugins/callgraph/callgraph_api.ml
@@ -43,6 +43,9 @@ module type Graph = sig
   val is_computed: unit -> bool
   (** Is the graph already built? *)
 
+  val add_hook: (G.t -> unit) -> unit
+  (** Call registered hook each time the graph is computed *)
+
   val self: State.t
 
 end
@@ -60,8 +63,6 @@ module type Services = sig
 
   val entry_point: unit -> G.V.t option
   val is_root: Kernel_function.t -> bool
-
-  val add_hook: (G.t -> unit) -> unit
 end
 
 (*
diff --git a/src/plugins/callgraph/cg.ml b/src/plugins/callgraph/cg.ml
index 24c12d9a2ea..1d081686740 100644
--- a/src/plugins/callgraph/cg.ml
+++ b/src/plugins/callgraph/cg.ml
@@ -66,8 +66,11 @@ module State =
       let dependencies = [ Eva.Analysis.self; Globals.Functions.self ]
     end)
 
+module StateHook = Hook.Build (D)
+
 let self = State.self
 let is_computed () = State.is_computed ()
+let add_hook = StateHook.extend
 
 (** @return the list of functions which address is taken.*)
 let get_pointed_kfs =
@@ -101,7 +104,6 @@ let get_pointed_kfs =
     match !res with
     | None ->
       let l = compute () in
-      State.mark_as_computed ();
       res := Some l;
       l
     | Some l -> l
@@ -232,6 +234,8 @@ let compute () =
     semantic_compute g
   end else
     (if Eva.Analysis.is_computed () then semantic_compute else syntactic_compute) g;
+  State.mark_as_computed ();
+  StateHook.apply g;
   g
 
 let get () = State.memo compute
diff --git a/src/plugins/callgraph/requests.ml b/src/plugins/callgraph/requests.ml
index 42db7a20ad8..cdef7f503d1 100644
--- a/src/plugins/callgraph/requests.ml
+++ b/src/plugins/callgraph/requests.ml
@@ -22,8 +22,15 @@
 
 open Server
 
+module G = Services.G
+
+(* --- Package declaration --- *)
+
 let package = Package.package ~plugin:"callgraph" ~title:"Callgraph" ()
 
+
+(* --- Helper modules --- *)
+
 module Record () =
 struct
   module Record = Data.Record
@@ -31,14 +38,11 @@ struct
   let record : record Record.signature = Record.signature ()
   let field name ?(descr = name) =
     Record.field record ~name ~descr:(Markdown.plain descr)
-  let option name ?(descr = name) =
-    Record.option record ~name ~descr:(Markdown.plain descr)
   let publish ?descr name =
     let descr = Option.map Markdown.plain descr in
     Record.publish record ~package ~name ?descr
 end
 
-
 module Enum (X: sig type t end) =
 struct
   module Enum = Data.Enum
@@ -49,12 +53,19 @@ struct
     Request.dictionary ~package ~name ~descr:(Markdown.plain descr) dictionary
 end
 
+
+(* --- Types --- *)
+
 module Vertex =
 struct
   include Record ()
 
   let kf = field "kf" (module Kernel_ast.Function)
+      ~descr: "The function represented by the node"
   let is_root = field "is_root" Data.jbool
+      ~descr: "whether this node is the root of a service"
+  let root = field "root" (module Kernel_ast.Function)
+      ~descr: "the root of this node's service"
 
   include (val publish "vertex")
   type t = Cil_types.kernel_function Service_graph.vertex
@@ -63,6 +74,7 @@ struct
     default |>
     set kf v.node |>
     set is_root v.is_root |>
+    set root v.root.node |>
     to_json
 
   let of_json _js = Data.failure "Vertex.of_json not implemented"
@@ -72,16 +84,17 @@ module EdgeKind =
 struct
   include Enum (struct type t = Service_graph.edge end)
 
-  let inter_services =  tag "inter_services" ""
-  let inter_functions = tag "inter_functions" ""
-  let both =            tag "both" ""
+  let inter_services =  tag "inter_services" "a call between two services"
+  let inter_functions = tag "inter_functions" "a call inside a service"
+  let both =            tag "both" "both cases above"
 
   let lookup = function
     | Service_graph.Inter_services -> inter_services
     | Inter_functions -> inter_functions
     | Both -> both
 
-  include (val publish lookup "edgeKind" "Whether The nature of a node")
+  include (val publish lookup
+              "edgeKind" "Whether the call goes through services or not")
 end
 
 module Edge =
@@ -93,13 +106,13 @@ struct
   let kind = field "kind" (module EdgeKind)
 
   include (val publish "edge")
-  type t = Services.G.E.t
+  type t = G.E.t
 
   let to_json (e : t) =
     default |>
-    set src (Services.G.E.src e).node |>
-    set dst (Services.G.E.dst e).node |>
-    set kind (Services.G.E.label e) |>
+    set src (G.E.src e).node |>
+    set dst (G.E.dst e).node |>
+    set kind (G.E.label e) |>
     to_json
 
   let of_json _js = Data.failure "Edge.of_json not implemented"
@@ -111,31 +124,28 @@ struct
 
   let vertices = field "vertices" (module Data.Jlist (Vertex))
   let edges = field "edges" (module Data.Jlist (Edge))
-  let entry_point = option "entry_point" (module Kernel_ast.Function)
 
   include (val publish "graph" ~descr:"The callgraph of the current project")
-  type t = Services.G.t
+  type t = G.t
 
   let get_vertices (g : t) =
-    Services.G.fold_vertex (fun v acc -> v :: acc ) g []
+    G.fold_vertex (fun v acc -> v :: acc ) g []
 
   let get_edges (g : t) =
-    Services.G.fold_edges_e (fun v acc -> v :: acc ) g []
-
-  let get_entry_point () =
-    Services.entry_point () |>
-    Option.map (fun v -> v.Service_graph.node)
+    G.fold_edges_e (fun v acc -> v :: acc ) g []
 
   let to_json (g : t) =
     default |>
     set vertices (get_vertices g) |>
     set edges (get_edges g) |>
-    set entry_point (get_entry_point ()) |>
     to_json
 
   let of_json _js = Data.failure "Graph.of_json not implemented"
 end
 
+
+(* --- Requests --- *)
+
 let _signal =
   States.register_value
     ~package ~name:"callgraph"
-- 
GitLab