diff --git a/ivette/Makefile b/ivette/Makefile index 29010215b7bddf4c90c1036b10c4c090a1818e0f..a30dcad4c5f3265efb03fb9bdf283e93d0243a8f 100644 --- a/ivette/Makefile +++ b/ivette/Makefile @@ -35,7 +35,7 @@ tsc: dome-pkg dome-templ api: @echo "[Ivette] Generating TypeScript API" - @find api -name "*.ts" -exec rm -f {} \; + @rm -fr api/generated ../bin/frama-c.byte -load-module api/server_tsc.ml -server-tsc @find api -name "*.ts" -exec chmod a-w {} \; diff --git a/ivette/api/generated/kernel/ast/index.ts b/ivette/api/generated/kernel/ast/index.ts index 2a910fb94ae5a99b0f515c71fca9aff3bfa3da03..9ae32c6bf691af3a416da8b033a070e337f6c38a 100644 --- a/ivette/api/generated/kernel/ast/index.ts +++ b/ivette/api/generated/kernel/ast/index.ts @@ -120,7 +120,7 @@ export const markerVarTags: Server.GetRequest<null,tag[]>= markerVarTags_interna /** Data for array rows [`markerInfo`](#markerinfo) */ export interface markerInfoData { /** Entry identifier. */ - key: Json.key<'#markerInfo'>; + key: string; /** Marker kind */ kind: markerKind; /** Marker variable */ @@ -136,8 +136,7 @@ export interface markerInfoData { /** Loose decoder for `markerInfoData` */ export const jMarkerInfoData: Json.Loose<markerInfoData> = Json.jObject({ - key: Json.jFail(Json.jKey<'#markerInfo'>('#markerInfo'), - '#markerInfo expected'), + key: Json.jFail(Json.jString,'String expected'), kind: jMarkerKindSafe, var: jMarkerVarSafe, name: Json.jFail(Json.jString,'String expected'), @@ -152,8 +151,8 @@ export const jMarkerInfoDataSafe: Json.Safe<markerInfoData> = /** Natural order for `markerInfoData` */ export const byMarkerInfoData: Compare.Order<markerInfoData> = Compare.byFields - <{ key: Json.key<'#markerInfo'>, kind: markerKind, var: markerVar, - name: string, descr: string, sloc: source }>({ + <{ key: string, kind: markerKind, var: markerVar, name: string, + descr: string, sloc: source }>({ key: Compare.string, kind: byMarkerKind, var: byMarkerVar, @@ -178,8 +177,8 @@ export const reloadMarkerInfo: Server.GetRequest<null,null>= reloadMarkerInfo_in const fetchMarkerInfo_internal: Server.GetRequest< number, - { pending: number, updated: markerInfoData[], - removed: Json.key<'#markerInfo'>[], reload: boolean } + { pending: number, updated: markerInfoData[], removed: string[], + reload: boolean } > = { kind: Server.RqKind.GET, name: 'kernel.ast.fetchMarkerInfo', @@ -187,21 +186,18 @@ const fetchMarkerInfo_internal: Server.GetRequest< output: Json.jObject({ pending: Json.jFail(Json.jNumber,'Number expected'), updated: Json.jList(jMarkerInfoData), - removed: Json.jList(Json.jKey<'#markerInfo'>('#markerInfo')), + removed: Json.jList(Json.jString), reload: Json.jFail(Json.jBoolean,'Boolean expected'), }), }; /** Data fetcher for array [`markerInfo`](#markerinfo) */ export const fetchMarkerInfo: Server.GetRequest< number, - { pending: number, updated: markerInfoData[], - removed: Json.key<'#markerInfo'>[], reload: boolean } + { pending: number, updated: markerInfoData[], removed: string[], + reload: boolean } >= fetchMarkerInfo_internal; -const markerInfo_internal: State.Array< - Json.key<'#markerInfo'>, - markerInfoData - > = { +const markerInfo_internal: State.Array<string,markerInfoData> = { name: 'kernel.ast.markerInfo', getkey: ((d:markerInfoData) => d.key), signal: signalMarkerInfo, @@ -210,7 +206,7 @@ const markerInfo_internal: State.Array< order: byMarkerInfoData, }; /** Marker informations */ -export const markerInfo: State.Array<Json.key<'#markerInfo'>,markerInfoData> = markerInfo_internal; +export const markerInfo: State.Array<string,markerInfoData> = markerInfo_internal; /** Localizable AST markers */ export type marker = @@ -242,7 +238,7 @@ export const byMarker: Compare.Order<marker> = Compare.structural; /** Location: function and marker */ export interface location { /** Function */ - function: Json.key<'#fct'>; + fct: Json.key<'#fct'>; /** Marker */ marker: marker; } @@ -250,7 +246,7 @@ export interface location { /** Loose decoder for `location` */ export const jLocation: Json.Loose<location> = Json.jObject({ - function: Json.jFail(Json.jKey<'#fct'>('#fct'),'#fct expected'), + fct: Json.jFail(Json.jKey<'#fct'>('#fct'),'#fct expected'), marker: jMarkerSafe, }); @@ -261,8 +257,8 @@ export const jLocationSafe: Json.Safe<location> = /** Natural order for `location` */ export const byLocation: Compare.Order<location> = Compare.byFields - <{ function: Json.key<'#fct'>, marker: marker }>({ - function: Compare.string, + <{ fct: Json.key<'#fct'>, marker: marker }>({ + fct: Compare.string, marker: byMarker, }); diff --git a/ivette/api/generated/kernel/properties/index.ts b/ivette/api/generated/kernel/properties/index.ts index 7ca5ce68b252ecb5308fe822caec7a05ef680773..dcba1484d490140e593cacd56ab49052929aed4e 100644 --- a/ivette/api/generated/kernel/properties/index.ts +++ b/ivette/api/generated/kernel/properties/index.ts @@ -225,7 +225,7 @@ export const alarmsTags: Server.GetRequest<null,tag[]>= alarmsTags_internal; /** Data for array rows [`status`](#status) */ export interface statusData { /** Entry identifier. */ - key: Json.key<'#status'>; + key: Json.key<'#property'>; /** Full description */ descr: string; /** Kind */ @@ -235,7 +235,7 @@ export interface statusData { /** Status */ status: propStatus; /** Function */ - function?: Json.key<'#fct'>; + fct?: Json.key<'#fct'>; /** Instruction */ kinstr?: Json.key<'#stmt'>; /** Position */ @@ -251,12 +251,12 @@ export interface statusData { /** Loose decoder for `statusData` */ export const jStatusData: Json.Loose<statusData> = Json.jObject({ - key: Json.jFail(Json.jKey<'#status'>('#status'),'#status expected'), + key: Json.jFail(Json.jKey<'#property'>('#property'),'#property expected'), descr: Json.jFail(Json.jString,'String expected'), kind: jPropKindSafe, names: Json.jList(Json.jString), status: jPropStatusSafe, - function: Json.jKey<'#fct'>('#fct'), + fct: Json.jKey<'#fct'>('#fct'), kinstr: Json.jKey<'#stmt'>('#stmt'), source: jSourceSafe, alarm: Json.jString, @@ -271,8 +271,8 @@ export const jStatusDataSafe: Json.Safe<statusData> = /** Natural order for `statusData` */ export const byStatusData: Compare.Order<statusData> = Compare.byFields - <{ key: Json.key<'#status'>, descr: string, kind: propKind, - names: string[], status: propStatus, function?: Json.key<'#fct'>, + <{ key: Json.key<'#property'>, descr: string, kind: propKind, + names: string[], status: propStatus, fct?: Json.key<'#fct'>, kinstr?: Json.key<'#stmt'>, source: source, alarm?: string, alarm_descr?: string, predicate?: string }>({ key: Compare.string, @@ -280,7 +280,7 @@ export const byStatusData: Compare.Order<statusData> = kind: byPropKind, names: Compare.array(Compare.string), status: byPropStatus, - function: Compare.defined(Compare.string), + fct: Compare.defined(Compare.string), kinstr: Compare.defined(Compare.string), source: bySource, alarm: Compare.defined(Compare.string), @@ -304,7 +304,7 @@ export const reloadStatus: Server.GetRequest<null,null>= reloadStatus_internal; const fetchStatus_internal: Server.GetRequest< number, - { pending: number, updated: statusData[], removed: Json.key<'#status'>[], + { pending: number, updated: statusData[], removed: Json.key<'#property'>[], reload: boolean } > = { kind: Server.RqKind.GET, @@ -313,18 +313,18 @@ const fetchStatus_internal: Server.GetRequest< output: Json.jObject({ pending: Json.jFail(Json.jNumber,'Number expected'), updated: Json.jList(jStatusData), - removed: Json.jList(Json.jKey<'#status'>('#status')), + removed: Json.jList(Json.jKey<'#property'>('#property')), reload: Json.jFail(Json.jBoolean,'Boolean expected'), }), }; /** Data fetcher for array [`status`](#status) */ export const fetchStatus: Server.GetRequest< number, - { pending: number, updated: statusData[], removed: Json.key<'#status'>[], + { pending: number, updated: statusData[], removed: Json.key<'#property'>[], reload: boolean } >= fetchStatus_internal; -const status_internal: State.Array<Json.key<'#status'>,statusData> = { +const status_internal: State.Array<Json.key<'#property'>,statusData> = { name: 'kernel.properties.status', getkey: ((d:statusData) => d.key), signal: signalStatus, @@ -333,6 +333,6 @@ const status_internal: State.Array<Json.key<'#status'>,statusData> = { order: byStatusData, }; /** Status of Registered Properties */ -export const status: State.Array<Json.key<'#status'>,statusData> = status_internal; +export const status: State.Array<Json.key<'#property'>,statusData> = status_internal; /* ------------------------------------- */ diff --git a/ivette/api/generated/plugins/eva/values/index.ts b/ivette/api/generated/plugins/eva/values/index.ts index bc7353fe239426e399d04330a865ab4b616facad..b08b8540d749cde916fd0650a73acd73c4c6dac2 100644 --- a/ivette/api/generated/plugins/eva/values/index.ts +++ b/ivette/api/generated/plugins/eva/values/index.ts @@ -43,17 +43,14 @@ export const jCallstackSafe: Json.Safe<callstack> = /** Natural order for `callstack` */ export const byCallstack: Compare.Order<callstack> = Compare.number; -const getCallstacks_internal: Server.GetRequest< - Json.key<'#stmt'>, - callstack[] - > = { +const getCallstacks_internal: Server.GetRequest<marker[],callstack[]> = { kind: Server.RqKind.GET, name: 'plugins.eva.values.getCallstacks', - input: Json.jKey<'#stmt'>('#stmt'), + input: Json.jList(jMarker), output: Json.jList(jCallstack), }; /** Callstacks for markers */ -export const getCallstacks: Server.GetRequest<Json.key<'#stmt'>,callstack[]>= getCallstacks_internal; +export const getCallstacks: Server.GetRequest<marker[],callstack[]>= getCallstacks_internal; const getCallstackInfo_internal: Server.GetRequest< callstack, diff --git a/ivette/api/server_tsc.ml b/ivette/api/server_tsc.ml index d190a3b3e19a78816c2a97fe4adef1be20f3a98b..669d83168c2cd58d3536c88a00742bbf9864f187 100644 --- a/ivette/api/server_tsc.ml +++ b/ivette/api/server_tsc.ml @@ -375,10 +375,9 @@ let makeDeclaration fmt names d = self.name jtype js self.name; - | D_array { arr_key ; arr_kind } -> + | D_array { arr_key ; arr_kind = jkey } -> let data = Pkg.Derived.data self in - let jkey = (Pkg.Jkey arr_kind) in - let jrow = (Pkg.Jdata data) in + let jrow = Pkg.Jdata data in Format.fprintf fmt "@[<hv 2>const %s_internal: State.Array<@,%a,@,%a@,>@] = {@\n" self.name jtype jkey jtype jrow ; diff --git a/ivette/src/dome/src/renderer/controls/gallery.json b/ivette/src/dome/src/renderer/controls/gallery.json index 93fb175304006f611729775c94b76890ae82d8dc..62fe91ead33c03e2bd5b866519ffd02dfc3574a8 100644 --- a/ivette/src/dome/src/renderer/controls/gallery.json +++ b/ivette/src/dome/src/renderer/controls/gallery.json @@ -17,6 +17,18 @@ "viewBox": "0 0 15 16", "path": "M9.143 7.143v0.571q0 0.116-0.085 0.201t-0.201 0.085h-5.143q-0.116 0-0.201-0.085t-0.085-0.201v-0.571q0-0.116 0.085-0.201t0.201-0.085h5.143q0.116 0 0.201 0.085t0.085 0.201zM10.286 7.429q0-1.652-1.174-2.826t-2.826-1.174-2.826 1.174-1.174 2.826 1.174 2.826 2.826 1.174 2.826-1.174 1.174-2.826zM14.857 14.857q0 0.473-0.335 0.808t-0.808 0.335q-0.482 0-0.804-0.339l-3.063-3.054q-1.598 1.107-3.563 1.107-1.277 0-2.442-0.496t-2.009-1.339-1.339-2.009-0.496-2.442 0.496-2.442 1.339-2.009 2.009-1.339 2.442-0.496 2.442 0.496 2.009 1.339 1.339 2.009 0.496 2.442q0 1.964-1.107 3.563l3.063 3.063q0.33 0.33 0.33 0.804z" }, + "CURSOR": { + "section": "Tools", + "viewBox": "0 0 32 32", + "title": "Mouse Pointer", + "path": "M16.689 17.655l5.311 12.345-4 2-4.646-12.678-7.354 6.678v-26l20 16-9.311 1.655z" + }, + "PIN": { + "section": "Tools", + "title": "Push Pin", + "viewBox": "0 0 16 16", + "path": "M8.5 0l-1.5 1.5 1.5 1.5-3.5 4h-3.5l2.75 2.75-4.25 5.635v0.615h0.615l5.635-4.25 2.75 2.75v-3.5l4-3.5 1.5 1.5 1.5-1.5-7.5-7.5zM7 8.5l-1-1 3.5-3.5 1 1-3.5 3.5z" + }, "PAINTBRUSH": { "section": "Tools", "title": "Paint Brush", diff --git a/ivette/src/dome/src/renderer/controls/labels.tsx b/ivette/src/dome/src/renderer/controls/labels.tsx index a9250344cc81c7cf3a1f2e374fc259e711c3df64..ca1630fc302230e8ff3c11f447b030ce131c9e72 100644 --- a/ivette/src/dome/src/renderer/controls/labels.tsx +++ b/ivette/src/dome/src/renderer/controls/labels.tsx @@ -34,6 +34,8 @@ export interface LabelProps { children?: React.ReactNode; /** Click event callback. */ onClick?: (evt: React.MouseEvent) => void; + /** Click event callback. */ + onDoubleClick?: (evt: React.MouseEvent) => void; /** Right-click event callback. */ onContextMenu?: (evt: React.MouseEvent) => void; } @@ -52,6 +54,7 @@ const makeLabel = (className: string) => (props: LabelProps, ref: any) => { title={props.title} style={props.style} onClick={props.onClick} + onDoubleClick={props.onDoubleClick} onContextMenu={props.onContextMenu} > {props.icon && <Icon title={props.title} id={props.icon} />} @@ -70,6 +73,7 @@ const TITLE = 'dome-xLabel dome-text-title'; const DESCR = 'dome-xLabel dome-text-descr'; const TDATA = 'dome-xLabel dome-text-data'; const TCODE = 'dome-xLabel dome-text-code'; +const TCELL = 'dome-xLabel dome-text-cell'; // -------------------------------------------------------------------------- // --- Components @@ -90,4 +94,7 @@ export const Data = React.forwardRef(makeLabel(TDATA)); /** Selectable inlined source-code content. */ export const Code = React.forwardRef(makeLabel(TCODE)); +/** Selectable inlined source-code content with default cursor. */ +export const Cell = React.forwardRef(makeLabel(TCELL)); + // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/frame/style.css b/ivette/src/dome/src/renderer/frame/style.css index 9cdc1f5d44786efe4f31587d3c8cd593fe7ae051..4f7d144cae66f93d3117171f781da10a11fe86b4 100644 --- a/ivette/src/dome/src/renderer/frame/style.css +++ b/ivette/src/dome/src/renderer/frame/style.css @@ -384,7 +384,7 @@ background-image: none ; } -.dome-window-inactive .dome-xToolBar-control.dome-selected { +.dome-window-inactive .dome-xToolBar-control.dome-selected:not(:disabled) { fill: #eee; color: #eee; background-color: #ccc ; @@ -396,14 +396,14 @@ fill: #ccc ; color: #ccc ; border: 1px solid #bbb ; - background-color: #eee ; + background-color: #ddd ; } .dome-window-inactive .dome-xToolBar-control.dome-selected:disabled { fill: #ccc ; color: #ccc ; border-color: #ddd ; - background-color: #eee ; + background-color: #ddd ; } /* -------------------------------------------------------------------------- */ @@ -435,6 +435,10 @@ border-bottom-left-radius: 0 ; } +.dome-xToolBar-group > .dome-xToolBar-control:first-child:last-child { + border-radius: 4px ; +} + .dome-xToolBar-group > .dome-xToolBar-control:not(:first-child):not(:last-child) { border-radius: 0 ; } diff --git a/ivette/src/dome/src/renderer/style.css b/ivette/src/dome/src/renderer/style.css index 99cb9265aaf25c94f992f721246303c6797b0af6..2ace504d516cd976248542ca513371028b5d394c 100644 --- a/ivette/src/dome/src/renderer/style.css +++ b/ivette/src/dome/src/renderer/style.css @@ -31,6 +31,10 @@ body { /* --- Frame Colors --- */ /* -------------------------------------------------------------------------- */ +.dome-hidden { + visibility: hidden; +} + .dome-window-active .dome-color-frame { fill: #606060 ; color: #606060 ; @@ -128,4 +132,13 @@ div.dome-dragged { text-overflow: clip ; } +.dome-text-cell { + cursor: default ; + user-select: text ; + font-family: 'Andale mono', monospace ; + font-size: 9pt ; + white-space: nowrap ; + text-overflow: clip ; +} + /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/dome/src/renderer/text/editors.tsx b/ivette/src/dome/src/renderer/text/editors.tsx index a1592f1e971258232eb176afff4ba55d6b33a040..17fc74f263eb3c955d32ba2f30ff9904cd7665aa 100644 --- a/ivette/src/dome/src/renderer/text/editors.tsx +++ b/ivette/src/dome/src/renderer/text/editors.tsx @@ -25,7 +25,7 @@ const CSS_SELECTED = 'dome-xText-select'; /* --------------------------------------------------------------------------*/ export interface MarkerCallback { - (id: string): void; + (id: string, meta?: boolean): void; } /** @@ -329,10 +329,12 @@ class CodeMirrorWrapper extends React.Component<TextProps> { onMouseClick(evt: MouseEvt, callback: MarkerCallback | undefined) { // No need for throttling - const { target } = evt; - if (target instanceof Element && callback) { - const marker = this._findMarker(target); - if (marker && marker.id) callback(marker.id); + if (callback) { + const { target } = evt; + if (target instanceof Element) { + const marker = this._findMarker(target); + if (marker && marker.id) callback(marker.id, evt.altKey); + } } this.props.buffer?.setFocused(true); } diff --git a/ivette/src/frama-c/eva/Values.tsx b/ivette/src/frama-c/eva/Values.tsx index 6bdf15345c8728ade58b0881b2e451cca04f9264..6a2c28e4f063dd2197198b340fc3fe71d0cb1a4c 100644 --- a/ivette/src/frama-c/eva/Values.tsx +++ b/ivette/src/frama-c/eva/Values.tsx @@ -5,425 +5,21 @@ // React & Dome import React from 'react'; import * as Dome from 'dome'; -import { classes } from 'dome/misc/utils'; -import { VariableSizeList } from 'react-window'; -import { Vfill, Hpack, Vpack, Filler } from 'dome/layout/boxes'; -import { Icon } from 'dome/controls/icons'; -import { Label, Code } from 'dome/controls/labels'; +import { Vfill } from 'dome/layout/boxes'; import { IconButton } from 'dome/controls/buttons'; -import { ButtonGroup, Button } from 'dome/frame/toolbars'; // External Libs import { AutoSizer } from 'react-virtualized'; // Frama-C import { Component, TitleBar } from 'frama-c/LabViews'; -import * as States from 'frama-c/states'; - -// Plugins -import * as Ast from 'frama-c/api/kernel/ast'; // Locals -import { SizedArea, HSIZER, WSIZER } from './sized'; -import { Diff } from './diffed'; -import { sizeof, valueAt, diffAfter, diffThen, diffElse, EvaAlarm } - from './cells'; -import { Row } from './layout'; -import { Probe } from './probes'; -import { Callsite } from './stacks'; -import { Model, getModelInstance } from './model'; +import { ProbeInfos } from './probeinfos'; +import { Dimension, ValuesPanel } from './valuetable'; +import { AlarmsInfos, StackInfos } from './valueinfos'; import './style.css'; -// -------------------------------------------------------------------------- -// --- Use Model -// -------------------------------------------------------------------------- - -function useModel(): Model { - const model = getModelInstance(); - Dome.useUpdate(model.changed, model.laidout); - return model; -} - -// -------------------------------------------------------------------------- -// --- Stmt Printer -// -------------------------------------------------------------------------- - -interface StmtProps { - stmt?: string; - rank?: number; -} - -function Stmt(props: StmtProps) { - const { rank, stmt } = props; - if (rank === undefined || !stmt) return null; - const title = `Stmt id ${stmt} at rank ${rank}`; - return ( - <span className="dome-text-code eva-stmt" title={title}> - @S{rank} - </span> - ); -} - -// -------------------------------------------------------------------------- -// --- Probe Panel -// -------------------------------------------------------------------------- - -function ProbeInfos() { - const model = useModel(); - const probe = model.getFocused(); - const transient = probe?.transient ?? false; - const label = probe?.label; - const fct = probe?.fct; - const code = probe?.code; - const stmt = probe?.stmt; - const rank = probe?.rank; - const byCS = probe?.byCallstacks; - const stacks = model.getStacks(probe).length; - const stackable = byCS || stacks > 1; - const { cols, rows } = sizeof(code); - const width = WSIZER.dimension(cols) + 4; - const height = HSIZER.dimension(rows) + 3; - const visible = !!code; - const zoomed = probe?.zoomed; - const zoomable = probe?.zoomable; - const visibility = visible ? 'visible' : 'hidden'; - const effects = probe?.effects; - const condition = probe?.condition; - return ( - <Hpack style={{ visibility }} className="eva-probeinfo"> - <Label className="eva-probeinfo-label">{label && `${label}:`}</Label> - <div style={{ minWidth: width, height }} className="eva-probeinfo-code"> - <SizedArea cols={cols} rows={rows}>{code}</SizedArea> - </div> - <Code>{fct}<Stmt stmt={stmt} rank={rank} /></Code> - <IconButton - className="eva-probeinfo-button" - display={stackable} - selected={byCS} - icon="ITEMS.LIST" - title={`Details by callstack (${stacks})`} - onClick={() => { if (probe) probe.setByCallstacks(!byCS); }} - /> - <IconButton - className="eva-probeinfo-button" - display={zoomable} - selected={zoomed} - icon="SEARCH" - onClick={() => { if (probe) probe.setZoomed(!zoomed); }} - /> - <IconButton - className="eva-probeinfo-button" - kind={transient ? 'selected' : 'warning'} - icon={transient ? 'CIRC.CHECK' : 'CIRC.CLOSE'} - title={transient ? 'Make the probe persistent' : 'Release the probe'} - onClick={() => { if (probe) probe.setTransient(!transient); }} - /> - <Filler /> - <ButtonGroup - enabled={!!probe} - value={probe?.vstate} - onChange={(s) => { if (probe) probe.setState(s); }} - className="eva-probeinfo-state" - > - <Button - visible={effects || condition} - label="H" - value="Here" - /> - <Button - visible={condition} - label="T" - value="Then" - /> - <Button - visible={condition} - label="E" - value="Else" - /> - <Button - visible={effects} - label="A" - value="After" - /> - </ButtonGroup> - </Hpack> - ); -} - -// -------------------------------------------------------------------------- -// --- Alarms Panel -// -------------------------------------------------------------------------- - -function AlarmsInfo() { - const model = useModel(); - const probe = model.getFocused(); - if (probe) { - const callstack = model.getCallstack(); - const domain = - model.values.getValues(probe.marker, probe.stmt, callstack); - const alarms = domain?.alarms ?? []; - if (alarms.length > 0) { - // console.log('ALARMS', alarms); - 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"> - {alarms.map(renderAlarm)} - </Vpack> - ); - } - } - return null; -} - -// -------------------------------------------------------------------------- -// --- Stack Panel -// -------------------------------------------------------------------------- - -function StackInfo() { - const model = useModel(); - const [, setSelection] = States.useSelection(); - const callstack = model.getCalls(); - if (callstack.length <= 1) return null; - const makeCallsite = ({ caller, stmt, rank }: Callsite) => { - if (!caller || !stmt) return null; - const key = `${caller}@${stmt}`; - const onClick = () => { - const location = { function: caller, marker: stmt }; - setSelection({ location }); - }; - return ( - <Code - key={key} - icon="TRIANGLE.LEFT" - className="eva-callsite" - onClick={onClick} - > - {caller} - <Stmt stmt={stmt} rank={rank} /> - </Code> - ); - }; - return ( - <Hpack className="eva-info"> - {callstack.map(makeCallsite)} - </Hpack> - ); -} - -// -------------------------------------------------------------------------- -// --- Table Cell -// -------------------------------------------------------------------------- - -interface TableCellProps { - probe: Probe; - row: Row; -} - -const CELLPADDING = 12; - -function TableCell(props: TableCellProps) { - const model = useModel(); - const [, setSelection] = States.useSelection(); - const { probe, row } = props; - 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; - const { transient } = probe; - - switch (kind) { - - // ---- Probe Contents - case 'probes': - if (transient) { - contents = <span className="dome-text-label">« Probe »</span>; - } else { - const { stmt, rank, code, label } = probe; - const textClass = label ? 'dome-text-label' : 'dome-text-code'; - contents = ( - <> - <span className={textClass}>{label ?? code}</span> - <Stmt stmt={stmt} rank={rank} /> - </> - ); - } - break; - - // ---- Values Contents - case 'values': - case 'callstack': - { - const domain = model.values.getValues( - probe.marker, - probe.stmt, - callstack, - ); - const { alarms = [] } = domain; - const { vstate: s, effects, condition } = probe; - const text = valueAt(domain, s) ?? ''; - const diff = diffAfter(domain, effects, s); - const diffA = diffThen(domain, condition, s); - const diffB = diffElse(domain, condition, s); - const { cols, rows } = sizeof(text); - contents = ( - <> - {alarms.length > 0 && ( - <Icon className="eva-cell-alarms" size={10} id="WARNING" /> - )} - <SizedArea cols={cols} rows={rows}> - <span className={`eva-state-${s}`}> - <Diff text={text} diff={diff} diffA={diffA} diffB={diffB} /> - </span> - </SizedArea> - </> - ); - } - break; - - } - - // --- Cell Packing - const isFocused = model.getFocused() === probe; - const className = classes( - 'eva-cell', - transient && 'eva-transient', - isFocused && 'eva-focused', - ); - const onClick = () => { - if (probe) { - const location = { function: probe.fct, marker: probe.marker }; - setSelection({ location }); - model.setSelectedRow(row); - } - }; - const onDoubleClick = () => { - if (probe) { - if (probe.transient) probe.setTransient(false); - if (probe.zoomable) probe.setZoomed(!probe.zoomed); - } - }; - return ( - <div - className={className} - style={style} - onClick={onClick} - onDoubleClick={onDoubleClick} - > - {contents} - </div> - ); -} - -// -------------------------------------------------------------------------- -// --- Table Row -// -------------------------------------------------------------------------- - -interface TableRowProps { - style: React.CSSProperties; - index: number; -} - -function TableRow(props: TableRowProps) { - const model = useModel(); - const row = model.getRow(props.index); - if (!row) return null; - const { kind, probes } = row; - const sk = row.stackIndex; - const rowKind = `eva-${kind}`; - const rowParity = sk !== undefined && (sk % 2 === 0); - const rowStyle = - model.isSelectedRow(row) ? 'eva-row-selected' : - rowParity ? 'eva-row-odd' : 'eva-row-even'; - const className = classes( - rowKind, - rowStyle, - ); - const header = row.stacks && ( - <div className="eva-cell eva-stack"> - {sk === undefined ? '#' : `${1 + sk}`} - </div> - ); - const style: React.CSSProperties = { left: row.stacks ? 0 : 12 }; - const contents = probes.map((probe) => ( - <TableCell - key={probe.marker} - probe={probe} - row={row} - /> - )); - return ( - <Hpack className={className} style={props.style}> - <div style={style} className="eva-row"> - {header} - {contents} - </div> - <Filler /> - </Hpack> - ); -} - -// -------------------------------------------------------------------------- -// --- Values Panel -// -------------------------------------------------------------------------- - -interface Dimension { - width: number; - height: number; -} - -interface ValuesPanelProps extends Dimension { - zoom: number; -} - -function ValuesPanel(props: ValuesPanelProps) { - const model = useModel(); - const { zoom, width, height } = props; - // --- reset line cache - const listRef = React.useRef<VariableSizeList>(null); - Dome.useEvent(model.laidout, () => { - setImmediate(() => { - const vlist = listRef.current; - if (vlist) vlist.resetAfterIndex(0, true); - }); - }); - // --- compute line height - const getRowHeight = React.useCallback( - (k: number) => HSIZER.dimension(model.getRowLines(k)), - [model], - ); - // --- compute layout - const margin = WSIZER.capacity(width); - const rowHeight = HSIZER.dimension(1); - const [selection] = States.useSelection(); - React.useEffect(() => { - const curr = selection?.current; - const fct = curr?.function; - const marker = Ast.jMarker(curr?.marker); - model.setLayout({ zoom, margin, fct, marker }); - }); - // --- render list - return ( - <VariableSizeList - ref={listRef} - itemCount={model.getRowCount()} - itemKey={model.getRowKey} - itemSize={getRowHeight} - estimatedItemSize={rowHeight} - width={width} - height={height} - itemData={model} - > - {TableRow} - </VariableSizeList> - ); -} - // -------------------------------------------------------------------------- // --- Values Component // -------------------------------------------------------------------------- @@ -456,8 +52,8 @@ function ValuesComponent() { )} </AutoSizer> </Vfill> - <AlarmsInfo /> - <StackInfo /> + <AlarmsInfos /> + <StackInfos /> </Vfill> </> ); diff --git a/ivette/src/frama-c/eva/cells.ts b/ivette/src/frama-c/eva/cells.ts index acb3e9ea05432b1ef0952432b9cb098a0059e90f..b4242c27b06880a8e119bf6a74f5d6b572d8332f 100644 --- a/ivette/src/frama-c/eva/cells.ts +++ b/ivette/src/frama-c/eva/cells.ts @@ -23,7 +23,7 @@ export interface Size { cols: number; rows: number } export const EMPTY: Size = { cols: 0, rows: 0 }; export function sizeof(text?: string): Size { - if (!text) return EMPTY; + if (text === undefined) return EMPTY; const lines = text.split('\n'); return { rows: lines.length, @@ -82,55 +82,15 @@ export interface EvaValues { size: Size; } -export function valueAt(v: EvaValues, st: EvaState): string | undefined { - switch (st) { - case 'Here': return v.values; - case 'After': return v.v_after; - case 'Then': return v.v_then; - case 'Else': return v.v_else; - } -} - -export function diffAfter( - v: EvaValues, - effects: boolean, - st: EvaState, -): string | undefined { - if (!effects) return undefined; - switch (st) { - case 'Here': return v.v_after; - default: return v.values; - } -} - -export function diffThen( - v: EvaValues, - condition: boolean, - st: EvaState, -): string | undefined { - if (!condition) return undefined; - switch (st) { - case 'Here': return v.v_then; - default: return v.values; - } -} - -export function diffElse( - v: EvaValues, - condition: boolean, - st: EvaState, -): string | undefined { - if (!condition) return undefined; - switch (st) { - case 'Here': return v.v_else; - default: return v.values; - } -} - // -------------------------------------------------------------------------- // --- Value Cache // -------------------------------------------------------------------------- +export interface Probe { + fct: string | undefined; + marker: Ast.marker; +} + export class ValueCache { private readonly state: ModelCallbacks; @@ -159,33 +119,32 @@ export class ValueCache { return this.probes.get(target) ?? EMPTY; } - private static stackKey(stmt: string, callstack: Values.callstack) { - return `${stmt}::${callstack}`; + private static stackKey(fct: string, callstack: Values.callstack) { + return `${fct}::${callstack}`; } - getStackSize(stmt: string, callstack: Values.callstack) { - const key = ValueCache.stackKey(stmt, callstack); + getStackSize(fct: string, callstack: Values.callstack) { + const key = ValueCache.stackKey(fct, callstack); return this.stacks.get(key) ?? EMPTY; } // --- Cached Values & Request Update getValues( - target: Ast.marker, - stmt: string | undefined, + { fct, marker }: Probe, callstack: Values.callstack | undefined, ): EvaValues { - const key = callstack !== undefined ? `${target}@${callstack}` : target; + const key = callstack !== undefined ? `${marker}@${callstack}` : marker; const cache = this.vcache; const cached = cache.get(key); if (cached) return cached; const newValue: EvaValues = { values: '', size: EMPTY }; - if (callstack !== undefined && stmt === undefined) + if (callstack !== undefined && fct === undefined) return newValue; - // callstack !== undefined ==> stmt !== undefined) + // callstack !== undefined ==> fct !== undefined) cache.set(key, newValue); Server - .send(Values.getValues, { target, callstack }) + .send(Values.getValues, { target: marker, callstack }) .then((r) => { newValue.errors = undefined; newValue.values = r.values; @@ -193,7 +152,7 @@ export class ValueCache { newValue.v_then = r.v_then; newValue.v_else = r.v_else; newValue.alarms = r.alarms; - if (this.updateLayout(target, stmt, callstack, newValue)) + if (this.updateLayout(marker, fct, callstack, newValue)) this.state.forceLayout(); else this.state.forceUpdate(); @@ -209,7 +168,7 @@ export class ValueCache { private updateLayout( target: Ast.marker, - stmt: string | undefined, + fct: string | undefined, callstack: Values.callstack | undefined, v: EvaValues, ): boolean { @@ -231,9 +190,9 @@ export class ValueCache { } // max size for stack row if (callstack !== undefined) { - if (stmt === undefined) small = false; + if (fct === undefined) small = false; else { - const key = ValueCache.stackKey(stmt, callstack); + const key = ValueCache.stackKey(fct, callstack); const cs = this.stacks.get(key) ?? EMPTY; if (!leq(s, cs)) { this.stacks.set(key, merge(cs, s)); diff --git a/ivette/src/frama-c/eva/diffed.tsx b/ivette/src/frama-c/eva/diffed.tsx index 9edf57ee95f6edd03c49de4f3935ad4dbb70156a..19dd3b913268add01e6b2623a54dda1879b5232b 100644 --- a/ivette/src/frama-c/eva/diffed.tsx +++ b/ivette/src/frama-c/eva/diffed.tsx @@ -50,7 +50,7 @@ export class DiffBuffer { if (value) { this.scratch += '\0'.repeat(value.length); this.contents.push( - <span className={MODIFIED} title="Modified"> {value}</span>, + <span className={MODIFIED} title="Modified">{value}</span>, ); } } else if (removed) { @@ -74,7 +74,7 @@ export class DiffBuffer { getContents(): React.ReactNode { this.flush(); - return <>{React.Children.toArray(this.contents)}</>; + return React.Children.toArray(this.contents); } getScratch() { @@ -93,17 +93,16 @@ export interface Diff2Props { diff: string; } -export function Diff2(props: Diff2Props): JSX.Element { +export function Diff2(props: Diff2Props) { const { text, diff } = props; - const contents = React.useMemo(() => { + const contents = React.useMemo<React.ReactNode>(() => { if (text === diff) return text; const buffer = new DiffBuffer(); const chunks = diffChars(text, diff); - // console.log('DIFF', text, diff, chunks); chunks.forEach(buffer.push); return buffer.getContents(); }, [text, diff]); - return contents as JSX.Element; + return <>{contents}</>; } /* --------------------------------------------------------------------------*/ @@ -116,9 +115,9 @@ export interface Diff3Props { diffB: string; } -export function Diff3(props: Diff3Props): JSX.Element { +export function Diff3(props: Diff3Props) { const { text, diffA, diffB } = props; - const contents = React.useMemo(() => { + const contents = React.useMemo<React.ReactNode>(() => { if (text === diffA && text === diffB) return text; const buffer = new DiffBuffer(); diffChars(diffA, diffB).forEach(buffer.push); @@ -127,7 +126,7 @@ export function Diff3(props: Diff3Props): JSX.Element { diffChars(text, scratch).forEach(buffer.push); return buffer.getContents(); }, [text, diffA, diffB]); - return contents as JSX.Element; + return <>{contents}</>; } /* --------------------------------------------------------------------------*/ @@ -135,17 +134,20 @@ export function Diff3(props: Diff3Props): JSX.Element { /* --------------------------------------------------------------------------*/ export interface DiffProps { - text: string; + text?: string; diff?: string; diffA?: string; diffB?: string; } -export function Diff(props: DiffProps): JSX.Element { +export function Diff(props: DiffProps) { const { text, diff, diffA, diffB } = props; - if (text === diff) return <>{text}</>; - if (diff !== undefined) + if (text === undefined) + return diff ? <>{diff}</> : null; + if (diff !== undefined) { + if (diff === text) return <>{text}</>; return <Diff2 text={text} diff={diff} />; + } if (diffA === undefined) { if (diffB === undefined) return <>{text}</>; return <Diff2 text={text} diff={diffB} />; @@ -154,7 +156,6 @@ export function Diff(props: DiffProps): JSX.Element { return <Diff2 text={text} diff={diffA} />; } return <Diff3 text={text} diffA={diffA} diffB={diffB} />; - } // -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/eva/layout.ts b/ivette/src/frama-c/eva/layout.ts index c416fa7c8945753a5ac0ff2490a9618ac73bc430..c116ca319731b62b3b5cc456d666b07528931f2f 100644 --- a/ivette/src/frama-c/eva/layout.ts +++ b/ivette/src/frama-c/eva/layout.ts @@ -12,15 +12,16 @@ export interface LayoutProps { margin: number; } -export type RowKind = 'probes' | 'values' | 'callstack'; +export type RowKind = 'section' | 'probes' | 'values' | 'callstack'; export interface Row { key: string; + fct: string; kind: RowKind; probes: Probe[]; headstack?: string; - stacks?: number; stackIndex?: number; + stackCount?: number; callstack?: callstack; hlines: number; } @@ -29,8 +30,9 @@ export interface Row { /* --- Layout Enfine ---*/ /* --------------------------------------------------------------------------*/ -const PADDING = 2; -const INSET = 1; +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; @@ -38,6 +40,7 @@ export class LayoutEngine { // --- Setup + private readonly folded: (fct: string) => boolean; private readonly values: ValueCache; private readonly stacks: StacksCache; private readonly hcrop: number; @@ -48,25 +51,30 @@ export class LayoutEngine { 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 + 2 * zoom; + this.vcrop = VCROP + 3 * zoom; this.hcrop = HCROP + zoom; - this.margin = props?.margin ?? 80; + this.margin = (props?.margin ?? 80) - HEAD_PADDING; this.push = this.push.bind(this); } // --- Probe Buffer - private byStacks?: string; // stmt + private byFct?: string; // current function + private byStk?: 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 sCols = s.cols + INSET; - const cols = zoomed ? sCols : Math.min(sCols, this.hcrop); + 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), @@ -74,65 +82,128 @@ export class LayoutEngine { }; } - push(p: Probe) { + layout(ps: Probe[]): Row[] { + this.chained = undefined; + 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 cp = p.byCallstacks; + const cq = q.byCallstacks; + if (!cp && cq) return (-1); + if (cp && !cq) return (+1); + } + 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) { + // --- sectionning + const { fct, byCallstacks: stk } = p; + if (fct !== this.byFct) { + this.flush(); + this.rows.push({ + key: `S:${fct}`, + kind: 'section', + fct, + probes: [], + hlines: 1, + }); + this.byFct = fct; + this.byStk = stk; + this.skip = this.folded(fct); + } else if (stk !== this.byStk) { + this.flush(); + this.byStk = stk; + } + 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); - const stmt = p.byCallstacks ? p.stmt : undefined; - if (stmt !== this.byStacks) { - this.flush(); - this.byStacks = stmt; - } - if (!stmt && s.cols + this.rowSize.cols > this.margin) + // --- queueing + if (!stk && s.cols + this.rowSize.cols > this.margin) this.flush(); this.rowSize = addH(this.rowSize, s); - this.rowSize.cols += PADDING; + this.rowSize.cols += CELL_PADDING; this.buffer.push(p); } // --- Flush Rows - flush(): Row[] { + private flush(): Row[] { const ps = this.buffer; const rs = this.rows; - if (ps.length > 0) { - const stmt = this.byStacks; - if (stmt) { + const fct = this.byFct; + if (fct && ps.length > 0) { + const stk = this.byStk; + const hlines = this.rowSize.rows; + if (stk) { // --- by callstacks - const wcs = this.stacks.getStacks(stmt); + 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; rs.push({ - key: `P${stmt}`, + key: `P:${fct}`, kind: 'probes', probes: ps, - stacks: wcs.length, + stackCount: callstacks, + fct, hlines: 1, }); - wcs.forEach((cs, k) => { + if (summary) rs.push({ + key: `V:${fct}`, + kind: 'values', + probes: ps, + stackIndex: -1, + stackCount: stacks.length, + fct, + hlines: 1, + }); + stacks.forEach((cs, k) => { rs.push({ - key: `C${stmt}::${cs}`, + key: `C:${fct}:${cs}`, kind: 'callstack', probes: ps, stackIndex: k, - stacks: wcs.length, + stackCount: callstacks, callstack: cs, - hlines: this.values.getStackSize(stmt, cs).rows, + fct, + hlines, }); }); } else { - // --- by callstacks + // --- not by callstacks const n = rs.length; rs.push({ - key: `P${n}`, + key: `P#${n}`, kind: 'probes', probes: ps, + fct, hlines: 1, }, { - key: `V${n}`, + key: `V#${n}`, kind: 'values', probes: ps, - hlines: this.rowSize.rows, + fct, + hlines, }); } } diff --git a/ivette/src/frama-c/eva/model.ts b/ivette/src/frama-c/eva/model.ts index 6f24c809c84d2360cc6a461d60e7c09ef7dcd0d0..0c5f7c2bfe107e58f0d396178f448f2958979b29 100644 --- a/ivette/src/frama-c/eva/model.ts +++ b/ivette/src/frama-c/eva/model.ts @@ -8,18 +8,17 @@ 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/api/plugins/eva/values'; -import * as Ast from 'frama-c/api/kernel/ast'; // Model import { Probe } from './probes'; import { StacksCache, Callsite } from './stacks'; -import { ModelCallbacks, ValueCache } from './cells'; +import { ModelCallbacks, ValueCache, EvaState } from './cells'; import { LayoutProps, LayoutEngine, Row } from './layout'; export interface ModelLayout extends LayoutProps { - fct?: string; - marker?: Ast.marker; + location?: States.Location; } /* --------------------------------------------------------------------------*/ @@ -34,37 +33,62 @@ export class Model implements ModelCallbacks { 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); - Server.onSignal(Values.changed, this.forceReload); + this.isFolded = this.isFolded.bind(this); } // --- Probes + private vstmt: EvaState = 'After'; + private vcond: EvaState = 'Here'; 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 getFocused() { return this.focused; } isFocused(p: Probe | undefined) { return this.focused === p; } - isRemanent(p: Probe | undefined) { return this.remanent === p; } - - getProbe(fct: string, m: Ast.marker): Probe { - let p = this.probes.get(m); - if (!p) { - p = new Probe(this, fct, m); - this.probes.set(m, p); - p.requestProbeInfo(); + + 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 p; + return undefined; } getStacks(p: Probe | undefined): Values.callstack[] { - const stmt = p?.stmt; - return stmt ? this.stacks.getStacks(stmt) : []; + return p ? this.stacks.getStacks(p.marker) : []; + } + + getVstmt(): EvaState { return this.vstmt; } + getVcond(): EvaState { return this.vcond; } + setVstmt(s: EvaState) { this.vstmt = s; this.forceUpdate(); } + setVcond(s: EvaState) { 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(); } // --- Caches @@ -105,8 +129,15 @@ export class Model implements ModelCallbacks { } isSelectedRow(row: Row): boolean { + if (!this.focused?.byCallstacks) return false; + const cs = this.callstack; + return cs !== undefined && cs === row.callstack; + } + + isAlignedRow(row: Row): boolean { const cs = this.callstack; - return cs !== undefined ? cs === row.callstack : false; + const cr = row.callstack; + return cs !== undefined && cr !== undefined && this.stacks.aligned(cs, cr); } getCallstack(): Values.callstack | undefined { @@ -122,13 +153,30 @@ export class Model implements ModelCallbacks { setLayout(ly: ModelLayout) { if (!equal(this.layout, ly)) { this.layout = ly; - const { fct, marker } = ly; - this.selected = - fct && marker ? this.getProbe(fct, marker) : undefined; + this.selected = this.getProbe(ly.location); this.forceLayout(); } } + metaSelection(location: States.Location) { + const p = this.getProbe(location); + if (p) { + if (p.transient) { + if (this.focused?.byCallstacks) + p.setByCallstacks(true); + else + p.setPersistent(); + } + } + } + + clearSelection() { + this.focused = undefined; + this.selected = undefined; + this.remanent = undefined; + this.forceLayout(); + } + // --- Recompute Layout private computeLayout() { @@ -137,17 +185,9 @@ export class Model implements ModelCallbacks { const s = this.selected; if (!s) { this.focused = undefined; - this.callstack = undefined; this.remanent = undefined; } else if (!s.loading) { this.focused = s; - const stacks = this.getStacks(s); - if (s.byCallstacks) { - const cs0 = this.callstack; - if (cs0) this.callstack = stacks.find((cs) => cs === cs0); - } else { - this.callstack = stacks.length === 1 ? stacks[0] : undefined; - } if (s.code && s.transient) { this.remanent = s; } else { @@ -164,9 +204,9 @@ export class Model implements ModelCallbacks { this.layout, this.values, this.stacks, + this.isFolded, ); - toLayout.sort(Probe.order).forEach(engine.push); - this.rows = engine.flush(); + this.rows = engine.layout(toLayout); this.laidout.emit(); this.lock = false; } @@ -196,16 +236,39 @@ export class Model implements ModelCallbacks { // --- 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); + } + } // -------------------------------------------------------------------------- -// --- EVA Model +// --- EVA Model Hook // -------------------------------------------------------------------------- let MODEL: Model | undefined; -export function getModelInstance(): Model { - if (!MODEL) MODEL = new Model(); +Server.onShutdown(() => { + if (MODEL) { + MODEL.unmount(); + MODEL = undefined; + } +}); + +export function useModel(): Model { + if (!MODEL) { + MODEL = new Model(); + MODEL.mount(); + } + Dome.useUpdate(MODEL.changed, MODEL.laidout); return MODEL; } diff --git a/ivette/src/frama-c/eva/probeinfos.tsx b/ivette/src/frama-c/eva/probeinfos.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7f8d1da41fcd27494b5905f9e234b811ee2277b2 --- /dev/null +++ b/ivette/src/frama-c/eva/probeinfos.tsx @@ -0,0 +1,163 @@ +// -------------------------------------------------------------------------- +// --- Eva Values +// -------------------------------------------------------------------------- + +// React & Dome +import React from 'react'; +import { Hpack, Filler } from 'dome/layout/boxes'; +import { Label, 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 { useModel } from './model'; +import { Stmt } from './valueinfos'; + +// -------------------------------------------------------------------------- +// --- Probe Editor +// -------------------------------------------------------------------------- + +function ProbeEditor() { + const model = useModel(); + const probe = model.getFocused(); + if (!probe || !probe.code) return null; + const { label } = probe; + const { code } = probe; + const { stmt } = probe; + const { rank } = probe; + const byCS = probe.byCallstacks; + const stacks = model.getStacks(probe); + const stackable = byCS || stacks.length > 1; + const { cols, rows } = sizeof(code); + const { transient } = probe; + const { zoomed } = probe; + const { zoomable } = probe; + return ( + <> + <Label className="eva-probeinfo-label">{label}</Label> + <div className="eva-probeinfo-code"> + <SizedArea cols={cols} rows={rows}>{code}</SizedArea> + </div> + <Code><Stmt stmt={stmt} rank={rank} /></Code> + <IconButton + icon="ITEMS.LIST" + className="eva-probeinfo-button" + display={stackable} + selected={byCS} + title={`Details by callstack (${stacks})`} + onClick={() => { if (probe) probe.setByCallstacks(!byCS); }} + /> + <IconButton + icon="SEARCH" + className="eva-probeinfo-button" + display={zoomable} + selected={zoomed} + 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() { + const model = useModel(); + const probe = model.getFocused(); + const fct = probe?.fct; + const byCS = probe?.byCallstacks; + 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 /> + <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 === 'Here'} + title="Show values in all conditions" + onClick={() => model.setVcond('Here')} + /> + <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' ? 'Here' : 'After'); + }} + /> + </ButtonGroup> + </Hpack> + ); +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/eva/probes.ts b/ivette/src/frama-c/eva/probes.ts index c920d71d0068667623c4e98973e82a291bad2313..e301c9f6439052a0f86f958b488d3ede1a467508 100644 --- a/ivette/src/frama-c/eva/probes.ts +++ b/ivette/src/frama-c/eva/probes.ts @@ -8,7 +8,7 @@ import * as Values from 'frama-c/api/plugins/eva/values'; import * as Ast from 'frama-c/api/kernel/ast'; // Model -import { ModelCallbacks, EvaState } from './cells'; +import { ModelCallbacks } from './cells'; /* --------------------------------------------------------------------------*/ /* --- Probe Labelling ---*/ @@ -46,6 +46,8 @@ export class Probe { readonly fct: string; readonly marker: Ast.marker; readonly model: ModelCallbacks; + next?: Probe; + prev?: Probe; transient = true; loading = true; label?: string; @@ -57,7 +59,6 @@ export class Probe { byCallstacks = false; zoomed = false; zoomable = false; - vstate: EvaState = 'Here'; effects = false; condition = false; @@ -66,26 +67,30 @@ export class Probe { this.marker = marker; this.model = state; this.requestProbeInfo = this.requestProbeInfo.bind(this); - this.setTransient = this.setTransient.bind(this); } requestProbeInfo() { this.loading = true; + this.label = '…'; Server .send(Values.getProbeInfo, this.marker) .then(({ code, stmt, rank, effects, condition }) => { this.code = code; this.stmt = stmt; this.rank = rank; + this.label = undefined; this.effects = effects; this.condition = condition; - this.vstate = effects ? 'After' : 'Here'; this.loading = false; + this.updateLabel(); }) .catch(() => { this.code = '(error)'; this.stmt = undefined; this.rank = undefined; + this.label = undefined; + this.effects = false; + this.condition = false; this.loading = false; }) .finally(this.model.forceLayout); @@ -95,16 +100,23 @@ export class Probe { // --- Internal State // -------------------------------------------------------------------------- - setTransient(tr: boolean) { + setPersistent() { this.updateTransient(false); } + setTransient() { this.updateTransient(true); } + + private updateLabel() { + const { transient, label, code } = this; + if (transient && label) { + LabelRing.push(label); + this.label = undefined; + } + if (!transient && !label && code && code.length > LabelSize) + this.label = newLabel(); + } + + private updateTransient(tr: boolean) { if (this.transient !== tr) { this.transient = tr; - if (tr && this.label) { - LabelRing.push(this.label); - this.label = undefined; - } - if (!tr && !this.label && this.code && this.code.length > LabelSize) { - this.label = newLabel(); - } + this.updateLabel(); this.model.forceLayout(); } } @@ -112,6 +124,7 @@ export class Probe { setByCallstacks(byCS: boolean) { if (byCS !== this.byCallstacks) { this.byCallstacks = byCS; + if (byCS) this.setPersistent(); this.model.forceLayout(); } } @@ -123,29 +136,6 @@ export class Probe { } } - setState(s: EvaState | undefined) { - this.vstate = s ?? 'Here'; - this.model.forceUpdate(); - } - - // -------------------------------------------------------------------------- - // --- Ordering - // -------------------------------------------------------------------------- - - static order(p: Probe, q: Probe): number { - const rp = p.rank ?? 0; - const rq = q.rank ?? 0; - if (rp < rq) return (-1); - if (rp > rq) return (+1); - const cp = p.byCallstacks; - const cq = q.byCallstacks; - if (!cp && cq) return (-1); - if (cp && !cq) return (+1); - if (p.marker < q.marker) return (-1); - if (p.marker > q.marker) return (+1); - return 0; - } - } /* --------------------------------------------------------------------------*/ diff --git a/ivette/src/frama-c/eva/sized.tsx b/ivette/src/frama-c/eva/sized.tsx index e092f088c71ab39547c1d626951aa93648049807..bb4b564a1740f236f4d79965a22db56e9b9987ed 100644 --- a/ivette/src/frama-c/eva/sized.tsx +++ b/ivette/src/frama-c/eva/sized.tsx @@ -35,6 +35,7 @@ export class Streamer { } return this.v; } + } export class FontSizer { @@ -73,6 +74,12 @@ export class FontSizer { return p + n * k; } + dump(x: string) { + const k = this.k.mean(); + const p = this.p.mean(); + return `${k}.${x}+${p}`; + } + } /* --------------------------------------------------------------------------*/ @@ -102,7 +109,7 @@ export function SizedArea(props: SizedAreaProps) { return ( <div ref={refSizer} - className="eva-sized-area dome-text-code" + className="eva-sized-area dome-text-cell" > {children} </div> diff --git a/ivette/src/frama-c/eva/stacks.ts b/ivette/src/frama-c/eva/stacks.ts index 013638a1630db36810df46b65f3b8cc011226801..891d90fea51c93ca5e28fd5493ba85538cb5ca0c 100644 --- a/ivette/src/frama-c/eva/stacks.ts +++ b/ivette/src/frama-c/eva/stacks.ts @@ -3,6 +3,7 @@ // -------------------------------------------------------------------------- import * as Server from 'frama-c/server'; +import * as Ast from 'frama-c/api/kernel/ast'; import * as Values from 'frama-c/api/plugins/eva/values'; import { ModelCallbacks } from './cells'; @@ -15,10 +16,14 @@ export type callstacks = Values.callstack[]; export interface Callsite { callee: string; caller?: string; - stmt?: string; + stmt?: Ast.marker; rank?: number; } +function equalSite(a: Callsite, b: Callsite): boolean { + return a.stmt === b.stmt && a.callee === b.callee; +} + // -------------------------------------------------------------------------- // --- CallStacks Cache // -------------------------------------------------------------------------- @@ -27,6 +32,7 @@ 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[]>(); // -------------------------------------------------------------------------- @@ -39,17 +45,29 @@ export class StacksCache { clear() { this.stacks.clear(); + this.calls.clear(); } // -------------------------------------------------------------------------- // --- Getters // -------------------------------------------------------------------------- - getStacks(stmt: string): callstacks { - const cs = this.stacks.get(stmt); + getSummary(fct: string): boolean { + return this.summary.get(fct) ?? true; + } + + setSummary(fct: string, s: boolean) { + 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(stmt, []); - this.requestCallstacks(stmt); + this.stacks.set(key, []); + this.requestStacks(key, markers); return []; } @@ -61,16 +79,30 @@ export class StacksCache { 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 requestCallstacks(stmt: string) { + private requestStacks(key: string, markers: Ast.marker[]) { Server - .send(Values.getCallstacks, stmt) - .then((cs: callstacks) => { - this.stacks.set(stmt, cs); + .send(Values.getCallstacks, markers) + .then((stacks: callstacks) => { + this.stacks.set(key, stacks); this.model.forceLayout(); + this.model.forceUpdate(); }); } diff --git a/ivette/src/frama-c/eva/style.css b/ivette/src/frama-c/eva/style.css index 12ff6e4916e8c18aff58de792b27a599606f26d2..e578ea782ba99cb6cf9866b99429f404c0233191 100644 --- a/ivette/src/frama-c/eva/style.css +++ b/ivette/src/frama-c/eva/style.css @@ -1,19 +1,3 @@ -/* -------------------------------------------------------------------------- */ -/* --- Styling Values --- */ -/* -------------------------------------------------------------------------- */ - -.eva-sized-area { - padding: 3px; - white-space: pre; - overflow: hidden; - text-overflow: ellipsis; -} - -.eva-stmt { - cursor: default; - color: grey; -} - /* -------------------------------------------------------------------------- */ /* --- Probe Panel --- */ /* -------------------------------------------------------------------------- */ @@ -36,6 +20,7 @@ .eva-probeinfo-code { flex: 0 1 auto; + height: fit-content; background: lightgrey; min-width: 120px; width: auto; @@ -70,6 +55,7 @@ .eva-info { width: 100%; + flex-wrap: wrap; background: #ccc; padding-top: 3px; padding-left: 12px; @@ -77,6 +63,7 @@ } .eva-callsite { + flex: 0 0 auto; fill: #7cacbb; background: #eee; border-radius: 4px; @@ -84,8 +71,51 @@ padding-right: 7px; } -.eva-callsite { - cursor: default; +.eva-callsite.eva-focused { + background: #cbe4cb; +} + +.eva-callsite.eva-focused.eva-transient { + background: #f9dca6; +} + +/* -------------------------------------------------------------------------- */ +/* --- Alarms --- */ +/* -------------------------------------------------------------------------- */ + +.eva-cell-alarms { + fill: orange; + position: absolute; + top: -1px; + right: 3px; + z-index: 1; +} + +.eva-alarm-info { + font-size: x-small; + padding: 0px; + margin: 1px; +} + +.eva-alarm-none { display: none; } +.eva-alarm-True { fill: green; } +.eva-alarm-False { fill: #fb4e4a; } +.eva-alarm-Unknown { fill: darkorange; } + +/* -------------------------------------------------------------------------- */ +/* --- Styling Values --- */ +/* -------------------------------------------------------------------------- */ + +.eva-stmt { + color: grey; + text-select: none; +} + +.eva-sized-area { + padding: 3px; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; } /* -------------------------------------------------------------------------- */ @@ -97,41 +127,71 @@ position: relative; flex: 0 1 auto; height: 100%; - border-bottom: thin solid black; +} + +/* -------------------------------------------------------------------------- */ +/* --- Table Heads --- */ +/* -------------------------------------------------------------------------- */ + +.eva-function { + padding-top: 0px; + background: #ccc; +} + +.eva-head { + padding-top: 2px; + color: #777; + text-align: center; border-left: thin solid black; + border-bottom: thin solid black; } -.eva-probes .eva-row { +.eva-probes .eva-head { border-top: thin solid black; } +.eva-phantom { + visibility: hidden; + height: 0; +} + /* -------------------------------------------------------------------------- */ /* --- Table Cells --- */ /* -------------------------------------------------------------------------- */ -.eva-stack { - width: 12px; - padding-top: 1px; - color: #777; - text-align: center; +.eva-fct-fold { + margin-left: 4px; + margin-right: 8px; + padding: 0px; +} + +.eva-fct-name { + padding: 0px; + font-weight: bold; } .eva-cell { flex: 1 1 auto; border-right: thin solid black; + border-bottom: thin solid black; } -.eva-cell:nth-child(last) { - border-right: none; +.eva-cell:nth-child(2) { + border-left: thin solid black; } .eva-probes .eva-cell { padding: 2px; text-align: center; + border-top: thin solid black; } +/* -------------------------------------------------------------------------- */ +/* --- Cell Diffs --- */ +/* -------------------------------------------------------------------------- */ + .eva-diff-shadow { - border: solid red thin; + border: solid transparent 2px; position: relative; z-index: -1; } @@ -139,87 +199,62 @@ .eva-diff-added { } .eva-diff-removed { text-decoration: strike } -.eva-state-Here .eva-diff { background: pink; } -.eva-state-After .eva-diff { background: yellow; } -.eva-state-Then .eva-diff { background: green; } -.eva-state-Else .eva-diff { background: orange; } - -/* -------------------------------------------------------------------------- */ -/* --- Alarms --- */ -/* -------------------------------------------------------------------------- */ - -.eva-cell-alarms { - fill: orange; - position: absolute; - top: -1px; - right: 3px; - z-index: 1; -} - -.eva-alarm-info { - font-size: x-small; - padding: 0px; - margin: 1px; -} - -.eva-alarm-True { fill: green; } -.eva-alarm-False { fill: red; } -.eva-alarm-Unknown { fill: darkorange; } +.eva-state-Here .eva-diff { background: #95f5ff; } +.eva-state-After .eva-diff { background: #fff819; } +.eva-state-Then .eva-diff { background: #c8e06e; } +.eva-state-Else .eva-diff { background: #ff834e; } /* -------------------------------------------------------------------------- */ /* --- Table Rows Background --- */ /* -------------------------------------------------------------------------- */ -.eva-probes .eva-row { - background: #cbe4cb; -} +/* --- Probes --- */ -.eva-values .eva-row { - background: #eee; -} - -.eva-callstack.eva-row-odd .eva-row { - background: #eee; +.eva-probes .eva-cell { + background: #cbe4cb; } -.eva-callstack.eva-row-even .eva-row { - background: #e2e8e8; +.eva-probes .eva-focused { + background: #02da02; } -.eva-callstack.eva-row-odd .eva-row .eva-transient { +.eva-probes .eva-transient { background: #fff0d5; } -.eva-callstack.eva-row-even .eva-row .eva-transient { - background: #fff0d5; +.eva-probes .eva-transient.eva-focused { + background: orange; } -.eva-callstack.eva-row-selected .eva-row .eva-transient { - background: lightblue; -} -.eva-callstack.eva-row-selected .eva-row { - background: lightblue; +/* --- Values / Callstacks --- */ + +.eva-values .eva-cell { + background: #eee; } -.eva-probes .eva-focused { - background: #02da02; +.eva-values .eva-focused { + background: #e1e8f7; } -.eva-probes .eva-transient { +.eva-values .eva-transient.eva-focused { background: #fff0d5; } -.eva-probes .eva-transient.eva-focused { - background: orange; +.eva-callstack.eva-row-odd .eva-cell { + background: #eee; } -.eva-stack { - background: #eee; +.eva-callstack.eva-row-even .eva-cell { + background: #e2e8e8; } -.eva-callstack.eva-row-selected .eva-stack { +.eva-callstack.eva-row-aligned { background: lightblue; } +.eva-callstack.eva-row-selected { + background: #f9dca6; +} + /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/frama-c/eva/valueinfos.tsx b/ivette/src/frama-c/eva/valueinfos.tsx new file mode 100644 index 0000000000000000000000000000000000000000..72271f07b54b58aa5f8f0cb486e5a892ae205286 --- /dev/null +++ b/ivette/src/frama-c/eva/valueinfos.tsx @@ -0,0 +1,117 @@ +// -------------------------------------------------------------------------- +// --- 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'; + +// Locals +import { EvaAlarm } from './cells'; +import { Callsite } from './stacks'; +import { useModel } from './model'; + +// -------------------------------------------------------------------------- +// --- Stmt Printer +// -------------------------------------------------------------------------- + +interface StmtProps { + stmt?: string; + rank?: number; +} + +export function Stmt(props: StmtProps) { + const { rank, stmt } = props; + if (rank === undefined || !stmt) return null; + const title = `Stmt at global rank ${rank} (internal id: ${stmt})`; + return ( + <span className="dome-text-cell eva-stmt" title={title}> + @S{rank} + </span> + ); +} + +// -------------------------------------------------------------------------- +// --- Alarms Panel +// -------------------------------------------------------------------------- + +export function AlarmsInfos() { + const model = useModel(); + const probe = model.getFocused(); + if (probe) { + const callstack = model.getCallstack(); + const domain = model.values.getValues(probe, callstack); + const alarms = domain?.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() { + const model = useModel(); + const [, setSelection] = States.useSelection(); + const focused = model.getFocused(); + const callstack = model.getCalls(); + if (callstack.length <= 1) return null; + const makeCallsite = ({ caller, stmt, rank }: 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} rank={rank} /> + </Cell> + ); + }; + return ( + <Hpack className="eva-info"> + {callstack.map(makeCallsite)} + </Hpack> + ); +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/eva/valuetable.tsx b/ivette/src/frama-c/eva/valuetable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8a4dff4bac16f75bca3f4f18390d08df12333490 --- /dev/null +++ b/ivette/src/frama-c/eva/valuetable.tsx @@ -0,0 +1,358 @@ +// -------------------------------------------------------------------------- +// --- Eva Values +// -------------------------------------------------------------------------- + +// React & Dome +import React from 'react'; +import * as Dome from 'dome'; +import { classes } from 'dome/misc/utils'; +import { VariableSizeList } from 'react-window'; +import { Hpack, Filler } from 'dome/layout/boxes'; +import { Icon } from 'dome/controls/icons'; +import { Cell } from 'dome/controls/labels'; +import { IconButton } from 'dome/controls/buttons'; + +// Frama-C +import * as States from 'frama-c/states'; + +// Locals +import { SizedArea, HSIZER, WSIZER } from './sized'; +import { Diff, DiffProps } from './diffed'; +import { sizeof, EvaValues, EvaState } from './cells'; +import { Probe } from './probes'; +import { Row } from './layout'; +import { Callsite } from './stacks'; +import { useModel } from './model'; +import { Stmt } from './valueinfos'; +import './style.css'; + +// -------------------------------------------------------------------------- +// --- Cell Diffs +// -------------------------------------------------------------------------- + +function isTrivial(v: EvaValues) { + return v.values === '{0; 1}' && + v.v_then === '{1}' && + v.v_else === '{0}'; +} + +function computeDiffs( + condition: boolean, + v: EvaValues, + vstate: EvaState, +): DiffProps { + if (condition) { + const trv = isTrivial(v); + switch (vstate) { + case 'Here': + case 'After': + return trv ? { text: v.values } : + { text: v.values, diffA: v.v_then, diffB: v.v_else }; + case 'Then': + return { text: v.v_then, diff: !trv ? v.values : undefined }; + case 'Else': + return { text: v.v_else, diff: !trv ? v.values : undefined }; + } + } else { + switch (vstate) { + case 'Here': + case 'Then': + case 'Else': + return { text: v.values, diff: v.v_after }; + case 'After': + return { text: v.v_after, diff: v.values }; + } + } +} + +// -------------------------------------------------------------------------- +// --- Table Cell +// -------------------------------------------------------------------------- + +interface TableCellProps { + probe: Probe; + row: Row; +} + +const CELLPADDING = 12; + +function TableCell(props: TableCellProps) { + const model = useModel(); + const [, setSelection] = States.useSelection(); + const { probe, row } = props; + 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; + const { transient } = probe; + const focused = model.getFocused(); + const isFocused = focused === probe; + + switch (kind) { + + // ---- Probe Contents + case 'probes': + if (transient) { + contents = <span className="dome-text-label">« Probe »</span>; + } else { + const { stmt, rank, code, label } = probe; + const textClass = label ? 'dome-text-label' : 'dome-text-cell'; + contents = ( + <> + <span className={textClass}>{label ?? code}</span> + <Stmt stmt={stmt} rank={rank} /> + </> + ); + } + break; + + // ---- Values Contents + case 'values': + case 'callstack': + { + const domain = model.values.getValues(probe, callstack); + const { alarms = [] } = domain; + const { condition } = probe; + const vstate = condition ? model.getVcond() : model.getVstmt(); + const vdiffs = computeDiffs(condition, domain, vstate); + const text = vdiffs.text ?? vdiffs.diff; + const { cols, rows } = sizeof(text); + let status = 'none'; + if (alarms.length > 0) { + if (alarms.find(([st, _]) => st === 'False')) status = 'False'; + else status = 'Unknown'; + } + const alarmClass = `eva-cell-alarms eva-alarm-${status}`; + contents = ( + <> + <Icon className={alarmClass} size={10} id="WARNING" /> + <SizedArea cols={cols} rows={rows}> + <span className={`eva-state-${vstate}`}> + <Diff {...vdiffs} /> + </span> + </SizedArea> + </> + ); + } + break; + + } + + // --- 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); + }; + return ( + <div + className={className} + style={style} + onClick={onClick} + onDoubleClick={onDoubleClick} + > + {contents} + </div> + ); +} + +// -------------------------------------------------------------------------- +// --- Table Section +// -------------------------------------------------------------------------- + +interface TableSectionProps { + fct: string; + folded: boolean; + foldable: boolean; + onClick: () => void; +} + +function TableSection(props: TableSectionProps) { + const { fct, foldable, folded, onClick } = props; + const icon = folded ? 'ANGLE.RIGHT' : + foldable ? 'ANGLE.DOWN' : 'TRIANGLE.RIGHT'; + return ( + <> + <IconButton + className="eva-fct-fold" + size={10} + offset={-1} + icon={icon} + enabled={foldable} + onClick={onClick} + /> + <Cell className="eva-fct-name">{fct}</Cell> + </> + ); +} + +// -------------------------------------------------------------------------- +// --- Table Row Header +// -------------------------------------------------------------------------- + +interface TableHeadProps { + stackCalls: Callsite[]; + stackIndex: number | undefined; + stackCount: number | undefined; + onClick: () => void; +} + +function makeStackTitle(calls: Callsite[]) { + 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> + ); +} + +// -------------------------------------------------------------------------- +// --- Table Row +// -------------------------------------------------------------------------- + +interface TableRowProps { + style: React.CSSProperties; + index: number; +} + +function TableRow(props: TableRowProps) { + const model = useModel(); + 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); + return ( + <Hpack className="eva-function" style={props.style}> + <TableSection + fct={fct} + folded={folded} + foldable={foldable} + onClick={() => model.setFolded(fct, !folded)} + /> + </Hpack> + ); + } + 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} + /> + ); + return ( + <Hpack style={props.style}> + <div className={rowClass}> + <TableHead + stackIndex={sk} + stackCount={sc} + stackCalls={calls} + onClick={onHeaderClick} + /> + {probes.map(makeCell)} + </div> + <Filler /> + </Hpack> + ); +} + +// -------------------------------------------------------------------------- +// --- Values Panel +// -------------------------------------------------------------------------- + +export interface Dimension { + width: number; + height: number; +} + +export interface ValuesPanelProps extends Dimension { + zoom: number; +} + +export function ValuesPanel(props: ValuesPanelProps) { + const model = useModel(); + const { zoom, width, height } = props; + // --- reset line cache + const listRef = React.useRef<VariableSizeList>(null); + Dome.useEvent(model.laidout, () => { + setImmediate(() => { + const vlist = listRef.current; + if (vlist) vlist.resetAfterIndex(0, true); + }); + }); + // --- 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(); + React.useEffect(() => { + const location = selection?.current; + model.setLayout({ zoom, margin, location }); + }); + // --- render list + return ( + <VariableSizeList + ref={listRef} + itemCount={model.getRowCount()} + itemKey={model.getRowKey} + itemSize={getRowHeight} + estimatedItemSize={estimatedHeight} + width={width} + height={height} + itemData={model} + > + {TableRow} + </VariableSizeList> + ); +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 3895ee4978f75af68bed38a3677e579510941174..1feb79a7c847cd568d0ca8be8f36cd29288f9ebf 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -15,6 +15,7 @@ import { Order } from 'dome/data/compare'; import { GlobalState, useGlobalState } from 'dome/data/states'; import { useModel } from 'dome/table/models'; import { CompactModel } from 'dome/table/arrays'; +import * as Ast from 'frama-c/api/kernel/ast'; import * as Server from './server'; const PROJECT = new Dome.Event('frama-c.project'); @@ -445,22 +446,15 @@ export function useSyncArray<K, A>( // --- Selection // -------------------------------------------------------------------------- -type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = - Partial<T> & U[keyof U]; - -export interface FullLocation { - /** Function name. */ - readonly function: string; - /** Marker identifier. */ - readonly marker: string; -} - /** An AST location. * * Properties [[function]] and [[marker]] are optional, * but at least one of the two must be set. */ -export type Location = AtLeastOne<FullLocation>; +export type Location = { + fct?: string; + marker?: Ast.marker; +}; export interface HistorySelection { /** Previous locations with respect to the [[current]] one. */ @@ -554,7 +548,7 @@ function isNthSelect(a: SelectionActions): a is NthSelect { /** Update selection to the given location. */ function selectLocation(s: Selection, location: Location): Selection { const [prevSelections, nextSelections] = - s.current && s.current.function !== location.function ? + s.current && s.current.fct !== location.fct ? [[s.current, ...s.history.prevSelections], []] : [s.history.prevSelections, s.history.nextSelections]; return { @@ -603,19 +597,18 @@ function fromHistory(s: Selection, action: HistorySelectActions): Selection { */ function fromMultipleSelections( s: Selection, - action: 'MULTIPLE_PREV' | 'MULTIPLE_NEXT' | 'MULTIPLE_CYCLE' - | 'MULTIPLE_CLEAR', + a: 'MULTIPLE_PREV' | 'MULTIPLE_NEXT' | 'MULTIPLE_CYCLE' | 'MULTIPLE_CLEAR', ): Selection { - switch (action) { + switch (a) { case 'MULTIPLE_PREV': case 'MULTIPLE_NEXT': case 'MULTIPLE_CYCLE': { const idx = - action === 'MULTIPLE_PREV' ? + a === 'MULTIPLE_PREV' ? s.multiple.index - 1 : s.multiple.index + 1; const index = - action === 'MULTIPLE_CYCLE' && idx >= s.multiple.allSelections.length ? + a === 'MULTIPLE_CYCLE' && idx >= s.multiple.allSelections.length ? 0 : idx; if (0 <= index && index < s.multiple.allSelections.length) { @@ -701,19 +694,20 @@ const emptySelection = { }, }; -const GlobalSelection = new GlobalState<Selection>(emptySelection); +export const MetaSelection = new Dome.Event<Location>('frama-c-meta-selection'); +export const GlobalSelection = new GlobalState<Selection>(emptySelection); Server.onShutdown(() => GlobalSelection.setValue(emptySelection)); +export function setSelection(location: Location, meta = false) { + const s = GlobalSelection.getValue(); + GlobalSelection.setValue(reducer(s, { location })); + if (meta) MetaSelection.emit(location); +} + /** Current selection. */ export function useSelection(): [Selection, (a: SelectionActions) => void] { - const [selection, setSelection] = useGlobalState(GlobalSelection); - - function update(action: SelectionActions) { - const nextSelection = reducer(selection, action); - setSelection(nextSelection); - } - - return [selection, update]; + const [current, setCurrent] = useGlobalState(GlobalSelection); + return [current, (action) => setCurrent(reducer(current, action))]; } // -------------------------------------------------------------------------- diff --git a/ivette/src/renderer/ASTinfo.tsx b/ivette/src/renderer/ASTinfo.tsx index be2aa17a99d70cb07f0ab23ac1a61174ac880922..4ad40c6cfb0f6f7a11ea12881388d5a2e20da908 100644 --- a/ivette/src/renderer/ASTinfo.tsx +++ b/ivette/src/renderer/ASTinfo.tsx @@ -33,7 +33,7 @@ const ASTinfo = () => { // Callbacks function onTextSelection(id: string) { // For now, the only markers are functions. - const location = { function: id }; + const location = { fct: id }; updateSelection({ location }); } diff --git a/ivette/src/renderer/ASTview.tsx b/ivette/src/renderer/ASTview.tsx index 8583af381634c5a90bebcf4e82ac79ff56515e73..36fe6b36ddb648b80bd617ff0c4fea518523f7e5 100644 --- a/ivette/src/renderer/ASTview.tsx +++ b/ivette/src/renderer/ASTview.tsx @@ -9,11 +9,10 @@ import * as States from 'frama-c/states'; import * as Utils from 'frama-c/utils'; import * as Dome from 'dome'; -import * as Json from 'dome/data/json'; import { RichTextBuffer } from 'dome/text/buffers'; import { Text } from 'dome/text/editors'; import { Component, TitleBar } from 'frama-c/LabViews'; -import * as AST from 'frama-c/api/kernel/ast'; +import * as Ast from 'frama-c/api/kernel/ast'; import * as Properties from 'frama-c/api/kernel/properties'; import { getCallers, getDeadCode } from 'frama-c/api/plugins/eva/general'; import { getWritesLval, getReadsLval } from 'frama-c/api/plugins/studia/studia'; @@ -38,7 +37,7 @@ async function loadAST( buffer.log('// Loading', theFunction, '…'); (async () => { try { - const data = await Server.send(AST.printFunction, theFunction); + const data = await Server.send(Ast.printFunction, theFunction); buffer.clear(); if (!data) { buffer.log('// No code for function', theFunction); @@ -63,7 +62,7 @@ async function loadAST( async function functionCallers(functionName: string) { try { const data = await Server.send(getCallers, functionName); - const locations = data.map(([fct, marker]) => ({ function: fct, marker })); + const locations = data.map(([fct, marker]) => ({ fct, marker })); return locations; } catch (err) { D.error(`Fail to retrieve callers of function '${functionName}':`, err); @@ -79,12 +78,12 @@ type access = 'Reads' | 'Writes'; async function studia( marker: string, - info: AST.markerInfoData, + info: Ast.markerInfoData, kind: access, ) { const request = kind === 'Reads' ? getReadsLval : getWritesLval; const data = await Server.send(request, marker); - const locations = data.direct.map(([f, m]) => ({ function: f, marker: m })); + const locations = data.direct.map(([f, m]) => ({ fct: f, marker: m })); const lval = info.name; if (locations.length > 0) { const name = `${kind} of ${lval}`; @@ -139,7 +138,7 @@ const ASTview = () => { const printed = React.useRef<string | undefined>(); const [selection, updateSelection] = States.useSelection(); const multipleSelections = selection?.multiple.allSelections; - const theFunction = selection?.current?.function; + const theFunction = selection?.current?.fct; const theMarker = selection?.current?.marker; const { buttons: themeButtons, theme, fontSize, wrapText } = Preferences.useThemeButtons({ @@ -150,7 +149,7 @@ const ASTview = () => { disabled: !theFunction, }); - const markersInfo = States.useSyncArray(AST.markerInfo); + const markersInfo = States.useSyncArray(Ast.markerInfo); const deadCode = States.useRequest(getDeadCode, theFunction); const propertyStatus = States.useSyncArray(Properties.status).getArray(); const statusDict = States.useTags(Properties.propStatusTags); @@ -158,7 +157,7 @@ const ASTview = () => { const setBullets = React.useCallback(() => { if (theFunction) { propertyStatus.forEach((prop) => { - if (prop.function === theFunction) { + if (prop.fct === theFunction) { const status = statusDict.get(prop.status); if (status) { const bullet = makeBullet(status); @@ -206,26 +205,25 @@ const ASTview = () => { if (theMarker) buffer.scroll(theMarker); }, [buffer, theMarker]); - function onTextSelection(id: string) { - if (selection.current) { - const location = { ...selection.current, marker: id }; - updateSelection({ location }); - } + function onSelection(markerId: string, meta = false) { + const fct = selection?.current?.fct; + const location = { fct, marker: Ast.jMarker(markerId) }; + updateSelection({ location }); + if (meta) States.MetaSelection.emit(location); } - async function onContextMenu(id: string) { + async function onContextMenu(markerId: string) { const items = []; - const markerId = (id as Json.key<'#markerInfo'>); const selectedMarkerInfo = markersInfo.getData(markerId); if (selectedMarkerInfo?.var === 'function') { if (selectedMarkerInfo.kind === 'declaration') { const name = selectedMarkerInfo?.name; if (name) { const locations = await functionCallers(name); - const locationsByFunction = _.groupBy(locations, (e) => e.function); + const locationsByFunction = _.groupBy(locations, (e) => e.fct); _.forEach(locationsByFunction, (e) => { - const callerName = e[0].function; + const callerName = e[0].fct; items.push({ label: `Go to caller ${callerName} ` + @@ -233,7 +231,7 @@ const ASTview = () => { onClick: () => updateSelection({ name: `Call sites of function ${name}`, locations, - index: locations.findIndex((l) => l.function === callerName), + index: locations.findIndex((l) => l.fct === callerName), }), }); }); @@ -242,7 +240,7 @@ const ASTview = () => { items.push({ label: `Go to definition of ${selectedMarkerInfo.name}`, onClick: () => { - const location = { function: selectedMarkerInfo.name }; + const location = { fct: selectedMarkerInfo.name }; updateSelection({ location }); }, }); @@ -252,7 +250,11 @@ const ASTview = () => { || selectedMarkerInfo?.var === 'variable'; function onClick(kind: access) { if (selectedMarkerInfo) - studia(markerId, selectedMarkerInfo, kind).then(updateSelection); + studia( + markerId, + selectedMarkerInfo, + kind, + ).then(updateSelection); } items.push({ label: 'Studia: select writes', @@ -281,7 +283,7 @@ const ASTview = () => { fontSize={fontSize} lineWrapping={wrapText} selection={theMarker} - onSelection={onTextSelection} + onSelection={onSelection} onContextMenu={onContextMenu} gutters={['bullet']} readOnly diff --git a/ivette/src/renderer/Globals.tsx b/ivette/src/renderer/Globals.tsx index 27d21bc47181ee8f530e98abf2c209324240992b..0a4d754b4fdcfb12cd00740af485edd0ae92275e 100644 --- a/ivette/src/renderer/Globals.tsx +++ b/ivette/src/renderer/Globals.tsx @@ -23,7 +23,7 @@ const makeHint = (fct: functionsData): GlobalHint => ({ id: fct.key, label: fct.name, title: fct.signature, - value: { function: fct.name }, + value: { fct: fct.name }, }); export function useHints(): [GlobalHint[], (pattern: string) => void] { @@ -101,12 +101,12 @@ export default () => { function isSelected(fct: functionsData) { return multipleSelection?.allSelections.some( - (l) => fct.name === l?.function + (l) => fct.name === l?.fct ); } // Currently selected function. - const current: undefined | string = selection?.current?.function; + const current: undefined | string = selection?.current?.fct; function showFunction(fct: functionsData) { const visible = @@ -119,7 +119,7 @@ export default () => { } function onSelection(name: string) { - updateSelection({ location: { function: name } }); + updateSelection({ location: { fct: name } }); } async function onContextMenu() { diff --git a/ivette/src/renderer/Locations.tsx b/ivette/src/renderer/Locations.tsx index b0e3f6c197ada15e1e21c12657c0a839b2dc6a07..22595c3cf9208ea6d51036380624b726333babf4 100644 --- a/ivette/src/renderer/Locations.tsx +++ b/ivette/src/renderer/Locations.tsx @@ -124,7 +124,7 @@ const LocationsTable = () => { width={25} getter={(r: { id: number }) => r.id + 1} /> - <Column id="function" label="Function" width={120} /> + <Column id="fct" label="Function" width={120} /> <Column id="marker" label="Statement" diff --git a/ivette/src/renderer/Properties.tsx b/ivette/src/renderer/Properties.tsx index 89737980d9c4855cf01eed52d767aef811f418df..5c77dd726e54444108a87bf9bf74a326d297b005 100644 --- a/ivette/src/renderer/Properties.tsx +++ b/ivette/src/renderer/Properties.tsx @@ -235,7 +235,7 @@ const byStatus = const byProperty: Compare.ByFields<Property> = { status: byStatus, - function: Compare.defined(Compare.alpha), + fct: Compare.defined(Compare.alpha), source: bySource, kind: Compare.structural, alarm: Compare.defined(Compare.alpha), @@ -270,7 +270,7 @@ class PropertyModel extends Arrays.CompactModel<Json.key<'#status'>, Property> { } filterItem(prop: Property) { - const kf = prop.function; + const kf = prop.fct; const cf = this.filterFun; const filteringFun = cf && filter('currentFunction'); const filterFunction = filteringFun ? kf === cf : true; @@ -424,7 +424,7 @@ const PropertyColumns = () => { getter={(prop: Property) => prop?.source} render={renderFile} /> - <ColumnCode id="function" label="Function" width={120} /> + <ColumnCode id="fct" label="Function" width={120} /> <ColumnTag id="kind" label="Property kind" getter={getKind} width={120} /> <ColumnTag id="alarm" label="Alarms" getter={getAlarm} width={160} /> <Column @@ -477,15 +477,14 @@ const RenderTable = () => { model.reload(); }, [model, data]); - const [selection, updateSelection] = - States.useSelection(); + const [selection, updateSelection] = States.useSelection(); const [showFilter, flipFilter] = Dome.useFlipSettings('ivette.properties.showFilter'); // Updating the filter Dome.useEvent(Reload, model.reload); - const selectedFunction = selection?.current?.function; + const selectedFunction = selection?.current?.fct; React.useEffect(() => { model.setFilterFunction(selectedFunction); }, [model, selectedFunction]); @@ -493,8 +492,8 @@ const RenderTable = () => { // Callbacks const onPropertySelection = React.useCallback( - ({ key: propKey, function: fct }: Property) => { - const location = { function: fct, marker: propKey }; + ({ key: marker, fct }: Property) => { + const location = { fct, marker }; updateSelection({ location }); }, [updateSelection], ); diff --git a/ivette/src/renderer/SourceCode.tsx b/ivette/src/renderer/SourceCode.tsx index 0c9f8a7dfa42d7a626938d1fe10f5d24016c76a8..aa1ca31d7984a27de13b2a886f9231e5afe1158b 100644 --- a/ivette/src/renderer/SourceCode.tsx +++ b/ivette/src/renderer/SourceCode.tsx @@ -7,7 +7,6 @@ import * as States from 'frama-c/states'; import * as Dome from 'dome'; import { readFile } from 'dome/system'; -import * as Json from 'dome/data/json'; import { RichTextBuffer } from 'dome/text/buffers'; import { Text } from 'dome/text/editors'; import { Component, TitleBar } from 'frama-c/LabViews'; @@ -37,7 +36,7 @@ const SourceCode = () => { // Hooks const buffer = React.useMemo(() => new RichTextBuffer(), []); const [selection] = States.useSelection(); - const theFunction = selection?.current?.function; + const theFunction = selection?.current?.fct; const theMarker = selection?.current?.marker; const { buttons: themeButtons, theme, fontSize, wrapText } = Preferences.useThemeButtons({ @@ -72,10 +71,8 @@ const SourceCode = () => { } // Actual source code loading upon function or marker update. const sloc = - /* Non-empty [selection] has defined either marker or function: we give - precedence to marker as it provides more precise source location. */ - (theMarker && - markersInfo.getData(theMarker as Json.key<'#markerInfo'>)?.sloc) + /* markers have more precise source location */ + (theMarker && markersInfo.getData(theMarker)?.sloc) ?? (theFunction && functionsData.find((e) => e.name === theFunction)?.sloc); if (sloc) { diff --git a/src/plugins/server/data.ml b/src/plugins/server/data.ml index 0dc576f61ca97541dd4e906cac794a5827f24732..1f20d6d4c36fb901dfa0745f3250418c76edc568 100644 --- a/src/plugins/server/data.ml +++ b/src/plugins/server/data.ml @@ -635,8 +635,8 @@ struct let get m a = try M.find a m.index with Not_found -> - let id = m.kid in - m.kid <- succ id ; + let id = succ m.kid in + m.kid <- id ; m.index <- M.add a id m.index ; Hashtbl.add m.lookup id a ; id diff --git a/src/plugins/server/kernel_ast.ml b/src/plugins/server/kernel_ast.ml index a8e380889208883e6e45eff039c4180a26dd898c..dfc1510305d2ee0835c65483225a72f20f85c703 100644 --- a/src/plugins/server/kernel_ast.ml +++ b/src/plugins/server/kernel_ast.ml @@ -206,7 +206,7 @@ struct ~package ~name:"markerInfo" ~descr:(Md.plain "Marker informations") - ~key:snd + ~key:snd ~keyType:Jstring ~iter model let create_tag = function @@ -322,7 +322,7 @@ end module KfMarker = struct type record let record : record Record.signature = Record.signature () - let fct = Record.field record ~name:"function" + let fct = Record.field record ~name:"fct" ~descr:(Md.plain "Function") (module Kf) let marker = Record.field record ~name:"marker" ~descr:(Md.plain "Marker") (module Marker) diff --git a/src/plugins/server/kernel_properties.ml b/src/plugins/server/kernel_properties.ml index 511c5299a0e354b86bef51f466c5d02b060e0eef..4f09c05dab339a6a5f2f94916f04dd81cdedea4f 100644 --- a/src/plugins/server/kernel_properties.ml +++ b/src/plugins/server/kernel_properties.ml @@ -281,7 +281,7 @@ let () = States.column model ~name:"status" ~data:(module PropStatus) ~get:(Property_status.Feedback.get) -let () = States.column model ~name:"function" +let () = States.column model ~name:"fct" ~descr:(Md.plain "Function") ~data:(module Joption(Kf)) ~get:Property.get_kf @@ -330,6 +330,7 @@ let array = ~name:"status" ~descr:(Md.plain "Status of Registered Properties") ~key:(fun ip -> Kernel_ast.Marker.create (PIP ip)) + ~keyType:Kernel_ast.Marker.jproperty ~iter ~add_update_hook ~add_remove_hook diff --git a/src/plugins/server/package.ml b/src/plugins/server/package.ml index 309cae632565703288b5cc95ca1a5793215cfba5..bb87b1da3d58d915060b0455fa705ae855e78eba 100644 --- a/src/plugins/server/package.ml +++ b/src/plugins/server/package.ml @@ -208,7 +208,7 @@ type requestInfo = { type arrayInfo = { arr_key: string; - arr_kind: string; + arr_kind: jtype; } type declKindInfo = diff --git a/src/plugins/server/package.mli b/src/plugins/server/package.mli index 952f0dd05659e32932bce624cb57c19fcc2e8741..30075f6243c419aca7d1e4e98ba70462c1704d1d 100644 --- a/src/plugins/server/package.mli +++ b/src/plugins/server/package.mli @@ -72,7 +72,7 @@ type requestInfo = { type arrayInfo = { arr_key: string; - arr_kind: string; + arr_kind: jtype; } type declKindInfo = diff --git a/src/plugins/server/states.ml b/src/plugins/server/states.ml index f9ae51a5b8acb166d074e7b2dc88b1624e90c1b1..52a682f2bc7b854e86c8435201fa231ebcc3c4e6 100644 --- a/src/plugins/server/states.ml +++ b/src/plugins/server/states.ml @@ -293,9 +293,14 @@ let fetch array n = (* --- Signature Registry --- *) (* -------------------------------------------------------------------------- *) +let rec is_keyType = function + | Package.Junion js -> List.for_all is_keyType js + | Jstring | Jalpha | Jkey _ | Jtag _ -> true + | _ -> false + let register_array ~package ~name ~descr ~key ?(keyName="key") - ?(keyKind=name) + ?(keyType=Package.Jkey name) ~(iter : 'a callback) ?(add_update_hook : 'a callback option) ?(add_remove_hook : 'a callback option) @@ -304,15 +309,24 @@ let register_array ~package ~name ~descr ~key let open Markdown in let href = link ~name () in let columns = List.rev !model in - if List.exists (fun (fd,_) -> fd.Package.fd_name = keyName) columns then - raise (Invalid_argument "States.array: key name overrides column name") ; + begin + if List.exists (fun (fd,_) -> fd.Package.fd_name = keyName) columns then + raise (Invalid_argument ( + Printf.sprintf "States.array(%S) : invalid key %S" + name keyName + )); + if not (is_keyType keyType) then + raise (Invalid_argument ( + Printf.sprintf "States.array(%S): invalid key type" name + )); + end ; let fields = Package.{ fd_name = keyName ; - fd_type = Jkey keyKind ; + fd_type = keyType ; fd_descr = plain "Entry identifier." ; } :: List.map fst columns in let id = Package.declare_id ~package:package ~name:name ~descr - (D_array { arr_key = keyName ; arr_kind = keyKind }) in + (D_array { arr_key = keyName ; arr_kind = keyType }) in let signal = Request.signal ~package ~name:(Package.Derived.signal id).name ~descr:(plain "Signal for array" @ href) in @@ -331,7 +345,7 @@ let register_array ~package ~name ~descr ~key let signature = Request.signature ~input:(module Jint) () in let module Jkeys = Jlist(struct include Jstring - let jtype = Package.Jkey keyKind + let jtype = keyType end) in let module Jrows = Jlist (struct include Jany diff --git a/src/plugins/server/states.mli b/src/plugins/server/states.mli index a75c6c76c8776b067e39b4544c66198d3424fe89..1f38038f83ee1d2b4ea80a1fb1704119e9bf9021 100644 --- a/src/plugins/server/states.mli +++ b/src/plugins/server/states.mli @@ -136,7 +136,7 @@ val register_array : descr:Markdown.text -> key:('a -> string) -> ?keyName:string -> - ?keyKind:string -> + ?keyType:jtype -> iter:('a callback) -> ?add_update_hook:('a callback) -> ?add_remove_hook:('a callback) -> diff --git a/src/plugins/value/api/values_request.ml b/src/plugins/value/api/values_request.ml index 035340717f3f43761da0c8aeb4d49614d572d6ab..e6587ea85824ad563a6d556b6d85d5c615992eda 100644 --- a/src/plugins/value/api/values_request.ml +++ b/src/plugins/value/api/values_request.ml @@ -27,6 +27,7 @@ open Cil_types module Kmap = Kernel_function.Hashtbl module Smap = Cil_datatype.Stmt.Hashtbl module CS = Value_types.Callstack +module CSet = CS.Set module CSmap = CS.Hashtbl module Md = Markdown @@ -365,11 +366,18 @@ let proxy = let () = Request.register ~package ~kind:`GET ~name:"getCallstacks" ~descr:(Md.plain "Callstacks for markers") - ~input:(module Jstmt) + ~input:(module Jlist(Jmarker)) ~output:(module Jlist(Jcallstack)) - begin fun stmt -> + begin fun markers -> let module A = (val !proxy) in - Ranking.sort (A.callstacks stmt) + let cset = List.fold_left + (fun cset marker -> + match probe marker with + | Pexpr(_,stmt) | Plval(_,stmt) -> + List.fold_right CSet.add (A.callstacks stmt) cset + | Pnone -> cset + ) CSet.empty markers in + Ranking.sort (CSet.elements cset) end (* -------------------------------------------------------------------------- *)