import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax"; import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter"; import { BigStep, BigStepOutput, RT_Event } from "../statecharts/runtime_types"; import { InsertMode, VisualEditor, VisualEditorState } from "../VisualEditor/VisualEditor"; import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; import "../index.css"; import "./App.css"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import { TopPanel } from "./TopPanel"; import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST"; import { TraceableError } from "../statecharts/parser"; import { getKeyHandler } from "./shortcut_handler"; import { BottomPanel } from "./BottomPanel"; import { emptyState } from "@/statecharts/concrete_syntax"; import { PersistentDetails } from "./PersistentDetails"; import { DigitalWatchPlant } from "@/Plant/DigitalWatch/DigitalWatch"; import { DummyPlant } from "@/Plant/Dummy/Dummy"; import { Plant } from "@/Plant/Plant"; import { usePersistentState } from "@/util/persistent_state"; import { RTHistory } from "./RTHistory"; export type EditHistory = { current: VisualEditorState, history: VisualEditorState[], future: VisualEditorState[], } const plants: [string, Plant][] = [ ["dummy", DummyPlant], ["digital watch", DigitalWatchPlant], ] export type BigStepError = { inputEvent: string, simtime: number, error: RuntimeError, } export type TraceItem = { kind: "error" } & BigStepError | { kind: "bigstep", plantState: any } & BigStep; export type TraceState = { trace: [TraceItem, ...TraceItem[]], // non-empty idx: number, }; // <-- null if there is no trace function current(ts: TraceState) { return ts.trace[ts.idx]!; } function getPlantState(plant: Plant, trace: TraceItem[], idx: number): T | null { if (idx === -1) { return plant.initial; } let plantState = getPlantState(plant, trace, idx-1); if (plantState !== null) { const currentConfig = trace[idx]; if (currentConfig.kind === "bigstep") { for (const o of currentConfig.outputEvents) { plantState = plant.reduce(o, plantState); } } return plantState; } return null; } export function App() { const [mode, setMode] = useState("and"); const [historyState, setHistoryState] = useState({current: emptyState, history: [], future: []}); const [ast, setAST] = useState(emptyStatechart); const [errors, setErrors] = useState([]); const [trace, setTrace] = useState(null); const [time, setTime] = useState({kind: "paused", simtime: 0}); const [modal, setModal] = useState(null); const [plantName, setPlantName] = usePersistentState("plant", "dummy"); const [zoom, setZoom] = usePersistentState("zoom", 1); const [showKeys, setShowKeys] = usePersistentState("shortcuts", true); const plant = plants.find(([pn, p]) => pn === plantName)![1]; const editorState = historyState.current; const setEditorState = useCallback((cb: (value: VisualEditorState) => VisualEditorState) => { setHistoryState(historyState => ({...historyState, current: cb(historyState.current)})); }, [setHistoryState]); const refRightSideBar = useRef(null); // append editor state to undo history function makeCheckPoint() { setHistoryState(historyState => ({ ...historyState, history: [...historyState.history, historyState.current], future: [], })); } function onUndo() { setHistoryState(historyState => { if (historyState.history.length === 0) { return historyState; // no change } return { current: historyState.history.at(-1)!, history: historyState.history.slice(0,-1), future: [...historyState.future, historyState.current], } }) } function onRedo() { setHistoryState(historyState => { if (historyState.future.length === 0) { return historyState; // no change } return { current: historyState.future.at(-1)!, history: [...historyState.history, historyState.current], future: historyState.future.slice(0,-1), } }); } function onInit() { const timestampedEvent = {simtime: 0, inputEvent: ""}; let config; try { config = initialize(ast); const item = {kind: "bigstep", ...timestampedEvent, ...config}; const plantState = getPlantState(plant, [item], 0); setTrace({trace: [{...item, plantState}], idx: 0}); } catch (error) { if (error instanceof RuntimeError) { setTrace({trace: [{kind: "error", ...timestampedEvent, error}], idx: 0}); } else { throw error; // probably a bug in the interpreter } } setTime({kind: "paused", simtime: 0}); scrollDownSidebar(); } function onClear() { setTrace(null); setTime({kind: "paused", simtime: 0}); } // raise input event, producing a new runtime configuration (or a runtime error) function onRaise(inputEvent: string, param: any) { if (trace !== null && ast.inputEvents.some(e => e.event === inputEvent)) { const config = current(trace); if (config.kind === "bigstep") { const simtime = getSimTime(time, Math.round(performance.now())); produceNextConfig(simtime, {kind: "input", name: inputEvent, param}, config); } } } // timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout) useEffect(() => { let timeout: NodeJS.Timeout | undefined; if (trace !== null) { const config = current(trace); if (config.kind === "bigstep") { const timers = config.environment.get("_timers") || []; if (timers.length > 0) { const [nextInterrupt, timeElapsedEvent] = timers[0]; const raiseTimeEvent = () => { produceNextConfig(nextInterrupt, timeElapsedEvent, config); } // depending on whether paused or realtime, raise immediately or in the future: if (time.kind === "realtime") { const wallclkDelay = getWallClkDelay(time, nextInterrupt, Math.round(performance.now())); timeout = setTimeout(raiseTimeEvent, wallclkDelay); } else if (time.kind === "paused") { if (nextInterrupt <= time.simtime) { raiseTimeEvent(); } } } } } return () => { if (timeout) clearTimeout(timeout); } }, [time, trace]); // <-- todo: is this really efficient? function produceNextConfig(simtime: number, event: RT_Event, config: TraceItem) { const timedEvent = { simtime, inputEvent: event.kind === "timer" ? "" : event.name, }; let newItem: TraceItem; try { const nextConfig = handleInputEvent(simtime, event, ast, config as BigStep); // may throw let plantState = config.plantState; for (const o of nextConfig.outputEvents) { plantState = plant.reduce(o, plantState); } console.log({plantState}); newItem = {kind: "bigstep", plantState, ...timedEvent, ...nextConfig}; } catch (error) { if (error instanceof RuntimeError) { newItem = {kind: "error", ...timedEvent, error}; } else { throw error; } } // @ts-ignore setTrace(trace => ({ trace: [ ...trace!.trace.slice(0, trace!.idx+1), // remove everything after current item newItem, ], idx: trace!.idx+1, })); scrollDownSidebar(); } function onBack() { if (trace !== null) { setTime(() => { if (trace !== null) { return { kind: "paused", simtime: trace.trace[trace.idx-1].simtime, } } return { kind: "paused", simtime: 0 }; }); setTrace({ ...trace, idx: trace.idx-1, }); } } function scrollDownSidebar() { if (refRightSideBar.current) { const el = refRightSideBar.current; // hack: we want to scroll to the new element, but we have to wait until it is rendered... setTimeout(() => { el.scrollIntoView({block: "end", behavior: "smooth"}); }, 50); } } useEffect(() => { console.log("Welcome to StateBuddy!"); () => { console.log("Goodbye!"); } }, []); useEffect(() => { const onKeyDown = getKeyHandler(setMode); window.addEventListener("keydown", onKeyDown); return () => { window.removeEventListener("keydown", onKeyDown); }; }, []); let highlightActive: Set; let highlightTransitions: string[]; if (trace === null) { highlightActive = new Set(); highlightTransitions = []; } else { const item = current(trace); console.log(trace); if (item.kind === "bigstep") { highlightActive = item.mode; highlightTransitions = item.firedTransitions; } else { highlightActive = new Set(); highlightTransitions = []; } } // const plantState = trace && getPlantState(plant, trace.trace, trace.idx); const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true); return <> {/* Modal dialog */} {modal &&
setModal(null)}>
e.stopPropagation()}> {modal}
} {/* Left: top bar and main editor */} {/* Top bar */} {/* Below the top bar: Editor */} {/* Right: sidebar */} state tree
input events internal events output events plant {trace !== null && plant.render(trace.trace[trace.idx].plantState, event => onRaise(event.name, event.param))}
setShowExecutionTrace(e.newState === "open")}>execution trace
{showExecutionTrace && {/* */} {/* execution trace */}
{/*
*/}
}
{/* Bottom panel */}
; } export default App;