From 06fd5237e576882f054a9a2f8f70f03564948190 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Tue, 21 Mar 2023 17:01:51 +0100
Subject: [PATCH] [ivette] refactored sync array

---
 ivette/src/dome/renderer/table/arrays.ts    |  5 ++
 ivette/src/frama-c/kernel/ASTview.tsx       |  4 +-
 ivette/src/frama-c/kernel/Globals.tsx       | 19 ++---
 ivette/src/frama-c/kernel/Messages.tsx      |  2 +-
 ivette/src/frama-c/kernel/Properties.tsx    | 12 ++-
 ivette/src/frama-c/kernel/SourceCode.tsx    |  9 +-
 ivette/src/frama-c/plugins/dive/index.tsx   |  2 +-
 ivette/src/frama-c/plugins/eva/Coverage.tsx | 22 ++---
 ivette/src/frama-c/states.ts                | 95 +++++++++++++++++----
 9 files changed, 110 insertions(+), 60 deletions(-)

diff --git a/ivette/src/dome/renderer/table/arrays.ts b/ivette/src/dome/renderer/table/arrays.ts
index 45d3d48bb99..16d1e29ba1f 100644
--- a/ivette/src/dome/renderer/table/arrays.ts
+++ b/ivette/src/dome/renderer/table/arrays.ts
@@ -454,6 +454,11 @@ export class ArrayModel<Key, Row>
     this.index.forEach((p) => { fn(p.row); });
   }
 
+  /** Number of non-filtered entries. */
+  length(): number {
+    return this.index.size;
+  }
+
 }
 
 // --------------------------------------------------------------------------
