diff --git a/ivette/src/dome/main/menubar.ts b/ivette/src/dome/main/menubar.ts index a0095ce60be15ec10382a3c6638d91fcde4dd805..52f4212c96f0e09a6e8a85866acf4ac1b1e5b16e 100644 --- a/ivette/src/dome/main/menubar.ts +++ b/ivette/src/dome/main/menubar.ts @@ -404,7 +404,7 @@ export function setMenuItem({ id, ...options }: CustomMenuItem) { if (entry) { if (entry.spec) Object.assign(entry.spec, options); if (entry.item) Object.assign(entry.item, options); - if (options.label || options.type || options.click) requestUpdate(); + requestUpdate (); } else console.warn(`[Dome] unknown menu item #${id}`); } diff --git a/ivette/src/dome/renderer/dark.css b/ivette/src/dome/renderer/dark.css index 265d1d7d23c681419dac7002fb30920581eabc86..f53d8d9120eeed3dc74c02d0e7761f13c7cdea87 100644 --- a/ivette/src/dome/renderer/dark.css +++ b/ivette/src/dome/renderer/dark.css @@ -88,6 +88,7 @@ --meter-subopti: linear-gradient(to bottom, #770 0%, #cc0 20%, #770 100%); --meter-evenless: linear-gradient(to bottom, #710 0%, #c50 20%, #710 100%); + --eva-evaluation-mode: #714074; --eva-state-before: #082032; --eva-state-after: #321428; --eva-state-then: #98a02e; diff --git a/ivette/src/dome/renderer/frame/style.css b/ivette/src/dome/renderer/frame/style.css index f9d783ae1212313089d556f9bc563263e9a68f22..25343e43b59d10a0fd6761ceb38540c4e51a2063 100644 --- a/ivette/src/dome/renderer/frame/style.css +++ b/ivette/src/dome/renderer/frame/style.css @@ -480,6 +480,7 @@ width: 200px; outline: none; background: var(--background-softer); + color: var(--text); } .dome-xToolBar-actionComponent div.dome-xToolBar-suggestions { diff --git a/ivette/src/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx index 596a7ece181bfaeef49980544cf062a33958d779..9d7e9f196a9500a5e50299b99b956b5db906ada4 100644 --- a/ivette/src/dome/renderer/frame/toolbars.tsx +++ b/ivette/src/dome/renderer/frame/toolbars.tsx @@ -272,7 +272,7 @@ export function Select(props: SelectionProps<string>) { export interface Hint { id: string | number; icon?: string; - label: string | JSX.Element; + label: string; title?: string; value(): void; rank?: number; @@ -288,8 +288,10 @@ export type HintsEvaluator = (pattern: string) => Promise<Hint[]>; /** Description of an action mode. */ export interface ActionMode { - /** Mode tooltip text. */ - title: string; + /** Mode tooltip title. */ + title?: string; + /** Mode tooltip label. */ + label: string; /** Mode placeholder text. */ placeholder?: string; /** Icon displayed when the mode is selected. */ @@ -329,7 +331,8 @@ async function searchHints(pattern: string) { } const searchMode: ActionMode = { - title: "Search", + label: "Search", + title: 'Search through the global definitions', placeholder: "Search…", icon: "SEARCH", className: 'dome-xToolBar-searchMode', @@ -415,7 +418,7 @@ function Suggestions(props: SuggestionsProps) { // -------------------------------------------------------------------------- interface ActionInputProps { - title: string; + title?: string; placeholder?: string; hints: Hint[]; onHint: (hint: Hint) => void; @@ -529,16 +532,16 @@ export function ModalActionField() { // Auxiliary function that build a Hint from an ActionMode. const modeToHint = (mode: ActionMode) => { - const { title, icon } = mode; + const { label, title = '', icon } = mode; const id = 'ActionMode-' + title + '-' + icon; const value = () => { onModeChange(mode); }; - return { id, icon, label: title, title, value, rank: -1000 }; + return { id, icon, label, title, value, rank: -1000 }; }; // Hints provider for the mode of all modes. const modesHints = React.useCallback((input: string) => { const p = input.toLowerCase(); - const fit = (m: ActionMode) => m.title.toLowerCase().includes(p); + const fit = (m: ActionMode) => m.label.toLowerCase().includes(p); return Promise.resolve(Array.from(allModes).filter(fit).map(modeToHint)); }, [allModes]); @@ -546,20 +549,20 @@ export function ModalActionField() { // on the current mode icon and allows to change the current mode, displaying // a list of all available modes as hints. const modesMode = React.useMemo(() => { - const title = "Mode selection"; + const label = "Mode selection"; const placeholder = "Search mode"; const icon = "TUNINGS"; const className = 'dome-xToolBar-modeOfModes'; - return { title, placeholder, icon, className, hints: modesHints }; + return { label, placeholder, icon, className, hints: modesHints }; }, [modesHints]); // Build a new search engine for the search mode, adding available modes to // the possible search hints. const searchModeHints = React.useCallback(async (input: string) => { const hs = await modesMode.hints(input); - const notCurrent = (h: Hint) => !(h.title?.includes(current.title)); + const notCurrent = (h: Hint) => !(h.label.includes(current.label)); return hs.filter(notCurrent); - }, [current.title, modesMode]); + }, [current.label, modesMode]); // Register the new search engine. React.useEffect(() => { diff --git a/ivette/src/dome/renderer/light.css b/ivette/src/dome/renderer/light.css index 1eb31da53fc00624364f9f6577ef1114ef196c60..b9563c7d155cfdada7aeaf15bc7ed7f133cd3800 100644 --- a/ivette/src/dome/renderer/light.css +++ b/ivette/src/dome/renderer/light.css @@ -88,6 +88,7 @@ --meter-subopti: linear-gradient(to bottom, #aa0 0%, #ff0 20%, #aa0 100%); --meter-evenless: linear-gradient(to bottom, #a40 0%, #f80 20%, #a40 100%); + --eva-evaluation-mode: #dc8fe1; --eva-state-before: #95f5ff; --eva-state-after: #fff819; --eva-state-then: #c8e06e; diff --git a/ivette/src/frama-c/kernel/api/ast/index.ts b/ivette/src/frama-c/kernel/api/ast/index.ts index 40bf034596919523dba48b67e06bb05d0d6a69d2..70b14dd814e7cb57c693dff3a5a86b5515feed05 100644 --- a/ivette/src/frama-c/kernel/api/ast/index.ts +++ b/ivette/src/frama-c/kernel/api/ast/index.ts @@ -528,4 +528,49 @@ const setFiles_internal: Server.SetRequest<string[],null> = { /** Set the source file names to analyze. */ export const setFiles: Server.SetRequest<string[],null>= setFiles_internal; +/** <markerFromTerm> input */ +export interface markerFromTermInput { + /** The statement at which we will build the marker. */ + atStmt: marker; + /** The ACSL term. */ + term: string; +} + +/** Loose decoder for `markerFromTermInput` */ +export const jMarkerFromTermInput: Json.Loose<markerFromTermInput> = + Json.jObject({ + atStmt: jMarkerSafe, + term: Json.jFail(Json.jString,'String expected'), + }); + +/** Safe decoder for `markerFromTermInput` */ +export const jMarkerFromTermInputSafe: Json.Safe<markerFromTermInput> = + Json.jFail(jMarkerFromTermInput,'MarkerFromTermInput expected'); + +/** Natural order for `markerFromTermInput` */ +export const byMarkerFromTermInput: Compare.Order<markerFromTermInput> = + Compare.byFields + <{ atStmt: marker, term: string }>({ + atStmt: byMarker, + term: Compare.string, + }); + +const markerFromTerm_internal: Server.GetRequest< + markerFromTermInput, + marker | + undefined + > = { + kind: Server.RqKind.GET, + name: 'kernel.ast.markerFromTerm', + input: jMarkerFromTermInput, + output: jMarker, + signals: [], +}; +/** Build a marker from an ACSL term. */ +export const markerFromTerm: Server.GetRequest< + markerFromTermInput, + marker | + undefined + >= markerFromTerm_internal; + /* ------------------------------------- */ diff --git a/ivette/src/frama-c/plugins/eva/style.css b/ivette/src/frama-c/plugins/eva/style.css index 34a3fce5060b1aea5442781b961a7bd06b85d05e..e4a17e65e23a5e3baf3af13f49d6f319b4c86f35 100644 --- a/ivette/src/frama-c/plugins/eva/style.css +++ b/ivette/src/frama-c/plugins/eva/style.css @@ -254,7 +254,7 @@ tr:first-of-type > .eva-table-callsite-box { .eva-table-descrs { background-color: var(--background-sidebar); - border-right: thin solid var(--border); + text-align: center; } .eva-table-descr-sticky { @@ -302,4 +302,18 @@ tr:first-of-type > .eva-table-callsite-box { background: var(--code-select); } +.eva-evaluation-mode { + background: purple; + fill: white; +} + +/* -------------------------------------------------------------------------- */ +/* --- Evaluation Mode CSS --- */ +/* -------------------------------------------------------------------------- */ + +.eva-evaluation-mode { + background: var(--eva-evaluation-mode); + fill: var(--text-discrete); +} + /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/frama-c/plugins/eva/valuetable.tsx b/ivette/src/frama-c/plugins/eva/valuetable.tsx index 63b225fd27088c3374be59051e7d0653566ff4ce..00c3b7738bac9049344c90b3addc42ce9b28d34f 100644 --- a/ivette/src/frama-c/plugins/eva/valuetable.tsx +++ b/ivette/src/frama-c/plugins/eva/valuetable.tsx @@ -24,18 +24,22 @@ import React from 'react'; import _ from 'lodash'; import * as Ivette from 'ivette'; import * as Dome from 'dome/dome'; +import * as System from 'dome/system'; import * as States from 'frama-c/states'; import * as Server from 'frama-c/server'; import * as Ast from 'frama-c/kernel/api/ast'; +import * as Eva from 'frama-c/plugins/eva/api/general'; import * as Values from 'frama-c/plugins/eva/api/values'; import { GlobalState, useGlobalState } from 'dome/data/states'; import { classes } from 'dome/misc/utils'; import { Icon } from 'dome/controls/icons'; import { Inset } from 'dome/frame/toolbars'; +import * as Toolbars from 'dome/frame/toolbars'; import { Cell, Code } from 'dome/controls/labels'; import { IconButton } from 'dome/controls/buttons'; import { Filler, Hpack, Hfill, Vpack, Vfill } from 'dome/layout/boxes'; +import { ipcRenderer } from 'electron'; @@ -897,6 +901,64 @@ class FunctionsManager { +/* -------------------------------------------------------------------------- */ +/* --- Evaluation Mode Handling --- */ +/* -------------------------------------------------------------------------- */ + +export const evaluateEvent = new Dome.Event('dome.evaluate'); +ipcRenderer.on('dome.ipc.evaluate', () => evaluateEvent.emit()); + +interface EvaluationModeProps { + computationState : Eva.computationStateType | undefined; + selection: States.Selection; + setLocPin: (loc: Location, pin: boolean) => void; +} + +function useEvaluationMode(props: EvaluationModeProps): void { + const { computationState, selection, setLocPin } = props; + const handleError = (): void => { return; }; + const addProbe = (target: Ast.marker | undefined): void => { + const fct = selection?.current?.fct; + if (fct && target) setLocPin({ fct, target }, true); + }; + React.useEffect(() => { + if (computationState !== 'computed') return () => { return; }; + const shortcut = System.platform === 'macos' ? 'Cmd+E' : 'Ctrl+E'; + const onEnter = (pattern: string): void => { + const marker = selection?.current?.marker; + const data = { atStmt: marker, term: pattern }; + Server.send(Ast.markerFromTerm, data).then(addProbe).catch(handleError); + }; + const evalMode = { + label: 'Evaluation', + title: `Evaluate an ACSL expression (shortcut: ${shortcut})`, + icon: 'TERMINAL', + className: 'eva-evaluation-mode', + hints: () => { return Promise.resolve([]); }, + onEnter, + event: evaluateEvent, + }; + Toolbars.RegisterMode.emit(evalMode); + return () => Toolbars.UnregisterMode.emit(evalMode); + }); + Dome.addMenuItem({ + menu: 'Edit', + id: 'EvaluateMenu', + type: 'normal', + label: 'Evaluate', + key: 'Cmd+E', + onClick: () => evaluateEvent.emit(), + }); + React.useEffect(() => { + Dome.setMenuItem({ id: 'EvaluateMenu', enabled: true }); + return () => Dome.setMenuItem({ id: 'EvaluateMenu', enabled: false }); + }); +} + +/* -------------------------------------------------------------------------- */ + + + /* -------------------------------------------------------------------------- */ /* --- Eva Table Complet Component --- */ /* -------------------------------------------------------------------------- */ @@ -1061,6 +1123,10 @@ function EvaTable(): JSX.Element { }, [ cs, setCS, select, getCallsites, selection ]); const { result: stackInfos } = Dome.usePromise(stackInfosPromise); + /* Handle Evaluation mode */ + const computationState = States.useSyncValue(Eva.computationState); + useEvaluationMode({ computationState, selection, setLocPin }); + /* Builds the component */ return ( <> diff --git a/src/plugins/server/kernel_ast.ml b/src/plugins/server/kernel_ast.ml index 84d21cb8a070f28a812fe2a47965b64a0191e041..61134be680a1e7fbd1fb15fddabbc0d372a4a22e 100644 --- a/src/plugins/server/kernel_ast.ml +++ b/src/plugins/server/kernel_ast.ml @@ -28,6 +28,36 @@ open Cil_types let package = Pkg.package ~title:"Ast Services" ~name:"ast" ~readme:"ast.md" () +(* -------------------------------------------------------------------------- *) +(* --- Marker Cache System --- *) +(* -------------------------------------------------------------------------- *) + +let logic_environment () = + let open Logic_typing in + Lenv.empty () |> append_pre_label |> append_init_label |> append_here_label + +module Key = Datatype.Pair(Cil_datatype.Stmt)(Cil_datatype.Term) +module Cache = Hashtbl.Make(Key) + +let get_term kf term = + let env = logic_environment () in + try Some (!Db.Properties.Interp.term ~env kf term) + with Logic_interp.Error _ | Parsing.Parse_error -> None + +let key_of_localizable = + let open Printer_tag in + function + | PStmt _ | PStmtStart _ | PTermLval _ | PVDecl _ | PGlobal _ | PIP _ -> None + | PLval (_, Kglobal, _) | PExp (_, Kglobal, _) -> None + | PLval (kf, Kstmt stmt, lval) -> + let str = Format.asprintf "%a" Cil_datatype.Lval.pretty lval in + Option.(bind kf (fun kf -> get_term kf str) |> map (fun t -> (stmt, t))) + | PExp (kf, Kstmt stmt, exp) -> + let str = Format.asprintf "%a" Cil_datatype.Exp.pretty exp in + Option.(bind kf (fun kf -> get_term kf str) |> map (fun t -> (stmt, t))) + +let cache = Cache.create 10 + (* -------------------------------------------------------------------------- *) (* --- Compute Ast --- *) (* -------------------------------------------------------------------------- *) @@ -276,6 +306,8 @@ struct | PIP _ -> Printf.sprintf "#p%d" (incr kid ; !kid) let create loc = + let add_cache key = Cache.add cache key loc in + key_of_localizable loc |> Option.iter add_cache; let { tags ; locs } = STATE.get () in try Localizable.Hashtbl.find tags loc with Not_found -> @@ -746,3 +778,66 @@ let () = set_files (* -------------------------------------------------------------------------- *) +(* ----- Build a marker from an ACSL term ----------------------------------- *) +(* -------------------------------------------------------------------------- *) + +type marker_term_input = { atStmt : stmt ; term : string } + +module MarkerTermInput = struct + type record + let record : record Record.signature = Record.signature () + + let atStmt = + let descr = "The statement at which we will build the marker." in + Record.field record ~name:"atStmt" ~descr:(Markdown.plain descr) + (module Marker) + + let term = + let descr = "The ACSL term." in + Record.field record ~name:"term" ~descr:(Markdown.plain descr) + (module Data.Jstring) + + let data = + Record.publish record ~package ~name:"markerFromTermInput" + ~descr:(Markdown.plain "<markerFromTerm> input") + + module R : Record.S with type r = record = (val data) + type t = marker_term_input option + let jtype = R.jtype + + let of_json js = + let record = R.of_json js in + match R.get atStmt record with + | PStmt (_, s) | PStmtStart (_, s) + | PLval (_, Kstmt s, _) | PExp (_, Kstmt s, _) + | PTermLval (_, Kstmt s, _, _) + | PVDecl (_, Kstmt s, _) -> + let term = R.get term record in + Some { atStmt = s ; term } + | _ -> None + +end + +module MarkerTermOutput = Data.Joption (Marker) + +let build_marker = + Option.map @@ fun input -> + let env = logic_environment () in + let kf = Kernel_function.find_englobing_kf input.atStmt in + let term = !Db.Properties.Interp.term ~env kf input.term in + let key = (input.atStmt, term) in + match Cache.find_opt cache key with + | Some tag -> tag + | None -> + let exp = !Db.Properties.Interp.term_to_exp ~result:None term in + let tag = Printer_tag.PExp (Some kf, Kstmt input.atStmt, exp) in + Cache.add cache key tag ; tag + +let descr = "Build a marker from an ACSL term." + +let () = Request.register ~package + ~kind:`GET ~name:"markerFromTerm" ~descr:(Markdown.plain descr) + ~input:(module MarkerTermInput) ~output:(module MarkerTermOutput) + build_marker + +(**************************************************************************)