From 9922f8588daebd04e28d2e1132533d3879ccf248 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Sat, 8 Nov 2025 10:32:28 +0100 Subject: [PATCH] clean up code a bit (split of SideBar component and simulator callbacks from App) + fix bug in property checker --- src/App/App.tsx | 601 +++----------------------- src/App/SideBar.tsx | 331 ++++++++++++++ src/App/TopPanel/TopPanel.tsx | 3 +- src/App/VisualEditor/VisualEditor.tsx | 14 +- src/App/check_property.ts | 6 +- src/App/makePartialSetter.ts | 36 ++ src/App/plants.ts | 14 + src/App/useSimulator.ts | 226 +++++++++- src/App/useUrlHashState.ts | 191 +------- src/statecharts/interpreter.ts | 3 - 10 files changed, 707 insertions(+), 718 deletions(-) create mode 100644 src/App/SideBar.tsx create mode 100644 src/App/makePartialSetter.ts create mode 100644 src/App/plants.ts diff --git a/src/App/App.tsx b/src/App/App.tsx index b18d96a..88450c4 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,36 +1,20 @@ import "../index.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 { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive"; -import { RuntimeError } from "../statecharts/interpreter"; import { parseStatechart } from "../statecharts/parser"; -import { BigStep, RaisedEvent } from "../statecharts/runtime_types"; -import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; import { BottomPanel } from "./BottomPanel"; -import { PersistentDetails, PersistentDetailsLocalStorage } from "./PersistentDetails"; -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"; -import { RTHistory } from "./RTHistory"; -import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST"; +import { defaultSideBarState, SideBar, SideBarState } from "./SideBar"; +import { InsertMode } from "./TopPanel/InsertModes"; import { TopPanel } from "./TopPanel/TopPanel"; import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; -import { checkProperty, PropertyCheckResult } from "./check_property"; +import { makeIndividualSetters } from "./makePartialSetter"; import { useEditor } from "./useEditor"; +import { useSimulator } from "./useSimulator"; import { useUrlHashState } from "./useUrlHashState"; +import { plants } from "./plants"; export type EditHistory = { current: VisualEditorState, @@ -38,53 +22,22 @@ export type EditHistory = { future: VisualEditorState[], } -type UniversalPlantState = {[property: string]: boolean|number}; +export type AppState = { + showKeys: boolean, + zoom: number, + insertMode: InsertMode, +} & SideBarState; -const plants: [string, Plant][] = [ - ["dummy", dummyPlant], - ["microwave", microwavePlant as unknown as Plant], - ["digital watch", digitalWatchPlant as unknown as Plant], - ["traffic light", trafficLightPlant as unknown as Plant], -] +const defaultAppState: AppState = { + showKeys: true, + zoom: 1, + insertMode: 'and', -export type TraceItemError = { - cause: BigStepCause, // event name, or - simtime: number, - error: RuntimeError, + ...defaultSideBarState, } -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() { const [editHistory, setEditHistory] = useState(null); - const [trace, setTrace] = useState(null); - const [time, setTime] = useState({kind: "paused", simtime: 0}); const [modal, setModal] = useState(null); const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory); @@ -94,54 +47,51 @@ export function App() { setEditHistory(historyState => historyState && ({...historyState, current: cb(historyState.current)})); }, [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(null); - // parse concrete syntax always: const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]); const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]); 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(defaultAppState); + + const persist = useUrlHashState( + 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(null); const scrollDownSidebar = useCallback(() => { if (autoScroll && refRightSideBar.current) { const el = refRightSideBar.current; @@ -152,221 +102,25 @@ export function App() { } }, [refRightSideBar.current, autoScroll]); - // 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()}}; - } - }); - 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 simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar); + + const setters = makeIndividualSetters(setAppState, Object.keys(appState) as (keyof AppState)[]); + const syntaxErrors = parsed && parsed[1] || []; + const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx]; 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 highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || []; - - const speed = time.kind === "paused" ? 0 : time.scale; - const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1]; - useEffect(() => { - ast && autoConnect && autoDetectConns(ast, plant, setPlantConns); - }, [ast, plant, autoConnect]); - - const [propertyResults, setPropertyResults] = useState(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 <> {/* Modal dialog */} @@ -394,13 +148,13 @@ export function App() { style={{flex: '0 0 content'}} > {editHistory && } {/* Editor */}
{editorState && conns && syntaxErrors && - } + }
@@ -413,142 +167,7 @@ export function App() { maxWidth: 'min(400px, 50vw)', }}>
-
- {/* State tree */} - - state tree -
    - {ast && } -
-
- {/* Input events */} - - input events - {ast && onRaise("debug."+e,p)} - disabled={trace===null || trace.trace[trace.idx].kind === "error"} - showKeys={showKeys}/>} - - {/* Internal events */} - - internal events - {ast && } - - {/* Output events */} - - output events - {ast && } - - {/* Plant */} - - plant - -
- {/* Render plant */} - { onRaise("plant.ui."+e.name, e.param)} - />} -
- {/* Connections */} - - connections - - {ast && ConnEditor(ast, plant, plantConns, setPlantConns)} - - {/* Properties */} -
setShowProperties(e.newState === "open")}> - properties - {plant &&
- available signals: -   - {plant.signals.join(', ')} -
} - {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
-
- - setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/> - - {propertyError &&
{propertyError}
} -
; - })} -
- -
-
- {/* Traces */} -
setShowExecutionTrace(e.newState === "open")}>execution trace -
- {savedTraces.map((savedTrace, i) => -
- -   - {(Math.floor(savedTrace[1].at(-1)!.simtime/1000))}s - ({savedTrace[1].length}) -   - setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1, [e.target.value, savedTraces[i][1]]))}/> - -
- )} -
-
- setShowPlantTrace(e.target.checked)}/> - - setAutoScroll(e.target.checked)}/> - -   - -
-
-
- - {/* We cheat a bit, and render the execution trace depending on whether the
above is 'open' or not, rather than putting it as a child of the
. We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */} - {showExecutionTrace && -
-
- {ast && } -
-
} -
-
+
@@ -561,83 +180,5 @@ export function App() { ; } -function autoDetectConns(ast: Statechart, plant: Plant, setPlantConns: Dispatch>) { - 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, plantConns: Conns, setPlantConns: Dispatch>) { - const plantInputs = <>{plant.inputEvents.map(e => )} - const scInputs = <>{ast.inputEvents.map(e => )}; - return <> - - {/* SC output events can go to Plant */} - {[...ast.outputEvents].map(e =>
- - -
)} - - {/* Plant output events can go to Statechart */} - {[...plant.outputEvents.map(e =>
- - -
)]} - - {/* Plant UI events typically go to the Plant */} - {plant.uiEvents.map(e =>
- - -
)} - ; -} - export default App; diff --git a/src/App/SideBar.tsx b/src/App/SideBar.tsx new file mode 100644 index 0000000..0fc79df --- /dev/null +++ b/src/App/SideBar.tsx @@ -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, + ast: Statechart | null, + plant: Plant, + // setSavedTraces: Dispatch>, + trace: TraceState|null, + setTrace: Dispatch>, + plantState: UniversalPlantState, + onRaise: (inputEvent: string, param: any) => void, + onReplayTrace: (causes: BigStepCause[]) => void, + setTime: Dispatch>, + time: TimeMode, +} & Setters; + +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(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 <> +
+ {/* State tree */} + + state tree +
    + {ast && } +
