clean up code a bit (split of SideBar component and simulator callbacks from App) + fix bug in property checker
This commit is contained in:
parent
2dd35ab079
commit
9922f8588d
10 changed files with 707 additions and 718 deletions
599
src/App/App.tsx
599
src/App/App.tsx
|
|
@ -1,36 +1,20 @@
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
import { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
|
||||||
import CachedOutlinedIcon from '@mui/icons-material/CachedOutlined';
|
|
||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
|
||||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
|
||||||
|
|
||||||
import { Statechart } from "@/statecharts/abstract_syntax";
|
|
||||||
import { detectConnections } from "@/statecharts/detect_connections";
|
import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
|
|
||||||
import { RuntimeError } from "../statecharts/interpreter";
|
|
||||||
import { parseStatechart } from "../statecharts/parser";
|
import { parseStatechart } from "../statecharts/parser";
|
||||||
import { BigStep, RaisedEvent } from "../statecharts/runtime_types";
|
|
||||||
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
|
||||||
import { BottomPanel } from "./BottomPanel";
|
import { BottomPanel } from "./BottomPanel";
|
||||||
import { PersistentDetails, PersistentDetailsLocalStorage } from "./PersistentDetails";
|
import { defaultSideBarState, SideBar, SideBarState } from "./SideBar";
|
||||||
import { digitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch";
|
import { InsertMode } from "./TopPanel/InsertModes";
|
||||||
import { dummyPlant } from "./Plant/Dummy/Dummy";
|
|
||||||
import { microwavePlant } from "./Plant/Microwave/Microwave";
|
|
||||||
import { Plant } from "./Plant/Plant";
|
|
||||||
import { trafficLightPlant } from "./Plant/TrafficLight/TrafficLight";
|
|
||||||
import { RTHistory } from "./RTHistory";
|
|
||||||
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
|
|
||||||
import { TopPanel } from "./TopPanel/TopPanel";
|
import { TopPanel } from "./TopPanel/TopPanel";
|
||||||
import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
|
import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
|
||||||
import { checkProperty, PropertyCheckResult } from "./check_property";
|
import { makeIndividualSetters } from "./makePartialSetter";
|
||||||
import { useEditor } from "./useEditor";
|
import { useEditor } from "./useEditor";
|
||||||
|
import { useSimulator } from "./useSimulator";
|
||||||
import { useUrlHashState } from "./useUrlHashState";
|
import { useUrlHashState } from "./useUrlHashState";
|
||||||
|
import { plants } from "./plants";
|
||||||
|
|
||||||
export type EditHistory = {
|
export type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
|
|
@ -38,53 +22,22 @@ export type EditHistory = {
|
||||||
future: VisualEditorState[],
|
future: VisualEditorState[],
|
||||||
}
|
}
|
||||||
|
|
||||||
type UniversalPlantState = {[property: string]: boolean|number};
|
export type AppState = {
|
||||||
|
showKeys: boolean,
|
||||||
|
zoom: number,
|
||||||
|
insertMode: InsertMode,
|
||||||
|
} & SideBarState;
|
||||||
|
|
||||||
const plants: [string, Plant<any, UniversalPlantState>][] = [
|
const defaultAppState: AppState = {
|
||||||
["dummy", dummyPlant],
|
showKeys: true,
|
||||||
["microwave", microwavePlant as unknown as Plant<any, UniversalPlantState>],
|
zoom: 1,
|
||||||
["digital watch", digitalWatchPlant as unknown as Plant<any, UniversalPlantState>],
|
insertMode: 'and',
|
||||||
["traffic light", trafficLightPlant as unknown as Plant<any, UniversalPlantState>],
|
|
||||||
]
|
|
||||||
|
|
||||||
export type TraceItemError = {
|
...defaultSideBarState,
|
||||||
cause: BigStepCause, // event name, <init> or <timer>
|
|
||||||
simtime: number,
|
|
||||||
error: RuntimeError,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoupledState = {
|
|
||||||
sc: BigStep,
|
|
||||||
plant: BigStep,
|
|
||||||
// plantCleanState: {[prop: string]: boolean|number},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BigStepCause = {
|
|
||||||
kind: "init",
|
|
||||||
simtime: 0,
|
|
||||||
} | {
|
|
||||||
kind: "input",
|
|
||||||
simtime: number,
|
|
||||||
eventName: string,
|
|
||||||
param?: any,
|
|
||||||
} | {
|
|
||||||
kind: "timer",
|
|
||||||
simtime: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TraceItem =
|
|
||||||
{ kind: "error" } & TraceItemError
|
|
||||||
| { kind: "bigstep", simtime: number, cause: BigStepCause, state: CoupledState, outputEvents: RaisedEvent[] };
|
|
||||||
|
|
||||||
export type TraceState = {
|
|
||||||
trace: [TraceItem, ...TraceItem[]], // non-empty
|
|
||||||
idx: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
||||||
const [trace, setTrace] = useState<TraceState|null>(null);
|
|
||||||
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
|
||||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||||
|
|
||||||
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
||||||
|
|
@ -94,54 +47,51 @@ export function App() {
|
||||||
setEditHistory(historyState => historyState && ({...historyState, current: cb(historyState.current)}));
|
setEditHistory(historyState => historyState && ({...historyState, current: cb(historyState.current)}));
|
||||||
}, [setEditHistory]);
|
}, [setEditHistory]);
|
||||||
|
|
||||||
const {
|
|
||||||
autoConnect,
|
|
||||||
setAutoConnect,
|
|
||||||
autoScroll,
|
|
||||||
setAutoScroll,
|
|
||||||
plantConns,
|
|
||||||
setPlantConns,
|
|
||||||
showKeys,
|
|
||||||
setShowKeys,
|
|
||||||
zoom,
|
|
||||||
setZoom,
|
|
||||||
insertMode,
|
|
||||||
setInsertMode,
|
|
||||||
plantName,
|
|
||||||
setPlantName,
|
|
||||||
showConnections,
|
|
||||||
setShowConnections,
|
|
||||||
showProperties,
|
|
||||||
setShowProperties,
|
|
||||||
showExecutionTrace,
|
|
||||||
setShowExecutionTrace,
|
|
||||||
showPlantTrace,
|
|
||||||
setShowPlantTrace,
|
|
||||||
properties,
|
|
||||||
setProperties,
|
|
||||||
savedTraces,
|
|
||||||
setSavedTraces,
|
|
||||||
activeProperty,
|
|
||||||
setActiveProperty,
|
|
||||||
} = useUrlHashState(editorState, setEditHistory);
|
|
||||||
const plant = plants.find(([pn, p]) => pn === plantName)![1];
|
|
||||||
|
|
||||||
const refRightSideBar = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// parse concrete syntax always:
|
// parse concrete syntax always:
|
||||||
const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]);
|
const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]);
|
||||||
const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]);
|
const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]);
|
||||||
const ast = parsed && parsed[0];
|
const ast = parsed && parsed[0];
|
||||||
const syntaxErrors = parsed && parsed[1] || [];
|
|
||||||
const currentTraceItem = trace && trace.trace[trace.idx];
|
|
||||||
const allErrors = [
|
|
||||||
...syntaxErrors,
|
|
||||||
...(currentTraceItem && currentTraceItem.kind === "error") ? [{
|
|
||||||
message: currentTraceItem.error.message,
|
|
||||||
shapeUid: currentTraceItem.error.highlight[0],
|
|
||||||
}] : [],
|
|
||||||
];
|
|
||||||
|
|
||||||
|
const [appState, setAppState] = useState<AppState>(defaultAppState);
|
||||||
|
|
||||||
|
const persist = useUrlHashState<VisualEditorState | AppState & {editorState: VisualEditorState}>(
|
||||||
|
recoveredState => {
|
||||||
|
// we support two formats
|
||||||
|
// @ts-ignore
|
||||||
|
if (recoveredState.nextID) {
|
||||||
|
// old format
|
||||||
|
setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// new format
|
||||||
|
// @ts-ignore
|
||||||
|
if (recoveredState.editorState !== undefined) {
|
||||||
|
const {editorState, ...appState} = recoveredState as AppState & {editorState: VisualEditorState};
|
||||||
|
setEditHistory(() => ({current: editorState, history: [], future: []}));
|
||||||
|
setAppState(() => appState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (editorState !== null) {
|
||||||
|
persist({editorState, ...appState});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [editorState, appState]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
autoScroll,
|
||||||
|
plantConns,
|
||||||
|
plantName,
|
||||||
|
} = appState;
|
||||||
|
|
||||||
|
const plant = plants.find(([pn, p]) => pn === plantName)![1];
|
||||||
|
|
||||||
|
const refRightSideBar = useRef<HTMLDivElement>(null);
|
||||||
const scrollDownSidebar = useCallback(() => {
|
const scrollDownSidebar = useCallback(() => {
|
||||||
if (autoScroll && refRightSideBar.current) {
|
if (autoScroll && refRightSideBar.current) {
|
||||||
const el = refRightSideBar.current;
|
const el = refRightSideBar.current;
|
||||||
|
|
@ -152,221 +102,25 @@ export function App() {
|
||||||
}
|
}
|
||||||
}, [refRightSideBar.current, autoScroll]);
|
}, [refRightSideBar.current, autoScroll]);
|
||||||
|
|
||||||
// coupled execution
|
const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar);
|
||||||
const cE = useMemo(() => ast && coupledExecution({
|
|
||||||
sc: statechartExecution(ast),
|
|
||||||
plant: plant.execution,
|
|
||||||
}, {
|
|
||||||
...plantConns,
|
|
||||||
...Object.fromEntries(ast.inputEvents.map(({event}) => ["debug."+event, ['sc',event] as [string,string]])),
|
|
||||||
}), [ast]);
|
|
||||||
|
|
||||||
const onInit = useCallback(() => {
|
const setters = makeIndividualSetters(setAppState, Object.keys(appState) as (keyof AppState)[]);
|
||||||
if (cE === null) return;
|
|
||||||
const metadata = {simtime: 0, cause: {kind: "init" as const, simtime: 0 as const}};
|
|
||||||
try {
|
|
||||||
const [outputEvents, state] = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
|
|
||||||
setTrace({
|
|
||||||
trace: [{kind: "bigstep", ...metadata, state, outputEvents}],
|
|
||||||
idx: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (error instanceof RuntimeError) {
|
|
||||||
setTrace({
|
|
||||||
trace: [{kind: "error", ...metadata, error}],
|
|
||||||
idx: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw error; // probably a bug in the interpreter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTime(time => {
|
|
||||||
if (time.kind === "paused") {
|
|
||||||
return {...time, simtime: 0};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {...time, since: {simtime: 0, wallclktime: performance.now()}};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
scrollDownSidebar();
|
|
||||||
}, [cE, scrollDownSidebar]);
|
|
||||||
|
|
||||||
const onClear = useCallback(() => {
|
|
||||||
setTrace(null);
|
|
||||||
setTime({kind: "paused", simtime: 0});
|
|
||||||
}, [setTrace, setTime]);
|
|
||||||
|
|
||||||
// raise input event, producing a new runtime configuration (or a runtime error)
|
|
||||||
const onRaise = (inputEvent: string, param: any) => {
|
|
||||||
if (cE === null) return;
|
|
||||||
if (currentTraceItem !== null /*&& ast.inputEvents.some(e => e.event === inputEvent)*/) {
|
|
||||||
if (currentTraceItem.kind === "bigstep") {
|
|
||||||
const simtime = getSimTime(time, Math.round(performance.now()));
|
|
||||||
appendNewConfig(simtime, {kind: "input", simtime, eventName: inputEvent, param}, () => {
|
|
||||||
return cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 (currentTraceItem !== null && cE !== null) {
|
|
||||||
if (currentTraceItem.kind === "bigstep") {
|
|
||||||
const nextTimeout = cE?.timeAdvance(currentTraceItem.state);
|
|
||||||
|
|
||||||
const raiseTimeEvent = () => {
|
|
||||||
appendNewConfig(nextTimeout, {kind: "timer", simtime: nextTimeout}, () => {
|
|
||||||
return cE.intTransition(currentTraceItem.state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time.kind === "realtime") {
|
|
||||||
const wallclkDelay = getWallClkDelay(time, nextTimeout, Math.round(performance.now()));
|
|
||||||
if (wallclkDelay !== Infinity) {
|
|
||||||
timeout = setTimeout(raiseTimeEvent, wallclkDelay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (time.kind === "paused") {
|
|
||||||
if (nextTimeout <= time.simtime) {
|
|
||||||
raiseTimeEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (timeout) clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, [time, currentTraceItem]); // <-- todo: is this really efficient?
|
|
||||||
|
|
||||||
function appendNewConfig(simtime: number, cause: BigStepCause, computeNewState: () => [RaisedEvent[], CoupledState]) {
|
|
||||||
let newItem: TraceItem;
|
|
||||||
const metadata = {simtime, cause}
|
|
||||||
try {
|
|
||||||
const [outputEvents, state] = computeNewState(); // may throw RuntimeError
|
|
||||||
newItem = {kind: "bigstep", ...metadata, state, outputEvents};
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (error instanceof RuntimeError) {
|
|
||||||
newItem = {kind: "error", ...metadata, error};
|
|
||||||
// also pause the simulation, for dramatic effect:
|
|
||||||
setTime({kind: "paused", simtime});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
setTrace(trace => ({
|
|
||||||
trace: [
|
|
||||||
...trace!.trace.slice(0, trace!.idx+1), // remove everything after current item
|
|
||||||
newItem,
|
|
||||||
],
|
|
||||||
// idx: 0,
|
|
||||||
idx: trace!.idx+1,
|
|
||||||
}));
|
|
||||||
scrollDownSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBack = useCallback(() => {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [trace]);
|
|
||||||
|
|
||||||
|
const syntaxErrors = parsed && parsed[1] || [];
|
||||||
|
const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx];
|
||||||
const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem;
|
const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem;
|
||||||
|
const allErrors = [
|
||||||
|
...syntaxErrors,
|
||||||
|
...(currentTraceItem && currentTraceItem.kind === "error") ? [{
|
||||||
|
message: currentTraceItem.error.message,
|
||||||
|
shapeUid: currentTraceItem.error.highlight[0],
|
||||||
|
}] : [],
|
||||||
|
];
|
||||||
const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set();
|
const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set();
|
||||||
const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || [];
|
const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || [];
|
||||||
|
|
||||||
|
|
||||||
const speed = time.kind === "paused" ? 0 : time.scale;
|
|
||||||
|
|
||||||
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
|
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
ast && autoConnect && autoDetectConns(ast, plant, setPlantConns);
|
|
||||||
}, [ast, plant, autoConnect]);
|
|
||||||
|
|
||||||
const [propertyResults, setPropertyResults] = useState<PropertyCheckResult[] | null>(null);
|
|
||||||
|
|
||||||
|
|
||||||
const onSaveTrace = () => {
|
|
||||||
if (trace) {
|
|
||||||
setSavedTraces(savedTraces => [
|
|
||||||
...savedTraces,
|
|
||||||
["untitled", trace.trace.map((item) => item.cause)] as [string, BigStepCause[]],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onReplayTrace = (causes: BigStepCause[]) => {
|
|
||||||
if (cE) {
|
|
||||||
function run_until(simtime: number) {
|
|
||||||
while (true) {
|
|
||||||
const nextTimeout = cE!.timeAdvance(lastState);
|
|
||||||
if (nextTimeout > simtime) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const [outputEvents, coupledState] = cE!.intTransition(lastState);
|
|
||||||
lastState = coupledState;
|
|
||||||
lastSimtime = nextTimeout;
|
|
||||||
newTrace.push({kind: "bigstep", simtime: nextTimeout, state: coupledState, outputEvents, cause: {kind: "timer", simtime: nextTimeout}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const [outputEvents, coupledState] = cE.initial();
|
|
||||||
const newTrace = [{kind: "bigstep", simtime: 0, state: coupledState, outputEvents, cause: {kind: "init"} as BigStepCause} as TraceItem] as [TraceItem, ...TraceItem[]];
|
|
||||||
let lastState = coupledState;
|
|
||||||
let lastSimtime = 0;
|
|
||||||
for (const cause of causes) {
|
|
||||||
if (cause.kind === "input") {
|
|
||||||
run_until(cause.simtime); // <-- just make sure we haven't missed any timers elapsing
|
|
||||||
// @ts-ignore
|
|
||||||
const [outputEvents, coupledState] = cE.extTransition(cause.simtime, newTrace.at(-1)!.state, {kind: "input", name: cause.eventName, param: cause.param});
|
|
||||||
lastState = coupledState;
|
|
||||||
lastSimtime = cause.simtime;
|
|
||||||
newTrace.push({kind: "bigstep", simtime: cause.simtime, state: coupledState, outputEvents, cause});
|
|
||||||
}
|
|
||||||
else if (cause.kind === "timer") {
|
|
||||||
run_until(cause.simtime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTrace({trace: newTrace, idx: newTrace.length-1});
|
|
||||||
setTime({kind: "paused", simtime: lastSimtime});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if some properties change, re-evaluate them:
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout: NodeJS.Timeout;
|
|
||||||
if (trace) {
|
|
||||||
setPropertyResults(null);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
Promise.all(properties.map((property, i) => {
|
|
||||||
return checkProperty(plant, property, trace.trace);
|
|
||||||
}))
|
|
||||||
.then(results => {
|
|
||||||
setPropertyResults(results);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [properties, trace, plant]);
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
||||||
{/* Modal dialog */}
|
{/* Modal dialog */}
|
||||||
|
|
@ -394,13 +148,13 @@ export function App() {
|
||||||
style={{flex: '0 0 content'}}
|
style={{flex: '0 0 content'}}
|
||||||
>
|
>
|
||||||
{editHistory && <TopPanel
|
{editHistory && <TopPanel
|
||||||
{...{trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}}
|
{...{onUndo, onRedo, onRotate, setModal, editHistory, ...simulator, ...setters, ...appState}}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||||
{editorState && conns && syntaxErrors &&
|
{editorState && conns && syntaxErrors &&
|
||||||
<VisualEditor {...{state: editorState, setState: setEditorState, conns, trace, syntaxErrors: allErrors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>}
|
<VisualEditor {...{state: editorState, setState: setEditorState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, makeCheckPoint, ...appState}}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -413,142 +167,7 @@ export function App() {
|
||||||
maxWidth: 'min(400px, 50vw)',
|
maxWidth: 'min(400px, 50vw)',
|
||||||
}}>
|
}}>
|
||||||
<div className="stackVertical" style={{height:'100%'}}>
|
<div className="stackVertical" style={{height:'100%'}}>
|
||||||
<div
|
<SideBar {...{...appState, refRightSideBar, ast, plantState, ...simulator, ...setters}} />
|
||||||
className={showExecutionTrace ? "shadowBelow" : ""}
|
|
||||||
style={{flex: '0 0 content', backgroundColor: ''}}
|
|
||||||
>
|
|
||||||
{/* State tree */}
|
|
||||||
<PersistentDetailsLocalStorage localStorageKey="showStateTree" initiallyOpen={true}>
|
|
||||||
<summary>state tree</summary>
|
|
||||||
<ul>
|
|
||||||
{ast && <ShowAST {...{...ast, trace, highlightActive}}/>}
|
|
||||||
</ul>
|
|
||||||
</PersistentDetailsLocalStorage>
|
|
||||||
{/* Input events */}
|
|
||||||
<PersistentDetailsLocalStorage localStorageKey="showInputEvents" initiallyOpen={true}>
|
|
||||||
<summary>input events</summary>
|
|
||||||
{ast && <ShowInputEvents
|
|
||||||
inputEvents={ast.inputEvents}
|
|
||||||
onRaise={(e,p) => onRaise("debug."+e,p)}
|
|
||||||
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
|
||||||
showKeys={showKeys}/>}
|
|
||||||
</PersistentDetailsLocalStorage>
|
|
||||||
{/* Internal events */}
|
|
||||||
<PersistentDetailsLocalStorage localStorageKey="showInternalEvents" initiallyOpen={true}>
|
|
||||||
<summary>internal events</summary>
|
|
||||||
{ast && <ShowInternalEvents internalEvents={ast.internalEvents}/>}
|
|
||||||
</PersistentDetailsLocalStorage>
|
|
||||||
{/* Output events */}
|
|
||||||
<PersistentDetailsLocalStorage localStorageKey="showOutputEvents" initiallyOpen={true}>
|
|
||||||
<summary>output events</summary>
|
|
||||||
{ast && <ShowOutputEvents outputEvents={ast.outputEvents}/>}
|
|
||||||
</PersistentDetailsLocalStorage>
|
|
||||||
{/* Plant */}
|
|
||||||
<PersistentDetailsLocalStorage localStorageKey="showPlant" initiallyOpen={true}>
|
|
||||||
<summary>plant</summary>
|
|
||||||
<select
|
|
||||||
disabled={trace!==null}
|
|
||||||
value={plantName}
|
|
||||||
onChange={e => setPlantName(() => e.target.value)}>
|
|
||||||
{plants.map(([plantName, p]) =>
|
|
||||||
<option>{plantName}</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<br/>
|
|
||||||
{/* Render plant */}
|
|
||||||
{<plant.render state={plant.cleanupState(plantState)} speed={speed}
|
|
||||||
raiseUIEvent={e => onRaise("plant.ui."+e.name, e.param)}
|
|
||||||
/>}
|
|
||||||
</PersistentDetailsLocalStorage>
|
|
||||||
{/* Connections */}
|
|
||||||
<PersistentDetails state={showConnections} setState={setShowConnections}>
|
|
||||||
<summary>connections</summary>
|
|
||||||
<button title="auto-connect (name-based)" className={autoConnect?"active":""}
|
|
||||||
onClick={() => setAutoConnect(c => !c)}>
|
|
||||||
<AutoAwesomeIcon fontSize="small"/>
|
|
||||||
</button>
|
|
||||||
{ast && ConnEditor(ast, plant, plantConns, setPlantConns)}
|
|
||||||
</PersistentDetails>
|
|
||||||
{/* Properties */}
|
|
||||||
<details open={showProperties} onToggle={e => setShowProperties(e.newState === "open")}>
|
|
||||||
<summary>properties</summary>
|
|
||||||
{plant && <div>
|
|
||||||
available signals:
|
|
||||||
|
|
||||||
{plant.signals.join(', ')}
|
|
||||||
</div>}
|
|
||||||
{properties.map((property, i) => {
|
|
||||||
const result = propertyResults && propertyResults[i];
|
|
||||||
let violated = null, propertyError = null;
|
|
||||||
if (result) {
|
|
||||||
violated = result[0] && result[0].length > 0 && !result[0][0].satisfied;
|
|
||||||
propertyError = result[1];
|
|
||||||
}
|
|
||||||
return <div style={{width:'100%'}} key={i} className="toolbar">
|
|
||||||
<div className={"status" + (violated === null ? "" : (violated ? " violated" : " satisfied"))}></div>
|
|
||||||
<button title="see in trace (below)" className={activeProperty === i ? "active" : ""} onClick={() => setActiveProperty(i)}>
|
|
||||||
<VisibilityIcon fontSize="small"/>
|
|
||||||
</button>
|
|
||||||
<input type="text" style={{width:'calc(100% - 90px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
|
|
||||||
<button title="delete this property" onClick={() => setProperties(properties => properties.toSpliced(i, 1))}>
|
|
||||||
<DeleteOutlineIcon fontSize="small"/>
|
|
||||||
</button>
|
|
||||||
{propertyError && <div style={{color: 'var(--error-color)'}}>{propertyError}</div>}
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
<div className="toolbar">
|
|
||||||
<button title="add property" onClick={() => setProperties(properties => [...properties, ""])}>
|
|
||||||
<AddIcon fontSize="small"/> add property
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
{/* Traces */}
|
|
||||||
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary>
|
|
||||||
<div>
|
|
||||||
{savedTraces.map((savedTrace, i) =>
|
|
||||||
<div key={i} className="toolbar">
|
|
||||||
<button title="replay trace (may give a different result if you changed your model since recording the trace because only input and timer events are recorded)" onClick={() => onReplayTrace(savedTrace[1])}>
|
|
||||||
<CachedOutlinedIcon fontSize="small"/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span style={{display:'inline-block', width: 26, fontSize: 9}}>{(Math.floor(savedTrace[1].at(-1)!.simtime/1000))}s</span>
|
|
||||||
<span style={{display:'inline-block', width: 22, fontSize: 9}}>({savedTrace[1].length})</span>
|
|
||||||
|
|
||||||
<input title="name of the trace (only for humans - names don't have to be unique or anything)" type="text" value={savedTrace[0]} style={{width: 'calc(100% - 124px)'}} onChange={e => setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1, [e.target.value, savedTraces[i][1]]))}/>
|
|
||||||
<button title="forget trace" onClick={() => setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1))}>
|
|
||||||
<DeleteOutlineIcon fontSize="small"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="toolbar">
|
|
||||||
<input id="checkbox-show-plant-items" type="checkbox" checked={showPlantTrace} onChange={e => setShowPlantTrace(e.target.checked)}/>
|
|
||||||
<label title="plant steps are steps where only the state of the plant changed" htmlFor="checkbox-show-plant-items">show plant steps</label>
|
|
||||||
<input id="checkbox-autoscroll" type="checkbox" checked={autoScroll} onChange={e => setAutoScroll(e.target.checked)}/>
|
|
||||||
<label title="automatically scroll down event trace when new events occur" htmlFor="checkbox-autoscroll">auto-scroll</label>
|
|
||||||
 
|
|
||||||
<button title="save current trace" disabled={trace === null} onClick={() => onSaveTrace()}>
|
|
||||||
<SaveOutlinedIcon fontSize="small"/> save trace
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* We cheat a bit, and render the execution trace depending on whether the <details> above is 'open' or not, rather than putting it as a child of the <details>. We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */}
|
|
||||||
{showExecutionTrace &&
|
|
||||||
<div style={{
|
|
||||||
flexGrow:1,
|
|
||||||
overflow:'auto',
|
|
||||||
minHeight: '50vh',
|
|
||||||
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
|
||||||
}}>
|
|
||||||
<div ref={refRightSideBar}>
|
|
||||||
{ast && <RTHistory {...{ast, trace, setTrace, setTime, showPlantTrace,
|
|
||||||
propertyTrace: propertyResults && propertyResults[activeProperty] && propertyResults[activeProperty][0] || []}}/>}
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
<div style={{flex: '0 0 content'}}>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -561,83 +180,5 @@ export function App() {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoDetectConns(ast: Statechart, plant: Plant<any, any>, setPlantConns: Dispatch<SetStateAction<Conns>>) {
|
|
||||||
for (const {event: a} of plant.uiEvents) {
|
|
||||||
for (const {event: b} of plant.inputEvents) {
|
|
||||||
if (a === b) {
|
|
||||||
setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['plant', b]}));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const {event: b} of ast.inputEvents) {
|
|
||||||
if (a === b) {
|
|
||||||
setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['sc', b]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const a of ast.outputEvents) {
|
|
||||||
for (const {event: b} of plant.inputEvents) {
|
|
||||||
if (a === b) {
|
|
||||||
setPlantConns(conns => ({...conns, ['sc.'+a]: ['plant', b]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const {event: a} of plant.outputEvents) {
|
|
||||||
for (const {event: b} of ast.inputEvents) {
|
|
||||||
if (a === b) {
|
|
||||||
setPlantConns(conns => ({...conns, ['plant.'+a]: ['sc', b]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ConnEditor(ast: Statechart, plant: Plant<any, any>, plantConns: Conns, setPlantConns: Dispatch<SetStateAction<Conns>>) {
|
|
||||||
const plantInputs = <>{plant.inputEvents.map(e => <option key={'plant.'+e.event} value={'plant.'+e.event}>plant.{e.event}</option>)}</>
|
|
||||||
const scInputs = <>{ast.inputEvents.map(e => <option key={'sc.'+e.event} value={'sc.'+e.event}>sc.{e.event}</option>)}</>;
|
|
||||||
return <>
|
|
||||||
|
|
||||||
{/* SC output events can go to Plant */}
|
|
||||||
{[...ast.outputEvents].map(e => <div style={{width:'100%', textAlign:'right'}}>
|
|
||||||
<label htmlFor={`select-dst-sc-${e}`} style={{width:'50%'}}>sc.{e} → </label>
|
|
||||||
<select id={`select-dst-sc-${e}`}
|
|
||||||
style={{width:'50%'}}
|
|
||||||
value={plantConns['sc.'+e]?.join('.')}
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={domEvent => setPlantConns(conns => ({...conns, [`sc.${e}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
|
|
||||||
<option key="none" value=""></option>
|
|
||||||
{plantInputs}
|
|
||||||
</select>
|
|
||||||
</div>)}
|
|
||||||
|
|
||||||
{/* Plant output events can go to Statechart */}
|
|
||||||
{[...plant.outputEvents.map(e => <div style={{width:'100%', textAlign:'right'}}>
|
|
||||||
<label htmlFor={`select-dst-plant-${e.event}`} style={{width:'50%'}}>plant.{e.event} → </label>
|
|
||||||
<select id={`select-dst-plant-${e.event}`}
|
|
||||||
style={{width:'50%'}}
|
|
||||||
value={plantConns['plant.'+e.event]?.join('.')}
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={(domEvent => setPlantConns(conns => ({...conns, [`plant.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))})))}>
|
|
||||||
<option key="none" value=""></option>
|
|
||||||
{scInputs}
|
|
||||||
</select>
|
|
||||||
</div>)]}
|
|
||||||
|
|
||||||
{/* Plant UI events typically go to the Plant */}
|
|
||||||
{plant.uiEvents.map(e => <div style={{width:'100%', textAlign:'right'}}>
|
|
||||||
<label htmlFor={`select-dst-plant-ui-${e.event}`} style={{width:'50%', color: 'grey'}}>ui.{e.event} → </label>
|
|
||||||
<select id={`select-dst-plant-ui-${e.event}`}
|
|
||||||
style={{width:'50%'}}
|
|
||||||
value={plantConns['plant.ui.'+e.event]?.join('.')}
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={domEvent => setPlantConns(conns => ({...conns, [`plant.ui.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
|
|
||||||
<option key="none" value=""></option>
|
|
||||||
{scInputs}
|
|
||||||
{plantInputs}
|
|
||||||
</select>
|
|
||||||
</div>)}
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|
|
||||||
331
src/App/SideBar.tsx
Normal file
331
src/App/SideBar.tsx
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||||
|
import CachedOutlinedIcon from '@mui/icons-material/CachedOutlined';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
|
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import { Conns } from '@/statecharts/timed_reactive';
|
||||||
|
import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Statechart } from '@/statecharts/abstract_syntax';
|
||||||
|
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST';
|
||||||
|
import { Plant } from './Plant/Plant';
|
||||||
|
import { checkProperty, PropertyCheckResult } from './check_property';
|
||||||
|
import { Setters } from './makePartialSetter';
|
||||||
|
import { RTHistory } from './RTHistory';
|
||||||
|
import { BigStepCause, TraceState } from './useSimulator';
|
||||||
|
import { plants, UniversalPlantState } from './plants';
|
||||||
|
import { TimeMode } from '@/statecharts/time';
|
||||||
|
import { PersistentDetails } from './PersistentDetails';
|
||||||
|
|
||||||
|
type SavedTraces = [string, BigStepCause[]][];
|
||||||
|
|
||||||
|
export type SideBarState = {
|
||||||
|
showStateTree: boolean,
|
||||||
|
showInputEvents: boolean,
|
||||||
|
showInternalEvents: boolean,
|
||||||
|
showOutputEvents: boolean,
|
||||||
|
showPlant: boolean,
|
||||||
|
showConnections: boolean,
|
||||||
|
showProperties: boolean,
|
||||||
|
showExecutionTrace: boolean,
|
||||||
|
|
||||||
|
plantName: string,
|
||||||
|
plantConns: Conns,
|
||||||
|
autoConnect: boolean,
|
||||||
|
|
||||||
|
properties: string[],
|
||||||
|
activeProperty: number,
|
||||||
|
savedTraces: SavedTraces,
|
||||||
|
autoScroll: boolean,
|
||||||
|
showPlantTrace: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultSideBarState = {
|
||||||
|
showStateTree: false,
|
||||||
|
showInputEvents: true,
|
||||||
|
showInternalEvents: true,
|
||||||
|
showOutputEvents: true,
|
||||||
|
showPlant: false,
|
||||||
|
showConnections: false,
|
||||||
|
showProperties: false,
|
||||||
|
showExecutionTrace: true,
|
||||||
|
|
||||||
|
plantName: 'dummy',
|
||||||
|
plantConns: {},
|
||||||
|
autoConnect: true,
|
||||||
|
|
||||||
|
properties: [],
|
||||||
|
activeProperty: 0,
|
||||||
|
savedTraces: [],
|
||||||
|
autoScroll: false,
|
||||||
|
showPlantTrace: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
type SideBarProps = SideBarState & {
|
||||||
|
refRightSideBar: Ref<HTMLDivElement>,
|
||||||
|
ast: Statechart | null,
|
||||||
|
plant: Plant<any, UniversalPlantState>,
|
||||||
|
// setSavedTraces: Dispatch<SetStateAction<SavedTraces>>,
|
||||||
|
trace: TraceState|null,
|
||||||
|
setTrace: Dispatch<SetStateAction<TraceState|null>>,
|
||||||
|
plantState: UniversalPlantState,
|
||||||
|
onRaise: (inputEvent: string, param: any) => void,
|
||||||
|
onReplayTrace: (causes: BigStepCause[]) => void,
|
||||||
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
|
time: TimeMode,
|
||||||
|
} & Setters<SideBarState>;
|
||||||
|
|
||||||
|
export function SideBar({showExecutionTrace, showConnections, plantName, showPlantTrace, showProperties, activeProperty, autoConnect, autoScroll, plantConns, properties, savedTraces, refRightSideBar, ast, plant, setSavedTraces, trace, setTrace, setProperties, setShowPlantTrace, setActiveProperty, setPlantConns, setPlantName, setAutoConnect, setShowProperties, setAutoScroll, time, plantState, onReplayTrace, onRaise, setTime, setShowConnections, setShowExecutionTrace, showPlant, setShowPlant, showOutputEvents, setShowOutputEvents, setShowInternalEvents, showInternalEvents, setShowInputEvents, setShowStateTree, showInputEvents, showStateTree}: SideBarProps) {
|
||||||
|
|
||||||
|
const [propertyResults, setPropertyResults] = useState<PropertyCheckResult[] | null>(null);
|
||||||
|
|
||||||
|
const speed = time.kind === "paused" ? 0 : time.scale;
|
||||||
|
|
||||||
|
const onSaveTrace = () => {
|
||||||
|
if (trace) {
|
||||||
|
setSavedTraces(savedTraces => [
|
||||||
|
...savedTraces,
|
||||||
|
["untitled", trace.trace.map((item) => item.cause)] as [string, BigStepCause[]],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if some properties change, re-evaluate them:
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
if (trace) {
|
||||||
|
setPropertyResults(null);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
Promise.all(properties.map((property, i) => {
|
||||||
|
return checkProperty(plant, property, trace.trace);
|
||||||
|
}))
|
||||||
|
.then(results => {
|
||||||
|
setPropertyResults(results);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [properties, trace, plant]);
|
||||||
|
|
||||||
|
// whenever the ast, the plant or 'autoconnect' option changes, detect connections:
|
||||||
|
useEffect(() => {
|
||||||
|
if (ast && autoConnect) {
|
||||||
|
autoDetectConns(ast, plant, setPlantConns);
|
||||||
|
}
|
||||||
|
}, [ast, plant, autoConnect]);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div
|
||||||
|
className={showExecutionTrace ? "shadowBelow" : ""}
|
||||||
|
style={{flex: '0 0 content', backgroundColor: ''}}
|
||||||
|
>
|
||||||
|
{/* State tree */}
|
||||||
|
<PersistentDetails state={showStateTree} setState={setShowStateTree}>
|
||||||
|
<summary>state tree</summary>
|
||||||
|
<ul>
|
||||||
|
{ast && <ShowAST {...{...ast, trace, highlightActive: new Set()}}/>}
|
||||||
|
</ul>
|
||||||
|
</PersistentDetails>
|
||||||
|
{/* Input events */}
|
||||||
|
<PersistentDetails state={showInputEvents} setState={setShowInputEvents}>
|
||||||
|
<summary>input events</summary>
|
||||||
|
{ast && <ShowInputEvents
|
||||||
|
inputEvents={ast.inputEvents}
|
||||||
|
onRaise={(e,p) => onRaise("debug."+e,p)}
|
||||||
|
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
||||||
|
showKeys={true}/>}
|
||||||
|
</PersistentDetails>
|
||||||
|
{/* Internal events */}
|
||||||
|
<PersistentDetails state={showInternalEvents} setState={setShowInternalEvents}>
|
||||||
|
<summary>internal events</summary>
|
||||||
|
{ast && <ShowInternalEvents internalEvents={ast.internalEvents}/>}
|
||||||
|
</PersistentDetails>
|
||||||
|
{/* Output events */}
|
||||||
|
<PersistentDetails state={showOutputEvents} setState={setShowOutputEvents}>
|
||||||
|
<summary>output events</summary>
|
||||||
|
{ast && <ShowOutputEvents outputEvents={ast.outputEvents}/>}
|
||||||
|
</PersistentDetails>
|
||||||
|
{/* Plant */}
|
||||||
|
<PersistentDetails state={showPlant} setState={setShowPlant}>
|
||||||
|
<summary>plant</summary>
|
||||||
|
<select
|
||||||
|
disabled={trace!==null}
|
||||||
|
value={plantName}
|
||||||
|
onChange={e => setPlantName(() => e.target.value)}>
|
||||||
|
{plants.map(([plantName, p]) =>
|
||||||
|
<option>{plantName}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<br/>
|
||||||
|
{/* Render plant */}
|
||||||
|
{<plant.render state={plant.cleanupState(plantState)} speed={speed}
|
||||||
|
raiseUIEvent={e => onRaise("plant.ui."+e.name, e.param)}
|
||||||
|
/>}
|
||||||
|
</PersistentDetails>
|
||||||
|
{/* Connections */}
|
||||||
|
<PersistentDetails state={showConnections} setState={setShowConnections}>
|
||||||
|
<summary>connections</summary>
|
||||||
|
<button title="auto-connect (name-based)" className={autoConnect?"active":""}
|
||||||
|
onClick={() => setAutoConnect(c => !c)}>
|
||||||
|
<AutoAwesomeIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
{ast && ConnEditor(ast, plant, plantConns, setPlantConns)}
|
||||||
|
</PersistentDetails>
|
||||||
|
{/* Properties */}
|
||||||
|
<details open={showProperties} onToggle={e => setShowProperties(e.newState === "open")}>
|
||||||
|
<summary>properties</summary>
|
||||||
|
{plant && <div>
|
||||||
|
available signals:
|
||||||
|
|
||||||
|
{plant.signals.join(', ')}
|
||||||
|
</div>}
|
||||||
|
{properties.map((property, i) => {
|
||||||
|
const result = propertyResults && propertyResults[i];
|
||||||
|
let violated = null, propertyError = null;
|
||||||
|
if (result) {
|
||||||
|
violated = result[0] && result[0].length > 0 && !result[0][0].satisfied;
|
||||||
|
propertyError = result[1];
|
||||||
|
}
|
||||||
|
return <div style={{width:'100%'}} key={i} className="toolbar">
|
||||||
|
<div className={"status" + (violated === null ? "" : (violated ? " violated" : " satisfied"))}></div>
|
||||||
|
<button title="see in trace (below)" className={activeProperty === i ? "active" : ""} onClick={() => setActiveProperty(i)}>
|
||||||
|
<VisibilityIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
<input type="text" style={{width:'calc(100% - 90px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
|
||||||
|
<button title="delete this property" onClick={() => setProperties(properties => properties.toSpliced(i, 1))}>
|
||||||
|
<DeleteOutlineIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
{propertyError && <div style={{color: 'var(--error-color)'}}>{propertyError}</div>}
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
<div className="toolbar">
|
||||||
|
<button title="add property" onClick={() => setProperties(properties => [...properties, ""])}>
|
||||||
|
<AddIcon fontSize="small"/> add property
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/* Traces */}
|
||||||
|
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary>
|
||||||
|
<div>
|
||||||
|
{savedTraces.map((savedTrace, i) =>
|
||||||
|
<div key={i} className="toolbar">
|
||||||
|
<button title="replay trace (may give a different result if you changed your model since recording the trace because only input and timer events are recorded)" onClick={() => onReplayTrace(savedTrace[1])}>
|
||||||
|
<CachedOutlinedIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span style={{display:'inline-block', width: 26, fontSize: 9}}>{(Math.floor(savedTrace[1].at(-1)!.simtime/1000))}s</span>
|
||||||
|
<span style={{display:'inline-block', width: 22, fontSize: 9}}>({savedTrace[1].length})</span>
|
||||||
|
|
||||||
|
<input title="name of the trace (only for humans - names don't have to be unique or anything)" type="text" value={savedTrace[0]} style={{width: 'calc(100% - 124px)'}} onChange={e => setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1, [e.target.value, savedTraces[i][1]]))}/>
|
||||||
|
<button title="forget trace" onClick={() => setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1))}>
|
||||||
|
<DeleteOutlineIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="toolbar">
|
||||||
|
<input id="checkbox-show-plant-items" type="checkbox" checked={showPlantTrace} onChange={e => setShowPlantTrace(e.target.checked)}/>
|
||||||
|
<label title="plant steps are steps where only the state of the plant changed" htmlFor="checkbox-show-plant-items">show plant steps</label>
|
||||||
|
<input id="checkbox-autoscroll" type="checkbox" checked={autoScroll} onChange={e => setAutoScroll(e.target.checked)}/>
|
||||||
|
<label title="automatically scroll down event trace when new events occur" htmlFor="checkbox-autoscroll">auto-scroll</label>
|
||||||
|
 
|
||||||
|
<button title="save current trace" disabled={trace === null} onClick={() => onSaveTrace()}>
|
||||||
|
<SaveOutlinedIcon fontSize="small"/> save trace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* We cheat a bit, and render the execution trace depending on whether the <details> above is 'open' or not, rather than putting it as a child of the <details>. We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */}
|
||||||
|
{showExecutionTrace &&
|
||||||
|
<div style={{
|
||||||
|
flexGrow:1,
|
||||||
|
overflow:'auto',
|
||||||
|
minHeight: '50vh',
|
||||||
|
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
||||||
|
}}>
|
||||||
|
<div ref={refRightSideBar}>
|
||||||
|
{ast && <RTHistory {...{ast, trace, setTrace, setTime, showPlantTrace,
|
||||||
|
propertyTrace: propertyResults && propertyResults[activeProperty] && propertyResults[activeProperty][0] || []}}/>}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoDetectConns(ast: Statechart, plant: Plant<any, any>, setPlantConns: Dispatch<SetStateAction<Conns>>) {
|
||||||
|
for (const {event: a} of plant.uiEvents) {
|
||||||
|
for (const {event: b} of plant.inputEvents) {
|
||||||
|
if (a === b) {
|
||||||
|
setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['plant', b]}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const {event: b} of ast.inputEvents) {
|
||||||
|
if (a === b) {
|
||||||
|
setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['sc', b]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const a of ast.outputEvents) {
|
||||||
|
for (const {event: b} of plant.inputEvents) {
|
||||||
|
if (a === b) {
|
||||||
|
setPlantConns(conns => ({...conns, ['sc.'+a]: ['plant', b]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const {event: a} of plant.outputEvents) {
|
||||||
|
for (const {event: b} of ast.inputEvents) {
|
||||||
|
if (a === b) {
|
||||||
|
setPlantConns(conns => ({...conns, ['plant.'+a]: ['sc', b]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConnEditor(ast: Statechart, plant: Plant<any, any>, plantConns: Conns, setPlantConns: Dispatch<SetStateAction<Conns>>) {
|
||||||
|
const plantInputs = <>{plant.inputEvents.map(e => <option key={'plant.'+e.event} value={'plant.'+e.event}>plant.{e.event}</option>)}</>
|
||||||
|
const scInputs = <>{ast.inputEvents.map(e => <option key={'sc.'+e.event} value={'sc.'+e.event}>sc.{e.event}</option>)}</>;
|
||||||
|
return <>
|
||||||
|
|
||||||
|
{/* SC output events can go to Plant */}
|
||||||
|
{[...ast.outputEvents].map(e => <div style={{width:'100%', textAlign:'right'}}>
|
||||||
|
<label htmlFor={`select-dst-sc-${e}`} style={{width:'50%'}}>sc.{e} → </label>
|
||||||
|
<select id={`select-dst-sc-${e}`}
|
||||||
|
style={{width:'50%'}}
|
||||||
|
value={plantConns['sc.'+e]?.join('.')}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={domEvent => setPlantConns(conns => ({...conns, [`sc.${e}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
|
||||||
|
<option key="none" value=""></option>
|
||||||
|
{plantInputs}
|
||||||
|
</select>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
{/* Plant output events can go to Statechart */}
|
||||||
|
{[...plant.outputEvents.map(e => <div style={{width:'100%', textAlign:'right'}}>
|
||||||
|
<label htmlFor={`select-dst-plant-${e.event}`} style={{width:'50%'}}>plant.{e.event} → </label>
|
||||||
|
<select id={`select-dst-plant-${e.event}`}
|
||||||
|
style={{width:'50%'}}
|
||||||
|
value={plantConns['plant.'+e.event]?.join('.')}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={(domEvent => setPlantConns(conns => ({...conns, [`plant.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))})))}>
|
||||||
|
<option key="none" value=""></option>
|
||||||
|
{scInputs}
|
||||||
|
</select>
|
||||||
|
</div>)]}
|
||||||
|
|
||||||
|
{/* Plant UI events typically go to the Plant */}
|
||||||
|
{plant.uiEvents.map(e => <div style={{width:'100%', textAlign:'right'}}>
|
||||||
|
<label htmlFor={`select-dst-plant-ui-${e.event}`} style={{width:'50%', color: 'grey'}}>ui.{e.event} → </label>
|
||||||
|
<select id={`select-dst-plant-ui-${e.event}`}
|
||||||
|
style={{width:'50%'}}
|
||||||
|
value={plantConns['plant.ui.'+e.event]?.join('.')}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={domEvent => setPlantConns(conns => ({...conns, [`plant.ui.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
|
||||||
|
<option key="none" value=""></option>
|
||||||
|
{scInputs}
|
||||||
|
{plantInputs}
|
||||||
|
</select>
|
||||||
|
</div>)}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
|
||||||
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
|
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
|
||||||
import { InsertMode } from "./InsertModes";
|
import { InsertMode } from "./InsertModes";
|
||||||
import { About } from "../Modals/About";
|
import { About } from "../Modals/About";
|
||||||
import { EditHistory, TraceState } from "../App";
|
import { EditHistory } from "../App";
|
||||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
import { UndoRedoButtons } from "./UndoRedoButtons";
|
import { UndoRedoButtons } from "./UndoRedoButtons";
|
||||||
import { ZoomButtons } from "./ZoomButtons";
|
import { ZoomButtons } from "./ZoomButtons";
|
||||||
|
|
@ -21,6 +21,7 @@ import { InsertModes } from "./InsertModes";
|
||||||
import { usePersistentState } from "@/App/persistent_state";
|
import { usePersistentState } from "@/App/persistent_state";
|
||||||
import { RotateButtons } from "./RotateButtons";
|
import { RotateButtons } from "./RotateButtons";
|
||||||
import { SpeedControl } from "./SpeedControl";
|
import { SpeedControl } from "./SpeedControl";
|
||||||
|
import { TraceState } from "../useSimulator";
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
trace: TraceState | null,
|
trace: TraceState | null,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { TraceState } from "@/App/App";
|
|
||||||
import { InsertMode } from "../TopPanel/InsertModes";
|
import { InsertMode } from "../TopPanel/InsertModes";
|
||||||
import { Mode } from "@/statecharts/runtime_types";
|
import { Mode } from "@/statecharts/runtime_types";
|
||||||
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
|
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
|
||||||
|
|
@ -17,7 +16,6 @@ import { useCopyPaste } from "./useCopyPaste";
|
||||||
|
|
||||||
import "./VisualEditor.css";
|
import "./VisualEditor.css";
|
||||||
import { useMouse } from "./useMouse";
|
import { useMouse } from "./useMouse";
|
||||||
import { Selecting } from "./Selection";
|
|
||||||
|
|
||||||
export type ConcreteSyntax = {
|
export type ConcreteSyntax = {
|
||||||
rountangles: Rountangle[];
|
rountangles: Rountangle[];
|
||||||
|
|
@ -59,7 +57,8 @@ type VisualEditorProps = {
|
||||||
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
||||||
conns: Connections,
|
conns: Connections,
|
||||||
syntaxErrors: TraceableError[],
|
syntaxErrors: TraceableError[],
|
||||||
trace: TraceState | null,
|
// trace: TraceState | null,
|
||||||
|
// activeStates: Set<string>,
|
||||||
insertMode: InsertMode,
|
insertMode: InsertMode,
|
||||||
highlightActive: Set<string>,
|
highlightActive: Set<string>,
|
||||||
highlightTransitions: string[],
|
highlightTransitions: string[],
|
||||||
|
|
@ -68,7 +67,7 @@ type VisualEditorProps = {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VisualEditor = memo(function VisualEditor({state, setState, trace, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
export const VisualEditor = memo(function VisualEditor({state, setState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
||||||
|
|
||||||
// uid's of selected rountangles
|
// uid's of selected rountangles
|
||||||
const selection = state.selection || [];
|
const selection = state.selection || [];
|
||||||
|
|
@ -87,7 +86,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}, [trace && trace.idx]);
|
}, [highlightTransitions]);
|
||||||
|
|
||||||
|
|
||||||
const {onCopy, onPaste, onCut, deleteSelection} = useCopyPaste(makeCheckPoint, state, setState, selection);
|
const {onCopy, onPaste, onCut, deleteSelection} = useCopyPaste(makeCheckPoint, state, setState, selection);
|
||||||
|
|
@ -167,15 +166,12 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace,
|
||||||
}
|
}
|
||||||
}, [setState]);
|
}, [setState]);
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const active = trace && trace.trace[trace.idx].mode || new Set();
|
|
||||||
|
|
||||||
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
||||||
const size = 4000*zoom;
|
const size = 4000*zoom;
|
||||||
|
|
||||||
return <svg width={size} height={size}
|
return <svg width={size} height={size}
|
||||||
className={"svgCanvas"+(active.has("root")?" active":"")/*+(dragging ? " dragging" : "")*/}
|
className={"svgCanvas"+(highlightActive.has("root")?" active":"")/*+(dragging ? " dragging" : "")*/}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
ref={refSVG}
|
ref={refSVG}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { RT_Statechart } from "@/statecharts/runtime_types";
|
import { RT_Statechart } from "@/statecharts/runtime_types";
|
||||||
import { TraceItem } from "./App";
|
|
||||||
import { Plant } from "./Plant/Plant";
|
import { Plant } from "./Plant/Plant";
|
||||||
|
import { TraceItem } from "./useSimulator";
|
||||||
|
|
||||||
// const endpoint = "http://localhost:15478/check_property";
|
// const endpoint = "http://localhost:15478/check_property";
|
||||||
const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
|
const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
|
||||||
|
|
@ -37,8 +37,8 @@ export async function checkProperty(plant: Plant<RT_Statechart, any>, property:
|
||||||
}, [] as {simtime: number, state: any}[]);
|
}, [] as {simtime: number, state: any}[]);
|
||||||
|
|
||||||
let traces = {
|
let traces = {
|
||||||
'true': [0, true] as [number, any],
|
'true': [[0, true] as [number, any]],
|
||||||
'false': [0, false] as [number, any],
|
'false': [[0, false] as [number, any]],
|
||||||
} as {[key: string]: [number, any][]};
|
} as {[key: string]: [number, any][]};
|
||||||
for (const {simtime, state} of cleanPlantStates) {
|
for (const {simtime, state} of cleanPlantStates) {
|
||||||
for (const [key, value] of Object.entries(state)) {
|
for (const [key, value] of Object.entries(state)) {
|
||||||
|
|
|
||||||
36
src/App/makePartialSetter.ts
Normal file
36
src/App/makePartialSetter.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
|
||||||
|
|
||||||
|
export function makePartialSetter<T, K extends keyof T>(fullSetter: Dispatch<SetStateAction<T>>, key: K): Dispatch<SetStateAction<T[typeof key]>> {
|
||||||
|
return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => {
|
||||||
|
fullSetter(oldFullValue => {
|
||||||
|
if (typeof newValueOrCallback === 'function') {
|
||||||
|
return {
|
||||||
|
...oldFullValue,
|
||||||
|
[key]: (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...oldFullValue,
|
||||||
|
[key]: newValueOrCallback as T[K],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Setters<T extends {[key: string]: any}> = {
|
||||||
|
[K in keyof T as `set${Capitalize<Extract<K, string>>}`]: Dispatch<SetStateAction<T[K]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeIndividualSetters<T extends {[key: string]: any}>(
|
||||||
|
fullSetter: Dispatch<SetStateAction<T>>,
|
||||||
|
keys: (keyof T)[],
|
||||||
|
): Setters<T> {
|
||||||
|
// @ts-ignore
|
||||||
|
return useMemo(() =>
|
||||||
|
// @ts-ignore
|
||||||
|
Object.fromEntries(keys.map((key: string) => {
|
||||||
|
return [`set${key.charAt(0).toUpperCase()}${key.slice(1)}`, makePartialSetter(fullSetter, key)];
|
||||||
|
})),
|
||||||
|
[fullSetter]
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/App/plants.ts
Normal file
14
src/App/plants.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { digitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch";
|
||||||
|
import { dummyPlant } from "./Plant/Dummy/Dummy";
|
||||||
|
import { microwavePlant } from "./Plant/Microwave/Microwave";
|
||||||
|
import { Plant } from "./Plant/Plant";
|
||||||
|
import { trafficLightPlant } from "./Plant/TrafficLight/TrafficLight";
|
||||||
|
|
||||||
|
export type UniversalPlantState = {[property: string]: boolean|number};
|
||||||
|
|
||||||
|
export const plants: [string, Plant<any, UniversalPlantState>][] = [
|
||||||
|
["dummy", dummyPlant],
|
||||||
|
["microwave", microwavePlant as unknown as Plant<any, UniversalPlantState>],
|
||||||
|
["digital watch", digitalWatchPlant as unknown as Plant<any, UniversalPlantState>],
|
||||||
|
["traffic light", trafficLightPlant as unknown as Plant<any, UniversalPlantState>],
|
||||||
|
];
|
||||||
|
|
@ -1,3 +1,223 @@
|
||||||
export function useSimulator() {
|
import { Statechart } from "@/statecharts/abstract_syntax";
|
||||||
|
import { RuntimeError } from "@/statecharts/interpreter";
|
||||||
|
import { BigStep, RaisedEvent } from "@/statecharts/runtime_types";
|
||||||
|
import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Plant } from "./Plant/Plant";
|
||||||
|
import { getSimTime, getWallClkDelay, TimeMode } from "@/statecharts/time";
|
||||||
|
import { UniversalPlantState } from "./plants";
|
||||||
|
|
||||||
|
type CoupledState = {
|
||||||
|
sc: BigStep,
|
||||||
|
plant: BigStep,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TraceItemError = {
|
||||||
|
cause: BigStepCause, // event name, <init> or <timer>
|
||||||
|
simtime: number,
|
||||||
|
error: RuntimeError,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigStepCause = {
|
||||||
|
kind: "init",
|
||||||
|
simtime: 0,
|
||||||
|
} | {
|
||||||
|
kind: "input",
|
||||||
|
simtime: number,
|
||||||
|
eventName: string,
|
||||||
|
param?: any,
|
||||||
|
} | {
|
||||||
|
kind: "timer",
|
||||||
|
simtime: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TraceItem =
|
||||||
|
{ kind: "error" } & TraceItemError
|
||||||
|
| { kind: "bigstep", simtime: number, cause: BigStepCause, state: CoupledState, outputEvents: RaisedEvent[] };
|
||||||
|
|
||||||
|
export type TraceState = {
|
||||||
|
trace: [TraceItem, ...TraceItem[]], // non-empty
|
||||||
|
idx: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function useSimulator(ast: Statechart|null, plant: Plant<any, UniversalPlantState>, plantConns: Conns, onStep: () => void) {
|
||||||
|
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
||||||
|
const [trace, setTrace] = useState<TraceState|null>(null);
|
||||||
|
const currentTraceItem = trace && trace.trace[trace.idx];
|
||||||
|
|
||||||
|
// coupled execution
|
||||||
|
const cE = useMemo(() => ast && coupledExecution({
|
||||||
|
sc: statechartExecution(ast),
|
||||||
|
plant: plant.execution,
|
||||||
|
}, {
|
||||||
|
...plantConns,
|
||||||
|
...Object.fromEntries(ast.inputEvents.map(({event}) => ["debug."+event, ['sc',event] as [string,string]])),
|
||||||
|
}), [ast]);
|
||||||
|
|
||||||
|
const onInit = useCallback(() => {
|
||||||
|
if (cE === null) return;
|
||||||
|
const metadata = {simtime: 0, cause: {kind: "init" as const, simtime: 0 as const}};
|
||||||
|
try {
|
||||||
|
const [outputEvents, state] = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
|
||||||
|
setTrace({
|
||||||
|
trace: [{kind: "bigstep", ...metadata, state, outputEvents}],
|
||||||
|
idx: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof RuntimeError) {
|
||||||
|
setTrace({
|
||||||
|
trace: [{kind: "error", ...metadata, error}],
|
||||||
|
idx: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw error; // probably a bug in the interpreter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTime(time => {
|
||||||
|
if (time.kind === "paused") {
|
||||||
|
return {...time, simtime: 0};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {...time, since: {simtime: 0, wallclktime: performance.now()}};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onStep();
|
||||||
|
}, [cE, onStep]);
|
||||||
|
|
||||||
|
const onClear = useCallback(() => {
|
||||||
|
setTrace(null);
|
||||||
|
setTime({kind: "paused", simtime: 0});
|
||||||
|
}, [setTrace, setTime]);
|
||||||
|
|
||||||
|
// raise input event, producing a new runtime configuration (or a runtime error)
|
||||||
|
const onRaise = (inputEvent: string, param: any) => {
|
||||||
|
if (cE === null) return;
|
||||||
|
if (currentTraceItem !== null /*&& ast.inputEvents.some(e => e.event === inputEvent)*/) {
|
||||||
|
if (currentTraceItem.kind === "bigstep") {
|
||||||
|
const simtime = getSimTime(time, Math.round(performance.now()));
|
||||||
|
appendNewConfig(simtime, {kind: "input", simtime, eventName: inputEvent, param}, () => {
|
||||||
|
return cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 (currentTraceItem !== null && cE !== null) {
|
||||||
|
if (currentTraceItem.kind === "bigstep") {
|
||||||
|
const nextTimeout = cE?.timeAdvance(currentTraceItem.state);
|
||||||
|
|
||||||
|
const raiseTimeEvent = () => {
|
||||||
|
appendNewConfig(nextTimeout, {kind: "timer", simtime: nextTimeout}, () => {
|
||||||
|
return cE.intTransition(currentTraceItem.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.kind === "realtime") {
|
||||||
|
const wallclkDelay = getWallClkDelay(time, nextTimeout, Math.round(performance.now()));
|
||||||
|
if (wallclkDelay !== Infinity) {
|
||||||
|
timeout = setTimeout(raiseTimeEvent, wallclkDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (time.kind === "paused") {
|
||||||
|
if (nextTimeout <= time.simtime) {
|
||||||
|
raiseTimeEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [time, currentTraceItem]); // <-- todo: is this really efficient?
|
||||||
|
|
||||||
|
function appendNewConfig(simtime: number, cause: BigStepCause, computeNewState: () => [RaisedEvent[], CoupledState]) {
|
||||||
|
let newItem: TraceItem;
|
||||||
|
const metadata = {simtime, cause}
|
||||||
|
try {
|
||||||
|
const [outputEvents, state] = computeNewState(); // may throw RuntimeError
|
||||||
|
newItem = {kind: "bigstep", ...metadata, state, outputEvents};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof RuntimeError) {
|
||||||
|
newItem = {kind: "error", ...metadata, error};
|
||||||
|
// also pause the simulation, for dramatic effect:
|
||||||
|
setTime({kind: "paused", simtime});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
setTrace(trace => ({
|
||||||
|
trace: [
|
||||||
|
...trace!.trace.slice(0, trace!.idx+1), // remove everything after current item
|
||||||
|
newItem,
|
||||||
|
],
|
||||||
|
// idx: 0,
|
||||||
|
idx: trace!.idx+1,
|
||||||
|
}));
|
||||||
|
onStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBack = useCallback(() => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [trace]);
|
||||||
|
|
||||||
|
const onReplayTrace = (causes: BigStepCause[]) => {
|
||||||
|
if (cE) {
|
||||||
|
function run_until(simtime: number) {
|
||||||
|
while (true) {
|
||||||
|
const nextTimeout = cE!.timeAdvance(lastState);
|
||||||
|
if (nextTimeout > simtime) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const [outputEvents, coupledState] = cE!.intTransition(lastState);
|
||||||
|
lastState = coupledState;
|
||||||
|
lastSimtime = nextTimeout;
|
||||||
|
newTrace.push({kind: "bigstep", simtime: nextTimeout, state: coupledState, outputEvents, cause: {kind: "timer", simtime: nextTimeout}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [outputEvents, coupledState] = cE.initial();
|
||||||
|
const newTrace = [{kind: "bigstep", simtime: 0, state: coupledState, outputEvents, cause: {kind: "init"} as BigStepCause} as TraceItem] as [TraceItem, ...TraceItem[]];
|
||||||
|
let lastState = coupledState;
|
||||||
|
let lastSimtime = 0;
|
||||||
|
for (const cause of causes) {
|
||||||
|
if (cause.kind === "input") {
|
||||||
|
run_until(cause.simtime); // <-- just make sure we haven't missed any timers elapsing
|
||||||
|
// @ts-ignore
|
||||||
|
const [outputEvents, coupledState] = cE.extTransition(cause.simtime, newTrace.at(-1)!.state, {kind: "input", name: cause.eventName, param: cause.param});
|
||||||
|
lastState = coupledState;
|
||||||
|
lastSimtime = cause.simtime;
|
||||||
|
newTrace.push({kind: "bigstep", simtime: cause.simtime, state: coupledState, outputEvents, cause});
|
||||||
|
}
|
||||||
|
else if (cause.kind === "timer") {
|
||||||
|
run_until(cause.simtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTrace({trace: newTrace, idx: newTrace.length-1});
|
||||||
|
setTime({kind: "paused", simtime: lastSimtime});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {trace, setTrace, plant, onInit, onClear, onBack, onRaise, onReplayTrace, time, setTime};
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +1,7 @@
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { BigStepCause, EditHistory } from "./App";
|
|
||||||
import { VisualEditorState } from "./VisualEditor/VisualEditor";
|
|
||||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
|
||||||
import { InsertMode } from "./TopPanel/InsertModes";
|
|
||||||
import { Conns } from "@/statecharts/timed_reactive";
|
|
||||||
|
|
||||||
export function useUrlHashState(editorState: VisualEditorState | null, setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) {
|
|
||||||
|
|
||||||
// i should probably put all these things into a single object, the 'app state'...
|
|
||||||
const [autoScroll, setAutoScroll] = useState(false);
|
|
||||||
const [autoConnect, setAutoConnect] = useState(true);
|
|
||||||
const [plantConns, setPlantConns] = useState<Conns>({});
|
|
||||||
const [showKeys, setShowKeys] = useState(true);
|
|
||||||
const [zoom, setZoom] = useState(1);
|
|
||||||
const [insertMode, setInsertMode] = useState<InsertMode>("and");
|
|
||||||
const [plantName, setPlantName] = useState("dummy");
|
|
||||||
|
|
||||||
const [showConnections, setShowConnections] = useState(false);
|
|
||||||
const [showProperties, setShowProperties] = useState(false);
|
|
||||||
const [showExecutionTrace, setShowExecutionTrace] = useState(true);
|
|
||||||
const [showPlantTrace, setShowPlantTrace] = useState(false);
|
|
||||||
const [properties, setProperties] = useState<string[]>([]);
|
|
||||||
const [savedTraces, setSavedTraces] = useState<[string, BigStepCause[]][]>([]);
|
|
||||||
const [activeProperty, setActiveProperty] = useState<number>(0);
|
|
||||||
|
|
||||||
|
// persist state in URL hash
|
||||||
|
export function useUrlHashState<T>(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void {
|
||||||
|
|
||||||
// recover editor state from URL - we need an effect here because decompression is asynchronous
|
// recover editor state from URL - we need an effect here because decompression is asynchronous
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -32,7 +10,7 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi
|
||||||
if (compressedState.length === 0) {
|
if (compressedState.length === 0) {
|
||||||
// empty URL hash
|
// empty URL hash
|
||||||
console.log("no state to recover");
|
console.log("no state to recover");
|
||||||
setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
// setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let compressedBuffer;
|
let compressedBuffer;
|
||||||
|
|
@ -41,7 +19,7 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// probably invalid base64
|
// probably invalid base64
|
||||||
console.error("failed to recover state:", e);
|
console.error("failed to recover state:", e);
|
||||||
setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
// setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ds = new DecompressionStream("deflate");
|
const ds = new DecompressionStream("deflate");
|
||||||
|
|
@ -51,153 +29,28 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi
|
||||||
new Response(ds.readable).arrayBuffer()
|
new Response(ds.readable).arrayBuffer()
|
||||||
.then(decompressedBuffer => {
|
.then(decompressedBuffer => {
|
||||||
const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer));
|
const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer));
|
||||||
// we support two formats
|
recoverCallback(recoveredState);
|
||||||
if (recoveredState.nextID) {
|
|
||||||
// old format
|
|
||||||
setEditHistory(() => ({current: recoveredState, history: [], future: []}));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(recoveredState);
|
|
||||||
// new format
|
|
||||||
if (recoveredState.editorState !== undefined) {
|
|
||||||
setEditHistory(() => ({current: recoveredState.editorState, history: [], future: []}));
|
|
||||||
}
|
|
||||||
if (recoveredState.plantName !== undefined) {
|
|
||||||
setPlantName(recoveredState.plantName);
|
|
||||||
}
|
|
||||||
if (recoveredState.autoScroll !== undefined) {
|
|
||||||
setAutoScroll(recoveredState.autoScroll);
|
|
||||||
}
|
|
||||||
if (recoveredState.autoConnect !== undefined) {
|
|
||||||
setAutoConnect(recoveredState.autoConnect);
|
|
||||||
}
|
|
||||||
if (recoveredState.plantConns !== undefined) {
|
|
||||||
setPlantConns(recoveredState.plantConns);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recoveredState.showKeys !== undefined) {
|
|
||||||
setShowKeys(recoveredState.showKeys);
|
|
||||||
}
|
|
||||||
if (recoveredState.zoom !== undefined) {
|
|
||||||
setZoom(recoveredState.zoom);
|
|
||||||
}
|
|
||||||
if (recoveredState.insertMode !== undefined) {
|
|
||||||
setInsertMode(recoveredState.insertMode);
|
|
||||||
}
|
|
||||||
if (recoveredState.showConnections !== undefined) {
|
|
||||||
setShowConnections(recoveredState.showConnections);
|
|
||||||
}
|
|
||||||
if (recoveredState.showProperties !== undefined) {
|
|
||||||
setShowProperties(recoveredState.showProperties);
|
|
||||||
}
|
|
||||||
if (recoveredState.showExecutionTrace !== undefined) {
|
|
||||||
setShowExecutionTrace(recoveredState.showExecutionTrace);
|
|
||||||
}
|
|
||||||
if (recoveredState.showPlantTrace !== undefined) {
|
|
||||||
setShowPlantTrace(recoveredState.showPlantTrace);
|
|
||||||
}
|
|
||||||
if (recoveredState.properties !== undefined) {
|
|
||||||
setProperties(recoveredState.properties);
|
|
||||||
}
|
|
||||||
if (recoveredState.savedTraces !== undefined) {
|
|
||||||
setSavedTraces(recoveredState.savedTraces);
|
|
||||||
}
|
|
||||||
if (recoveredState.activeProperty !== undefined) {
|
|
||||||
setActiveProperty(recoveredState.activeProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
// any other error: invalid JSON, or decompression failed.
|
// any other error: invalid JSON, or decompression failed.
|
||||||
console.error("failed to recover state:", e);
|
console.error("failed to recover state:", e);
|
||||||
setEditHistory({current: emptyState, history: [], future: []});
|
// setEditHistory({current: emptyState, history: [], future: []});
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// save editor state in URL
|
function persist(state: T) {
|
||||||
useEffect(() => {
|
const serializedState = JSON.stringify(state);
|
||||||
const timeout = setTimeout(() => {
|
const stateBuffer = new TextEncoder().encode(serializedState);
|
||||||
if (editorState === null) {
|
const cs = new CompressionStream("deflate");
|
||||||
window.location.hash = "#";
|
const writer = cs.writable.getWriter();
|
||||||
return;
|
writer.write(stateBuffer);
|
||||||
}
|
writer.close();
|
||||||
const serializedState = JSON.stringify({
|
// todo: cancel this promise handler when concurrently starting another compression job
|
||||||
autoConnect,
|
new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => {
|
||||||
autoScroll,
|
const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64();
|
||||||
plantConns,
|
window.location.hash = "#"+compressedStateString;
|
||||||
showKeys,
|
});
|
||||||
zoom,
|
|
||||||
insertMode,
|
|
||||||
plantName,
|
|
||||||
editorState,
|
|
||||||
showConnections,
|
|
||||||
showProperties,
|
|
||||||
showExecutionTrace,
|
|
||||||
showPlantTrace,
|
|
||||||
properties,
|
|
||||||
savedTraces,
|
|
||||||
activeProperty,
|
|
||||||
});
|
|
||||||
const stateBuffer = new TextEncoder().encode(serializedState);
|
|
||||||
const cs = new CompressionStream("deflate");
|
|
||||||
const writer = cs.writable.getWriter();
|
|
||||||
writer.write(stateBuffer);
|
|
||||||
writer.close();
|
|
||||||
// todo: cancel this promise handler when concurrently starting another compression job
|
|
||||||
new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => {
|
|
||||||
const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64();
|
|
||||||
window.location.hash = "#"+compressedStateString;
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [
|
|
||||||
editorState,
|
|
||||||
|
|
||||||
autoConnect,
|
|
||||||
autoScroll,
|
|
||||||
plantConns,
|
|
||||||
showKeys,
|
|
||||||
zoom,
|
|
||||||
insertMode,
|
|
||||||
plantName,
|
|
||||||
showConnections,
|
|
||||||
showProperties,
|
|
||||||
showExecutionTrace,
|
|
||||||
showPlantTrace,
|
|
||||||
properties,
|
|
||||||
savedTraces,
|
|
||||||
activeProperty,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
autoConnect,
|
|
||||||
setAutoConnect,
|
|
||||||
autoScroll,
|
|
||||||
setAutoScroll,
|
|
||||||
plantConns,
|
|
||||||
setPlantConns,
|
|
||||||
showKeys,
|
|
||||||
setShowKeys,
|
|
||||||
zoom,
|
|
||||||
setZoom,
|
|
||||||
insertMode,
|
|
||||||
setInsertMode,
|
|
||||||
plantName,
|
|
||||||
setPlantName,
|
|
||||||
showConnections,
|
|
||||||
setShowConnections,
|
|
||||||
showProperties,
|
|
||||||
setShowProperties,
|
|
||||||
showExecutionTrace,
|
|
||||||
setShowExecutionTrace,
|
|
||||||
showPlantTrace,
|
|
||||||
setShowPlantTrace,
|
|
||||||
properties,
|
|
||||||
setProperties,
|
|
||||||
savedTraces,
|
|
||||||
setSavedTraces,
|
|
||||||
activeProperty,
|
|
||||||
setActiveProperty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return persist;
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +326,6 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_
|
||||||
|
|
||||||
// A fair step is a response to one (input|internal) event, where possibly multiple transitions are made as long as their arenas do not overlap. A reasonably accurate and more intuitive explanation is that every orthogonal region is allowed to fire at most one transition.
|
// A fair step is a response to one (input|internal) event, where possibly multiple transitions are made as long as their arenas do not overlap. A reasonably accurate and more intuitive explanation is that every orthogonal region is allowed to fire at most one transition.
|
||||||
export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, environment, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, environment, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
console.log('fair step', event, activeParent);
|
|
||||||
environment = environment.enterScope(activeParent.uid);
|
environment = environment.enterScope(activeParent.uid);
|
||||||
// console.log('fairStep', arenasFired);
|
// console.log('fairStep', arenasFired);
|
||||||
for (const state of activeParent.children) {
|
for (const state of activeParent.children) {
|
||||||
|
|
@ -347,7 +346,6 @@ export function fairStep(simtime: number, event: RT_Event, statechart: Statechar
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStep {
|
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStep {
|
||||||
console.log('handleInputEvent', event);
|
|
||||||
let raised = initialRaised;
|
let raised = initialRaised;
|
||||||
|
|
||||||
({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised}));
|
({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised}));
|
||||||
|
|
@ -356,7 +354,6 @@ export function handleInputEvent(simtime: number, event: RT_Event, statechart: S
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents) {
|
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents) {
|
||||||
console.log('handleInternalEvents');
|
|
||||||
while (internalEvents.length > 0) {
|
while (internalEvents.length > 0) {
|
||||||
const [nextEvent, ...remainingEvents] = internalEvents;
|
const [nextEvent, ...remainingEvents] = internalEvents;
|
||||||
({internalEvents, ...rest} = fairStep(simtime,
|
({internalEvents, ...rest} = fairStep(simtime,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue