From 3c5680cae9f18433152d072cf3dac3b7b34f617a Mon Sep 17 00:00:00 2001
From: Valentin Perrelle <valentin.perrelle@cea.fr>
Date: Fri, 31 Mar 2023 11:18:16 +0200
Subject: [PATCH] [Callgraph] Add an Ivette component to display service graphs

---
 .../frama-c/plugins/callgraph/api/index.ts    | 221 ++++++++++++++++++
 .../frama-c/plugins/callgraph/callgraph.css   |   6 +
 .../plugins/callgraph/graph-style.json        |  34 +++
 .../src/frama-c/plugins/callgraph/index.tsx   | 138 +++++++++++
 ivette/src/frama-c/plugins/callgraph/pkg.json |   3 +
 src/plugins/callgraph/callgraph_api.ml        |   1 +
 src/plugins/callgraph/requests.ml             | 169 ++++++++++++++
 src/plugins/callgraph/requests.mli            |  23 ++
 src/plugins/callgraph/services.ml             |   4 +
 9 files changed, 599 insertions(+)
 create mode 100644 ivette/src/frama-c/plugins/callgraph/api/index.ts
 create mode 100644 ivette/src/frama-c/plugins/callgraph/callgraph.css
 create mode 100644 ivette/src/frama-c/plugins/callgraph/graph-style.json
 create mode 100644 ivette/src/frama-c/plugins/callgraph/index.tsx
 create mode 100644 ivette/src/frama-c/plugins/callgraph/pkg.json
 create mode 100644 src/plugins/callgraph/requests.ml
 create mode 100644 src/plugins/callgraph/requests.mli