+
+ {/* Input events */} + + input events + {ast && onRaise("debug."+e,p)} + disabled={trace===null || trace.trace[trace.idx].kind === "error"} + showKeys={true}/>} + + {/* Internal events */} + + internal events + {ast && } + + {/* Output events */} + + output events + {ast && } + + {/* Plant */} + + plant + +
+ {/* Render plant */} + { onRaise("plant.ui."+e.name, e.param)} + />} +
+ {/* Connections */} + + connections + + {ast && ConnEditor(ast, plant, plantConns, setPlantConns)} + + {/* Properties */} +
setShowProperties(e.newState === "open")}> + properties + {plant &&
+ available signals: +   + {plant.signals.join(', ')} +
} + {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
+
+ + setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/> + + {propertyError &&
{propertyError}
} +
; + })} +
+ +
+
+ {/* Traces */} +
setShowExecutionTrace(e.newState === "open")}>execution trace +
+ {savedTraces.map((savedTrace, i) => +
+ +   + {(Math.floor(savedTrace[1].at(-1)!.simtime/1000))}s + ({savedTrace[1].length}) +   + setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1, [e.target.value, savedTraces[i][1]]))}/> + +
+ )} +
+
+ setShowPlantTrace(e.target.checked)}/> + + setAutoScroll(e.target.checked)}/> + +   + +
+
+
+ + {/* We cheat a bit, and render the execution trace depending on whether the
above is 'open' or not, rather than putting it as a child of the
. We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */} + {showExecutionTrace && +
+
+ {ast && } +
+
} + ; +} + +function autoDetectConns(ast: Statechart, plant: Plant, setPlantConns: Dispatch>) { + 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, plantConns: Conns, setPlantConns: Dispatch>) { + const plantInputs = <>{plant.inputEvents.map(e => )} + const scInputs = <>{ast.inputEvents.map(e => )}; + return <> + + {/* SC output events can go to Plant */} + {[...ast.outputEvents].map(e =>
+ + +
)} + + {/* Plant output events can go to Statechart */} + {[...plant.outputEvents.map(e =>
+ + +
)]} + + {/* Plant UI events typically go to the Plant */} + {plant.uiEvents.map(e =>
+ + +
)} + ; +} + diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index 73260c9..ed59b0a 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -3,7 +3,7 @@ import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types"; import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time"; import { InsertMode } from "./InsertModes"; import { About } from "../Modals/About"; -import { EditHistory, TraceState } from "../App"; +import { EditHistory } from "../App"; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; import { UndoRedoButtons } from "./UndoRedoButtons"; import { ZoomButtons } from "./ZoomButtons"; @@ -21,6 +21,7 @@ import { InsertModes } from "./InsertModes"; import { usePersistentState } from "@/App/persistent_state"; import { RotateButtons } from "./RotateButtons"; import { SpeedControl } from "./SpeedControl"; +import { TraceState } from "../useSimulator"; export type TopPanelProps = { trace: TraceState | null, diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx index 4505dc6..9741520 100644 --- a/src/App/VisualEditor/VisualEditor.tsx +++ b/src/App/VisualEditor/VisualEditor.tsx @@ -1,6 +1,5 @@ import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react"; -import { TraceState } from "@/App/App"; import { InsertMode } from "../TopPanel/InsertModes"; import { Mode } from "@/statecharts/runtime_types"; import { arraysEqual, objectsEqual, setsEqual } from "@/util/util"; @@ -17,7 +16,6 @@ import { useCopyPaste } from "./useCopyPaste"; import "./VisualEditor.css"; import { useMouse } from "./useMouse"; -import { Selecting } from "./Selection"; export type ConcreteSyntax = { rountangles: Rountangle[]; @@ -59,7 +57,8 @@ type VisualEditorProps = { setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, conns: Connections, syntaxErrors: TraceableError[], - trace: TraceState | null, + // trace: TraceState | null, + // activeStates: Set, insertMode: InsertMode, highlightActive: Set, highlightTransitions: string[], @@ -68,7 +67,7 @@ type VisualEditorProps = { 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 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); @@ -167,15 +166,12 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, } }, [setState]); - // @ts-ignore - const active = trace && trace.trace[trace.idx].mode || new Set(); - const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message); const size = 4000*zoom; return e.preventDefault()} ref={refSVG} diff --git a/src/App/check_property.ts b/src/App/check_property.ts index 85e1cce..110112b 100644 --- a/src/App/check_property.ts +++ b/src/App/check_property.ts @@ -1,6 +1,6 @@ import { RT_Statechart } from "@/statecharts/runtime_types"; -import { TraceItem } from "./App"; import { Plant } from "./Plant/Plant"; +import { TraceItem } from "./useSimulator"; // const endpoint = "http://localhost:15478/check_property"; const endpoint = "https://deemz.org/apis/mtl-aas/check_property"; @@ -37,8 +37,8 @@ export async function checkProperty(plant: Plant, property: }, [] as {simtime: number, state: any}[]); let traces = { - 'true': [0, true] as [number, any], - 'false': [0, false] as [number, any], + 'true': [[0, true] as [number, any]], + 'false': [[0, false] as [number, any]], } as {[key: string]: [number, any][]}; for (const {simtime, state} of cleanPlantStates) { for (const [key, value] of Object.entries(state)) { diff --git a/src/App/makePartialSetter.ts b/src/App/makePartialSetter.ts new file mode 100644 index 0000000..a9ef4a7 --- /dev/null +++ b/src/App/makePartialSetter.ts @@ -0,0 +1,36 @@ +import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; + +export function makePartialSetter(fullSetter: Dispatch>, key: K): Dispatch> { + 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 = { + [K in keyof T as `set${Capitalize>}`]: Dispatch>; +} + +export function makeIndividualSetters( + fullSetter: Dispatch>, + keys: (keyof T)[], +): Setters { + // @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] + ); +} diff --git a/src/App/plants.ts b/src/App/plants.ts new file mode 100644 index 0000000..84157a9 --- /dev/null +++ b/src/App/plants.ts @@ -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][] = [ + ["dummy", dummyPlant], + ["microwave", microwavePlant as unknown as Plant], + ["digital watch", digitalWatchPlant as unknown as Plant], + ["traffic light", trafficLightPlant as unknown as Plant], +]; diff --git a/src/App/useSimulator.ts b/src/App/useSimulator.ts index 1f75028..9844455 100644 --- a/src/App/useSimulator.ts +++ b/src/App/useSimulator.ts @@ -1,3 +1,223 @@ -export function useSimulator() { - -} \ No newline at end of file +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, or + 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, plantConns: Conns, onStep: () => void) { + const [time, setTime] = useState({kind: "paused", simtime: 0}); + const [trace, setTrace] = useState(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}; +} diff --git a/src/App/useUrlHashState.ts b/src/App/useUrlHashState.ts index 3c9dd16..3859d54 100644 --- a/src/App/useUrlHashState.ts +++ b/src/App/useUrlHashState.ts @@ -1,29 +1,7 @@ -import { Dispatch, SetStateAction, useEffect, useState } 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>) { - - // 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({}); - const [showKeys, setShowKeys] = useState(true); - const [zoom, setZoom] = useState(1); - const [insertMode, setInsertMode] = useState("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([]); - const [savedTraces, setSavedTraces] = useState<[string, BigStepCause[]][]>([]); - const [activeProperty, setActiveProperty] = useState(0); +import { useEffect } from "react"; +// persist state in URL hash +export function useUrlHashState(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void { // recover editor state from URL - we need an effect here because decompression is asynchronous useEffect(() => { @@ -32,7 +10,7 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi if (compressedState.length === 0) { // empty URL hash console.log("no state to recover"); - setEditHistory(() => ({current: emptyState, history: [], future: []})); + // setEditHistory(() => ({current: emptyState, history: [], future: []})); return; } let compressedBuffer; @@ -41,7 +19,7 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi } catch (e) { // probably invalid base64 console.error("failed to recover state:", e); - setEditHistory(() => ({current: emptyState, history: [], future: []})); + // setEditHistory(() => ({current: emptyState, history: [], future: []})); return; } const ds = new DecompressionStream("deflate"); @@ -51,153 +29,28 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi new Response(ds.readable).arrayBuffer() .then(decompressedBuffer => { const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer)); - // we support two formats - 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); - } - - } + recoverCallback(recoveredState); }) .catch(e => { // any other error: invalid JSON, or decompression failed. console.error("failed to recover state:", e); - setEditHistory({current: emptyState, history: [], future: []}); + // setEditHistory({current: emptyState, history: [], future: []}); }); }, []); - // save editor state in URL - useEffect(() => { - const timeout = setTimeout(() => { - if (editorState === null) { - window.location.hash = "#"; - return; - } - const serializedState = JSON.stringify({ - autoConnect, - autoScroll, - plantConns, - 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, + function persist(state: T) { + const serializedState = JSON.stringify(state); + 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; + }); } -} \ No newline at end of file + + return persist; +} diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index f7db64c..deabf61 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -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. 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); // console.log('fairStep', arenasFired); 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 { - console.log('handleInputEvent', event); let raised = initialRaised; ({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) { - console.log('handleInternalEvents'); while (internalEvents.length > 0) { const [nextEvent, ...remainingEvents] = internalEvents; ({internalEvents, ...rest} = fairStep(simtime,