From 9000c742fa3b00a6029c2d02425a31c56bb25de5 Mon Sep 17 00:00:00 2001
From: Maxime Jacquemin <maxime2.jacquemin@gmail.com>
Date: Fri, 11 Mar 2022 08:48:43 +0100
Subject: [PATCH] [ivette][WIP] Recreating the Eva componant

---
 ivette/src/frama-c/plugins/eva/state.tsx | 541 +++++++++++++++++++++++
 ivette/src/frama-c/plugins/eva/style.css |   1 -
 2 files changed, 541 insertions(+), 1 deletion(-)
 create mode 100644 ivette/src/frama-c/plugins/eva/state.tsx

diff --git a/ivette/src/frama-c/plugins/eva/state.tsx b/ivette/src/frama-c/plugins/eva/state.tsx
new file mode 100644
index 00000000000..e138d1ced57
--- /dev/null
+++ b/ivette/src/frama-c/plugins/eva/state.tsx
@@ -0,0 +1,541 @@
+import React from 'react';
+import * as Dome from 'dome/dome';
+import * as States from 'frama-c/states';
+import * as Server from 'frama-c/server';
+import * as Ast from 'frama-c/kernel/api/ast';
+import * as Values from 'frama-c/plugins/eva/api/values';
+import { callstack, evaluation } from 'frama-c/plugins/eva/api/values';
+
+import { Icon } from 'dome/controls/icons';
+// import { Stmt } from 'frama-c/plugins/eva/valueinfos';
+// import { Diff } from 'frama-c/plugins/eva/diffed';
+// import { FontSizer, SizedArea } from 'frama-c/plugins/eva/sized';
+
+import * as Ivette from 'ivette';
+import { Cell, Code } from 'dome/controls/labels';
+import { IconButton } from 'dome/controls/buttons';
+import { Filler, Hpack, Vpack, Vfill } from 'dome/layout/boxes';
+import { Inset, Button, ButtonGroup } from 'dome/frame/toolbars';
+
+
+
+type Status = 'True' | 'False' | 'Unknown'
+type Alarm = [ Status, string ]
+
+export interface Location {
+  target: Ast.marker;
+  fct: string;
+}
+
+interface Info {
+  code?: string;
+  stmt?: Ast.marker;
+  rank?: number;
+  effects?: boolean;
+  condition?: boolean;
+}
+
+interface Values {
+  errors?: string;
+  vBefore?: evaluation;
+  vAfter?: evaluation;
+  vThen?: evaluation;
+  vElse?: evaluation;
+}
+
+function defaultValues(e: any): Values {
+  const empty: evaluation = { value: '', alarms: [], pointedVars: [] };
+  return { errors: `Error: ${e}`, vBefore: empty };
+}
+
+export interface Probe extends Location, Info {
+  values: (c?: callstack) => Promise<Values>
+}
+
+
+
+function getInfo(loc: Location): Info {
+  const { target } = loc;
+  const [ cache ] = React.useState<Map<Ast.marker, Info>>(new Map());
+  if (cache.has(target)) return cache.get(target) as Info;
+  const error: Info = { code: '(error)' };
+  const request = Server.send(Values.getProbeInfo, target);
+  const { result = error } = Dome.usePromise(request.catch(() => error));
+  cache.set(target, result);
+  return result;
+}
+
+export function useProbe(loc: Location): Probe {
+  const [ cache ] = React.useState<Map<Location, Probe>>(new Map());
+  if (cache.has(loc)) return cache.get(loc) as Probe;
+  const info = getInfo(loc);
+  const values = async (c?: Values.callstack): Promise<Values> => {
+    const req = Server.send(Values.getValues, {...loc, callstack:c });
+    return req.catch(defaultValues);
+  };
+  const result = { ...loc, ...info, values };
+  cache.set(loc, result);
+  return result;
+}
+
+
+
+interface StmtProps {
+  stmt?: Ast.marker;
+  marker: Ast.marker;
+  short?: boolean;
+}
+
+function Stmt(props: StmtProps): JSX.Element {
+  const markersInfo = States.useSyncArray(Ast.markerInfo);
+  const { stmt, marker, short } = props;
+  if (!stmt) return <></>;
+  const line = markersInfo.getData(marker)?.sloc?.line;
+  const filename = markersInfo.getData(marker)?.sloc?.base;
+  const title = markersInfo.getData(stmt)?.descr;
+  const text = short ? `@L${line}` : `@${filename}:${line}`;
+  const className = 'dome-text-cell eva-stmt';
+  return <span className={className} title={title}>{text}</span>;
+}
+
+
+
+interface AlarmsInfosProps {
+  probe?: Probe;
+  callstack?: callstack;
+}
+
+function AlarmsInfos(props: AlarmsInfosProps): JSX.Element {
+  const { probe, callstack: c } = props;
+  if (!probe) return <></>;
+  const { result: domain } = Dome.usePromise(probe.values(c));
+  const alarms = domain?.vBefore?.alarms ?? [];
+  if (alarms.length > 0) {
+    const renderAlarm = ([status, alarm]: Alarm) => {
+      const className = `eva-alarm-info eva-alarm-${status}`;
+      return <Code className={className} icon="WARNING">{alarm}</Code>;
+    };
+    return (
+      <Vpack className="eva-info">
+        {React.Children.toArray(alarms.map(renderAlarm))}
+      </Vpack>
+    );
+  }
+  return <></>;
+}
+
+
+
+interface StackInfosProps {
+  callstack?: callstack;
+}
+
+export interface Callsite {
+  callee: string;
+  caller?: string;
+  stmt?: Ast.marker;
+}
+
+function getCallsites(callstack?: callstack): Promise<Callsite[]> {
+  if (!callstack) return Promise.resolve([]);
+  return Server.send(Values.getCallstackInfo, callstack);
+}
+
+function StackInfos(props: StackInfosProps): JSX.Element {
+  const [, setSelection] = States.useSelection();
+  const { callstack } = props;
+  const { result: callsites = [] } = Dome.usePromise(getCallsites(callstack));
+  if (callsites.length <= 1) return <></>;
+  const makeCallsite = ({ caller, stmt }: Callsite) => {
+    if (!caller || !stmt) return null;
+    const key = `${caller}@${stmt}`;
+    const select = (meta: boolean) => {
+      const location = { fct: caller, marker: stmt };
+      setSelection({ location });
+      if (meta) States.MetaSelection.emit(location);
+    };
+    const onClick = (evt: React.MouseEvent) => { select(evt.altKey); };
+    const onDoubleClick = (evt: React.MouseEvent) => {
+      evt.preventDefault();
+      select(true);
+    };
+    return (
+      <Cell
+        key={key}
+        icon='TRIANGLE.LEFT'
+        className='eva-callsite'
+        onClick={onClick}
+        onDoubleClick={onDoubleClick}
+      >
+        {caller}
+        <Stmt stmt={stmt} marker={stmt} />
+      </Cell>
+    );
+  };
+
+  return (
+    <Hpack className="eva-info">
+      {callsites.map(makeCallsite)}
+    </Hpack>
+  );
+}
+
+
+
+interface ProbeInfosProps {
+  probe?: Probe;
+  removeLoc: (loc: Location) => void;
+  display: StateToDisplay;
+  setDisplay: (display: StateToDisplay) => void;
+}
+
+export function ProbeInfos(props: ProbeInfosProps): JSX.Element {
+  const { probe, removeLoc, display, setDisplay } = props;
+  if (!probe || !probe.code) return <></>;
+  const { code, stmt, target } = probe;
+  return (
+    <Hpack className="eva-probeinfo">
+      <div className="eva-probeinfo-code">
+        <div className='eva-sized-area dome-text-cell'>{code}</div>
+      </div>
+      <Code><Stmt stmt={stmt} marker={target} /></Code>
+      <IconButton
+        icon="CIRC.CLOSE"
+        className="eva-probeinfo-button"
+        title="Discard the probe"
+        onClick={() => removeLoc(probe)}
+      />
+      <Filler />
+      <ButtonGroup className='eva-probeinfo-state'>
+        <Button
+          label='B'
+          value='Before'
+          selected={display === 'Before' || display === 'Both'}
+          title='Show values before statement effects'
+          onClick={() => {
+            if (display === 'Before') setDisplay('None')
+            else if (display === 'After') setDisplay('Both')
+            else if (display === 'None') setDisplay('Before')
+            else if (display === 'Both') setDisplay('After')
+          }}
+        />
+        <Button
+          label='A'
+          value='After'
+          selected={display === 'After' || display === 'Both'}
+          title='Show values after statement effects'
+          onClick={() => {
+            if (display === 'Before') setDisplay('Both')
+            else if (display === 'After') setDisplay('None')
+            else if (display === 'None') setDisplay('After')
+            else if (display === 'Both') setDisplay('Before')
+          }}
+        />
+      </ButtonGroup>
+    </Hpack>
+  );
+  /*
+        <Button
+          visible={condition}
+          label="C"
+          selected={vcond === 'Cond'}
+          title="Show values in all conditions"
+          onClick={() => model.setVcond('Cond')}
+        />
+        <Button
+          visible={condition || vcond === 'Then'}
+          selected={vcond === 'Then'}
+          enabled={condition}
+          label="T"
+          value="Then"
+          title="Show reduced values when condition holds (Then)"
+          onClick={() => model.setVcond('Then')}
+        />
+        <Button
+          visible={condition || vcond === 'Else'}
+          selected={vcond === 'Else'}
+          enabled={condition}
+          label="E"
+          value="Else"
+          title="Show reduced values when condition does not hold (Else)"
+          onClick={() => model.setVcond('Else')}
+        />
+        <Button
+          visible={condition || effects}
+          selected={vstmt === 'After'}
+          label="A"
+          value="After"
+          title="Show values after/before statement effects"
+          onClick={() => {
+            model.setVstmt(vstmt === 'After' ? 'Before' : 'After');
+          }}
+        />
+      </ButtonGroup>
+    </Hpack>
+  );
+  */
+}
+
+
+
+export type StateToDisplay = 'Before' | 'After' | 'Both' | 'None'
+
+export interface ProbeRenderProps {
+  probe: Probe;
+  focused: boolean
+  display: StateToDisplay;
+  selectProbe: () => void;
+  removeProbe: () => void;
+  addLoc: (loc: Location) => void;
+}
+
+
+
+export function ProbeHeader(props: ProbeRenderProps): JSX.Element {
+  const { probe, focused, display } = props;
+  const { code = '(error)', stmt, target } = probe;
+  const color = 'eva-probes eva-' + focused ? 'focused' : 'cell';
+  const { selectProbe, removeProbe } = props;
+  const [, setSelection] = States.useSelection();
+
+  // Computing the number of columns. By design, either vAfter or vThen and
+  // vElse are empty. Also by design (hypothesis), it is not function of the
+  // considered callstacks, so we check on the consolidated.
+  const { result = defaultValues('Unknown')} = Dome.usePromise(probe.values());
+  const { vBefore, vAfter, vThen, vElse } = result;
+  let colSpan = 0;
+  if (display !== 'After' && vBefore) colSpan += 1;
+  if (display !== 'Before' && vAfter) colSpan += 1;
+  if (display !== 'Before' && vThen && vElse) colSpan += 2;
+
+  const loc: States.SelectionActions = { location: probe };
+  const onClick = (): void => { setSelection(loc); selectProbe(); };
+  const onContextMenu = (): void => {
+    const items: Dome.PopupMenuItem[] = [];
+    const removeLabel = `Remove column for ${code}`;
+    items.push({ label: removeLabel, onClick: removeProbe });
+    Dome.popupMenu(items);
+  };
+  
+  return (
+    <th
+      className={color}
+      colSpan={colSpan}
+      onClick={onClick}
+      onContextMenu={onContextMenu}
+    >
+      <span className='dome-text-cell'>{code}</span>
+      <Stmt stmt={stmt} marker={target} short={true}/>
+    </th>
+  );
+}
+
+
+
+type CallstackElement = (c?: callstack) => JSX.Element
+
+function getAlarmStatus(alarms: Alarm[]): string {
+  if (alarms.length === 0) return 'none';
+  if (alarms.find(([st, _]) => st === 'False')) return 'False';
+  else return 'Unknown';
+}
+
+export function ProbeValues(props: ProbeRenderProps): CallstackElement {
+  const { probe, display } = props;
+  const { selectProbe, addLoc } = props;
+  const [, setSelection] = States.useSelection();
+
+  // Building common parts
+  const loc: States.SelectionActions = { location: probe };
+  const onClick = (): void => { setSelection(loc); selectProbe(); };
+  const onContextMenu = (evaluation: Values.evaluation) => (): void => {
+    const { value, pointedVars } = evaluation;
+    const items: Dome.PopupMenuItem[] = [];
+    const copy = () => navigator.clipboard.writeText(value);
+    if (value !== '') items.push({ label: 'Copy to clipboard', onClick: copy });
+    if (items.length > 0 && pointedVars.length > 0) items.push('separator');
+    pointedVars.forEach((lval) => {
+      const [text, lvalMarker] = lval;
+      const label = `Display values for ${text}`;
+      const location = { fct: probe.fct, target: lvalMarker };
+      const onItemClick = () => addLoc(location);
+      items.push({ label, onClick: onItemClick });
+    });
+    if (items.length > 0) Dome.popupMenu(items);
+  };
+
+  function result(c?: callstack): JSX.Element {
+  const { result = defaultValues('Unknown')} = Dome.usePromise(probe.values(c));
+  const { vBefore, vAfter, vThen, vElse } = result;
+    function td(e: Values.evaluation, state: string): JSX.Element {
+      const status = getAlarmStatus(e.alarms);
+      const alarmClass = `eva-cell-alarms eva-alarm-${status}`;
+      const title = 'At least one alarm is raised in one callstack';
+      return (
+        <td
+          className='eva-values eva-cell'
+          onClick={onClick}
+          onContextMenu={onContextMenu(e)}
+        >
+          <Icon className={alarmClass} size={10} title={title} id="WARNING" />
+          <span className={`eva-state-${state}`}>{e.value}</span>
+        </td>
+      );
+    }
+    if (display === 'Before' && vBefore) return td(vBefore, 'Before');
+    if (display === 'After' && vAfter) return td(vAfter, 'After');
+    if (display === 'After' && vThen && vElse)
+      return <>{td(vThen, 'Then')}{td(vElse, 'Else')}</>;
+    if (display === 'Both' && vBefore && vAfter)
+      return <>{td(vBefore, 'Before')}{td(vAfter, 'After')}</>;
+    if (display === 'Both' && vBefore && vThen && vElse)
+      return <>{td(vBefore, 'Before')}{td(vThen, 'Then')}{td(vElse, 'Else')}</>;
+    return <></>;
+  }
+
+  return result;
+}
+
+
+
+interface FunctionProps {
+  fct: string;
+  markers: Set<Ast.marker>;
+  display: StateToDisplay;
+  close: () => void;
+  selectLoc: (loc: Location) => void;
+  isSelected: (loc: Location) => boolean;
+  removeLoc: (loc: Location) => void;
+  addLoc: (loc: Location) => void;
+}
+
+interface FunctionTitleProps extends FunctionProps {
+  byCallstacks: boolean;
+  folded: boolean;
+  setFolded: (folded: boolean) => void;
+  setByCallstacks: (byCallstack: boolean) => void;
+}
+
+function FunctionTitle(props: FunctionTitleProps): JSX.Element {
+  const { fct, folded, byCallstacks } = props;
+  const { setFolded, setByCallstacks, close } = props;
+  return (
+    <>
+      <IconButton
+        className="eva-fct-fold"
+        icon={folded ? 'ANGLE.RIGHT' : 'ANGLE.DOWN'}
+        onClick={() => setFolded(!folded)}
+      />
+      <Cell className="eva-fct-name">{fct}</Cell>
+      <Filler />
+      <IconButton
+        icon="ITEMS.LIST"
+        className="eva-probeinfo-button"
+        selected={byCallstacks}
+        title="Details by callstack"
+        onClick={() => setByCallstacks(!byCallstacks)}
+      />
+      <Inset />
+      <IconButton
+        icon="CROSS"
+        className="eva-probeinfo-button"
+        title="Close"
+        onClick={close}
+      />
+    </>
+  );
+}
+
+function FunctionSection(props: FunctionProps): JSX.Element {
+  const { fct, markers, display } = props;
+  const [ byCallstacks, setByCallstacks ] = React.useState(false);
+  const [ folded, setFolded ] = React.useState(false);
+
+  const reqCallstacks = Server.send(Values.getCallstacks, [...markers]);
+  const { result } = Dome.usePromise(reqCallstacks.catch(() => [undefined]));
+  const callstacks = byCallstacks ? result ?? [undefined] : [undefined];
+
+  const probes: Set<Probe> = new Set();
+  markers.forEach((target) => probes.add(useProbe({ target, fct })));
+
+  let renderProps: ProbeRenderProps[] = [];
+  const { isSelected, addLoc } = props;
+  probes.forEach((probe) => {
+    const focused = isSelected(probe);
+    const selectProbe = (): void => props.selectLoc(probe);
+    const removeProbe = (): void => props.removeLoc(probe);
+    const fcts = { selectProbe, removeProbe, addLoc };
+    renderProps.push({ probe, focused, display, ...fcts });
+  });
+
+  const headers = renderProps.map(ProbeHeader);
+  const values = callstacks.map((callstack) => {
+    return renderProps.map((props) => ProbeValues(props)(callstack))
+  });
+
+  return (
+    <>
+      <FunctionTitle
+        {...props}
+        byCallstacks={byCallstacks}
+        setByCallstacks={setByCallstacks}
+        folded={folded}
+        setFolded={setFolded}
+      />
+      <table>
+        <tr>{headers}</tr>
+        {values.map((callstackValues) => <tr>{callstackValues}</tr>)}
+      </table>
+    </>
+  );
+}
+
+
+
+type Maybe<A> = A | undefined;
+
+export function EvaTable(): JSX.Element {
+  const [ display, setDisplay ] = React.useState<StateToDisplay>('Before');
+  const [ fcts ] = React.useState<Map<string, Set<Ast.marker>>>(new Map());
+
+  const addLoc = (loc: Location): void => {
+    const { fct, target: m } = loc;
+    const ms = fcts.get(fct);
+    fcts.set(fct, ms ? ms.add(m) : (new Set<Ast.marker>()).add(m));
+  };
+
+  const removeLoc = (loc: Location): void => {
+    const { fct, target: m } = loc;
+    const ms = fcts.get(fct);
+    if (ms) ms.delete(m);
+  };
+
+  // TODO: Faire descendre setCS pour pouvoir sélectionner les callstacks
+  const [ cs, setCS ] = React.useState<Maybe<callstack>>(undefined);
+
+  const [ selected, select ] = React.useState<Maybe<Location>>(undefined);
+  const selectedProbe = selected ? useProbe(selected) : undefined;
+  const isSelected = (loc: Location): boolean => loc === selected;
+  const closeFct = (fct: string): () => void => () => { fcts.delete(fct); };
+
+  let functionsProps: FunctionProps[] = [];
+  fcts.forEach((markers, fct) => {
+    const selectLoc = select;
+    const close = closeFct(fct);
+    const calls = { selectLoc, isSelected, removeLoc, addLoc };
+    functionsProps.push({ fct, markers, display, close, ...calls });
+  });
+
+
+  return (
+    <>
+      <Ivette.TitleBar />
+      <Vfill>
+        <ProbeInfos probe={selectedProbe} removeLoc={removeLoc} />
+        <Vfill>{functionsProps.map(FunctionSection)}</Vfill>
+        <AlarmsInfos probe={selectedProbe} callstack={cs} />
+        <StackInfos callstack={cs}/>
+      </Vfill>
+    </>
+  );
+}
diff --git a/ivette/src/frama-c/plugins/eva/style.css b/ivette/src/frama-c/plugins/eva/style.css
index 3dd5be0b821..cf8dbf70ee0 100644
--- a/ivette/src/frama-c/plugins/eva/style.css
+++ b/ivette/src/frama-c/plugins/eva/style.css
@@ -242,7 +242,6 @@
 
 .eva-probes .eva-transient {
     background: var(--eva-probes-transient);
-    align-items: center;
 }
 
 .eva-probes .eva-transient.eva-focused {
-- 
GitLab