diff --git a/ivette/Makefile.distrib b/ivette/Makefile.distrib index 87068f0322f67cc1f5c8ab9c6f920bfad9e9c91d..5a9f51b860bd01aa024cfba11c79fe817b7c2e66 100644 --- a/ivette/Makefile.distrib +++ b/ivette/Makefile.distrib @@ -175,19 +175,10 @@ DISTRIB_FILES += ivette/src/frama-c/plugins/eva/CoverageMeter.tsx DISTRIB_FILES += ivette/src/frama-c/plugins/eva/Summary.tsx DISTRIB_FILES += ivette/src/frama-c/plugins/eva/api/general/index.ts DISTRIB_FILES += ivette/src/frama-c/plugins/eva/api/values/index.ts -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/cells.ts -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/diffed.tsx DISTRIB_FILES += ivette/src/frama-c/plugins/eva/index.tsx -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/layout.ts -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/model.ts DISTRIB_FILES += ivette/src/frama-c/plugins/eva/pkg.json -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/probeinfos.tsx -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/probes.ts -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/sized.tsx -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/stacks.ts DISTRIB_FILES += ivette/src/frama-c/plugins/eva/style.css DISTRIB_FILES += ivette/src/frama-c/plugins/eva/summary.css -DISTRIB_FILES += ivette/src/frama-c/plugins/eva/valueinfos.tsx DISTRIB_FILES += ivette/src/frama-c/plugins/eva/valuetable.tsx DISTRIB_FILES += ivette/src/frama-c/plugins/pivot/api/general/index.ts DISTRIB_FILES += ivette/src/frama-c/plugins/studia/api/studia/index.ts diff --git a/ivette/headers/header_spec.txt b/ivette/headers/header_spec.txt index 75e97fe5eb7d202f311cc244e23d696311dd3544..1bc4d6658222a8e3a2f26ecc01df72c5e949cb30 100644 --- a/ivette/headers/header_spec.txt +++ b/ivette/headers/header_spec.txt @@ -173,22 +173,13 @@ src/frama-c/plugins/dive/tippy.css: .ignore src/frama-c/plugins/eva/Coverage.tsx: CEA_LGPL src/frama-c/plugins/eva/CoverageMeter.tsx: CEA_LGPL src/frama-c/plugins/eva/Summary.tsx: CEA_LGPL +src/frama-c/plugins/eva/valuetable.tsx: CEA_LGPL src/frama-c/plugins/eva/api/general/index.ts: CEA_LGPL src/frama-c/plugins/eva/api/values/index.ts: CEA_LGPL -src/frama-c/plugins/eva/cells.ts: CEA_LGPL -src/frama-c/plugins/eva/diffed.tsx: CEA_LGPL src/frama-c/plugins/eva/index.tsx: CEA_LGPL -src/frama-c/plugins/eva/layout.ts: CEA_LGPL -src/frama-c/plugins/eva/model.ts: CEA_LGPL src/frama-c/plugins/eva/pkg.json: .ignore -src/frama-c/plugins/eva/probeinfos.tsx: CEA_LGPL -src/frama-c/plugins/eva/probes.ts: CEA_LGPL -src/frama-c/plugins/eva/sized.tsx: CEA_LGPL -src/frama-c/plugins/eva/stacks.ts: CEA_LGPL src/frama-c/plugins/eva/style.css: .ignore src/frama-c/plugins/eva/summary.css: .ignore -src/frama-c/plugins/eva/valueinfos.tsx: CEA_LGPL -src/frama-c/plugins/eva/valuetable.tsx: CEA_LGPL src/frama-c/plugins/pivot/api/general/index.ts: CEA_LGPL src/frama-c/plugins/studia/api/studia/index.ts: CEA_LGPL src/frama-c/react-pivottable.d.ts: CEA_LGPL diff --git a/ivette/src/dome/renderer/dome.tsx b/ivette/src/dome/renderer/dome.tsx index 2b083a73ee982c656767deb4ec20b2d9829b2af2..51eb64b19f4f5f77a58e6cb45c3ecacd0fcfc471 100644 --- a/ivette/src/dome/renderer/dome.tsx +++ b/ivette/src/dome/renderer/dome.tsx @@ -582,6 +582,29 @@ export function usePromise<T>(job: Promise<T>) { return { result, error, loading }; } +/* Internal type alias */ +type Serialize<A> = (a: A) => string; + +/** + Hook to add a cache system to a function, allowing to reuse previous results. + As the equality used in JS maps does not allow to effectively implement a + cache for complex type, a serialization function can be procured. + The hook returns the cached version of the function. +*/ +export function useCache<K, V>(r: (k: K) => V, s?: Serialize<K>): (k: K) => V { + const { current: cache } = React.useRef(new Map<string, V>()); + const serialize = React.useMemo(() => s ? s : (k: K) => `${k}`, [s]); + const get = React.useCallback((k: K): V => { + const id = serialize(k); + if (cache.has(id)) + return cache.get(id) as V; + const v = r(k); + cache.set(id, v); + return v; + }, [ cache, r, serialize ]); + return get; +} + // -------------------------------------------------------------------------- // --- Timer Hooks // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx index 44f69e6012a22edccb1e207f77f3b0ebfd15977f..51ab7f1a51a34b6623b7188a55f2d270c6132eef 100644 --- a/ivette/src/dome/renderer/frame/toolbars.tsx +++ b/ivette/src/dome/renderer/frame/toolbars.tsx @@ -124,6 +124,8 @@ export interface ButtonProps<A> { selection?: A; /** Selection callback. Receives the button's value. */ onClick?: (value: A | undefined) => void; + /** Button contents */ + children?: React.ReactNode; } /** Toolbar Button. */ @@ -145,6 +147,7 @@ export function Button<A = undefined>(props: ButtonProps<A>) { > {props.icon && <SVG id={props.icon} />} {props.label && <label>{props.label}</label>} + {props.children} </button> ); } diff --git a/ivette/src/frama-c/plugins/eva/api/values/index.ts b/ivette/src/frama-c/plugins/eva/api/values/index.ts index 4f550b91b0a00dd4cabbff1f89bf799c2741af30..c286426c70a761b23db0616b0870188f7456068e 100644 --- a/ivette/src/frama-c/plugins/eva/api/values/index.ts +++ b/ivette/src/frama-c/plugins/eva/api/values/index.ts @@ -121,7 +121,7 @@ export const getStmtInfo: Server.GetRequest< const getProbeInfo_internal: Server.GetRequest< marker, { condition: boolean, effects: boolean, rank: number, - stmt?: Json.key<'#stmt'>, code?: string } + stmt?: Json.key<'#stmt'>, code?: string, evaluable: boolean } > = { kind: Server.RqKind.GET, name: 'plugins.eva.values.getProbeInfo', @@ -132,6 +132,7 @@ const getProbeInfo_internal: Server.GetRequest< rank: Json.jFail(Json.jNumber,'Number expected'), stmt: Json.jKey<'#stmt'>('#stmt'), code: Json.jString, + evaluable: Json.jFail(Json.jBoolean,'Boolean expected'), }), signals: [], }; @@ -139,7 +140,7 @@ const getProbeInfo_internal: Server.GetRequest< export const getProbeInfo: Server.GetRequest< marker, { condition: boolean, effects: boolean, rank: number, - stmt?: Json.key<'#stmt'>, code?: string } + stmt?: Json.key<'#stmt'>, code?: string, evaluable: boolean } >= getProbeInfo_internal; /** Evaluation of an expression or lvalue */ diff --git a/ivette/src/frama-c/plugins/eva/cells.ts b/ivette/src/frama-c/plugins/eva/cells.ts deleted file mode 100644 index 02e3ef3ffdcd13688398468a33c0eb1947b5dd40..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/cells.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -// -------------------------------------------------------------------------- -// --- Cells -// -------------------------------------------------------------------------- - -// Frama-C -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'; - -// -------------------------------------------------------------------------- -// --- Cell Utilities -// -------------------------------------------------------------------------- - -export type callback = () => void; - -export interface ModelCallbacks { - forceUpdate: callback; - forceLayout: callback; -} - -export interface Size { cols: number; rows: number } - -export const EMPTY: Size = { cols: 0, rows: 0 }; - -export function sizeof(text?: string): Size { - if (text === undefined) return EMPTY; - const lines = text.split('\n'); - return { - rows: lines.length, - cols: lines.reduce((w, l) => Math.max(w, l.length), 0), - }; -} - -export function merge(a: Size, b: Size): Size { - return { - cols: Math.max(a.cols, b.cols), - rows: Math.max(a.rows, b.rows), - }; -} - -export function addH(a: Size, b: Size, padding = 0): Size { - return { - cols: a.cols + b.cols + padding, - rows: Math.max(a.rows, b.rows), - }; -} - -export function addV(a: Size, b: Size, padding = 0): Size { - return { - cols: Math.max(a.cols, b.cols), - rows: a.rows + b.rows + padding, - }; -} - -export function addS(s: Size, t: string | undefined): Size { - return t ? merge(s, sizeof(t)) : s; -} - -export function lt(a: Size, b: Size): boolean { - return a.rows < b.rows && a.cols < b.cols; -} - -export function leq(a: Size, b: Size): boolean { - return a.rows <= b.rows && a.cols <= b.cols; -} - -// -------------------------------------------------------------------------- -// --- Data -// -------------------------------------------------------------------------- - -export type EvaStatus = 'True' | 'False' | 'Unknown'; -export type EvaAlarm = [EvaStatus, string]; -export type EvaPointedVar = [string, Ast.marker]; -export type Evaluation = Values.evaluation; - -const emptyEvaluation: Values.evaluation = { - value: '', - alarms: [], - pointedVars: [], -}; - -export interface EvaValues { - errors?: string; - vBefore: Evaluation; - vAfter?: Evaluation; - vThen?: Evaluation; - vElse?: Evaluation; - size: Size; -} - -// -------------------------------------------------------------------------- -// --- Value Cache -// -------------------------------------------------------------------------- - -export interface Probe { - fct: string | undefined; - marker: Ast.marker; -} - -export class ValueCache { - - private readonly state: ModelCallbacks; - private readonly probes = new Map<Ast.marker, Size>(); // Marker -> max in column - private readonly stacks = new Map<string, Size>(); // Callstack -> max in row - private readonly vcache = new Map<string, EvaValues>(); // '<Marker><@Callstack>?' -> value - private smax = EMPTY; // max cell size - - constructor(state: ModelCallbacks) { - this.state = state; - } - - clear(): void { - this.smax = EMPTY; - this.probes.clear(); - this.stacks.clear(); - this.vcache.clear(); - this.state.forceLayout(); - } - - // --- Cached Measures - - getMaxSize(): Size { return this.smax; } - - getProbeSize(target: Ast.marker): Size { - return this.probes.get(target) ?? EMPTY; - } - - private static stackKey(fct: string, callstack: Values.callstack): string { - return `${fct}::${callstack}`; - } - - getStackSize(fct: string, callstack: Values.callstack): Size { - const key = ValueCache.stackKey(fct, callstack); - return this.stacks.get(key) ?? EMPTY; - } - - // --- Cached Values & Request Update - - getValues( - { fct, marker }: Probe, - callstack: Values.callstack | undefined, - ): EvaValues { - const key = callstack !== undefined ? `${marker}@${callstack}` : marker; - const cache = this.vcache; - const cached = cache.get(key); - if (cached) return cached; - const newValue: EvaValues = { vBefore: emptyEvaluation, size: EMPTY }; - if (callstack !== undefined && fct === undefined) - return newValue; - // callstack !== undefined ==> fct !== undefined) - cache.set(key, newValue); - Server - .send(Values.getValues, { target: marker, callstack }) - .then((r) => { - newValue.errors = undefined; - if (r.vBefore) newValue.vBefore = r.vBefore; - newValue.vAfter = r.vAfter; - newValue.vThen = r.vThen; - newValue.vElse = r.vElse; - if (this.updateLayout(marker, fct, callstack, newValue)) - this.state.forceLayout(); - else - this.state.forceUpdate(); - }) - .catch((err) => { - newValue.errors = `$Error: ${err}`; - this.state.forceUpdate(); - }); - return newValue; - } - - // --- Updating Measures - - private updateLayout( - target: Ast.marker, - fct: string | undefined, - callstack: Values.callstack | undefined, - v: EvaValues, - ): boolean { - // measuring cell - let s = sizeof(v.vBefore.value); - s = addS(s, v.vAfter?.value); - s = addS(s, v.vThen?.value); - s = addS(s, v.vElse?.value); - v.size = s; - // max cell size - const { smax } = this; - let small = leq(s, smax); - if (!small) this.smax = merge(s, smax); - // max size for probe column - const ps = this.getProbeSize(target); - if (!leq(s, ps)) { - this.probes.set(target, merge(ps, s)); - small = false; - } - // max size for stack row - if (callstack !== undefined) { - if (fct === undefined) small = false; - else { - const key = ValueCache.stackKey(fct, callstack); - const cs = this.stacks.get(key) ?? EMPTY; - if (!leq(s, cs)) { - this.stacks.set(key, merge(cs, s)); - small = false; - } - } - } - // request new layout if not small enough - return !small; - } - -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/diffed.tsx b/ivette/src/frama-c/plugins/eva/diffed.tsx deleted file mode 100644 index 5859784ad91d73f565d7fa09b46ac37dfd224170..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/diffed.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -// -------------------------------------------------------------------------- -// --- Diff Text Rendering -// -------------------------------------------------------------------------- - -import React from 'react'; -import { Change, diffChars } from 'diff'; - -const MODIFIED = 'eva-diff eva-diff-modified'; -const REMOVED = 'eva-diff eva-diff-removed'; -const ADDED = 'eva-diff eva-diff-added'; -const SHADOW = 'eva-diff eva-diff-shadow'; - -export class DiffBuffer { - - private added = false; - private removed = false; - private value = ''; - private scratch = ''; - private contents: React.ReactNode[] = []; - - constructor() { - this.push = this.push.bind(this); - } - - clear(): void { - this.added = false; - this.removed = false; - this.value = ''; - this.scratch = ''; - this.contents = []; - } - - push(c: Change): void { - if (!c.added && !c.removed) { - this.flush(); - this.value += c.value; - this.flush(); - } else { - if (c.added) this.added = true; - if (c.removed) { - this.removed = true; - this.value += c.value; - } - } - } - - private flush(): void { - const { value, added, removed } = this; - if (added && removed) { - if (value) { - this.scratch += '\0'.repeat(value.length); - this.contents.push( - <span className={MODIFIED} title="Modified">{value}</span>, - ); - } - } else if (removed) { - if (value) { - this.contents.push( - <span className={REMOVED} title="Removed">{value}</span>, - ); - } - } else if (added) { - this.contents.push( - <span className={ADDED}><span className={SHADOW} /></span>, - ); - } else if (value) { - this.scratch += value; - this.contents.push(value); - } - this.value = ''; - this.added = false; - this.removed = false; - } - - getContents(): React.ReactNode { - this.flush(); - return React.Children.toArray(this.contents); - } - - getScratch(): string { - this.flush(); - return this.scratch; - } - -} - -/* --------------------------------------------------------------------------*/ -/* --- Diff2 Component ---*/ -/* --------------------------------------------------------------------------*/ - -export interface Diff2Props { - text: string; - diff: string; -} - -export function Diff2(props: Diff2Props): JSX.Element { - const { text, diff } = props; - const contents = React.useMemo<React.ReactNode>(() => { - if (text === diff) return text; - const buffer = new DiffBuffer(); - const chunks = diffChars(text, diff); - chunks.forEach(buffer.push); - return buffer.getContents(); - }, [text, diff]); - return <>{contents}</>; -} - -/* --------------------------------------------------------------------------*/ -/* --- Diff3 Component ---*/ -/* --------------------------------------------------------------------------*/ - -export interface Diff3Props { - text: string; - diffA: string; - diffB: string; -} - -export function Diff3(props: Diff3Props): JSX.Element { - const { text, diffA, diffB } = props; - const contents = React.useMemo<React.ReactNode>(() => { - if (text === diffA && text === diffB) return text; - const buffer = new DiffBuffer(); - diffChars(diffA, diffB).forEach(buffer.push); - const scratch = buffer.getScratch(); - buffer.clear(); - diffChars(text, scratch).forEach(buffer.push); - return buffer.getContents(); - }, [text, diffA, diffB]); - return <>{contents}</>; -} - -/* --------------------------------------------------------------------------*/ -/* --- Diff Component ---*/ -/* --------------------------------------------------------------------------*/ - -export interface DiffProps { - text?: string; - diff?: string; - diff2?: string; -} - - -export function Diff(props: DiffProps): JSX.Element | null { - const { text, diff, diff2 } = props; - if (text === undefined) - return null; - if (diff !== undefined && diff2 !== undefined) - return <Diff3 text={text} diffA={diff} diffB={diff2} />; - if (diff !== undefined) { - if (diff === text) return <>{text}</>; - return <Diff2 text={text} diff={diff} />; - } - if (diff2 !== undefined) { - if (diff2 === text) return <>{text}</>; - return <Diff2 text={text} diff={diff2} />; - } - return <>{text}</>; -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/index.tsx b/ivette/src/frama-c/plugins/eva/index.tsx index 26595d2539b505e4c4d894dd61c780abbcabd0f5..2df245b2abe7bf7506b891b4be2c375a894f0e8a 100644 --- a/ivette/src/frama-c/plugins/eva/index.tsx +++ b/ivette/src/frama-c/plugins/eva/index.tsx @@ -24,84 +24,16 @@ // --- Eva Values // -------------------------------------------------------------------------- -// React & Dome -import React from 'react'; -import * as Dome from 'dome'; import * as Ivette from 'ivette'; -import * as Server from 'frama-c/server'; -import { GlobalState, useGlobalState } from 'dome/data/states'; -import { Vfill } from 'dome/layout/boxes'; -import { IconButton } from 'dome/controls/buttons'; -import { AutoSizer } from 'react-virtualized'; -import { Model } from './model'; - -// Locals -import { ProbeInfos } from './probeinfos'; -import { Dimension, ValuesPanel } from './valuetable'; -import { AlarmsInfos, StackInfos } from './valueinfos'; +import { } from 'frama-c/plugins/eva/valuetable'; import { } from './Summary'; import { } from './Coverage'; import './style.css'; -// -------------------------------------------------------------------------- -// --- Values Component -// -------------------------------------------------------------------------- - -const globalModelState = new GlobalState(new Model()); - -function ValuesComponent(): JSX.Element { - const [model] = useGlobalState(globalModelState); - model.mount(); - Dome.useUpdate(model.changed, model.laidout); - Server.onShutdown(() => model.unmount()); - const [zoom, setZoom] = Dome.useNumberSettings('eva-zoom-factor', 0); - return ( - <> - <Ivette.TitleBar> - <IconButton - enabled={zoom > 0} - icon="ZOOM.OUT" - onClick={() => setZoom(zoom - 1)} - /> - <IconButton - enabled={zoom < 20} - icon="ZOOM.IN" - onClick={() => setZoom(zoom + 1)} - /> - </Ivette.TitleBar> - <Vfill> - <ProbeInfos model={model} /> - <Vfill> - <AutoSizer> - {(dim: Dimension) => ( - <ValuesPanel - zoom={zoom} - model={model} - {...dim} - /> - )} - </AutoSizer> - </Vfill> - <AlarmsInfos model={model} /> - <StackInfos model={model} /> - </Vfill> - </> - ); -} - // -------------------------------------------------------------------------- // --- Export Component // -------------------------------------------------------------------------- -Ivette.registerComponent({ - id: 'frama-c.plugins.values', - group: 'frama-c.plugins', - rank: 1, - label: 'Eva Values', - title: 'Values inferred by the Eva analysis', - children: <ValuesComponent />, -}); - Ivette.registerView({ id: 'summary', rank: 1, diff --git a/ivette/src/frama-c/plugins/eva/layout.ts b/ivette/src/frama-c/plugins/eva/layout.ts deleted file mode 100644 index b8e4c1e27ebe807313b4f41b88f06e4e9e15f094..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/layout.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -/* --------------------------------------------------------------------------*/ -/* --- Layout ---*/ -/* --------------------------------------------------------------------------*/ - -import { callstack } from 'frama-c/plugins/eva/api/values'; -import { Probe } from './probes'; -import { StacksCache } from './stacks'; -import { Size, EMPTY, leq, addH, ValueCache } from './cells'; - -export interface LayoutProps { - zoom?: number; - margin: number; -} - -export type RowKind = 'section' | 'probes' | 'values' | 'callstack'; - -export interface Row { - key: string; - fct: string; - kind: RowKind; - probes: Probe[]; - headstack?: string; - stackIndex?: number; - stackCount?: number; - callstack?: callstack; - hlines: number; -} - -/* --------------------------------------------------------------------------*/ -/* --- Layout Enfine ---*/ -/* --------------------------------------------------------------------------*/ - -const HEAD_PADDING = 4; // Left margin -const CELL_PADDING = 4; // Inter cell padding -const TEXT_PADDING = 2; // Intra cell padding -const HCROP = 18; -const VCROP = 1; - -export class LayoutEngine { - - // --- Setup - - private readonly folded: (fct: string) => boolean; - private readonly values: ValueCache; - private readonly stacks: StacksCache; - private readonly hcrop: number; - private readonly vcrop: number; - private readonly margin: number; - - constructor( - props: undefined | LayoutProps, - values: ValueCache, - stacks: StacksCache, - folded: (fct: string) => boolean, - ) { - this.values = values; - this.stacks = stacks; - this.folded = folded; - const zoom = Math.max(0, props?.zoom ?? 0); - this.vcrop = VCROP + 3 * zoom; - this.hcrop = HCROP + zoom; - this.margin = (props?.margin ?? 80) - HEAD_PADDING; - this.push = this.push.bind(this); - } - - // --- Probe Buffer - private byFct?: string; // current function - private byStk = new Map<string, boolean>(); // callstack probes - private skip?: boolean; // skip current function - private rowSize: Size = EMPTY; - private buffer: Probe[] = []; - private rows: Row[] = []; - private chained?: Probe; - - crop(zoomed: boolean, s: Size): Size { - const s$cols = s.cols + TEXT_PADDING; - const cols = zoomed ? s$cols : Math.min(s$cols, this.hcrop); - const rows = zoomed ? s.rows : Math.min(s.rows, this.vcrop); - return { - cols: Math.max(HCROP, cols), - rows: Math.max(VCROP, rows), - }; - } - - layout(ps: Probe[], byCallstacks: Map<string, boolean>): Row[] { - this.chained = undefined; - this.byStk = byCallstacks; - ps.sort(LayoutEngine.order).forEach(this.push); - return this.flush(); - } - - private static order(p: Probe, q: Probe): number { - const fp = p.fct; - const fq = q.fct; - if (fp === fq) { - const rp = p.rank ?? 0; - const rq = q.rank ?? 0; - if (rp < rq) return (-1); - if (rp > rq) return (+1); - if (p.marker < q.marker) return (-1); - if (p.marker > q.marker) return (+1); - } - return 0; - } - - private push(p: Probe): void { - // --- sectionning - const { fct } = p; - if (fct !== this.byFct) { - this.flush(); - this.rows.push({ - key: `S:${fct}`, - kind: 'section', - fct, - probes: [], - hlines: 1, - }); - this.byFct = fct; - this.csRowsCounter = 1; - this.skip = this.folded(fct); - } - if (this.skip) return; - // --- chaining - const q = this.chained; - if (q) q.next = p; - p.prev = q; - this.chained = p; - // --- sizing - const probeSize = this.values.getProbeSize(p.marker); - const s = this.crop(p.zoomed, probeSize); - p.zoomable = p.zoomed || !leq(probeSize, s); - p.minCols = s.cols; - p.maxCols = Math.max(p.minCols, probeSize.cols); - // --- queueing - if (s.cols + this.rowSize.cols > this.margin) - this.flush(); - this.rowSize = addH(this.rowSize, s); - this.rowSize.cols += CELL_PADDING; - this.buffer.push(p); - } - - // --- Flush Rows - - private csRowsCounter = 1; - - private flush(): Row[] { - const ps = this.buffer; - const rs = this.rows; - const fct = this.byFct; - if (fct && ps.length > 0) { - const stk = this.byStk.get(fct); - const hlines = this.rowSize.rows; - if (stk) { - // --- by callstacks - const markers = ps.map((p) => p.marker); - const stacks = this.stacks.getStacks(...markers); - const summary = fct ? this.stacks.getSummary(fct) : false; - const callstacks = stacks.length; - const { csRowsCounter } = this; - rs.push({ - key: `P:${fct}:${csRowsCounter}`, - kind: 'probes', - probes: ps, - stackCount: callstacks, - fct, - hlines: 1, - }); - if (summary && callstacks > 1) rs.push({ - key: `V:${fct}:${csRowsCounter}`, - kind: 'values', - probes: ps, - stackIndex: -1, - stackCount: stacks.length, - fct, - hlines, - }); - stacks.forEach((cs, k) => { - rs.push({ - key: `C:${fct}:${csRowsCounter}:${cs}`, - kind: 'callstack', - probes: ps, - stackIndex: k, - stackCount: callstacks, - callstack: cs, - fct, - hlines, - }); - }); - } else { - // --- not by callstacks - const n = rs.length; - rs.push({ - key: `P#${n}`, - kind: 'probes', - probes: ps, - fct, - hlines: 1, - }, { - key: `V#${n}`, - kind: 'values', - probes: ps, - fct, - hlines, - }); - } - } - this.buffer = []; - this.rowSize = EMPTY; - this.csRowsCounter++; - return rs; - } - -} - -/* --------------------------------------------------------------------------*/ diff --git a/ivette/src/frama-c/plugins/eva/model.ts b/ivette/src/frama-c/plugins/eva/model.ts deleted file mode 100644 index 01e3d14334b3f900c850d943f1c2815a7ae76fca..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/model.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - -// -------------------------------------------------------------------------- -// --- Eva Values -// -------------------------------------------------------------------------- - -// External Libs -import { throttle } from 'lodash'; -import equal from 'react-fast-compare'; -import * as Dome from 'dome'; - -import * as Server from 'frama-c/server'; -import * as States from 'frama-c/states'; -import * as Values from 'frama-c/plugins/eva/api/values'; - -// Model -import { Probe } from './probes'; -import { StacksCache, Callsite } from './stacks'; -import { ModelCallbacks, ValueCache } from './cells'; -import { LayoutProps, LayoutEngine, Row } from './layout'; - -export interface ModelLayout extends LayoutProps { - location?: States.Location; -} - -export type EvalStmt = 'Before' | 'After'; -export type EvalCond = 'Cond' | 'Then' | 'Else'; - -/* --------------------------------------------------------------------------*/ -/* --- EVA Values Model ---*/ -/* --------------------------------------------------------------------------*/ - -export class Model implements ModelCallbacks { - - constructor() { - this.forceUpdate = this.forceUpdate.bind(this); - this.forceLayout = this.forceLayout.bind(this); - this.forceReload = this.forceReload.bind(this); - this.computeLayout = this.computeLayout.bind(this); - this.setLayout = throttle(this.setLayout.bind(this), 300); - this.metaSelection = this.metaSelection.bind(this); - this.getRowKey = this.getRowKey.bind(this); - this.getRowCount = this.getRowCount.bind(this); - this.getRowLines = this.getRowLines.bind(this); - this.isFolded = this.isFolded.bind(this); - } - - // --- Probes - - private vstmt: EvalStmt = 'Before'; - private vcond: EvalCond = 'Cond'; - private selected?: Probe; - private focused?: Probe; - private callstack?: Values.callstack; - private remanent?: Probe; // last transient - private probes = new Map<string, Probe>(); - private folded = new Map<string, boolean>(); // folded functions - private byCallstacks = new Map<string, boolean>(); - - getFocused() { return this.focused; } - isFocused(p: Probe | undefined) { return this.focused === p; } - - getProbe(location: States.Location | undefined): Probe | undefined { - if (!location) return undefined; - const { fct, marker } = location; - if (fct && marker) { - let p = this.probes.get(marker); - if (!p) { - p = new Probe(this, fct, marker); - this.probes.set(marker, p); - p.requestProbeInfo(); - } - return p; - } - return undefined; - } - - addProbe(location: States.Location) { - const { fct, marker } = location; - if (fct && marker) { - const probe = new Probe(this, fct, marker); - probe.setPersistent(); - probe.requestProbeInfo(); - this.probes.set(marker, probe); - this.forceLayout(); - } - } - - removeProbe(probe: Probe) { - probe.setTransient(); - if (this.selected === probe) - this.clearSelection(); - } - - getStacks(p: Probe | undefined): Values.callstack[] { - return p ? this.stacks.getStacks(p.marker) : []; - } - - getVstmt(): EvalStmt { return this.vstmt; } - getVcond(): EvalCond { return this.vcond; } - setVstmt(s: EvalStmt) { this.vstmt = s; this.forceUpdate(); } - setVcond(s: EvalCond) { this.vcond = s; this.forceUpdate(); } - - isFolded(fct: string): boolean { - return (this.focused?.fct !== fct) && (this.folded.get(fct) ?? false); - } - - isFoldable(fct: string): boolean { - return this.focused?.fct !== fct; - } - - setFolded(fct: string, folded: boolean) { - this.folded.set(fct, folded); - this.forceLayout(); - } - - isByCallstacks(fct: string): boolean { - return this.byCallstacks.get(fct) ?? false; - } - - setByCallstacks(fct: string, b: boolean) { - this.byCallstacks.set(fct, b); - this.forceLayout(); - } - - clearFunction(fct: string) { - let selected = false; - this.probes.forEach((p) => { - if (p.fct === fct) { - p.setTransient(); - if (this.selected === p) - selected = true; - } - }); - if (selected) - this.clearSelection(); - } - - // --- Caches - - readonly stacks = new StacksCache(this); - readonly values = new ValueCache(this); - - // --- Rows - - private lock = false; - private layout: ModelLayout = { margin: 80 }; - private rows: Row[] = []; - - getRow(index: number): Row | undefined { - return this.rows[index]; - } - - getRowCount() { - return this.rows.length; - } - - getRowKey(index: number): string { - const row = this.rows[index]; - return row ? row.key : `#${index}`; - } - - getRowLines(index: number): number { - const row = this.rows[index]; - return row ? row.hlines : 0; - } - - setSelectedRow(row: Row) { - const cs = row.callstack; - if (cs !== this.callstack) { - this.callstack = cs; - this.forceUpdate(); - } - } - - isSelectedRow(row: Row): boolean { - if (!this.byCallstacks) return false; - const cs = this.callstack; - return cs !== undefined && cs === row.callstack; - } - - isAlignedRow(row: Row): boolean { - const cs = this.callstack; - const cr = row.callstack; - return cs !== undefined && cr !== undefined && this.stacks.aligned(cs, cr); - } - - getCallstack(): Values.callstack | undefined { - return this.callstack; - } - - getCalls(): Callsite[] { - const c = this.callstack; - return c === undefined ? [] : this.stacks.getCalls(c); - } - - // --- Throttled - setLayout(ly: ModelLayout) { - if (!equal(this.layout, ly)) { - this.layout = ly; - this.selected = this.getProbe(ly.location); - this.forceLayout(); - } - } - - metaSelection(location: States.Location) { - const p = this.getProbe(location); - if (p && p.transient) - p.setPersistent(); - } - - clearSelection() { - this.focused = undefined; - this.selected = undefined; - this.remanent = undefined; - this.forceLayout(); - } - - // --- Recompute Layout - - private computeLayout() { - if (this.lock) return; - this.lock = true; - const s = this.selected; - if (!s) { - this.focused = undefined; - this.remanent = undefined; - } else if (!s.loading) { - this.focused = s; - if (s.code && s.transient) { - this.remanent = s; - } else { - this.remanent = undefined; - } - } - const toLayout: Probe[] = []; - this.probes.forEach((p) => { - if (p.code && (!p.transient || p === this.remanent)) { - toLayout.push(p); - } - }); - const engine = new LayoutEngine( - this.layout, - this.values, - this.stacks, - this.isFolded, - ); - this.rows = engine.layout(toLayout, this.byCallstacks); - this.laidout.emit(); - this.lock = false; - } - - // --- Force Reload (empty caches) - forceReload() { - this.focused = undefined; - this.remanent = undefined; - this.selected = undefined; - this.callstack = undefined; - this.probes.clear(); - this.stacks.clear(); - this.values.clear(); - this.forceLayout(); - this.forceUpdate(); - } - - // --- Events - readonly changed = new Dome.Event('eva-changed'); - readonly laidout = new Dome.Event('eva-laidout'); - - // --- Force Layout - forceLayout() { - setImmediate(this.computeLayout); - } - - // --- Foce Update - forceUpdate() { this.changed.emit(); } - - // --- Global Signals - - mount() { - States.MetaSelection.on(this.metaSelection); - Server.onSignal(Values.changed, this.forceReload); - } - - unmount() { - States.MetaSelection.off(this.metaSelection); - Server.offSignal(Values.changed, this.forceReload); - } - -} - -export interface ModelProp { - model: Model; -} diff --git a/ivette/src/frama-c/plugins/eva/probeinfos.tsx b/ivette/src/frama-c/plugins/eva/probeinfos.tsx deleted file mode 100644 index 9baa4c9acdb5fc6ee9a3291fbee5eacd8d73f385..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/probeinfos.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -// -------------------------------------------------------------------------- -// --- Eva Values -// -------------------------------------------------------------------------- - -// React & Dome -import React from 'react'; -import { Hpack, Filler } from 'dome/layout/boxes'; -import { Code } from 'dome/controls/labels'; -import { IconButton } from 'dome/controls/buttons'; -import { ButtonGroup, Button } from 'dome/frame/toolbars'; - -// Frama-C -import * as States from 'frama-c/states'; - -// Locals -import { SizedArea } from './sized'; -import { sizeof } from './cells'; -import { ModelProp } from './model'; -import { Stmt } from './valueinfos'; - -// -------------------------------------------------------------------------- -// --- Probe Editor -// -------------------------------------------------------------------------- - -function ProbeEditor(props: ModelProp): JSX.Element | null { - const { model } = props; - const probe = model.getFocused(); - if (!probe || !probe.code) return null; - const { code } = probe; - const { stmt, marker } = probe; - const { cols, rows } = sizeof(code); - const { transient } = probe; - const { zoomed } = probe; - const { zoomable } = probe; - return ( - <> - <div className="eva-probeinfo-code"> - <SizedArea cols={cols} rows={rows}>{code}</SizedArea> - </div> - <Code><Stmt stmt={stmt} marker={marker} /></Code> - <IconButton - icon="SEARCH" - className="eva-probeinfo-button" - display={zoomable} - selected={zoomed} - title="Resize cells to fit values" - onClick={() => { if (probe) probe.setZoomed(!zoomed); }} - /> - <IconButton - icon="PIN" - className="eva-probeinfo-button" - selected={!transient} - title={transient ? 'Make the probe persistent' : 'Release the probe'} - onClick={() => { - if (probe) { - if (transient) probe.setPersistent(); - else probe.setTransient(); - } - }} - /> - <IconButton - icon="CIRC.CLOSE" - className="eva-probeinfo-button" - display={!transient} - title="Discard the probe" - onClick={() => { - if (probe) { - probe.setTransient(); - const p = probe.next ?? probe.prev; - if (p) setImmediate(() => { - States.setSelection({ fct: p.fct, marker: p.marker }); - }); - else model.clearSelection(); - } - }} - /> - </> - ); -} - -// -------------------------------------------------------------------------- -// --- Probe Panel -// -------------------------------------------------------------------------- - -export function ProbeInfos(props: ModelProp): JSX.Element { - const { model } = props; - const probe = model.getFocused(); - const fct = probe?.fct; - const byCS = fct ? model.isByCallstacks(fct) : false; - const effects = probe ? probe.effects : false; - const condition = probe ? probe.condition : false; - const summary = fct ? model.stacks.getSummary(fct) : false; - const vcond = model.getVcond(); - const vstmt = model.getVstmt(); - return ( - <Hpack className="eva-probeinfo"> - <ProbeEditor model={model} /> - <Filler /> - <ButtonGroup - enabled={!!probe} - className="eva-probeinfo-state" - > - <Button - label={'\u2211'} - title="Show Callstacks Summary" - selected={summary} - visible={byCS} - onClick={() => { if (fct) model.stacks.setSummary(fct, !summary); }} - /> - <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> - ); -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/probes.ts b/ivette/src/frama-c/plugins/eva/probes.ts deleted file mode 100644 index e9bdb0da9ca7d49f0887b03e57d475913505cfe2..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/probes.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -/* --------------------------------------------------------------------------*/ -/* --- Probes ---*/ -/* --------------------------------------------------------------------------*/ - -// Frama-C -import * as Server from 'frama-c/server'; -import * as Values from 'frama-c/plugins/eva/api/values'; -import * as Ast from 'frama-c/kernel/api/ast'; - -// Model -import { ModelCallbacks } from './cells'; - -/* --------------------------------------------------------------------------*/ -/* --- Probe State ---*/ -/* --------------------------------------------------------------------------*/ - -const LabelSize = 12; - -export class Probe { - - // properties - readonly fct: string; - readonly marker: Ast.marker; - readonly model: ModelCallbacks; - next?: Probe; - prev?: Probe; - transient = true; - loading = true; - code?: string; - stmt?: Ast.marker; - rank?: number; - minCols: number = LabelSize; - maxCols: number = LabelSize; - zoomed = true; - zoomable = false; - effects = false; - condition = false; - - constructor(state: ModelCallbacks, fct: string, marker: Ast.marker) { - this.fct = fct; - this.marker = marker; - this.model = state; - this.requestProbeInfo = this.requestProbeInfo.bind(this); - } - - requestProbeInfo(): void { - this.loading = true; - Server - .send(Values.getProbeInfo, this.marker) - .then(({ code, stmt, rank, effects, condition }) => { - this.code = code; - this.stmt = stmt; - this.rank = rank; - this.effects = effects; - this.condition = condition; - this.loading = false; - }) - .catch(() => { - this.code = '(error)'; - this.stmt = undefined; - this.rank = undefined; - this.effects = false; - this.condition = false; - this.loading = false; - }) - .finally(this.model.forceLayout); - } - - // -------------------------------------------------------------------------- - // --- Internal State - // -------------------------------------------------------------------------- - - setPersistent(): void { this.updateTransient(false); } - setTransient(): void { this.updateTransient(true); } - - private updateTransient(tr: boolean): void { - if (this.transient !== tr) { - this.transient = tr; - this.model.forceLayout(); - } - } - - setZoomed(zoomed: boolean): void { - if (zoomed !== this.zoomed) { - this.zoomed = zoomed; - this.model.forceLayout(); - } - } - -} - -/* --------------------------------------------------------------------------*/ diff --git a/ivette/src/frama-c/plugins/eva/sized.tsx b/ivette/src/frama-c/plugins/eva/sized.tsx deleted file mode 100644 index 95496201039ff396d631d14007332faca894ac48..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/sized.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -// -------------------------------------------------------------------------- -// --- Sized Cell -// -------------------------------------------------------------------------- - -import React from 'react'; - -// -------------------------------------------------------------------------- -// --- Measurer -// -------------------------------------------------------------------------- - -export class Streamer { - private readonly v0: number; - private readonly vs: number[] = []; - private v?: number; - constructor(v0: number) { - this.v0 = v0; - } - - push(v: number): void { - const { vs } = this; - vs.push(Math.round(v)); - if (vs.length > 200) vs.shift(); - } - - mean(): number { - if (this.v === undefined) { - const { vs } = this; - const n = vs.length; - if (n > 0) { - const m = vs.reduce((s, v) => s + v, 0) / n; - this.v = Math.round(m + 0.5); - } else { - this.v = this.v0; - } - } - return this.v; - } - -} - -export class FontSizer { - a = 0; - b = 0; - k: Streamer; - p: Streamer; - - constructor(k: number, p: number) { - this.k = new Streamer(k); - this.p = new Streamer(p); - } - - push(x: number, y: number): void { - const a0 = this.a; - const b0 = this.b; - if (x !== a0 && a0 !== 0) { - const k = (y - b0) / (x - a0); - const p = y - k * x; - this.k.push(k); - this.p.push(p); - } - this.a = x; - this.b = y; - } - - capacity(y: number): number { - const k = this.k.mean(); - const p = this.p.mean(); - return Math.round(0.5 + (y - p) / k); - } - - dimension(n: number): number { - const k = this.k.mean(); - const p = this.p.mean(); - return p + n * k; - } - - dump(x: string): string { - const k = this.k.mean(); - const p = this.p.mean(); - return `${k}.${x}+${p}`; - } - -} - -/* --------------------------------------------------------------------------*/ -/* --- Sizing Component ---*/ -/* --------------------------------------------------------------------------*/ - -export const WSIZER = new FontSizer(7, 6); -export const HSIZER = new FontSizer(15, 6); - -export interface SizedAreaProps { - cols: number; - rows: number; - children?: React.ReactNode; -} - -export function SizedArea(props: SizedAreaProps): JSX.Element { - const { rows, cols, children } = props; - const refSizer = React.useCallback( - (ref: null | HTMLDivElement) => { - if (ref) { - const r = ref.getBoundingClientRect(); - WSIZER.push(cols, r.width); - HSIZER.push(rows, r.height); - } - }, [rows, cols], - ); - return ( - <div - ref={refSizer} - className="eva-sized-area dome-text-cell" - > - {children} - </div> - ); -} - -/* --------------------------------------------------------------------------*/ diff --git a/ivette/src/frama-c/plugins/eva/stacks.ts b/ivette/src/frama-c/plugins/eva/stacks.ts deleted file mode 100644 index 447890eef2d65a65d95e4b89afc1dc25441241d4..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/stacks.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -// -------------------------------------------------------------------------- -// --- CallStacks -// -------------------------------------------------------------------------- - -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 { ModelCallbacks } from './cells'; - -// -------------------------------------------------------------------------- -// --- Callstack infos -// -------------------------------------------------------------------------- - -export type callstacks = Values.callstack[]; -export interface Callsite { - callee: string; - caller?: string; - stmt?: Ast.marker; -} - -function equalSite(a: Callsite, b: Callsite): boolean { - return a.stmt === b.stmt && a.callee === b.callee; -} - -// -------------------------------------------------------------------------- -// --- CallStacks Cache -// -------------------------------------------------------------------------- - -export class StacksCache { - - private readonly model: ModelCallbacks; - private readonly stacks = new Map<string, callstacks>(); - private readonly summary = new Map<string, boolean>(); - private readonly calls = new Map<Values.callstack, Callsite[]>(); - - // -------------------------------------------------------------------------- - // --- LifeCycle - // -------------------------------------------------------------------------- - - constructor(state: ModelCallbacks) { - this.model = state; - } - - clear(): void { - this.stacks.clear(); - this.calls.clear(); - } - - // -------------------------------------------------------------------------- - // --- Getters - // -------------------------------------------------------------------------- - - getSummary(fct: string): boolean { - return this.summary.get(fct) ?? true; - } - - setSummary(fct: string, s: boolean): void { - this.summary.set(fct, s); - this.model.forceLayout(); - } - - getStacks(...markers: Ast.marker[]): callstacks { - if (markers.length === 0) return []; - const key = markers.join('$'); - const cs = this.stacks.get(key); - if (cs !== undefined) return cs; - this.stacks.set(key, []); - this.requestStacks(key, markers); - return []; - } - - getCalls(cs: Values.callstack): Callsite[] { - const fs = this.calls.get(cs); - if (fs !== undefined) return fs; - this.calls.set(cs, []); - this.requestCalls(cs); - return []; - } - - aligned(a: Values.callstack, b: Values.callstack): boolean { - if (a === b) return true; - const ca = this.getCalls(a); - const cb = this.getCalls(b); - let ka = ca.length - 1; - let kb = cb.length - 1; - while (ka >= 0 && kb >= 0 && equalSite(ca[ka], cb[kb])) { - --ka; - --kb; - } - return ka < 0 || kb < 0; - } - - // -------------------------------------------------------------------------- - // --- Fetchers - // -------------------------------------------------------------------------- - - private requestStacks(key: string, markers: Ast.marker[]): void { - Server - .send(Values.getCallstacks, markers) - .then((stacks: callstacks) => { - this.stacks.set(key, stacks); - this.model.forceLayout(); - this.model.forceUpdate(); - }); - } - - private requestCalls(cs: Values.callstack): void { - Server - .send(Values.getCallstackInfo, cs) - .then((calls) => { - this.calls.set(cs, calls); - this.model.forceUpdate(); - }); - } - -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/style.css b/ivette/src/frama-c/plugins/eva/style.css index 3dd5be0b82191f7e234969da8d661bffaa387a1b..12f78a00a4358725fd5401b9c446e03d9fab7cdd 100644 --- a/ivette/src/frama-c/plugins/eva/style.css +++ b/ivette/src/frama-c/plugins/eva/style.css @@ -1,82 +1,62 @@ /* -------------------------------------------------------------------------- */ -/* --- Probe Panel --- */ +/* --- Call Stack Info --- */ /* -------------------------------------------------------------------------- */ -.eva-probeinfo { - min-height: 32px; - padding-left: 6px; - padding-top: 2px; - padding-bottom: 4px; - width: 100%; - background: var(--background-profound); - display: flex; +:root { + --eva-min-width: 90px; } -.eva-probeinfo-label { - flex: 0 1 auto; - min-width: 22px; - text-align: left; +.eva-info { + width: 100%; + display: flex; + flex-wrap: nowrap; + align-items: center; + background: var(--background-profound); + padding: 2px; } -.eva-probeinfo-code { - flex: 0 1 auto; - height: fit-content; - background: var(--background-intense); - min-width: 120px; - width: auto; - border: thin solid var(--border); - padding-left: 6px; - padding-right: 6px; - border-radius: 4px; - overflow: hidden; +.eva-info-wrap { + flex: auto; + flex-wrap: wrap; } -.eva-probeinfo-stmt { - flex: 0 0 auto; - margin-left: 2px; - margin-right: 0px; - margin-top: 2px; +.eva-callsite { + flex: 0 0 auto; + fill: var(--selected-element); + background: var(--background-softer); + border-radius: 4px; + border: thin solid var(--border); + padding-right: 7px; } -.eva-probeinfo-button { - flex: 0 0 auto; - margin: 1px; - min-width: 16px; +.eva-cell-container { + display: flex; + justify-content: center; } -.eva-probeinfo-state { - margin-top: 2px; - margin-bottom: 2px; +.eva-cell-left { + flex: 1; } -/* -------------------------------------------------------------------------- */ -/* --- Call Satck Info --- */ -/* -------------------------------------------------------------------------- */ - -.eva-info { - width: 100%; - flex-wrap: wrap; - background: var(--background-profound); - padding-top: 3px; - padding-left: 12px; - padding-bottom: 2px; +.eva-cell-content { + margin-left: 4px; + margin-right: 4px; + flex: 0 auto; + overflow: hidden; + display: flex; + align-items: baseline; } -.eva-callsite { - flex: 0 0 auto; - fill: var(--selected-element); - background: var(--background-softer); - border-radius: 4px; - border: thin solid var(--border); - padding-right: 7px; -} - -.eva-callsite.eva-focused { - background: var(--warning-button-color); +.eva-header-text-overflow { + overflow: hidden; + text-overflow: ellipsis; } -.eva-callsite.eva-focused.eva-transient { - background: var(--code-select); +.eva-cell-right { + display: flex; + flex: 1; + min-width: max-content; + justify-content: flex-end; } /* -------------------------------------------------------------------------- */ @@ -84,17 +64,16 @@ /* -------------------------------------------------------------------------- */ .eva-cell-alarms { - fill: orange; - position: absolute; - top: -1px; - right: 3px; - z-index: 1; + z-index: 1; + margin: 0px; + padding: 0px; + margin-right: 4px; } .eva-alarm-info { - font-size: x-small; - padding: 0px; - margin: 1px; + font-size: x-small; + padding: 0px; + margin: 1px; } .eva-alarm-none { display: none; } @@ -103,173 +82,233 @@ .eva-alarm-Unknown { fill: var(--eva-alarms-unknown); } /* -------------------------------------------------------------------------- */ -/* --- Styling Values --- */ +/* --- Styling Statement display --- */ /* -------------------------------------------------------------------------- */ .eva-stmt { - color: var(--info-text); - text-select: none; -} - -.eva-sized-area { - padding: 3px; - white-space: pre; - overflow: hidden; - text-overflow: ellipsis; + color: var(--info-text); + font-weight: normal; } /* -------------------------------------------------------------------------- */ -/* --- Table Rows --- */ +/* --- Function Section Header --- */ /* -------------------------------------------------------------------------- */ -.eva-row { - display: flex; - position: relative; - flex: 0 1 auto; - height: 100%; +.eva-function { + padding: 2px; + background: var(--background-profound); + height: 25px; + align-items: center; +} + +.eva-fct-fold { + margin-left: 4px; + margin-right: 8px; + padding: 0px; +} + +.eva-fct-name { + font-weight: bold; } /* -------------------------------------------------------------------------- */ -/* --- Table Heads --- */ +/* --- Table General CSS --- */ /* -------------------------------------------------------------------------- */ -.eva-function { - padding-top: 0px; - background: var(--background-profound); +.eva-table { + border: 0px; + border-spacing: 0px; +} + +.eva-table-header-sticky { + position: sticky; + top: 0; + z-index: +1; } -.eva-head { - padding-top: 2px; - color: var(--info-text); - text-align: center; - border-left: thin solid var(--border); - border-bottom: thin solid var(--border); +.eva-table tr:nth-child(n + 4) { + cursor: pointer; } -.eva-probes .eva-head { - border-top: thin solid var(--border); +.eva-table tr th { + border-right: thin solid var(--border); + border-top: thin solid var(--border); + border-bottom: thin solid var(--border); + height: 22px; + min-height: 22px; + max-height: 22px; + min-width: calc(var(--eva-min-width) + 30px); + max-width: calc(var(--eva-min-width) + 30px); } -.eva-phantom { - visibility: hidden; - height: 0; +.eva-table tr:nth-child(2n) { + background-color: var(--background-alterning-even); +} + +.eva-table tr:nth-child(2n + 1) { + background-color: var(--background-alterning-odd); +} + +.eva-table-container { + position: relative; + overflow: auto; + height: 100%; + min-height: 66px; +} + +.eva-functions-section { + display: grid; + overflow: auto; + align-content: start; +} + +.eva-nb-callstacks { + color: var(--info-text); + padding-right: 11px; } /* -------------------------------------------------------------------------- */ -/* --- Table Cells --- */ +/* --- Table Headers CSS --- */ /* -------------------------------------------------------------------------- */ -.eva-fct-fold { - margin-left: 4px; - margin-right: 8px; - padding: 0px; +.eva-header-tracked { } -.eva-fct-name { - padding: 2px; - font-weight: bold; +.eva-header-tracked-focused { } -.eva-cell { - flex: 1 1 auto; - border-right: thin solid var(--border); - border-bottom: thin solid var(--border); - overflow: hidden; - display: flex; - justify-content: center; - position: relative; +.eva-header-just-focused { + background: var(--code-select); } -.eva-cell .eva-stmt { - margin-left: 0.2em; - padding: 2px 0px; +.eva-header-pinned { + background: var(--eva-probes-pinned); } -.eva-cell-expression { - text-align: center; - text-overflow: ellipsis; - flex: 0 1 auto; - display: inline-block; - overflow: hidden; +.eva-header-pinned-focused { + background: var(--eva-probes-pinned-focused); } -.eva-cell:nth-child(2) { - border-left: thin solid var(--border); +.eva-header-after-condition { + display: flex; + justify-content: center; + gap: 0.3em; } -.eva-probes .eva-cell { - padding: 2px 4px; - text-align: center; - border-top: thin solid var(--border); +.eva-header-buttons-container { + position: absolute; + right: 1px; + top: 0px; + padding-left: 2px; + display: flex; + z-index: +1; + background: inherit; } -.eva-cell * { - user-select: text; +.eva-header-button { + padding: 2px 2px 3px 2px; +} + +.eva-header-text { + display: flex; } /* -------------------------------------------------------------------------- */ -/* --- Cell Diffs --- */ +/* --- Table Callsite Boxes --- */ /* -------------------------------------------------------------------------- */ -.eva-diff-shadow { - border: solid transparent 2px; - position: relative; - z-index: -1; +.eva-table-callsite-box { + width: 18px; + min-width: 18px; + background: var(--background); + border: 0px; + text-align: center; + color: var(--info-text); + border-left: thin solid var(--border); + border-right: thin solid var(--border); + border-bottom: thin solid var(--border); + padding: 2px; } -.eva-diff-added { } -.eva-diff-removed { text-decoration: strike } - -.eva-state-Before .eva-diff { background: var(--eva-state-before); } -.eva-state-After .eva-diff { background: var(--eva-state-after); } -.eva-state-Cond .eva-diff { background: var(--eva-state-before); } -.eva-state-Then .eva-diff { background: var(--eva-state-then); } -.eva-state-Else .eva-diff { background: var(--eva-state-else); } +tr:first-of-type > .eva-table-callsite-box { + border-top: thin solid var(--border); +} /* -------------------------------------------------------------------------- */ -/* --- Table Rows Background --- */ +/* --- Table Values --- */ /* -------------------------------------------------------------------------- */ -/* --- Probes --- */ +.eva-table-values { + position: relative; + border: 0px; + padding: 2px 3px 2px 3px; + border-bottom: thin solid var(--border); + border-right: thin solid var(--border); + min-width: var(--eva-min-width); + font-family: Andale Mono, monospace; + font-size: 9pt; + height: 22px; + min-height: 22px; + max-height: 22px; + white-space: pre; +} -.eva-probes .eva-cell { - background: var(--eva-probes-pinned); +.eva-table-values-left { + text-align: left; } -.eva-probes .eva-focused { - background: var(--eva-probes-pinned-focused); +.eva-table-values-center { + text-align: center; } -.eva-probes .eva-transient { - background: var(--eva-probes-transient); - align-items: center; +.eva-table-descrs { + background-color: var(--background-sidebar); + border-right: thin solid var(--border); } -.eva-probes .eva-transient.eva-focused { - background: var(--eva-probes-transient-focused); +.eva-table-descr-sticky { + position: sticky; + top: 22px; + z-index: +1; } +.eva-table-callsite-box.eva-table-header-sticky { + left: 0px; + z-index: +2; +} -/* --- Values / Callstacks --- */ +.eva-table-value-sticky { + position: sticky; + left: 0px; + z-index: +1; +} -.eva-values .eva-cell { - background: var(--background-alterning-odd); +.eva-values-position { + display: inline-block; + width: 0px; } -.eva-callstack.eva-row-odd .eva-cell { - background: var(--background-alterning-odd); +.eva-table-text { + cursor: text; + user-select: text; + margin-left: 1px; + margin-right: 1px; } -.eva-callstack.eva-row-even .eva-cell { - background: var(--background-alterning-even); +/* -------------------------------------------------------------------------- */ +/* --- Miscellaneous --- */ +/* -------------------------------------------------------------------------- */ + +.eva-button { + margin: 0px; } -.eva-callstack.eva-row-aligned { - background: var(--grid-layout-holder); +.eva-italic { + font-style: italic; } -.eva-callstack.eva-row-selected { - background: var(--code-select); +.eva-focused { + background: var(--code-select); } /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/frama-c/plugins/eva/valueinfos.tsx b/ivette/src/frama-c/plugins/eva/valueinfos.tsx deleted file mode 100644 index 28fb0a302494c2d5879a3b7ed5b9cf703650db45..0000000000000000000000000000000000000000 --- a/ivette/src/frama-c/plugins/eva/valueinfos.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* ************************************************************************ */ -/* */ -/* This file is part of Frama-C. */ -/* */ -/* Copyright (C) 2007-2022 */ -/* 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). */ -/* */ -/* ************************************************************************ */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - -// -------------------------------------------------------------------------- -// --- Info Components -// -------------------------------------------------------------------------- - -// React & Dome -import React from 'react'; -import { classes } from 'dome/misc/utils'; -import { Hpack, Vpack } from 'dome/layout/boxes'; -import { Code, Cell } from 'dome/controls/labels'; -import * as States from 'frama-c/states'; -import * as Ast from 'frama-c/kernel/api/ast'; -import { ModelProp } from 'frama-c/plugins/eva/model'; - -// Locals -import { EvaAlarm } from './cells'; -import { Callsite } from './stacks'; - -// -------------------------------------------------------------------------- -// --- Stmt Printer -// -------------------------------------------------------------------------- - -interface StmtProps { - stmt?: Ast.marker; - marker: Ast.marker; - short?: boolean; -} - -export function Stmt(props: StmtProps) { - const markersInfo = States.useSyncArray(Ast.markerInfo); - - const { stmt, marker, short } = props; - if (!stmt) return null; - 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}`; - return ( - <span className="dome-text-cell eva-stmt" title={title}> - {text} - </span> - ); -} - -// -------------------------------------------------------------------------- -// --- Alarms Panel -// -------------------------------------------------------------------------- - -export function AlarmsInfos(props: ModelProp) { - const { model } = props; - const probe = model.getFocused(); - if (probe) { - const callstack = model.getCallstack(); - const domain = model.values.getValues(probe, callstack); - const alarms = domain?.vBefore.alarms ?? []; - if (alarms.length > 0) { - const renderAlarm = ([status, alarm]: EvaAlarm) => { - 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 null; -} - -// -------------------------------------------------------------------------- -// --- Stack Panel -// -------------------------------------------------------------------------- - -export function StackInfos(props: ModelProp) { - const { model } = props; - const [, setSelection] = States.useSelection(); - const focused = model.getFocused(); - const callstack = model.getCalls(); - if (callstack.length <= 1) return null; - const makeCallsite = ({ caller, stmt }: Callsite) => { - if (!caller || !stmt) return null; - const key = `${caller}@${stmt}`; - const isFocused = focused?.marker === stmt; - const isTransient = focused?.transient; - const className = classes( - 'eva-callsite', - isFocused && 'eva-focused', - isTransient && 'eva-transient', - ); - 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={className} - onClick={onClick} - onDoubleClick={onDoubleClick} - > - {caller} - <Stmt stmt={stmt} marker={stmt} /> - </Cell> - ); - }; - return ( - <Hpack className="eva-info"> - {callstack.map(makeCallsite)} - </Hpack> - ); -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/valuetable.tsx b/ivette/src/frama-c/plugins/eva/valuetable.tsx index 3e09f9d2c5467581d8e64c430c858b864870b2d6..4c43d8c63f95e93be4924b2881075817f81c7864 100644 --- a/ivette/src/frama-c/plugins/eva/valuetable.tsx +++ b/ivette/src/frama-c/plugins/eva/valuetable.tsx @@ -20,396 +20,1070 @@ /* */ /* ************************************************************************ */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - -// -------------------------------------------------------------------------- -// --- Eva Values -// -------------------------------------------------------------------------- - -// React & Dome import React from 'react'; -import * as Dome from 'dome'; +import _ from 'lodash'; +import * as Ivette from 'ivette'; +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 { GlobalState, useGlobalState } from 'dome/data/states'; + import { classes } from 'dome/misc/utils'; -import { VariableSizeList } from 'react-window'; -import { Hpack, Filler } from 'dome/layout/boxes'; -import { Inset } from 'dome/frame/toolbars'; import { Icon } from 'dome/controls/icons'; -import { Cell } from 'dome/controls/labels'; +import { Inset } from 'dome/frame/toolbars'; +import { Cell, Code } from 'dome/controls/labels'; import { IconButton } from 'dome/controls/buttons'; -import { ModelProp, EvalStmt, EvalCond } from 'frama-c/plugins/eva/model'; +import { Filler, Hpack, Hfill, Vpack, Vfill } from 'dome/layout/boxes'; -// Frama-C -import * as States from 'frama-c/states'; -// Locals -import { SizedArea, HSIZER, WSIZER } from './sized'; -import { Diff } from './diffed'; -import { sizeof, EvaValues, EvaPointedVar, Evaluation } from './cells'; -import { Probe } from './probes'; -import { Row } from './layout'; -import { Callsite } from './stacks'; -import { Stmt } from './valueinfos'; -import './style.css'; - -// -------------------------------------------------------------------------- -// --- Cell Diffs -// -------------------------------------------------------------------------- - -function computeValueDiffs(v: EvaValues, vstate: EvalStmt | EvalCond) { - let here = v.vBefore; - let diff: undefined | Evaluation; - let diff2: undefined | Evaluation; - function setValue(e?: Evaluation) { if (e) { here = e; diff = v.vBefore; } } - switch (vstate) { - case 'Before': diff = v.vAfter; break; - case 'After': setValue(v.vAfter); break; - case 'Then': setValue(v.vThen); break; - case 'Else': setValue(v.vElse); break; - case 'Cond': diff = v.vThen; diff2 = v.vElse; break; - } - const vdiffs = { text: here.value, diff: diff?.value, diff2: diff2?.value }; - return { value: here, vdiffs }; -} -// -------------------------------------------------------------------------- -// --- Table Cell -// -------------------------------------------------------------------------- +/* -------------------------------------------------------------------------- */ +/* --- Miscellaneous definitions --- */ +/* -------------------------------------------------------------------------- */ -interface TableCellProps extends ModelProp { - probe: Probe; - row: Row; -} - -const CELLPADDING = 12; - -function TableCell(props: TableCellProps) { - const { probe, row, model } = props; - const [, setSelection] = States.useSelection(); - const { kind, callstack } = row; - const minWidth = CELLPADDING + WSIZER.dimension(probe.minCols); - const maxWidth = CELLPADDING + WSIZER.dimension(probe.maxCols); - const style = { width: minWidth, maxWidth }; - let contents: React.ReactNode = props.probe.marker; - let valueText = ''; - let pointedVars: EvaPointedVar[] = []; - const { transient, marker } = probe; - const focused = model.getFocused(); - const isFocused = focused === probe; - - switch (kind) { - - // ---- Probe Contents - case 'probes': - { - const { stmt, code } = probe; - contents = ( - <> - <span className='dome-text-cell'>{code}</span> - <Stmt stmt={stmt} marker={marker} short /> - </> - ); - } - break; - - // ---- Values Contents - case 'values': - case 'callstack': - { - const domain = model.values.getValues(probe, callstack); - const { condition } = probe; - const vstate = condition ? model.getVcond() : model.getVstmt(); - const { value, vdiffs } = computeValueDiffs(domain, vstate); - valueText = value.value; - const text = vdiffs.text ?? vdiffs.diff; - const { cols, rows } = sizeof(text); - let status = 'none'; - if (value.alarms.length > 0) { - if (value.alarms.find(([st, _]) => st === 'False')) status = 'False'; - else status = 'Unknown'; - } - if (value.pointedVars.length > 0) - pointedVars = value.pointedVars; - const alarmClass = `eva-cell-alarms eva-alarm-${status}`; - const title = 'At least one alarm is raised in one callstack'; - contents = ( - <> - <Icon className={alarmClass} size={10} title={title} id="WARNING" /> - <SizedArea cols={cols} rows={rows}> - <span className={`eva-state-${vstate}`}> - <Diff {...vdiffs} /> - </span> - </SizedArea> - </> - ); - } - break; +type Request<A, B> = (a: A) => Promise<B>; - } +type Alarm = [ 'True' | 'False' | 'Unknown', string ] +function getAlarmStatus(alarms: Alarm[] | undefined): string { + if (!alarms) return 'none'; + if (alarms.length === 0) return 'none'; + if (alarms.find(([st, _]) => st === 'False')) return 'False'; + else return 'Unknown'; +} - // --- Cell Packing - const className = classes( - 'eva-cell', - transient && 'eva-transient', - isFocused && 'eva-focused', - ); - const onClick = () => { - const location = { fct: probe.fct, marker: probe.marker }; - setSelection({ location }); - model.setSelectedRow(row); - }; - const onDoubleClick = () => { - probe.setPersistent(); - if (probe.zoomable) probe.setZoomed(!probe.zoomed); - }; +type MarkerTracked = [ 'Tracked', boolean ] +type MarkerPinned = [ 'Pinned' , boolean ] +type MarkerStatus = MarkerTracked | MarkerPinned | 'JustFocused' - async function onContextMenu() { - const items: Dome.PopupMenuItem[] = []; - const copyValue = () => navigator.clipboard.writeText(valueText); - if (valueText !== '') - items.push({ label: 'Copy to clipboard', onClick: copyValue }); - 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, marker: lvalMarker }; - const onItemClick = () => model.addProbe(location); - items.push({ label, onClick: onItemClick }); - }); - if (items.length > 0) - items.push('separator'); - const remove = () => model.removeProbe(probe); - const removeLabel = `Remove column for ${probe.code}`; - items.push({ label: removeLabel, onClick: remove }); - if (items.length > 0) Dome.popupMenu(items); - } +function MarkerStatusClass(status: MarkerStatus): string { + if (status === 'JustFocused') return 'eva-header-just-focused'; + const [ kind, focused ] = status; + return 'eva-header-' + kind.toLowerCase() + (focused ? '-focused' : ''); +} + +function isPinnedMarker(status: MarkerStatus): boolean { + if (status === 'JustFocused') return false; + const [ kind ] = status; + return kind === 'Pinned'; +} + +interface TableCellProps { + children?: JSX.Element | JSX.Element[]; + right?: JSX.Element; +} +function TableCell(props: TableCellProps): JSX.Element { + const { children, right } = props; return ( - <div - className={className} - style={style} - onClick={onClick} - onDoubleClick={onDoubleClick} - onContextMenu={onContextMenu} - > - {contents} + <div className='eva-cell-container'> + <div className='eva-cell-left'/> + <div className='eva-cell-content'> + {children} + </div> + <div className='eva-cell-right'> + {right} + </div> </div> ); } -// -------------------------------------------------------------------------- -// --- Table Section -// -------------------------------------------------------------------------- +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Callstack related definitions --- */ +/* -------------------------------------------------------------------------- */ + +/* Callstacks are declared by the server. We add the `Summary` construction to + * cleanly represent the summary of all the callstacks. */ +type callstack = 'Summary' | Values.callstack + +/* Builds a cached version of the `getCallstacks` request */ +function useCallstacksCache(): Request<Ast.marker[], callstack[]> { + const g = React.useCallback((m) => Server.send(Values.getCallstacks, m), []); + const toString = React.useCallback((ms) => ms.join('|'), []); + return Dome.useCache(g, toString); +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Callsite related definitions --- */ +/* -------------------------------------------------------------------------- */ + +/* Representation of a callsite as described in the server */ +interface Callsite { + callee: string; + caller?: string; + stmt?: Ast.marker; +} + +/* Builds a cached version of the `getCallstackInfo` request */ +function useCallsitesCache(): Request<callstack, Callsite[]> { + const get = React.useCallback((c) => { + if (c !== 'Summary') return Server.send(Values.getCallstackInfo, c); + else return Promise.resolve([]); + }, []); + return Dome.useCache(get); +} + +/* -------------------------------------------------------------------------- */ + -interface TableSectionProps { + +/* -------------------------------------------------------------------------- */ +/* --- Probe related definitions --- */ +/* -------------------------------------------------------------------------- */ + +/* A Location is a marker in a function */ +interface Location { + target: Ast.marker; fct: string; - folded: boolean; - foldable: boolean; - onClick: () => void; - byCallstacks: boolean; - onCallstackClick: () => void; +} + +/* An Evaluation keeps track of the values at relevant control point around a + * statement, along with the potential errors */ +interface Evaluation { + errors?: string; + vBefore?: Values.evaluation; + vAfter?: Values.evaluation; + vThen?: Values.evaluation; + vElse?: Values.evaluation; +} + +/* A Probe is a location along with data representing textually what it is, the + * considered statement, if it is an effectfull one or one with conditions. + * Moreover, it gives a function that computes an Evaluation for a given + * callstack. This computation is asynchronous. */ +interface Probe extends Location { + code?: string; + stmt?: Ast.marker; + rank?: number; + evaluable: boolean; + effects?: boolean; + condition?: boolean; + evaluate: Request<callstack, Evaluation> +} + +/* Builds a cached version of the `getValues` request */ +function useEvaluationCache(): Request<[ Location, callstack ], Evaluation> { + type LocStack = [ Location, callstack ]; + const toString = React.useCallback(([ l, c ] : LocStack): string => { + return `${l.fct}:${l.target}:${c}`; + }, []); + const get: Request<LocStack, Evaluation> = React.useCallback(([ l, c ]) => { + const callstack = c === 'Summary' ? undefined : c; + return Server.send(Values.getValues, { ...l, callstack }); + }, []); + return Dome.useCache(get, toString); +} + +/* Builds a cached function that builds a Probe given a Location */ +function useProbeCache(): Request<Location, Probe> { + const toString = React.useCallback((l) => `${l.fct}:${l.target}`, []); + const cache = useEvaluationCache(); + const get = React.useCallback(async (loc: Location): Promise<Probe> => { + const infos = await Server.send(Values.getProbeInfo, loc.target); + const evaluate: Request<callstack, Evaluation> = (c) => cache([ loc, c ]); + return { ...loc, ...infos, evaluate }; + }, [ cache ]); + return Dome.useCache(get, toString); +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Statement Component --- */ +/* -------------------------------------------------------------------------- */ + +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 || !marker) 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>; +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Informations on the alarms in a given callstack --- */ +/* -------------------------------------------------------------------------- */ + +function AlarmsInfos(probe?: Probe): Request<callstack, JSX.Element> { + return async (c: callstack): Promise<JSX.Element> => { + const evaluation = await probe?.evaluate(c); + const alarms = evaluation?.vBefore?.alarms ?? []; + if (alarms.length <= 0) return <></>; + const renderAlarm = ([status, alarm]: Alarm): JSX.Element => { + const className = classes('eva-alarm-info', `eva-alarm-${status}`); + return <Code className={className} icon="WARNING">{alarm}</Code>; + }; + const children = React.Children.toArray(alarms.map(renderAlarm)); + return <Vpack className="eva-info">{children}</Vpack>; + }; +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Informations on the selected callstack --- */ +/* -------------------------------------------------------------------------- */ + +interface StackInfosProps { + callsites: Callsite[]; + isSelected: boolean; + setSelection: (a: States.SelectionActions) => void; close: () => void; } -function TableSection(props: TableSectionProps) { - const { fct, foldable, folded, onClick } = props; - const icon = folded ? 'ANGLE.RIGHT' : - foldable ? 'ANGLE.DOWN' : 'TRIANGLE.RIGHT'; +async function StackInfos(props: StackInfosProps): Promise<JSX.Element> { + const { callsites, setSelection, isSelected, close } = props; + const selectedClass = isSelected ? 'eva-focused' : ''; + const className = classes('eva-callsite', selectedClass); + if (callsites.length <= 1) return <></>; + const makeCallsite = ({ caller, stmt }: Callsite): JSX.Element => { + if (!caller || !stmt) return <></>; + const key = `${caller}@${stmt}`; + const location = { fct: caller, marker: stmt }; + const select = (meta: boolean): void => { + setSelection({ location }); + if (meta) States.MetaSelection.emit(location); + }; + const onClick = (evt: React.MouseEvent): void => { select(evt.altKey); }; + const onDoubleClick = (evt: React.MouseEvent): void => { + evt.preventDefault(); + select(true); + }; + return ( + <Cell + key={key} + icon='TRIANGLE.LEFT' + className={className} + onClick={onClick} + onDoubleClick={onDoubleClick} + > + {caller} + <Stmt stmt={stmt} marker={stmt} /> + </Cell> + ); + }; + const children = React.Children.toArray(callsites.map(makeCallsite)); return ( - <> + <div className='eva-info'> + <Hpack className='eva-info-wrap'>{children}</Hpack> + <Hfill /> <IconButton - className="eva-fct-fold" - size={10} - offset={-1} - icon={icon} - enabled={foldable} - onClick={onClick} + icon='CROSS' + className='eva-button' + onClick={close} /> - <Cell className="eva-fct-name">{fct}</Cell> - <Filler /> + </div> + ); +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Probe Header Component --- */ +/* -------------------------------------------------------------------------- */ +/* --- Header of a column, describing the evaluated expression and its --- */ +/* --- status inside the component (pinned, tracked, etc). --- */ +/* -------------------------------------------------------------------------- */ + +interface ProbeHeaderProps { + probe: Probe; + status: MarkerStatus; + pinProbe: (pin: boolean) => void; + selectProbe: () => void; + removeProbe: () => void; + setSelection: (a: States.SelectionActions) => void; + locEvt: Dome.Event<Location>; +} + +function ProbeHeader(props: ProbeHeaderProps): JSX.Element { + const { probe, status, setSelection, locEvt } = props; + const { code = '(error)', stmt, target, fct } = probe; + const color = classes(MarkerStatusClass(status), 'eva-table-header-sticky'); + const { selectProbe, removeProbe, pinProbe } = props; + const span = 1 + (probe.effects ? 1 : 0) + (probe.condition ? 2 : 0); + const buttonClass = classes('eva-button', 'eva-header-button'); + + // When the location is selected, we scroll the header into view, making it + // appears wherever it was. + const ref = React.createRef<HTMLTableCellElement>(); + locEvt.on((l) => { if (l === probe) ref.current?.scrollIntoView(); }); + + const isPinned = isPinnedMarker(status); + const pinText = isPinned ? 'Unpin' : 'Pin'; + const loc: States.SelectionActions = { location: { fct, marker: target} }; + const onClick = (): void => { setSelection(loc); selectProbe(); }; + const onDoubleClick = (): void => pinProbe(!isPinned); + const onContextMenu = (): void => { + const items: Dome.PopupMenuItem[] = []; + const pinLabel = `${pinText} column for ${code}`; + items.push({ label: pinLabel, onClick: onDoubleClick }); + const removeLabel = `Remove column for ${code}`; + items.push({ label: removeLabel, onClick: removeProbe }); + Dome.popupMenu(items); + }; + + const buttons = + <div> <IconButton - icon="ITEMS.LIST" - className="eva-probeinfo-button" - selected={props.byCallstacks} - title="Details by callstack" - onClick={props.onCallstackClick} + icon='PIN' + className={buttonClass} + title={`${pinText} the column`} + selected={isPinned} + onClick={onDoubleClick} /> - <Inset /> <IconButton - icon="CROSS" - className="eva-probeinfo-button" - title="Close" - onClick={props.close} + icon="CIRC.CLOSE" + className={buttonClass} + title="Remove the column" + onClick={() => removeProbe()} /> - </> + </div>; + + return ( + <th + ref={ref} + className={color} + colSpan={span} + onClick={onClick} + onDoubleClick={onDoubleClick} + onContextMenu={onContextMenu} + > + <TableCell right={buttons}> + <div className='eva-header-text-overflow'> + <span className='dome-text-cell' title={code}>{code}</span> + </div> + <Stmt stmt={stmt} marker={target} short={true}/> + </TableCell> + </th> ); } -// -------------------------------------------------------------------------- -// --- Table Row Header -// -------------------------------------------------------------------------- +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Probe Description Component --- */ +/* -------------------------------------------------------------------------- */ +/* --- Description of a table column, i.e. if it contains values --- */ +/* --- evaluated before or after the considered statement. --- */ +/* -------------------------------------------------------------------------- */ + +interface ProbeDescrProps { + probe: Probe; +} + +function ProbeDescr(props: ProbeDescrProps): JSX.Element[] { + const { probe } = props; + const valuesClass = classes('eva-table-values', 'eva-table-values-center'); + const tableClass = classes('eva-table-descrs', 'eva-table-descr-sticky'); + const cls = classes(valuesClass, tableClass); + const title = (s: string): string => `Values ${s} the statement evaluation`; + const elements: JSX.Element[] = []; + function push(title: string, children: JSX.Element | string): void { + elements.push(<td className={cls} title={title}>{children}</td>); + } + if (!probe.effects && !probe.condition) + push('Values at the statement', '-'); + if (probe.effects || probe.condition) + push(title('before'), 'Before'); + if (probe.effects) + push(title('after'), 'After'); + if (probe.condition) { + const pushCondition = (s: string): void => { + const t = `Values after the condition, in the ${s.toLowerCase()} branch`; + const child = + <div className='eva-header-after-condition'> + After + <div className='eva-stmt'>{`(${s})`}</div> + </div>; + push(t, child); + }; + pushCondition('Then'); + pushCondition('Else'); + } + return elements; +} + +/* -------------------------------------------------------------------------- */ + -interface TableHeadProps { - stackCalls: Callsite[]; - stackIndex: number | undefined; - stackCount: number | undefined; - onClick: () => void; + +/* -------------------------------------------------------------------------- */ +/* --- Probe Values Component --- */ +/* -------------------------------------------------------------------------- */ +/* --- This component represents the contents of one of the table that --- */ +/* --- displays values information. As the content depends on the --- */ +/* --- considered callstack, we decided to return a function that build --- */ +/* --- the actual component when given a callstack. It avoids useless --- */ +/* --- computational boilerplate. --- */ +/* -------------------------------------------------------------------------- */ + +interface ProbeValuesProps { + probe: Probe; + addLoc: (loc: Location) => void; + isSelectedCallstack: (c: callstack) => boolean; } -function makeStackTitle(calls: Callsite[]) { +function ProbeValues(props: ProbeValuesProps): Request<callstack, JSX.Element> { + const { probe, addLoc, isSelectedCallstack } = props; + + // Building common parts + const onContextMenu = (evaluation?: Values.evaluation) => (): void => { + const { value = '', pointedVars = [] } = evaluation ?? {}; + const items: Dome.PopupMenuItem[] = []; + const copy = (): void => { 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 = (): void => addLoc(location); + items.push({ label, onClick: onItemClick }); + }); + if (items.length > 0) Dome.popupMenu(items); + }; + + return async (callstack: callstack): Promise<JSX.Element> => { + const evaluation = await probe.evaluate(callstack); + const { vBefore, vAfter, vThen, vElse } = evaluation; + const isSelected = isSelectedCallstack(callstack); + const selected = isSelected && callstack !== 'Summary' ? 'eva-focused' : ''; + const font = callstack === 'Summary' ? 'eva-italic' : ''; + const className = classes('eva-table-values', selected, font); + const kind = callstack === 'Summary' ? 'one' : 'this'; + const title = `At least one alarm is raised in ${kind} callstack`; + function td(e?: Values.evaluation, colSpan = 1): JSX.Element { + const { alarms, value = '-' } = e ?? {}; + const status = getAlarmStatus(alarms); + const alarmClass = classes('eva-cell-alarms', `eva-alarm-${status}`); + const align = value?.includes('\n') ? 'left' : 'center'; + const alignClass = `eva-table-values-${align}`; + const c = classes(className, alignClass); + const warning = + <Icon className={alarmClass} size={10} title={title} id="WARNING" />; + return ( + <td className={c} colSpan={colSpan} onContextMenu={onContextMenu(e)}> + <TableCell right={warning}> + <span className='eva-table-text'>{value}</span> + </TableCell> + </td> + ); + } + const elements: JSX.Element[] = []; + if (probe.effects && _.isEqual(vBefore, vAfter)) + elements.push(td(vBefore, 2)); + else { + if (!probe.effects && !probe.condition) + elements.push(td(vBefore)); + if (probe.effects || probe.condition) + elements.push(td(vBefore)); + if (probe.effects) + elements.push(td(vAfter)); + if (probe.condition) + elements.push(td(vThen), td(vElse)); + } + return <>{React.Children.toArray(elements)}</>; + }; +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Row header describing the corresponding callstack --- */ +/* -------------------------------------------------------------------------- */ + +interface CallsiteCellProps { + callstack: callstack | 'Header'; + index?: number; + getCallsites: Request<callstack, Callsite[]>; + selectedClass?: string; +} + +function makeStackTitle(calls: Callsite[]): string { const cs = calls.slice(1); if (cs.length > 0) return `Callstack: ${cs.map((c) => c.callee).join(' \u2190 ')}`; return 'Callstack Details'; } -function TableHead(props: TableHeadProps) { - const sk = props.stackIndex; - const sc = props.stackCount; - const hdClass = classes('eva-head', sc ? undefined : 'dome-hidden'); - const hdHeader = sk === undefined; - const hdSummary = sk !== undefined && sk < 0; - const hdNumber = sk === undefined ? 0 : 1 + sk; - const hdTitle = - hdHeader ? 'Callstack / Summary' : - hdSummary ? 'Summary' : makeStackTitle(props.stackCalls); - return ( - <div - className={hdClass} - title={hdTitle} - onClick={props.onClick} - > - <div className="eva-phantom">{'\u2211'}{sc ?? '#'}</div> - {hdHeader ? '#' : hdSummary ? '\u2211' : `${hdNumber}`} - </div> - ); +async function CallsiteCell(props: CallsiteCellProps): Promise<JSX.Element> { + const { callstack, index, getCallsites, selectedClass = '' } = props; + const baseClasses = classes('eva-table-callsite-box', selectedClass); + switch (callstack) { + case 'Header': { + const cls = classes(baseClasses, 'eva-table-header-sticky'); + const title = 'Callstack at which expressions are evaluated'; + return <td className={cls} rowSpan={2} title={title}>{'#'}</td>; + } + default: { + const cls = classes(baseClasses, 'eva-table-value-sticky'); + const callsites = await getCallsites(callstack); + const isSummary = callstack === 'Summary'; + const summary = 'Summary: value consolidated accross all callstacks'; + const infos = isSummary ? summary : makeStackTitle(callsites); + const text = isSummary ? '∑' : (index ? index.toString() : '0'); + return <td className={cls} title={infos}>{text}</td>; + } + } } -// -------------------------------------------------------------------------- -// --- Table Row -// -------------------------------------------------------------------------- +/* -------------------------------------------------------------------------- */ + + -interface TableRowProps { - style: React.CSSProperties; - index: number; - data: ModelProp; +/* -------------------------------------------------------------------------- */ +/* --- Function Section Component --- */ +/* -------------------------------------------------------------------------- */ + +interface FunctionProps { + fct: string; + markers: Map<Ast.marker, MarkerStatus>; + close: () => void; + getProbe: Request<Location, Probe>; + pinProbe: (probe: Probe, pin: boolean) => void; + selectProbe: (probe: Probe) => void; + removeProbe: (probe: Probe) => void; + addLoc: (loc: Location) => void; + folded: boolean; + setFolded: (folded: boolean) => void; + getCallsites: Request<callstack, Callsite[]>; + byCallstacks: boolean; + getCallstacks: Request<Ast.marker[], callstack[]>; + setByCallstacks: (byCallstack: boolean) => void; + selectCallstack: (callstack: callstack) => void; + isSelectedCallstack: (c: callstack) => boolean; + setSelection: (a: States.SelectionActions) => void; + locEvt: Dome.Event<Location>; + startingCallstack: number; + changeStartingCallstack: (n: number) => void; } -function TableRow(props: TableRowProps) { - const { model } = props.data; - const row = model.getRow(props.index); - if (!row) return null; - const { kind, probes } = row; - if (kind === 'section') { - const { fct } = row; - if (!fct) return null; - const folded = model.isFolded(fct); - const foldable = model.isFoldable(fct); - const byCallstacks = model.isByCallstacks(fct); +const PageSize = 99; + +async function FunctionSection(props: FunctionProps): Promise<JSX.Element> { + const { fct, folded, isSelectedCallstack, locEvt } = props; + const { byCallstacks, setSelection, getCallsites } = props; + const { addLoc, getCallstacks: getCS } = props; + const { setFolded, setByCallstacks, close } = props; + const { startingCallstack, changeStartingCallstack } = props; + const displayTable = folded ? 'none' : 'block'; + type RowHandler = React.MouseEventHandler<HTMLTableRowElement>; + const onClick: (c: callstack) => RowHandler = (c) => (event) => { + const elt = document.elementFromPoint(event.clientX, event.clientY); + if (elt?.localName !== 'span') + props.selectCallstack(isSelectedCallstack(c) ? 'Summary' : c); + }; + + /* Computes the relevant callstacks */ + const markers = Array.from(props.markers.keys()); + const allCallstacks = await getCS(markers); + const summaryOnly = allCallstacks.length === 1; + const callstacks = byCallstacks || summaryOnly ? allCallstacks : []; + const nbCS = allCallstacks.length; + + /* Computes the relevant data for each marker */ + interface Data { probe: Probe; summary: Evaluation; status: MarkerStatus } + const entries = Array.from(props.markers.entries()); + const data = await Promise.all(entries.map(async ([ target, status ]) => { + const probe = await props.getProbe({ target, fct }); + const summary = await probe.evaluate('Summary'); + return { probe, summary, status } as Data; + })); + const doCall = data.length > 0; + + /* Computes the headers for each marker */ + const headerCall = await CallsiteCell({ getCallsites, callstack: 'Header' }); + const headers = await Promise.all(data.map((d: Data) => { + const pinProbe = (pin: boolean): void => props.pinProbe(d.probe, pin); + const selectProbe = (): void => props.selectProbe(d.probe); + const removeProbe = (): void => props.removeProbe(d.probe); + const fcts = { selectProbe, pinProbe, removeProbe, setSelection }; + return ProbeHeader({ ...d, ...fcts, locEvt }); + })); + + /* Computes the columns descriptions */ + const descrs = data.map((d) => ProbeDescr(d)).flat(); + + /* Computes the summary values */ + const miscs = { addLoc, isSelectedCallstack }; + const builders = data.map((d: Data) => ProbeValues({ ...d, ...miscs })); + const summary = await Promise.all(builders.map((b) => b('Summary'))); + const summCall = await CallsiteCell({ callstack: 'Summary', getCallsites }); + let summaryRow = <></>; + if (!summaryOnly) { + summaryRow = + <tr key={'Summary'} onClick={onClick('Summary')}> + {doCall ? summCall : undefined} + {React.Children.toArray(summary)} + </tr>; + } + + /* Computes the values for each callstack */ + const start = Math.max(1, startingCallstack); + const stop = Math.min(start + PageSize, callstacks.length); + const values = await Promise.all(callstacks.map(async (callstack, n) => { + const index = n + 1; + if (start > index || stop < index) return <></>; + const selectedClass = isSelectedCallstack(callstack) ? 'eva-focused' : ''; + const callProps = { selectedClass, getCallsites }; + const call = await CallsiteCell({ index, callstack, ...callProps }); + const values = await Promise.all(builders.map((b) => b(callstack))); return ( - <Hpack className="eva-function" style={props.style}> - <TableSection - fct={fct} - folded={folded} - foldable={foldable} - onClick={() => model.setFolded(fct, !folded)} - byCallstacks={byCallstacks} - onCallstackClick={() => model.setByCallstacks(fct, !byCallstacks)} - close={() => model.clearFunction(fct)} - /> - </Hpack> + <tr key={callstack} onClick={onClick(callstack)}> + {call} + {React.Children.toArray(values)} + </tr> ); - } - const sk = row.stackIndex; - const sc = row.stackCount; - const cs = row.callstack; - const calls = cs ? model.stacks.getCalls(cs) : []; - const rowKind = `eva-${kind}`; - const rowParity = sk !== undefined && sk % 2 === 1; - const rowIndexKind = - model.isSelectedRow(row) ? 'eva-row-selected' : - model.isAlignedRow(row) ? 'eva-row-aligned' : - rowParity ? 'eva-row-odd' : 'eva-row-even'; - const rowClass = classes('eva-row', rowKind, rowIndexKind); - const onHeaderClick = () => model.setSelectedRow(row); - const makeCell = (probe: Probe) => ( - <TableCell - key={probe.marker} - probe={probe} - row={row} - model={model} - /> - ); + })); + + /* We change the starting callstack dynamically when we reach the ends of the + * scroll to avoid to build the complete table */ + const onScroll: React.UIEventHandler<HTMLDivElement> = (event) => { + const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; + if (scrollTop / (scrollHeight - clientHeight) <= 0.1) + changeStartingCallstack(Math.max(startingCallstack - 10, 0)); + const botGap = (scrollHeight - scrollTop - clientHeight) / scrollHeight; + const lastCallstack = startingCallstack + PageSize; + if (botGap <= 0.1 && lastCallstack !== callstacks.length) { + const maxStart = callstacks.length - PageSize; + const start = Math.min(startingCallstack + 10, maxStart); + changeStartingCallstack(start); + } + }; + + /* Builds the component */ return ( - <Hpack style={props.style}> - <div className={rowClass}> - <TableHead - stackIndex={sk} - stackCount={sc} - stackCalls={calls} - onClick={onHeaderClick} + <> + <Hpack className="eva-function"> + <IconButton + className="eva-fct-fold" + icon={folded ? 'ANGLE.RIGHT' : 'ANGLE.DOWN'} + onClick={() => setFolded(!folded)} + /> + <Cell className="eva-fct-name">{fct}</Cell> + <Filler /> + <div className='eva-nb-callstacks'> + {`${nbCS} callstack${nbCS > 1 ? 's' : ''}`} + </div> + <IconButton + icon="ITEMS.LIST" + className="eva-button" + selected={byCallstacks} + disabled={summaryOnly} + title="Show values by callstack" + onClick={() => setByCallstacks(!byCallstacks)} + /> + <Inset /> + <IconButton + icon="CROSS" + className="eva-button" + title="Close" + onClick={close} /> - {probes.map(makeCell)} + </Hpack> + <div + onScroll={onScroll} + className='eva-table-container' + style={{ display: displayTable }} + > + <table className='eva-table'> + <tbody> + <tr> + {doCall ? headerCall : undefined} + {React.Children.toArray(headers)} + </tr> + <tr> + {React.Children.toArray(descrs)} + </tr> + {summaryRow} + {React.Children.toArray(values)} + </tbody> + </table> </div> - <Filler /> - </Hpack> + </> ); } -// -------------------------------------------------------------------------- -// --- Values Panel -// -------------------------------------------------------------------------- +/* -------------------------------------------------------------------------- */ -export interface Dimension { - width: number; - height: number; -} -export interface ValuesPanelProps extends Dimension, ModelProp { - zoom: number; + +/* -------------------------------------------------------------------------- */ +/* --- Function Manager --- */ +/* -------------------------------------------------------------------------- */ +/* --- The Function Manager is responsible of all the data related to --- */ +/* --- programs functions. --- */ +/* -------------------------------------------------------------------------- */ + +/* Informations on one function */ +class FunctionInfos { + + readonly fct: string; // Function's name + readonly pinned = new Set<Ast.marker>(); // Pinned markers + readonly tracked = new Set<Ast.marker>(); // Tracked markers + startingCallstack = 1; // First displayed callstack + byCallstacks = false; // True if displayed by callstacks + folded = false; // True if folded + + constructor(fct: string) { + this.fct = fct; + } + + has(marker: Ast.marker): boolean { + const pinned = this.pinned.has(marker); + const tracked = this.tracked.has(marker); + return pinned || tracked; + } + + pin(marker: Ast.marker): void { + this.pinned.add(marker); + this.tracked.delete(marker); + } + + track(marker: Ast.marker): void { + this.tracked.add(marker); + this.pinned.delete(marker); + } + + delete(marker: Ast.marker): void { + this.pinned.delete(marker); + this.tracked.delete(marker); + } + + isEmpty(): boolean { + return this.pinned.size === 0 && this.tracked.size === 0; + } + + markers(focusedLoc?: Location): Map<Ast.marker, MarkerStatus> { + const { target: tgt, fct } = focusedLoc ?? {}; + const inFct = fct !== undefined && fct === this.fct; + const ms = new Map<Ast.marker, MarkerStatus>(); + this.pinned.forEach((p) => ms.set(p, [ 'Pinned', inFct && tgt === p ])); + this.tracked.forEach((p) => ms.set(p, [ 'Tracked', inFct && tgt === p ])); + if (inFct && tgt && !this.has(tgt)) ms.set(tgt, 'JustFocused'); + return new Map(Array.from(ms.entries()).reverse()); + } + } -export function ValuesPanel(props: ValuesPanelProps) { - const { zoom, width, height, model } = props; - // --- reset line cache - const listRef = React.useRef<VariableSizeList>(null); - const [rowCount, setRowCount] = React.useState(model.getRowCount()); - Dome.useEvent(model.laidout, () => { - // The layout has changed, so the number of rows may also have changed. - setRowCount(model.getRowCount()); - setImmediate(() => { - const vlist = listRef.current; - if (vlist) vlist.resetAfterIndex(0, true); +/* State keeping tracks of informations for every relevant functions */ +class FunctionsManager { + + private readonly cache = new Map<string, FunctionInfos>(); + + constructor() { + this.newFunction = this.newFunction.bind(this); + this.getInfos = this.getInfos.bind(this); + this.setByCallstacks = this.setByCallstacks.bind(this); + this.setFolded = this.setFolded.bind(this); + this.pin = this.pin.bind(this); + this.track = this.track.bind(this); + this.removeLocation = this.removeLocation.bind(this); + this.delete = this.delete.bind(this); + this.clear = this.clear.bind(this); + this.map = this.map.bind(this); + } + + newFunction(fct: string): void { + if (!this.cache.has(fct)) this.cache.set(fct, new FunctionInfos(fct)); + } + + private getInfos(fct: string): FunctionInfos { + const { cache } = this; + if (cache.has(fct)) return cache.get(fct) as FunctionInfos; + const infos = new FunctionInfos(fct); + this.cache.set(fct, infos); + return infos; + } + + isEmpty(fct: string): boolean { + const infos = this.cache.get(fct); + return infos ? infos.isEmpty() : true; + } + + setByCallstacks(fct: string, byCallstacks: boolean): void { + const infos = this.cache.get(fct); + if (infos) infos.byCallstacks = byCallstacks; + } + + setFolded(fct: string, folded: boolean): void { + const infos = this.cache.get(fct); + if (infos) infos.folded = folded; + } + + changeStartingCallstack(fct: string, n: number): void { + const infos = this.cache.get(fct); + if (infos) infos.startingCallstack = n; + } + + pin(loc: Location): void { + const { target, fct } = loc; + this.getInfos(fct).pin(target); + } + + unpin(loc: Location): void { + const { target, fct } = loc; + this.cache.get(fct)?.pinned.delete(target); + } + + track(loc: Location): void { + const { target, fct } = loc; + this.getInfos(fct).track(target); + } + + removeLocation(loc: Location): void { + const { target, fct } = loc; + const infos = this.cache.get(fct); + if (infos) infos.delete(target); + } + + delete(fct: string): void { + this.cache.delete(fct); + } + + clear(): void { + this.cache.clear(); + } + + clean(focused?: Location): void { + const focusedFct = focused?.fct; + this.cache.forEach((infos) => { + if (focusedFct !== infos.fct && infos.isEmpty()) + this.cache.delete(infos.fct); }); - }); - // --- compute line height - const getRowHeight = React.useCallback( - (k: number) => HSIZER.dimension(model.getRowLines(k)), - [model], - ); - // --- compute layout - const margin = WSIZER.capacity(width); - const estimatedHeight = HSIZER.dimension(1); - const [selection] = States.useSelection(); + } + + map<A>(func: (infos: FunctionInfos, fct: string) => A): A[] { + const entries = Array.from(this.cache.entries()); + return entries.map(([ fct, infos ]) => func(infos, fct)); + } + +} + +/* -------------------------------------------------------------------------- */ + + + +/* -------------------------------------------------------------------------- */ +/* --- Eva Table Complet Component --- */ +/* -------------------------------------------------------------------------- */ + +/* Table's state. It is global for when the user changes the view. */ +const CallstackState = new GlobalState<callstack>('Summary'); +const FunctionsManagerState = new GlobalState(new FunctionsManager()); +const FocusState = new GlobalState<Probe | undefined>(undefined); + +/* Component */ +function EvaTable(): JSX.Element { + + /* Component state */ + const [ selection, select ] = States.useSelection(); + const [ cs, setCS ] = useGlobalState(CallstackState); + const [ fcts ] = useGlobalState(FunctionsManagerState); + const [ focus, setFocus ] = useGlobalState(FocusState); + + /* Used to force the component update. We cannot use the `forceUpdate` hook + * proposed by Dome as we need to be able to add dependencies on a changing + * value (here tac) explicitly. We need to force the update as modifications + * of the Function Manager internal data does NOT trigger the component + * update. */ + const [ tac, setTic ] = React.useState(0); + + /* Event use to communicate when a location is selected. Used to scroll + * related column into view if needed */ + const [ locEvt ] = React.useState(new Dome.Event<Location>('eva-location')); + + /* Build cached version of needed server's requests */ + const getProbe = useProbeCache(); + const getCallsites = useCallsitesCache(); + const getCallstacks = useCallstacksCache(); + + /* Computing the function corresponding to the selected callstack */ + const csFctPromise = React.useMemo(async () => { + const selectedCSInfos = await getCallsites(cs); + if (selectedCSInfos.length === 0) return undefined; + else return selectedCSInfos[0].callee; + }, [ cs, getCallsites ]); + const { result: csFct } = Dome.usePromise(csFctPromise); + + /* Reset the selected callstack when the corresponding function is removed */ + React.useEffect(() => { + if (csFct && fcts.isEmpty(csFct) && focus?.fct !== csFct) + setCS('Summary'); + }, [ csFct, setCS, fcts, focus?.fct ] ); + + /* Updated the focused Probe when the selection changes. Also emit on the + * `locEvent` event. */ + React.useEffect(() => { + const target = selection?.current?.marker; + const fct = selection?.current?.fct; + const loc = (target && fct) ? { target, fct } : undefined; + fcts.clean(loc); + const doUpdate = (p: Probe): void => { + if (!p.evaluable) { setFocus(undefined); return; } + if (fct && p.code) fcts.newFunction(fct); + setFocus(p); locEvt.emit(p); + }; + if (loc) getProbe(loc).then(doUpdate); + else setFocus(undefined); + }, [ fcts, selection, getProbe, setFocus, locEvt ]); + + /* Callback used to pin or unpin a location */ + const setLocPin = React.useCallback((loc: Location, pin: boolean): void => { + if (pin) fcts.pin(loc); + else fcts.unpin(loc); + setTic(tac + 1); + }, [fcts, setTic, tac]); + + /* On meta-selection, pin the selected location. */ React.useEffect(() => { - const location = selection?.current; - model.setLayout({ zoom, margin, location }); + const pin = (loc: States.Location): void => { + const {marker, fct} = loc; + if (marker && fct) setLocPin({ target: marker, fct }, true); + }; + States.MetaSelection.on(pin); + return () => States.MetaSelection.off(pin); }); - // --- render list + + /* Callback used to remove a probe */ + const remove = React.useCallback((probe: Probe): void => { + fcts.removeLocation(probe); + if (probe.target === focus?.target) { + setFocus(undefined); + fcts.clean(undefined); + } + else { + fcts.clean(focus); + } + setTic(tac + 1); + }, [ fcts, focus, setFocus, tac ]); + + /* Builds the sections for each function. As the component is built + * asynchronously, we have to use the `usePromise` hook, which forces us to + * memoize the promises building. */ + const functionsPromise = React.useMemo(() => { + const ps = fcts.map((infos, fct) => { + const { byCallstacks, folded } = infos; + const isSelectedCallstack = (c: callstack): boolean => c === cs; + const setFolded = (folded: boolean): void => { + fcts.setFolded(fct, folded); + setTic(tac + 1); + }; + const setByCS = (byCS: boolean): void => { + fcts.setByCallstacks(fct, byCS); + setTic(tac + 1); + }; + const changeStartingCallstack = (n: number): void => { + fcts.changeStartingCallstack(fct, n); + setTic(tac + 1); + }; + const close = (): void => { + fcts.delete(fct); + if (csFct === fct) setCS('Summary'); + setTic(tac + 1); + }; + return { + fct, + markers: infos.markers(focus), + close, + pinProbe: setLocPin, + getProbe, + selectProbe: setFocus, + removeProbe: remove, + addLoc: (loc: Location) => { fcts.pin(loc); setTic(tac + 1); }, + folded, + setFolded, + getCallsites, + byCallstacks, + getCallstacks, + setByCallstacks: setByCS, + selectCallstack: (c: callstack) => { setCS(c); setTic(tac + 1); }, + isSelectedCallstack, + setSelection: select, + locEvt, + startingCallstack: infos.startingCallstack, + changeStartingCallstack, + }; + }); + return Promise.all(ps.map(FunctionSection)); + }, + [ cs, setCS, fcts, focus, setFocus, tac, getCallsites, setLocPin, csFct, + getCallstacks, getProbe, remove, select, locEvt ]); + const { result: functions } = Dome.usePromise(functionsPromise); + + /* Builds the alarms component. As for the function sections, it is an + * asynchronous process. */ + const alarmsProm = React.useMemo(() => AlarmsInfos(focus)(cs), [ focus, cs ]); + const { result: alarmsInfos } = Dome.usePromise(alarmsProm); + + /* Builds the stacks component. As for the function sections, it is an + * asynchronous process. */ + const stackInfosPromise = React.useMemo(async () => { + const callsites = await getCallsites(cs); + const tgt = selection.current?.marker; + const p = (c: Callsite): boolean => c.stmt !== undefined && c.stmt === tgt; + const isSelected = callsites.find(p) !== undefined; + const close = (): void => setCS('Summary'); + return StackInfos({ callsites, isSelected, setSelection: select, close }); + }, [ cs, setCS, select, getCallsites, selection ]); + const { result: stackInfos } = Dome.usePromise(stackInfosPromise); + + /* Builds the component */ return ( - <VariableSizeList - ref={listRef} - itemCount={rowCount} - itemKey={model.getRowKey} - itemSize={getRowHeight} - estimatedItemSize={estimatedHeight} - width={width} - height={height} - itemData={{ model }} - > - {TableRow} - </VariableSizeList> + <> + <Ivette.TitleBar /> + <div className='eva-functions-section'> + {React.Children.toArray(functions)} + </div> + <Vfill/> + {alarmsInfos} + {stackInfos} + </> ); + } -// -------------------------------------------------------------------------- +/* Registers the component in Ivette */ +Ivette.registerComponent({ + id: 'frama-c.plugins.values', + group: 'frama-c.plugins', + rank: 1, + label: 'Eva Values', + title: 'Values inferred by the Eva analysis', + children: <EvaTable />, +}); + +/* -------------------------------------------------------------------------- */ diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 11b59f05e8aeaacd4265712bb5ad6f52d79cae78..c0d940c7f09c3083b19e9882d26889712d1812cf 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -788,7 +788,10 @@ export function setSelection(location: Location, meta = false) { /** Current selection. */ export function useSelection(): [Selection, (a: SelectionActions) => void] { const [current, setCurrent] = useGlobalState(GlobalSelection); - return [current, (action) => setCurrent(reducer(current, action))]; + const callback = React.useCallback((action) => { + setCurrent(reducer(current, action)); + }, [ current, setCurrent ]); + return [current, callback]; } /** Resets the selected locations. */ diff --git a/src/plugins/value/api/values_request.ml b/src/plugins/value/api/values_request.ml index a85d9720d60264fb8450f83a879f43216583e4b0..784b1a0fa9ad832637b2d1a9d7f761b8f95e8b2f 100644 --- a/src/plugins/value/api/values_request.ml +++ b/src/plugins/value/api/values_request.ml @@ -85,10 +85,10 @@ let () = Analysis.register_computation_hook ~on:Computed let next_steps s = match s.skind with | If (cond, _, _, _) -> [ `Then cond ; `Else cond ] - | Instr (Set _ | Call _ | Local_init _ | Asm _ | Code_annot _) + | Instr (Set _ | Call _ | Local_init _) -> [ `After ] + | Instr (Asm _ | Code_annot _) | Switch _ | Loop _ | Block _ | UnspecifiedSequence _ | TryCatch _ | TryFinally _ | TryExcept _ - -> [ `After ] | Instr (Skip _) | Return _ | Break _ | Continue _ | Goto _ | Throw _ -> [] let probe_stmt s = @@ -102,6 +102,7 @@ let probe_stmt s = let probe marker = let open Printer_tag in match marker with + | PLval (_, _, (Var vi, NoOffset)) when Cil.isFunctionType vi.vtype -> None | PLval(_,Kstmt s,l) -> Some (Plval (l,s)) | PExp(_,Kstmt s,e) -> Some (Pexpr (e,s)) | PStmt(_,s) | PStmtStart(_,s) -> probe_stmt s @@ -354,7 +355,7 @@ module Proxy(A : Analysis.S) : EvaProxy = struct let dstate ~after stmt = function | None -> (A.get_stmt_state ~after stmt :> dstate) | Some cs -> - match A.get_stmt_state_by_callstack ~after stmt with + match A.get_stmt_state_by_callstack ~selection:[cs] ~after stmt with | (`Top | `Bottom) as res -> res | `Value cmap -> try `Value (CSmap.find cmap cs) @@ -530,7 +531,10 @@ let () = let () = let getProbeInfo = Request.signature ~input:(module Jmarker) () in - let set_code = Request.result_opt getProbeInfo + let set_evaluable = Request.result getProbeInfo + ~name:"evaluable" ~descr:(Md.plain "Can the probe be evaluated?") + (module Jbool) + and set_code = Request.result_opt getProbeInfo ~name:"code" ~descr:(Md.plain "Probe source code") (module Jstring) and set_stmt = Request.result_opt getProbeInfo @@ -547,6 +551,9 @@ let () = ~default:false (module Jbool) in let set_probe rq pp p s = + let computed = Analysis.is_computed () in + let reachable = Results.is_reachable s in + set_evaluable rq (computed && reachable); set_code rq (Some (Pretty_utils.to_string pp p)) ; set_stmt rq (Some s) ; set_rank rq (Ranking.stmt s) ; @@ -563,7 +570,7 @@ let () = match probe marker with | Some (Plval (l, s)) -> set_probe rq Printer.pp_lval l s | Some (Pexpr (e, s)) -> set_probe rq Printer.pp_exp e s - | None -> () + | None -> set_evaluable rq false end (* -------------------------------------------------------------------------- *)