diff --git a/ivette/src/frama-c/kernel/ASTview.tsx b/ivette/src/frama-c/kernel/ASTview.tsx
index 112823a87b6..8b9d6c42473 100644
--- a/ivette/src/frama-c/kernel/ASTview.tsx
+++ b/ivette/src/frama-c/kernel/ASTview.tsx
@@ -725,7 +725,7 @@ export default function ASTview(): JSX.Element {
   React.useEffect(() => Hovered.set(view, hov?.marker ?? ''), [view, hov]);
 
   // Updating CodeMirror when the <properties> synchronized array is changed.
-  const props = States.useSyncArray(Properties.status).getArray();
+  const props = States.useSyncArrayData(Properties.status);
   React.useEffect(() => PropertiesStatuses.set(view, props), [view, props]);
 
   // Updating CodeMirror when the <propStatusTags> map is changed.
@@ -733,7 +733,7 @@ export default function ASTview(): JSX.Element {
   React.useEffect(() => Tags.set(view, tags), [view, tags]);
 
   // Updating CodeMirror when the <markersInfo> synchronized array is changed.
-  const info = States.useSyncArray(Ast.markerAttributes);
+  const info = States.useSyncArrayModel(Ast.markerAttributes);
   const getData = React.useCallback((key) => info.getData(key), [info]);
   React.useEffect(() => GetMarkerData.set(view, getData), [view, getData]);
 
diff --git a/ivette/src/frama-c/kernel/Globals.tsx b/ivette/src/frama-c/kernel/Globals.tsx
index a9d73184eb7..582875e68f9 100644
--- a/ivette/src/frama-c/kernel/Globals.tsx
+++ b/ivette/src/frama-c/kernel/Globals.tsx
@@ -32,8 +32,6 @@ import { alpha } from 'dome/data/compare';
 import { Section, Item } from 'dome/frame/sidebars';
 import { Button } from 'dome/controls/buttons';
 import * as Toolbars from 'dome/frame/toolbars';
-import * as Models from 'dome/table/models';
-import * as Arrays from 'dome/table/arrays';
 
 import * as States from 'frama-c/states';
 import * as Kernel from 'frama-c/kernel/api/ast';
@@ -106,10 +104,8 @@ type functionsData =
 type FctKey = Json.key<'#functions'>;
 
 function computeFcts(
-  ker: Arrays.CompactModel<FctKey,Kernel.functionsData>,
-  eva: Arrays.CompactModel<FctKey,Eva.functionsData>,
-  _kstamp: number,
-  _estamp: number,
+  ker: States.ArrayProxy<FctKey,Kernel.functionsData>,
+  eva: States.ArrayProxy<FctKey,Eva.functionsData>,
 ): functionsData[] {
   const arr: functionsData[] = [];
   ker.forEach((kf) => {
@@ -123,14 +119,9 @@ export default function Globals(): JSX.Element {
 
   // Hooks
   const [selection, updateSelection] = States.useSelection();
-  const kerFcts = States.useSyncArray(Kernel.functions);
-  const evaFcts = States.useSyncArray(Eva.functions);
-  const kerStamp = Models.useModel(kerFcts);
-  const evaStamp = Models.useModel(evaFcts);
-  const fcts = React.useMemo(
-    () => computeFcts(kerFcts,evaFcts,kerStamp,evaStamp),
-    [kerFcts,evaFcts,kerStamp,evaStamp]
-  );
+  const ker = States.useSyncArrayProxy(Kernel.functions);
+  const eva = States.useSyncArrayProxy(Eva.functions);
+  const fcts = React.useMemo(() => computeFcts(ker,eva), [ker,eva]);
   const { useFlipSettings } = Dome;
   const [stdlib, flipStdlib] =
     useFlipSettings('ivette.globals.stdlib', false);
diff --git a/ivette/src/frama-c/kernel/Messages.tsx b/ivette/src/frama-c/kernel/Messages.tsx
index b1b64a29067..b6421cda814 100644
--- a/ivette/src/frama-c/kernel/Messages.tsx
+++ b/ivette/src/frama-c/kernel/Messages.tsx
@@ -426,7 +426,7 @@ export default function RenderMessages(): JSX.Element {
     return m;
   });
 
-  const data = States.useSyncArray(Kernel.message).getArray();
+  const data = States.useSyncArrayData(Kernel.message);
 
   React.useEffect(() => {
     model.removeAllData();
diff --git a/ivette/src/frama-c/kernel/Properties.tsx b/ivette/src/frama-c/kernel/Properties.tsx
index 81d95fd7930..476cb53b1d0 100644
--- a/ivette/src/frama-c/kernel/Properties.tsx
+++ b/ivette/src/frama-c/kernel/Properties.tsx
@@ -597,8 +597,8 @@ function FilterRatio({ model }: { model: PropertyModel }): JSX.Element {
 // --- Properties Table
 // -------------------------------------------------------------------------
 
-type PropsModel = Arrays.CompactModel<PropKey,Properties.statusData>;
-type EvapsModel = Arrays.CompactModel<PropKey,Eva.propertiesData>;
+type PropsModel = States.ArrayProxy<PropKey,Properties.statusData>;
+type EvapsModel = States.ArrayProxy<PropKey,Eva.propertiesData>;
 
 function populateModel(
   model: PropertyModel,
@@ -618,13 +618,11 @@ export default function RenderProperties(): JSX.Element {
 
   // Hooks
   const model = React.useMemo(() => new PropertyModel(), []);
-  const props = States.useSyncArray(Properties.status);
-  const evaps = States.useSyncArray(Eva.properties);
-  const pstamp = Models.useModel(props);
-  const estamp = Models.useModel(evaps);
+  const props = States.useSyncArrayProxy(Properties.status);
+  const evaps = States.useSyncArrayProxy(Eva.properties);
   React.useEffect(() => {
     populateModel(model,props,evaps);
-  },[model,props,evaps,pstamp,estamp]);
+  },[model,props,evaps]);
 
   const [selection, updateSelection] = States.useSelection();
 
diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx
index 0d143b7fa6d..03bc698ca28 100644
--- a/ivette/src/frama-c/kernel/SourceCode.tsx
+++ b/ivette/src/frama-c/kernel/SourceCode.tsx
@@ -211,15 +211,14 @@ function useFctSource(file: string): string {
 
 // Build a callback that retrieves a location's source information.
 function useSourceGetter(): GetSource {
-  const attributes = States.useSyncArray(Ast.markerAttributes);
-  const functionsData = States.useSyncArray(Ast.functions).getArray();
+  const getAttr = States.useSyncArrayGetter(Ast.markerAttributes);
+  const functionsData = States.useSyncArrayData(Ast.functions);
   return React.useCallback(({ fct, marker }) => {
-    const markerSloc = (marker !== undefined && marker !== '') ?
-      attributes.getData(marker)?.sloc : undefined;
+    const markerSloc = getAttr(marker)?.sloc;
     const fctSloc = (fct !== undefined && fct !== '') ?
       functionsData.find((e) => e.name === fct)?.sloc : undefined;
     return markerSloc ?? fctSloc;
-  }, [attributes, functionsData]);
+  }, [getAttr, functionsData]);
 }
 
 // -----------------------------------------------------------------------------
diff --git a/ivette/src/frama-c/plugins/dive/index.tsx b/ivette/src/frama-c/plugins/dive/index.tsx
index 780a12a345f..ff649431bd3 100644
--- a/ivette/src/frama-c/plugins/dive/index.tsx
+++ b/ivette/src/frama-c/plugins/dive/index.tsx
@@ -582,7 +582,7 @@ const GraphView = React.forwardRef<GraphViewRef | undefined, GraphViewProps>(
 
   const [dive, setDive] = useState(() => new Dive());
   const [selection, updateSelection] = States.useSelection();
-  const graph = States.useSyncArray(API.graph, true).getArray();
+  const graph = States.useSyncArrayData(API.graph);
 
   function setCy(cy: Cytoscape.Core): void {
     if (cy !== dive.cy)
diff --git a/ivette/src/frama-c/plugins/eva/Coverage.tsx b/ivette/src/frama-c/plugins/eva/Coverage.tsx
index cad3b95c356..0a048970850 100644
--- a/ivette/src/frama-c/plugins/eva/Coverage.tsx
+++ b/ivette/src/frama-c/plugins/eva/Coverage.tsx
@@ -22,14 +22,12 @@
 
 import React from 'react';
 import { Table, Column } from 'dome/table/views';
-import { CompactModel } from 'dome/table/arrays';
 import * as Arrays from 'dome/table/arrays';
 import * as Compare from 'dome/data/compare';
 import * as Ivette from 'ivette';
 import * as States from 'frama-c/states';
-
-import * as Ast from 'frama-c/kernel/api/ast';
 import * as Eva from 'frama-c/plugins/eva/api/general';
+
 import CoverageMeter, { percent } from './CoverageMeter';
 
 type stats = Eva.functionStatsData;
@@ -70,21 +68,15 @@ const ordering: Arrays.ByColumns<stats> = {
   ),
 };
 
-class Model extends CompactModel<Ast.fct, stats> {
-  constructor() {
-    super(Eva.functionStats.getkey);
-    this.setColumnOrder(ordering);
-    this.setSorting({ sortBy: 'coverage', sortDirection: 'ASC' });
-  }
-}
-
 export function CoverageTable(): JSX.Element {
-  const data = States.useSyncArray(Eva.functionStats).getArray();
-  const [selection, updateSelection] = States.useSelection();
 
-  const model = React.useMemo(() => new Model(), []);
-  React.useEffect(() => model.replaceAllDataWith(data), [model, data]);
+  const model = States.useSyncArrayModel(Eva.functionStats);
+  React.useEffect(() => {
+    model.setColumnOrder(ordering);
+    model.setSorting({ sortBy: 'coverage', sortDirection: 'ASC' });
+  }, [model]);
 
+  const [selection, updateSelection] = States.useSelection();
   const onSelection = ({ key }: stats): void => {
     updateSelection({ location: { fct: key } });
   };
diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts
index 1078b51f4fe..01ff09edb8a 100644
--- a/ivette/src/frama-c/states.ts
+++ b/ivette/src/frama-c/states.ts
@@ -401,27 +401,95 @@ export function reloadArray<K, A>(arr: Array<K, A>): void {
   currentSyncArray(arr).reload();
 }
 
+/** Access to Synchronized Array elements. */
+export interface ArrayProxy<K,A> {
+  length: number;
+  getData(elt: K | undefined): (A | undefined);
+  forEach(fn: (row: A, elt: K) => void): void;
+}
+
+// --- Utility functions
+
+function arrayGet<K,A>(
+  model: CompactModel<K,A>,
+  elt: K | undefined,
+  _stamp: number,
+): A | undefined {
+  return elt ? model.getData(elt) : undefined;
+}
+
+function arrayProxy<K,A>(
+  model: CompactModel<K,A>,
+  _stamp: number,
+): ArrayProxy<K,A> {
+  return {
+    length: model.length(),
+    getData: (elt) => elt ? model.getData(elt) : undefined,
+    forEach: (fn) => model.forEach((r) => fn(r,model.getkey(r))),
+  };
+}
+
+// ---- Hooks
+
 /**
-   Use Synchronized Array (Custom React Hook).
+   Use Synchronized Array as a low level, ready to use, Table Compact Model.
 
-   Unless specified, the hook makes the component re-render on every
-   update. Disabling this automatic re-rendering can be an option when
-   using the model to make a table view, which automatically synchronizes on
-   model updates.
-   @param sync Whether the component re-renders on updates (default is `true`).
+   Warning: to be in sync with the array, one shall subscribe to model events,
+   eg. by using `useModel()` hook, like `<Table/>` element does.
  */
-export function useSyncArray<K, A>(
-  arr: Array<K, A>,
-  sync = true,
+export function useSyncArrayModel<K, A>(
+  arr: Array<K, A>
 ): CompactModel<K, A> {
   Server.useStatus();
   const st = currentSyncArray(arr);
   Server.useSignal(arr.signal, st.fetch);
   st.online();
-  useModel(st.model, sync);
   return st.model;
 }
 
+/** Use Synchronized Array as a data array. */
+export function useSyncArrayData<K, A>(arr: Array<K, A>): A[]
+{
+  return useSyncArrayModel(arr).getArray();
+}
+
+/** Use Synchronized Array element. */
+export function useSyncArrayElt<K, A>(
+  arr: Array<K, A>,
+  elt: K | undefined,
+): A | undefined {
+  const model = useSyncArrayModel(arr);
+  const stamp = useModel(model);
+  return React.useMemo(
+    () => arrayGet(model, elt, stamp),
+    [model, elt, stamp]
+  );
+}
+
+/** Use Synchronized Array as an element data getter. */
+export function useSyncArrayGetter<K, A>(
+  arr: Array<K, A>
+): (elt: K | undefined) => (A | undefined) {
+  const model = useSyncArrayModel(arr);
+  const stamp = useModel(model);
+  return React.useCallback(
+    (elt) => arrayGet(model, elt, stamp),
+    [model, stamp]
+  );
+}
+
+/** Use Synchronized Array as an array proxy. */
+export function useSyncArrayProxy<K, A>(
+  arr: Array<K, A>
+): ArrayProxy<K,A> {
+  const model = useSyncArrayModel<K, A>(arr);
+  const stamp = useModel(model);
+  return React.useMemo(
+    () => arrayProxy(model, stamp),
+    [model, stamp]
+  );
+}
+
 /**
    Return the associated array model.
 */
@@ -749,11 +817,8 @@ export type attributes = Ast.markerAttributesData;
 
 /** Access the marker attributes from AST. */
 export function useMarker(marker: Ast.marker | undefined): attributes {
-  const marks = useSyncArray(Ast.markerAttributes);
-  if (marker === undefined) return Ast.markerAttributesDataDefault;
-  const attrs = marks.getData(marker);
-  if (attrs === undefined) return Ast.markerAttributesDataDefault;
-  return attrs;
+  const marks = useSyncArrayElt(Ast.markerAttributes, marker);
+  return marks ?? Ast.markerAttributesDataDefault;
 }
 
 // --------------------------------------------------------------------------
-- 
GitLab