diff --git a/ivette/src/frama-c/plugins/callgraph/api/index.ts b/ivette/src/frama-c/plugins/callgraph/api/index.ts
new file mode 100644
index 00000000000..af0fd6b1f54
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/api/index.ts
@@ -0,0 +1,221 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+/* --- Generated Frama-C Server API --- */
+
+/**
+   Callgraph
+   @packageDocumentation
+   @module frama-c/plugins/callgraph/api
+*/
+
+//@ts-ignore
+import * as Json from 'dome/data/json';
+//@ts-ignore
+import * as Compare from 'dome/data/compare';
+//@ts-ignore
+import * as Server from 'frama-c/server';
+//@ts-ignore
+import * as State from 'frama-c/states';
+
+//@ts-ignore
+import { byFct } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { fct } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { fctDefault } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { jFct } from 'frama-c/kernel/api/ast';
+//@ts-ignore
+import { byTag } from 'frama-c/kernel/api/data';
+//@ts-ignore
+import { jTag } from 'frama-c/kernel/api/data';
+//@ts-ignore
+import { tag } from 'frama-c/kernel/api/data';
+//@ts-ignore
+import { tagDefault } from 'frama-c/kernel/api/data';
+
+export interface vertex {
+  /** kf */
+  kf: fct;
+  /** is_root */
+  is_root: boolean;
+}
+
+/** Decoder for `vertex` */
+export const jVertex: Json.Decoder<vertex> =
+  Json.jObject({ kf: jFct, is_root: Json.jBoolean,});
+
+/** Natural order for `vertex` */
+export const byVertex: Compare.Order<vertex> =
+  Compare.byFields
+    <{ kf: fct, is_root: boolean }>({
+    kf: byFct,
+    is_root: Compare.boolean,
+  });
+
+/** Default value for `vertex` */
+export const vertexDefault: vertex = { kf: fctDefault, is_root: false };
+
+/** Whether The nature of a node */
+export enum edgeKind {
+  /**  */
+  inter_services = 'inter_services',
+  /**  */
+  inter_functions = 'inter_functions',
+  /**  */
+  both = 'both',
+}
+
+/** Decoder for `edgeKind` */
+export const jEdgeKind: Json.Decoder<edgeKind> = Json.jEnum(edgeKind);
+
+/** Natural order for `edgeKind` */
+export const byEdgeKind: Compare.Order<edgeKind> = Compare.byEnum(edgeKind);
+
+/** Default value for `edgeKind` */
+export const edgeKindDefault: edgeKind = edgeKind.inter_services;
+
+const edgeKindTags_internal: Server.GetRequest<null,tag[]> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.callgraph.edgeKindTags',
+  input:  Json.jNull,
+  output: Json.jArray(jTag),
+  signals: [],
+};
+/** Registered tags for the above type. */
+export const edgeKindTags: Server.GetRequest<null,tag[]>= edgeKindTags_internal;
+
+export interface edge {
+  /** src */
+  src: fct;
+  /** dst */
+  dst: fct;
+  /** kind */
+  kind: edgeKind;
+}
+
+/** Decoder for `edge` */
+export const jEdge: Json.Decoder<edge> =
+  Json.jObject({ src: jFct, dst: jFct, kind: jEdgeKind,});
+
+/** Natural order for `edge` */
+export const byEdge: Compare.Order<edge> =
+  Compare.byFields
+    <{ src: fct, dst: fct, kind: edgeKind }>({
+    src: byFct,
+    dst: byFct,
+    kind: byEdgeKind,
+  });
+
+/** Default value for `edge` */
+export const edgeDefault: edge =
+  { src: fctDefault, dst: fctDefault, kind: edgeKindDefault };
+
+/** The callgraph of the current project */
+export interface graph {
+  /** vertices */
+  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),
+  });
+
+/** Natural order for `graph` */
+export const byGraph: Compare.Order<graph> =
+  Compare.byFields
+    <{ vertices: vertex[], edges: edge[], entry_point?: fct }>({
+    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 };
+
+/** Signal for state [`callgraph`](#callgraph)  */
+export const signalCallgraph: Server.Signal = {
+  name: 'plugins.callgraph.signalCallgraph',
+};
+
+const getCallgraph_internal: Server.GetRequest<null,graph | undefined> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.callgraph.getCallgraph',
+  input:  Json.jNull,
+  output: Json.jOption(jGraph),
+  signals: [],
+};
+/** Getter for state [`callgraph`](#callgraph)  */
+export const getCallgraph: Server.GetRequest<null,graph | undefined>= getCallgraph_internal;
+
+const callgraph_internal: State.Value<graph | undefined> = {
+  name: 'plugins.callgraph.callgraph',
+  signal: signalCallgraph,
+  getter: getCallgraph,
+};
+/** The current callgraph or an empty graph if it has not been computed yet */
+export const callgraph: State.Value<graph | undefined> = callgraph_internal;
+
+/** Signal for state [`isComputed`](#iscomputed)  */
+export const signalIsComputed: Server.Signal = {
+  name: 'plugins.callgraph.signalIsComputed',
+};
+
+const getIsComputed_internal: Server.GetRequest<null,boolean> = {
+  kind: Server.RqKind.GET,
+  name:   'plugins.callgraph.getIsComputed',
+  input:  Json.jNull,
+  output: Json.jBoolean,
+  signals: [],
+};
+/** Getter for state [`isComputed`](#iscomputed)  */
+export const getIsComputed: Server.GetRequest<null,boolean>= getIsComputed_internal;
+
+const isComputed_internal: State.Value<boolean> = {
+  name: 'plugins.callgraph.isComputed',
+  signal: signalIsComputed,
+  getter: getIsComputed,
+};
+/** This boolean is true if the graph has been computed */
+export const isComputed: State.Value<boolean> = isComputed_internal;
+
+const compute_internal: Server.ExecRequest<null,null> = {
+  kind: Server.RqKind.EXEC,
+  name:   'plugins.callgraph.compute',
+  input:  Json.jNull,
+  output: Json.jNull,
+  signals: [],
+};
+/** Compute the callgraph for the current project */
+export const compute: Server.ExecRequest<null,null>= compute_internal;
+
+/* ------------------------------------- */
diff --git a/ivette/src/frama-c/plugins/callgraph/callgraph.css b/ivette/src/frama-c/plugins/callgraph/callgraph.css
new file mode 100644
index 00000000000..4360e1461c2
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/callgraph.css
@@ -0,0 +1,6 @@
+.callgraph-computing {
+  max-width: 90%;
+  max-height: 200px;
+  fill: var(--info-text-discrete);
+  margin: auto;
+}
diff --git a/ivette/src/frama-c/plugins/callgraph/graph-style.json b/ivette/src/frama-c/plugins/callgraph/graph-style.json
new file mode 100644
index 00000000000..6a10b6d0882
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/graph-style.json
@@ -0,0 +1,34 @@
+[
+  {
+    "selector": "node",
+    "style": {
+      "shape": "round-rectangle",
+      "background-color": "#666",
+      "label": "data(id)",
+      "color": "white",
+      "text-outline-width": 2,
+      "text-outline-color": "#666",
+      "text-valign" : "center",
+      "padding" : "6px",
+      "border-width": 1,
+      "text-wrap" : "wrap"
+    }
+  },
+  {
+    "selector": "node[?is_root]",
+    "style": {
+      "border-width": "2px"
+    }
+  },
+  {
+    "selector": "edge",
+    "style": {
+      "width": 2,
+      "line-color": "#888",
+      "curve-style": "bezier",
+      "target-arrow-shape": "vee",
+      "target-arrow-color": "#888",
+      "arrow-scale": 2.0
+    }
+  }
+]
diff --git a/ivette/src/frama-c/plugins/callgraph/index.tsx b/ivette/src/frama-c/plugins/callgraph/index.tsx
new file mode 100644
index 00000000000..e5c8ccfc9d9
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/index.tsx
@@ -0,0 +1,138 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2023                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import React, { useState } from 'react';
+import _ from 'lodash';
+import * as Ivette from 'ivette';
+import * as Server from 'frama-c/server';
+
+import * as API from './api';
+
+import Cytoscape 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 gearsIcon from 'frama-c/plugins/eva/images/gears.svg';
+import './callgraph.css';
+
+
+// --------------------------------------------------------------------------
+// --- Nodes label measurement
+// --------------------------------------------------------------------------
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function getWidth(node: any): string {
+  const padding = 10;
+  const min = 50;
+  const canvas = document.querySelector('canvas[data-id="layer2-node"]');
+  if (canvas instanceof HTMLCanvasElement) {
+    const context = canvas.getContext('2d');
+    if (context) {
+      const fStyle = node.pstyle('font-style').strValue;
+      const weight = node.pstyle('font-weight').strValue;
+      const size = node.pstyle('font-size').pfValue;
+      const family = node.pstyle('font-family').strValue;
+      context.font = `${fStyle} ${weight} ${size}px ${family}`;
+      const width = context.measureText(node.data('id')).width;
+      return `${Math.max(min, width + padding)}px`;
+    }
+  }
+  return `${min}px`;
+}
+
+(style as unknown[]).push({
+    selector: 'node',
+    style: {width: getWidth}
+  });
+
+
+// --------------------------------------------------------------------------
+// --- Graph
+// --------------------------------------------------------------------------
+
+function convertGraph(graph: API.graph): object[] {
+  const elements = [];
+  for (const v of graph.vertices) {
+    elements.push({data: {...v, id:v.kf}});
+  }
+  for (const e of graph.edges) {
+    elements.push({data: {source: e.src, target: e.dst}});
+  }
+  console.log(elements);
+  return elements;
+}
+
+
+function Callgraph() : JSX.Element { 
+  const isComputed = useSyncValue(API.isComputed);
+  const graph = useSyncValue(API.callgraph);
+  const [cy, setCy] = useState<Cytoscape.Core>();
+  const layout = {name: 'cola', nodeSpacing: 32};
+
+  if (isComputed === false) {
+    Server.send(API.compute, null);
+    return (<img src={gearsIcon} className="callgraph-computing" />);
+  }
+  else if (graph !== undefined) {
+    return (
+      <CytoscapeComponent
+        elements={convertGraph(graph)}
+        stylesheet={style}
+        cy={setCy}
+        layout={layout}
+        style={{width: '100%', height: '100%'}}
+      />);  
+  }
+  else {
+    return (<></>);
+  }
+}
+
+
+// --------------------------------------------------------------------------
+// --- Ivette Component
+// --------------------------------------------------------------------------
+
+function CallgraphComponent(): JSX.Element {
+  // Component
+  return (
+    <>
+      <Ivette.TitleBar />
+      <Callgraph />
+    </>
+  );
+}
+
+
+Ivette.registerComponent({
+  id: 'frama-c.plugins.callgraph',
+  label: 'Call Graph',
+  group: 'frama-c.plugins',
+  rank: 3,
+  title:
+    'Display a graph showing calls between functions.',
+  children: <CallgraphComponent />,
+});
diff --git a/ivette/src/frama-c/plugins/callgraph/pkg.json b/ivette/src/frama-c/plugins/callgraph/pkg.json
new file mode 100644
index 00000000000..01be594c9ee
--- /dev/null
+++ b/ivette/src/frama-c/plugins/callgraph/pkg.json
@@ -0,0 +1,3 @@
+{
+  "name": "Frama-C/Callgraph"
+}
diff --git a/src/plugins/callgraph/callgraph_api.ml b/src/plugins/callgraph/callgraph_api.ml
index 712ccd11aed..a2d67c27747 100644
--- a/src/plugins/callgraph/callgraph_api.ml
+++ b/src/plugins/callgraph/callgraph_api.ml
@@ -61,6 +61,7 @@ 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/requests.ml b/src/plugins/callgraph/requests.ml
new file mode 100644
index 00000000000..42db7a20ad8
--- /dev/null
+++ b/src/plugins/callgraph/requests.ml
@@ -0,0 +1,169 @@
+(**************************************************************************)
+(*                                                                        *)
+(*  This file is part of Frama-C.                                         *)
+(*                                                                        *)
+(*  Copyright (C) 2007-2023                                               *)
+(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
+(*         alternatives)                                                  *)
+(*                                                                        *)
+(*  you can redistribute it and/or modify it under the terms of the GNU   *)
+(*  Lesser General Public License as published by the Free Software       *)
+(*  Foundation, version 2.1.                                              *)
+(*                                                                        *)
+(*  It is distributed in the hope that it will be useful,                 *)
+(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
+(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
+(*  GNU Lesser General Public License for more details.                   *)
+(*                                                                        *)
+(*  See the GNU Lesser General Public License version 2.1                 *)
+(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
+(*                                                                        *)
+(**************************************************************************)
+
+open Server
+
+let package = Package.package ~plugin:"callgraph" ~title:"Callgraph" ()
+
+module Record () =
+struct
+  module Record = Data.Record
+  type record
+  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
+  let dictionary: X.t Enum.dictionary = Enum.dictionary ()
+  let tag name descr = Enum.tag ~name ~descr:(Markdown.plain descr) dictionary
+  let publish lookup name descr =
+    Enum.set_lookup dictionary lookup;
+    Request.dictionary ~package ~name ~descr:(Markdown.plain descr) dictionary
+end
+
+module Vertex =
+struct
+  include Record ()
+
+  let kf = field "kf" (module Kernel_ast.Function)
+  let is_root = field "is_root" Data.jbool
+
+  include (val publish "vertex")
+  type t = Cil_types.kernel_function Service_graph.vertex
+
+  let to_json (v : Cil_types.kernel_function Service_graph.vertex) =
+    default |>
+    set kf v.node |>
+    set is_root v.is_root |>
+    to_json
+
+  let of_json _js = Data.failure "Vertex.of_json not implemented"
+end
+
+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 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")
+end
+
+module Edge =
+struct
+  include Record ()
+
+  let src = field "src" (module Kernel_ast.Function)
+  let dst = field "dst" (module Kernel_ast.Function)
+  let kind = field "kind" (module EdgeKind)
+
+  include (val publish "edge")
+  type t = Services.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) |>
+    to_json
+
+  let of_json _js = Data.failure "Edge.of_json not implemented"
+end
+
+module Graph =
+struct
+  include Record ()
+
+  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
+
+  let get_vertices (g : t) =
+    Services.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)
+
+  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
+
+let _signal =
+  States.register_value
+    ~package ~name:"callgraph"
+    ~descr:(Markdown.plain
+              "The current callgraph or an empty graph if it has not been computed yet")
+    ~output:(module Data.Joption (Graph))
+    ~add_hook:Services.add_hook
+    ~get:
+      begin fun () ->
+        if Services.is_computed () then
+          Some (Services.get ())
+        else
+          None
+      end
+    ()
+
+let _signal =
+  States.register_value
+    ~package ~name:"isComputed"
+    ~descr:(Markdown.plain
+              "This boolean is true if the graph has been computed")
+    ~output:(module Data.Jbool)
+    ~add_hook:Services.add_hook
+    ~get:Services.is_computed
+    ()
+
+let () = Request.register ~package
+    ~kind:`EXEC ~name:"compute"
+    ~descr:(Markdown.plain "Compute the callgraph for the current project")
+    ~input:(module Data.Junit) ~output:(module Data.Junit)
+    (fun () -> ignore (Services.get ()))
diff --git a/src/plugins/callgraph/requests.mli b/src/plugins/callgraph/requests.mli
new file mode 100644
index 00000000000..d68abd32119
--- /dev/null
+++ b/src/plugins/callgraph/requests.mli
@@ -0,0 +1,23 @@
+(**************************************************************************)
+(*                                                                        *)
+(*  This file is part of Frama-C.                                         *)
+(*                                                                        *)
+(*  Copyright (C) 2007-2023                                               *)
+(*    CEA (Commissariat à l'énergie atomique et aux énergies              *)
+(*         alternatives)                                                  *)
+(*                                                                        *)
+(*  you can redistribute it and/or modify it under the terms of the GNU   *)
+(*  Lesser General Public License as published by the Free Software       *)
+(*  Foundation, version 2.1.                                              *)
+(*                                                                        *)
+(*  It is distributed in the hope that it will be useful,                 *)
+(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
+(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
+(*  GNU Lesser General Public License for more details.                   *)
+(*                                                                        *)
+(*  See the GNU Lesser General Public License version 2.1                 *)
+(*  for more details (enclosed in the file licenses/LGPLv2.1).            *)
+(*                                                                        *)
+(**************************************************************************)
+
+(* This module only register requests for Frama-C Server. Nothing is exported *)
diff --git a/src/plugins/callgraph/services.ml b/src/plugins/callgraph/services.ml
index b2d3f2893fb..1345b6fcdf9 100644
--- a/src/plugins/callgraph/services.ml
+++ b/src/plugins/callgraph/services.ml
@@ -77,9 +77,12 @@ module State =
       let dependencies = [ Cg.self; Kernel.MainFunction.self ]
     end)
 
+module StateHook = Hook.Build (S.Service_graph.Datatype)
+
 (* eta-expansion required to mask optional argument [?project] *)
 let is_computed () = State.is_computed ()
 let self = State.self
+let add_hook = StateHook.extend
 
 let compute () =
   let cg = Cg.get () in
@@ -92,6 +95,7 @@ let compute () =
   in
   let sg = S.compute cg isr_names in
   State.mark_as_computed ();
+  StateHook.apply sg;
   sg
 
 let get () = State.memo compute
-- 
GitLab