diff --git a/src/App/App.css b/src/App/App.css index 0a4805b..0076de7 100644 --- a/src/App/App.css +++ b/src/App/App.css @@ -33,6 +33,9 @@ details:has(+ details) { background-color: rgba(0,0,255,0.2); border: solid blue 1px; } +.runtimeState.plantStep * { + color: grey; +} .runtimeState.runtimeError { background-color: lightpink; diff --git a/src/App/App.tsx b/src/App/App.tsx index c3a0df0..41fdc31 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,54 +1,26 @@ -import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter"; -import { BigStepOutput, RT_Event, RT_Statechart } from "../statecharts/runtime_types"; -import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; -import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; - import "../index.css"; import "./App.css"; -import { TopPanel } from "./TopPanel/TopPanel"; -import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST"; -import { parseStatechart } from "../statecharts/parser"; -import { getKeyHandler } from "./VisualEditor/shortcut_handler"; -import { BottomPanel } from "./BottomPanel"; +import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; + import { emptyState } from "@/statecharts/concrete_syntax"; -import { PersistentDetails } from "./PersistentDetails"; -import { DigitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch"; -import { DummyPlant } from "./Plant/Dummy/Dummy"; -import { Plant } from "./Plant/Plant"; -import { usePersistentState } from "./persistent_state"; -import { RTHistory } from "./RTHistory"; import { detectConnections } from "@/statecharts/detect_connections"; +import { Conns, coupledExecution, EventDestination, exposeStatechartInputs, 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 { usePersistentState } from "./persistent_state"; +import { PersistentDetails } from "./PersistentDetails"; +import { DummyPlant } from "./Plant/Dummy/Dummy"; import { MicrowavePlant } from "./Plant/Microwave/Microwave"; -import { coupledExecution, dummyExecution, exposeStatechartInputs, statechartExecution, TimedReactive } from "@/statecharts/timed_reactive"; - -// const clock1: TimedReactive<{nextTick: number}> = { -// initial: () => ({nextTick: 1}), -// timeAdvance: (c) => c.nextTick, -// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}], -// extTransition: (simtime, c, e) => [[], (c)], -// } - -// const clock2: TimedReactive<{nextTick: number}> = { -// initial: () => ({nextTick: 0.5}), -// timeAdvance: (c) => c.nextTick, -// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}], -// extTransition: (simtime, c, e) => [[], (c)], -// } - -// const coupled = coupledExecution({clock1, clock2}, {inputEvents: {}, outputEvents: { -// clock1: {tick: {kind:"output", eventName: 'tick'}}, -// clock2: {tick: {kind:"output", eventName: 'tick'}}, -// }}) - -// let state = coupled.initial(); -// for (let i=0; i<10; i++) { -// const nextWakeup = coupled.timeAdvance(state); -// console.log({state, nextWakeup}); -// [[], state] = coupled.intTransition(state); -// } +import { autoConnect, exposePlantInputs, Plant } from "./Plant/Plant"; +import { RTHistory } from "./RTHistory"; +import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST"; +import { TopPanel } from "./TopPanel/TopPanel"; +import { getKeyHandler } from "./VisualEditor/shortcut_handler"; +import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; export type EditHistory = { current: VisualEditorState, @@ -58,8 +30,9 @@ export type EditHistory = { const plants: [string, Plant][] = [ ["dummy", DummyPlant], - ["digital watch", DigitalWatchPlant], ["microwave", MicrowavePlant], + + // ["digital watch", DigitalWatchPlant], ] export type TraceItemError = { @@ -69,13 +42,13 @@ export type TraceItemError = { } type CoupledState = { - sc: BigStepOutput, - plant: any, + sc: BigStep, + plant: BigStep, }; export type TraceItem = { kind: "error" } & TraceItemError -| { kind: "bigstep", simtime: number, cause: string, state: CoupledState }; +| { kind: "bigstep", simtime: number, cause: string, state: CoupledState, outputEvents: RaisedEvent[] }; export type TraceState = { // executor: TimedReactive, @@ -83,10 +56,6 @@ export type TraceState = { idx: number, }; // <-- null if there is no trace -function current(ts: TraceState) { - return ts.trace[ts.idx]!; -} - // function getPlantState(plant: Plant, trace: TraceItem[], idx: number): T | null { // if (idx === -1) { // return plant.initial; @@ -240,21 +209,27 @@ export function App() { } }, [refRightSideBar.current]); - const cE = useMemo(() => ast && coupledExecution({sc: statechartExecution(ast), plant: dummyExecution}, exposeStatechartInputs(ast, "sc")), [ast]); + const plantConns = ast && ({ + inputEvents: { + ...exposeStatechartInputs(ast, "sc", (eventName: string) => "DEBUG_"+eventName), + ...exposePlantInputs(plant, "plant", (eventName: string) => "PLANT_UI_"+eventName), + }, + outputEvents: autoConnect(ast, "sc", plant, "plant"), + }) as Conns; + const cE = useMemo(() => ast && coupledExecution({ + sc: statechartExecution(ast), + plant: plant.execution, + }, plantConns!), [ast]); const onInit = useCallback(() => { if (cE === null) return; const metadata = {simtime: 0, cause: ""}; try { - const state = cE.initial(); // may throw if initialing the statechart results in a RuntimeError + const [outputEvents, state] = cE.initial(); // may throw if initialing the statechart results in a RuntimeError setTrace({ - trace: [{kind: "bigstep", ...metadata, state}], + trace: [{kind: "bigstep", ...metadata, state, outputEvents}], idx: 0, }); - // config = initialize(ast); - // const item = {kind: "bigstep", ...timestampedEvent, ...config}; - // const plantState = getPlantState(plant, [item], 0); - // setTrace({trace: [{...item, plantState}], idx: 0}); } catch (error) { if (error instanceof RuntimeError) { @@ -262,7 +237,6 @@ export function App() { trace: [{kind: "error", ...metadata, error}], idx: 0, }); - // setTrace({trace: [{kind: "error", ...timestampedEvent, error}], idx: 0}); } else { throw error; // probably a bug in the interpreter @@ -291,8 +265,7 @@ export function App() { if (currentTraceItem.kind === "bigstep") { const simtime = getSimTime(time, Math.round(performance.now())); appendNewConfig(simtime, inputEvent, () => { - const [_, newState] = cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param}); - return newState; + return cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param}); }); } } @@ -307,8 +280,7 @@ export function App() { const raiseTimeEvent = () => { appendNewConfig(nextTimeout, "", () => { - const [_, newState] = cE.intTransition(currentTraceItem.state); - return newState; + return cE.intTransition(currentTraceItem.state); }); } @@ -330,12 +302,12 @@ export function App() { } }, [time, trace]); // <-- todo: is this really efficient? - function appendNewConfig(simtime: number, cause: string, computeNewState: () => CoupledState) { + function appendNewConfig(simtime: number, cause: string, computeNewState: () => [RaisedEvent[], CoupledState]) { let newItem: TraceItem; const metadata = {simtime, cause} try { - const state = computeNewState(); // may throw RuntimeError - newItem = {kind: "bigstep", ...metadata, state}; + const [outputEvents, state] = computeNewState(); // may throw RuntimeError + newItem = {kind: "bigstep", ...metadata, state, outputEvents}; } catch (error) { if (error instanceof RuntimeError) { @@ -397,6 +369,9 @@ export function App() { const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || []; const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true); + const [showPlantTrace, setShowPlantTrace] = usePersistentState("showPlantTrace", false); + + const speed = time.kind === "paused" ? 0 : time.scale; return <> @@ -437,11 +412,10 @@ export function App() { {/* Right: sidebar */}
@@ -459,7 +433,7 @@ export function App() { input events {ast && onRaise("DEBUG_"+e,p)} disabled={trace===null || trace.trace[trace.idx].kind === "error"} showKeys={showKeys}/>} @@ -481,16 +455,15 @@ export function App() { )} - {/* {trace !== null && trace.trace[trace.idx].plantState && -
{ - plant.render( - trace.trace[trace.idx].plantState, - event => onRaise(event.name, event.param), - time.kind === "paused" ? 0 : time.scale, - ) - }
} */} + {plantConns && } + {currentBigStep && onRaise("PLANT_UI_"+e.name, e.param)} + raiseOutput={() => {}} + />} -
setShowExecutionTrace(e.newState === "open")}>execution trace
+
setShowExecutionTrace(e.newState === "open")}>execution trace + setShowPlantTrace(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. */} @@ -502,10 +475,9 @@ export function App() { // minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view }}>
- {ast && } + {ast && }
} -
@@ -520,4 +492,25 @@ export function App() { ; } +function ShowEventDestination(dst: EventDestination) { + if (dst.kind === "model") { + return <>{dst.model}.{dst.eventName}; + } + else if (dst.kind === "output") { + return <>{dst.eventName}; + } + else { + return <>🗑; // <-- garbage can icon + } +} + +function ShowConns({inputEvents, outputEvents}: Conns) { + return
+
+ {Object.entries(inputEvents).map(([eventName, destination]) =>
{eventName} →
)} +
+ {Object.entries(outputEvents).map(([modelName, mapping]) => <>{Object.entries(mapping).map(([eventName, destination]) =>
{modelName}.{eventName} →
)})} +
; +} + export default App; diff --git a/src/App/Plant/Dummy/Dummy.tsx b/src/App/Plant/Dummy/Dummy.tsx index 8274253..917ccde 100644 --- a/src/App/Plant/Dummy/Dummy.tsx +++ b/src/App/Plant/Dummy/Dummy.tsx @@ -1,10 +1,16 @@ -import { RaisedEvent } from "@/statecharts/runtime_types"; import { Plant } from "../Plant"; +import { TimedReactive } from "@/statecharts/timed_reactive"; -export const DummyPlant: Plant<{}> = { +export const dummyExecution: TimedReactive = { + initial: () => [[], null], + timeAdvance: () => Infinity, + intTransition: () => { throw new Error("dummy never makes intTransition"); }, + extTransition: () => [[], null], +}; + +export const DummyPlant: Plant = { inputEvents: [], outputEvents: [], - initial: () => ({}), - reduce: (_inputEvent: RaisedEvent, _state: {}) => ({}), - render: (_state: {}, _raise: (event: RaisedEvent) => void) => <>, + execution: dummyExecution, + render: (props) => <>, } diff --git a/src/App/Plant/Microwave/Microwave.css b/src/App/Plant/Microwave/Microwave.css index 48c7d9e..db6fdcd 100644 --- a/src/App/Plant/Microwave/Microwave.css +++ b/src/App/Plant/Microwave/Microwave.css @@ -14,5 +14,9 @@ rect.microwaveButtonHelper:active { } rect.microwaveDoorHelper { + fill: rgba(46, 211, 197); fill-opacity: 0; +} +rect.microwaveDoorHelper:hover { + fill-opacity: 0.3; } \ No newline at end of file diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx index 6600d2b..5f45b15 100644 --- a/src/App/Plant/Microwave/Microwave.tsx +++ b/src/App/Plant/Microwave/Microwave.tsx @@ -2,59 +2,31 @@ import { preload } from "react-dom"; import imgSmallClosedOff from "./small_closed_off.png"; import imgSmallClosedOn from "./small_closed_on.png"; import imgSmallOpenedOff from "./small_opened_off.png"; -import imgSmallOpenedOn from "./small_opened_off.png"; +import imgSmallOpenedOn from "./small_opened_on.png"; import fontDigital from "../DigitalWatch/digital-font.ttf"; import sndBell from "./bell.wav"; import sndRunning from "./running.wav"; -import { Plant } from "../Plant"; -import { RaisedEvent } from "@/statecharts/runtime_types"; -import { useEffect, useState } from "react"; +import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types"; +import { useEffect } from "react"; import "./Microwave.css"; import { useAudioContext } from "../../useAudioContext"; - -export type MagnetronState = "on" | "off"; -export type DoorState = "open" | "closed"; - -export function toggleDoor(d: DoorState) { - if (d === "open") { - return "closed"; - } - else return "open"; -} - -export function toggleMagnetron(m: MagnetronState) { - if (m === "on") { - return "off"; - } - return "on"; -} - -export type MicrowaveState = { - // Note: the door state is not part of the MicrowaveState because it is not controlled by the statechart, but by the plant. - timeDisplay: number, - bell: boolean, // whether the bell should ring - magnetron: MagnetronState, -} +import { Plant } from "../Plant"; +import { statechartExecution } from "@/statecharts/timed_reactive"; +import { microwaveAbstractSyntax } from "./model"; export type MicrowaveProps = { - state: MicrowaveState, + state: RT_Statechart, speed: number, - callbacks: { - startPressed: () => void; - stopPressed: () => void; - incTimePressed: () => void; - incTimeReleased: () => void; - doorOpened: () => void; - doorClosed: () => void; - } + raiseInput: (event: RaisedEvent) => void; + raiseOutput: (event: RaisedEvent) => void; } const imgs = { - closed: { off: imgSmallClosedOff, on: imgSmallClosedOn }, - open: { off: imgSmallOpenedOff, on: imgSmallOpenedOn }, + "false": { "false": imgSmallClosedOff, "true": imgSmallClosedOn }, + "true": { "false": imgSmallOpenedOff, "true": imgSmallOpenedOn }, } const BUTTON_HEIGHT = 18; @@ -71,9 +43,7 @@ const DOOR_Y0 = 68; const DOOR_WIDTH = 353; const DOOR_HEIGHT = 217; -export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbacks}: MicrowaveProps) { - const [door, setDoor] = useState("closed"); - +export function Magnetron({state, speed, raiseInput, raiseOutput}: MicrowaveProps) { const [playSound, preloadAudio] = useAudioContext(speed); // preload(imgSmallClosedOff, {as: "image"}); @@ -84,30 +54,25 @@ export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbac preloadAudio(sndRunning); preloadAudio(sndBell); + const bellRinging = state.mode.has("45"); + const magnetronRunning = state.mode.has("28"); + const doorOpen = state.mode.has("13"); + const timeDisplay = state.environment.get("timeDisplay"); + // a bit hacky: when the bell-state changes to true, we play the bell sound... useEffect(() => { - if (bell) { + if (bellRinging) { playSound(sndBell, false); } - }, [bell]); + }, [bellRinging]); useEffect(() => { - if (magnetron === "on") { + if (magnetronRunning) { const stopSoundRunning = playSound(sndRunning, true); return () => stopSoundRunning(); } return () => {}; - }, [magnetron]) - - - const openDoor = () => { - setDoor("open"); - callbacks.doorOpened(); - } - const closeDoor = () => { - setDoor("closed"); - callbacks.doorClosed(); - } + }, [magnetronRunning]) return <> - + {/* @ts-ignore */} + callbacks.startPressed()} + onMouseDown={() => raiseInput({name: "startPressed"})} + onMouseUp={() => raiseInput({name: "startReleased"})} /> callbacks.stopPressed()} + onMouseDown={() => raiseInput({name: "stopPressed"})} + onMouseUp={() => raiseInput({name: "stopReleased"})} /> callbacks.incTimePressed()} - onMouseUp={() => callbacks.incTimeReleased()} + onMouseDown={() => raiseInput({name: "incTimePressed"})} + onMouseUp={() => raiseInput({name: "incTimeReleased"})} /> - door === "open" ? closeDoor() : openDoor()} + raiseInput({name: "doorMouseDown"})} + onMouseUp={() => raiseInput({name: "doorMouseUp"})} /> - {timeDisplay} ; } -export const MicrowavePlant: Plant = { - inputEvents: [], - outputEvents: [], - initial: { - timeDisplay: 0, - magnetron: "off", - bell: false, - }, - reduce: (inputEvent: RaisedEvent, state: MicrowaveState) => { - if (inputEvent.name === "setMagnetron") { - return { ...state, magnetron: inputEvent.param, bell: false }; - } - if (inputEvent.name === "setTimeDisplay") { - return { ...state, timeDisplay: inputEvent.param, bell: false }; - } - if (inputEvent.name === "ringBell") { - return { ...state, bell: true }; - } - return state; // unknown event - ignore it - }, - render: (state, raiseEvent, speed) => raiseEvent({name: "startPressed"}), - stopPressed: () => raiseEvent({name: "stopPressed"}), - incTimePressed: () => raiseEvent({name: "incTimePressed"}), - incTimeReleased: () => raiseEvent({name: "incTimeReleased"}), - doorOpened: () => raiseEvent({name: "door", param: "open"}), - doorClosed: () => raiseEvent({name: "door", param: "closed"}), - }}/>, +export const MicrowavePlant: Plant = { + inputEvents: [ + // events coming from statechart + {kind: "event", event: "setTimeDisplay", paramName: "t"}, + {kind: "event", event: "setMagnetron", paramName: "state"}, + {kind: "event", event: "ringBell"}, + + // events coming from UI: + {kind: "event", event: "doorMouseDown"}, + {kind: "event", event: "doorMouseUp"}, + {kind: "event", event: "startPressed"}, + {kind: "event", event: "stopPressed"}, + {kind: "event", event: "incTimePressed"}, + {kind: "event", event: "startReleased"}, + {kind: "event", event: "stopReleased"}, + {kind: "event", event: "incTimeReleased"}, + ], + outputEvents: [ + {kind: "event", event: "door", paramName: "state"}, + {kind: "event", event: "startPressed"}, + {kind: "event", event: "stopPressed"}, + {kind: "event", event: "incTimePressed"}, + {kind: "event", event: "startReleased"}, + {kind: "event", event: "stopReleased"}, + {kind: "event", event: "incTimeReleased"}, + ], + execution: statechartExecution(microwaveAbstractSyntax), + render: Magnetron, } diff --git a/src/App/Plant/Microwave/model.ts b/src/App/Plant/Microwave/model.ts new file mode 100644 index 0000000..084e850 --- /dev/null +++ b/src/App/Plant/Microwave/model.ts @@ -0,0 +1,15 @@ +import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; +import { detectConnections } from "@/statecharts/detect_connections"; +import { parseStatechart } from "@/statecharts/parser"; + +// export const microwaveConcreteSyntax: ConcreteSyntax = {"rountangles":[{"uid":"25","topLeft":{"x":134.99999999999972,"y":136.9999999999999},"size":{"x":1686.9999999999995,"y":623.9999999999997},"kind":"and"},{"uid":"24","topLeft":{"x":517.9999999999994,"y":173.99999999999983},"size":{"x":482.99999999999943,"y":554.9999999999998},"kind":"or"},{"uid":"57","topLeft":{"x":1403.999999999999,"y":175.9999999999999},"size":{"x":378.99999999999983,"y":553.9999999999997},"kind":"or"},{"uid":"32","topLeft":{"x":558.9999999999993,"y":240.99999999999983},"size":{"x":392.9999999999999,"y":462.99999999999983},"kind":"or"},{"uid":"43","topLeft":{"x":1034.999999999999,"y":180.9999999999999},"size":{"x":328.9999999999999,"y":549.9999999999998},"kind":"or"},{"uid":"23","topLeft":{"x":169.99999999999972,"y":170.99999999999983},"size":{"x":317.9999999999998,"y":556.9999999999998},"kind":"or"},{"uid":"13","topLeft":{"x":232.99999999999972,"y":550.9999999999997},"size":{"x":198.9999999999999,"y":124.99999999999994},"kind":"and"},{"uid":"28","topLeft":{"x":728.9999999999993,"y":551.9999999999997},"size":{"x":183.99999999999994,"y":108.99999999999994},"kind":"and"},{"uid":"11","topLeft":{"x":244.99999999999972,"y":298.9999999999998},"size":{"x":183.99999999999994,"y":94.99999999999994},"kind":"and"},{"uid":"58","topLeft":{"x":1447.9999999999989,"y":407.9999999999997},"size":{"x":148.99999999999994,"y":113.99999999999997},"kind":"and"},{"uid":"44","topLeft":{"x":1128.9999999999989,"y":306.9999999999998},"size":{"x":156.99999999999994,"y":102.99999999999996},"kind":"and"},{"uid":"27","topLeft":{"x":731.9999999999993,"y":316.9999999999998},"size":{"x":173.9999999999999,"y":87.99999999999996},"kind":"and"},{"uid":"45","topLeft":{"x":1133.9999999999989,"y":571.9999999999997},"size":{"x":162.99999999999997,"y":89.99999999999999},"kind":"and"}],"diamonds":[{"uid":"34","topLeft":{"x":657.9999999999993,"y":458.99999999999966},"size":{"x":59,"y":60}}],"history":[],"arrows":[{"uid":"18","start":{"x":376.99999999999955,"y":393.9999999999998},"end":{"x":375.99999999999955,"y":541.9999999999998}},{"uid":"20","start":{"x":279.9999999999997,"y":547.9999999999997},"end":{"x":279.9999999999997,"y":401.99999999999966}},{"uid":"22","start":{"x":278.9999999999997,"y":244.99999999999977},"end":{"x":310.99999999999966,"y":295.9999999999998}},{"uid":"26","start":{"x":202.99999999999972,"y":82.99999999999991},"end":{"x":245.99999999999972,"y":130.99999999999983}},{"uid":"29","start":{"x":559.9999999999993,"y":493.99999999999966},"end":{"x":652.9999999999993,"y":492.99999999999966}},{"uid":"30","start":{"x":791.9999999999993,"y":275.99999999999983},"end":{"x":808.9999999999993,"y":306.99999999999983}},{"uid":"33","start":{"x":630.9999999999993,"y":201.99999999999986},"end":{"x":671.9999999999993,"y":234.99999999999986}},{"uid":"35","start":{"x":691.9999999999993,"y":517.9999999999997},"end":{"x":730.9999999999993,"y":553.9999999999997}},{"uid":"37","start":{"x":687.9999999999993,"y":458.99999999999966},"end":{"x":733.9999999999993,"y":398.99999999999966}},{"uid":"46","start":{"x":1163.9999999999989,"y":238.99999999999983},"end":{"x":1206.9999999999989,"y":303.99999999999983}},{"uid":"47","start":{"x":1260.9999999999989,"y":410.9999999999997},"end":{"x":1261.9999999999989,"y":569.9999999999997}},{"uid":"48","start":{"x":1170.9999999999989,"y":568.9999999999997},"end":{"x":1169.9999999999989,"y":413.99999999999966}},{"uid":"59","start":{"x":1600.9999999999989,"y":470.9999999999997},"end":{"x":1546.9999999999989,"y":524.9999999999997}},{"uid":"66","start":{"x":1467.9999999999989,"y":355.9999999999998},"end":{"x":1528.9999999999989,"y":402.9999999999998}}],"texts":[{"uid":"12","text":"// Door closed","topLeft":{"x":336.99999999999955,"y":334.99999999999983}},{"uid":"14","text":"// Door opened","topLeft":{"x":331.9999999999993,"y":592.9999999999997}},{"uid":"15","text":"entry / ^door(\"closed\")","topLeft":{"x":335.99999999999966,"y":364.9999999999998}},{"uid":"16","text":"entry / ^door(\"open\")","topLeft":{"x":331.99999999999966,"y":642.9999999999997}},{"uid":"19","text":"toggleDoor","topLeft":{"x":267.9999999999996,"y":463.9999999999998}},{"uid":"21","text":"toggleDoor","topLeft":{"x":373.99999999999955,"y":485.9999999999998}},{"uid":"31","text":"setMagnetron(state)","topLeft":{"x":616.9999999999993,"y":480.99999999999955}},{"uid":"36","text":"[state==\"on\"]","topLeft":{"x":718.9999999999991,"y":537.9999999999997}},{"uid":"38","text":"[state == \"off\"]","topLeft":{"x":706.9999999999993,"y":439.9999999999997}},{"uid":"39","text":"// Magnetron off","topLeft":{"x":818.9999999999991,"y":364.9999999999998}},{"uid":"40","text":"// Magnetron on","topLeft":{"x":824.9999999999991,"y":613.9999999999995}},{"uid":"49","text":"ringBell","topLeft":{"x":1269.9999999999989,"y":496.99999999999966}},{"uid":"50","text":"after 10 ms","topLeft":{"x":1163.9999999999989,"y":490.99999999999966}},{"uid":"51","text":"// bell","topLeft":{"x":1212.9999999999989,"y":616.9999999999995}},{"uid":"52","text":"// no bell","topLeft":{"x":1205.9999999999989,"y":364.9999999999998}},{"uid":"60","text":"setTimeDisplay(value) / timeDisplay = value;","topLeft":{"x":1610.9999999999989,"y":498.9999999999998}},{"uid":"65","text":"entry / timeDisplay = 0;","topLeft":{"x":435.9999999999997,"y":102.99999999999996}}]}; + + +export const microwaveConcreteSyntax: ConcreteSyntax = {"rountangles":[{"uid":"25","topLeft":{"x":53.99999999999977,"y":125.99999999999989},"size":{"x":2025.9999999999995,"y":623.9999999999997},"kind":"and"},{"uid":"57","topLeft":{"x":1432.999999999999,"y":164.9999999999999},"size":{"x":607.9999999999998,"y":553.9999999999997},"kind":"or"},{"uid":"24","topLeft":{"x":546.9999999999994,"y":162.99999999999983},"size":{"x":482.99999999999943,"y":554.9999999999998},"kind":"or"},{"uid":"23","topLeft":{"x":88.99999999999974,"y":159.99999999999983},"size":{"x":427.9999999999998,"y":556.9999999999998},"kind":"or"},{"uid":"32","topLeft":{"x":587.9999999999993,"y":229.99999999999983},"size":{"x":392.9999999999999,"y":462.99999999999983},"kind":"or"},{"uid":"43","topLeft":{"x":1063.999999999999,"y":169.9999999999999},"size":{"x":328.9999999999999,"y":549.9999999999998},"kind":"or"},{"uid":"58","topLeft":{"x":1638.9999999999989,"y":248.99999999999972},"size":{"x":148.99999999999994,"y":389.99999999999994},"kind":"and"},{"uid":"13","topLeft":{"x":261.9999999999997,"y":539.9999999999997},"size":{"x":198.9999999999999,"y":124.99999999999994},"kind":"and"},{"uid":"28","topLeft":{"x":757.9999999999993,"y":540.9999999999997},"size":{"x":183.99999999999994,"y":108.99999999999994},"kind":"and"},{"uid":"11","topLeft":{"x":273.9999999999997,"y":287.9999999999998},"size":{"x":183.99999999999994,"y":94.99999999999994},"kind":"and"},{"uid":"44","topLeft":{"x":1157.9999999999989,"y":295.9999999999998},"size":{"x":156.99999999999994,"y":102.99999999999996},"kind":"and"},{"uid":"27","topLeft":{"x":760.9999999999993,"y":305.9999999999998},"size":{"x":173.9999999999999,"y":87.99999999999996},"kind":"and"},{"uid":"45","topLeft":{"x":1162.9999999999989,"y":560.9999999999997},"size":{"x":162.99999999999997,"y":89.99999999999999},"kind":"and"},{"uid":"80","topLeft":{"x":127.99999999999993,"y":426.9999999999997},"size":{"x":108.99999999999996,"y":73.99999999999997},"kind":"and"}],"diamonds":[{"uid":"34","topLeft":{"x":686.9999999999993,"y":447.99999999999966},"size":{"x":59,"y":60}}],"history":[],"arrows":[{"uid":"18","start":{"x":405.99999999999955,"y":382.9999999999998},"end":{"x":404.99999999999955,"y":530.9999999999998}},{"uid":"20","start":{"x":308.9999999999997,"y":536.9999999999997},"end":{"x":240.99999999999972,"y":503.99999999999966}},{"uid":"22","start":{"x":307.9999999999997,"y":233.99999999999977},"end":{"x":339.99999999999966,"y":284.9999999999998}},{"uid":"26","start":{"x":231.99999999999972,"y":71.99999999999991},"end":{"x":274.9999999999997,"y":119.99999999999983}},{"uid":"29","start":{"x":588.9999999999993,"y":482.99999999999966},"end":{"x":681.9999999999993,"y":481.99999999999966}},{"uid":"30","start":{"x":820.9999999999993,"y":264.99999999999983},"end":{"x":837.9999999999993,"y":295.99999999999983}},{"uid":"33","start":{"x":659.9999999999993,"y":190.99999999999986},"end":{"x":700.9999999999993,"y":223.99999999999986}},{"uid":"35","start":{"x":720.9999999999993,"y":506.99999999999966},"end":{"x":759.9999999999993,"y":542.9999999999997}},{"uid":"37","start":{"x":716.9999999999993,"y":447.99999999999966},"end":{"x":762.9999999999993,"y":387.99999999999966}},{"uid":"46","start":{"x":1192.9999999999989,"y":227.99999999999983},"end":{"x":1235.9999999999989,"y":292.99999999999983}},{"uid":"47","start":{"x":1289.9999999999989,"y":399.9999999999997},"end":{"x":1290.9999999999989,"y":558.9999999999997}},{"uid":"48","start":{"x":1199.9999999999989,"y":557.9999999999997},"end":{"x":1198.9999999999989,"y":402.99999999999966}},{"uid":"59","start":{"x":1789.9999999999989,"y":587.9999999999997},"end":{"x":1736.9999999999989,"y":643.9999999999997}},{"uid":"66","start":{"x":1643.9999999999989,"y":200.99999999999977},"end":{"x":1672.9999999999989,"y":242.99999999999977}},{"uid":"67","start":{"x":1633.9999999999989,"y":322.99999999999983},"end":{"x":1630.9999999999989,"y":367.99999999999983}},{"uid":"70","start":{"x":1632.9999999999989,"y":405.99999999999983},"end":{"x":1629.9999999999989,"y":450.99999999999983}},{"uid":"72","start":{"x":1632.9999999999989,"y":482.99999999999983},"end":{"x":1629.9999999999989,"y":527.9999999999998}},{"uid":"74","start":{"x":1791.9999999999989,"y":454.9999999999998},"end":{"x":1788.9999999999989,"y":499.9999999999998}},{"uid":"76","start":{"x":1789.9999999999989,"y":373.9999999999998},"end":{"x":1786.9999999999989,"y":418.9999999999998}},{"uid":"78","start":{"x":1793.9999999999989,"y":288.9999999999998},"end":{"x":1790.9999999999989,"y":333.9999999999998}},{"uid":"81","start":{"x":209.9999999999999,"y":419.9999999999997},"end":{"x":265.9999999999999,"y":368.9999999999997}}],"texts":[{"uid":"12","text":"// Door closed","topLeft":{"x":365.99999999999955,"y":323.99999999999983}},{"uid":"14","text":"// Door opened","topLeft":{"x":360.9999999999993,"y":581.9999999999997}},{"uid":"15","text":"entry / ^door(\"closed\")","topLeft":{"x":364.99999999999966,"y":353.9999999999998}},{"uid":"16","text":"entry / ^door(\"open\")","topLeft":{"x":360.99999999999966,"y":631.9999999999997}},{"uid":"19","text":"doorMouseDown","topLeft":{"x":260.9999999999996,"y":527.9999999999998}},{"uid":"21","text":"doorMouseUp","topLeft":{"x":402.99999999999955,"y":474.9999999999998}},{"uid":"31","text":"setMagnetron(state)","topLeft":{"x":645.9999999999993,"y":469.99999999999955}},{"uid":"36","text":"[state==\"on\"]","topLeft":{"x":747.9999999999991,"y":526.9999999999997}},{"uid":"38","text":"[state == \"off\"]","topLeft":{"x":735.9999999999993,"y":428.9999999999997}},{"uid":"39","text":"// Magnetron off","topLeft":{"x":847.9999999999991,"y":353.9999999999998}},{"uid":"40","text":"// Magnetron on","topLeft":{"x":853.9999999999991,"y":602.9999999999995}},{"uid":"49","text":"ringBell","topLeft":{"x":1298.9999999999989,"y":485.99999999999966}},{"uid":"50","text":"after 10 ms","topLeft":{"x":1192.9999999999989,"y":479.99999999999966}},{"uid":"51","text":"// bell","topLeft":{"x":1241.9999999999989,"y":605.9999999999995}},{"uid":"52","text":"// no bell","topLeft":{"x":1234.9999999999989,"y":353.9999999999998}},{"uid":"60","text":"setTimeDisplay(value) / timeDisplay = value;","topLeft":{"x":1799.9999999999989,"y":615.9999999999998}},{"uid":"65","text":"entry / timeDisplay = 0;","topLeft":{"x":464.9999999999997,"y":91.99999999999996}},{"uid":"68","text":"startPressed / ^startPressed","topLeft":{"x":1664.9999999999989,"y":349.99999999999983}},{"uid":"71","text":"stopPressed / ^stopPressed","topLeft":{"x":1646.9999999999989,"y":433.99999999999983}},{"uid":"73","text":"incTimePressed / ^incTimePressed","topLeft":{"x":1643.9999999999989,"y":509.9999999999998}},{"uid":"75","text":"incTimeReleased / ^incTimeReleased","topLeft":{"x":1825.9999999999989,"y":477.9999999999998}},{"uid":"77","text":"stopReleased / ^stopReleased","topLeft":{"x":1823.9999999999989,"y":397.9999999999998}},{"uid":"79","text":"startReleased / ^startReleased","topLeft":{"x":1830.9999999999989,"y":311.9999999999998}},{"uid":"82","text":"doorMouseUp","topLeft":{"x":230.99999999999986,"y":404.9999999999998}},{"uid":"83","text":"// closing door","topLeft":{"x":181.9999999999999,"y":464.9999999999997}}]}; + +export const [microwaveAbstractSyntax, microwaveErrors] = parseStatechart(microwaveConcreteSyntax, detectConnections(microwaveConcreteSyntax)); + +if (microwaveErrors.length > 0) { + console.log({microwaveErrors}); + throw new Error("there were errors parsing microwave plant model. see console.") +} diff --git a/src/App/Plant/Plant.ts b/src/App/Plant/Plant.ts index 239f774..492bfef 100644 --- a/src/App/Plant/Plant.ts +++ b/src/App/Plant/Plant.ts @@ -1,12 +1,51 @@ +import { ReactElement } from "react"; +import { Statechart } from "@/statecharts/abstract_syntax"; import { EventTrigger } from "@/statecharts/label_ast"; import { RaisedEvent } from "@/statecharts/runtime_types"; -import { ReactElement } from "react"; +import { Conns, TimedReactive } from "@/statecharts/timed_reactive"; + +export type PlantRenderProps = { + state: StateType, + speed: number, + raiseInput: (e: RaisedEvent) => void, + raiseOutput: (e: RaisedEvent) => void, +}; export type Plant = { inputEvents: EventTrigger[]; outputEvents: EventTrigger[]; - - initial: StateType; - reduce: (inputEvent: RaisedEvent, state: StateType) => StateType; - render: (state: StateType, raise: (event: RaisedEvent) => void, timescale: number) => ReactElement; + execution: TimedReactive; + render: (props: PlantRenderProps) => ReactElement; +} + +// Automatically connect Statechart and Plant inputs/outputs if their event names match. +export function autoConnect(ast: Statechart, scName: string, plant: Plant, plantName: string) { + const outputs = { + [scName]: {}, + [plantName]: {}, + } + for (const o of ast.outputEvents) { + const plantInputEvent = plant.inputEvents.find(e => e.event === o) + if (plantInputEvent) { + // @ts-ignore + outputs[scName][o] = {kind: "model", model: plantName, eventName: plantInputEvent.event}; + } + } + for (const o of plant.outputEvents) { + const scInputEvent = ast.inputEvents.find(e => e.event === o.event); + if (scInputEvent) { + // @ts-ignore + outputs[plantName][o.event] = {kind: "model", model: scName, eventName: scInputEvent.event}; + } + } + return outputs; +} + +export function exposePlantInputs(plant: Plant, plantName: string, tfm = (s: string) => s) { + const inputs = {}; + for (const i of plant.inputEvents) { + // @ts-ignore + inputs[tfm(i.event)] = {kind: "model", model: plantName, eventName: i.event}; + } + return inputs } diff --git a/src/App/RTHistory.tsx b/src/App/RTHistory.tsx index 656c983..9019460 100644 --- a/src/App/RTHistory.tsx +++ b/src/App/RTHistory.tsx @@ -1,51 +1,77 @@ import { Dispatch, memo, SetStateAction, useCallback } from "react"; import { Statechart, stateDescription } from "../statecharts/abstract_syntax"; -import { Mode, RaisedEvent } from "../statecharts/runtime_types"; +import { Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types"; import { formatTime } from "../util/util"; -import { TimeMode } from "../statecharts/time"; +import { TimeMode, timeTravel } from "../statecharts/time"; import { TraceItem, TraceState } from "./App"; import { Environment } from "@/statecharts/environment"; +import { Conns } from "@/statecharts/timed_reactive"; type RTHistoryProps = { trace: TraceState|null, setTrace: Dispatch>; ast: Statechart, setTime: Dispatch>, + showPlantTrace: boolean, } -export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) { +export function RTHistory({trace, setTrace, ast, setTime, showPlantTrace}: RTHistoryProps) { const onMouseDown = useCallback((idx: number, timestamp: number) => { setTrace(trace => trace && { ...trace, idx, }); - setTime({kind: "paused", simtime: timestamp}); + setTime(time => timeTravel(time, timestamp, performance.now())); }, [setTrace, setTime]); if (trace === null) { return <>; } - return
- {trace.trace.map((item, i) => )} -
; + return trace.trace.map((item, i) => { + const prevItem = trace.trace[i-1]; + // @ts-ignore + const isPlantStep = item.state?.sc === prevItem?.state?.sc; + if (!showPlantTrace && isPlantStep) { + return <> + } + return ; + }); } -export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, active, onMouseDown}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, active: boolean, onMouseDown: (idx: number, timestamp: number) => void}) { +function RTCause(props: {cause?: RT_Event}) { + if (props.cause === undefined) { + return <>{""}; + } + if (props.cause.kind === "timer") { + return <>{""}; + } + else if (props.cause.kind === "input") { + return <>{props.cause.name} + } + console.log(props.cause); + throw new Error("unreachable"); +} + +function RTEventParam(props: {param?: any}) { + return <>{props.param !== undefined && <>({JSON.stringify(props.param)})}; +} + +export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, isPlantStep, active, onMouseDown}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, isPlantStep: boolean, active: boolean, onMouseDown: (idx: number, timestamp: number) => void}) { if (item.kind === "bigstep") { // @ts-ignore const newStates = item.state.sc.mode.difference(prevItem?.state.sc.mode || new Set()); return
onMouseDown(idx, item.simtime), [idx, item.simtime])}>
{formatTime(item.simtime)}   -
{item.cause}
+
{item.state.sc.outputEvents.length>0 && <>^ - {item.state.sc.outputEvents.map((e:RaisedEvent) => {e.name})} + {item.state.sc.outputEvents.map((e:RaisedEvent) => {e.name})} }
; } diff --git a/src/App/ShowAST.tsx b/src/App/ShowAST.tsx index 8a63081..bdfdecd 100644 --- a/src/App/ShowAST.tsx +++ b/src/App/ShowAST.tsx @@ -73,6 +73,7 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst import BoltIcon from '@mui/icons-material/Bolt'; import { KeyInfoHidden, KeyInfoVisible } from "./TopPanel/KeyInfo"; import { memo, useEffect } from "react"; +import { usePersistentState } from "./persistent_state"; export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) { const raiseHandlers = inputEvents.map(({event}) => { @@ -93,6 +94,9 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp }; }); const onKeyDown = (e: KeyboardEvent) => { + // don't capture keyboard events when focused on an input element: + if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; + const n = (parseInt(e.key)+9) % 10; if (raiseHandlers[n] !== undefined) { raiseHandlers[n](); @@ -106,10 +110,16 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp }, [raiseHandlers]); // const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them + + const [inputParams, setInputParams] = usePersistentState<{[eventName:string]: string}>("inputParams", {}); + return inputEvents.map(({event, paramName}, i) => { + const key = event+'/'+paramName; + const value = inputParams[key] || ""; + const width = Math.max(value.length, (paramName||"").length)*6; const shortcut = (i+1)%10; const KI = (i <= 10) ? KeyInfo : KeyInfoHidden; - return
+ return
{shortcut}} horizontal={true}>
; diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index e29c609..2beede9 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -122,6 +122,9 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { + // don't capture keyboard events when focused on an input element: + if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; + if (!e.ctrlKey) { if (e.key === " ") { e.preventDefault(); diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx index 160da43..150d21b 100644 --- a/src/App/VisualEditor/VisualEditor.tsx +++ b/src/App/VisualEditor/VisualEditor.tsx @@ -18,12 +18,15 @@ import { TraceState } from "@/App/App"; import { Mode } from "@/statecharts/runtime_types"; import { arraysEqual, objectsEqual, setsEqual } from "@/util/util"; -export type VisualEditorState = { +export type ConcreteSyntax = { rountangles: Rountangle[]; texts: Text[]; arrows: Arrow[]; diamonds: Diamond[]; history: History[]; +}; + +export type VisualEditorState = ConcreteSyntax & { nextID: number; selection: Selection; }; @@ -373,6 +376,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, }, [setState]); const onKeyDown = useCallback((e: KeyboardEvent) => { + // don't capture keyboard events when focused on an input element: + if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; + if (e.key === "Delete") { // delete selection makeCheckPoint(); diff --git a/src/App/VisualEditor/shortcut_handler.ts b/src/App/VisualEditor/shortcut_handler.ts index d7792f7..366d7d6 100644 --- a/src/App/VisualEditor/shortcut_handler.ts +++ b/src/App/VisualEditor/shortcut_handler.ts @@ -3,6 +3,9 @@ import { InsertMode } from "./VisualEditor"; export function getKeyHandler(setMode: Dispatch>) { return function onKeyDown(e: KeyboardEvent) { + // don't capture keyboard events when focused on an input element: + if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; + if (!e.ctrlKey) { if (e.key === "a") { setMode("and"); diff --git a/src/statecharts/detect_connections.ts b/src/statecharts/detect_connections.ts index 4d7b00f..eefc02e 100644 --- a/src/statecharts/detect_connections.ts +++ b/src/statecharts/detect_connections.ts @@ -1,4 +1,4 @@ -import { VisualEditorState } from "@/App/VisualEditor/VisualEditor"; +import { ConcreteSyntax, VisualEditorState } from "@/App/VisualEditor/VisualEditor"; import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RectSide } from "./concrete_syntax"; export type Connections = { @@ -12,7 +12,7 @@ export type Connections = { history2ArrowMap: Map, } -export function detectConnections(state: VisualEditorState): Connections { +export function detectConnections(state: ConcreteSyntax): Connections { const startTime = performance.now(); // detect what is 'connected' const arrow2SideMap = new Map(); diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index 28c53cd..e1326d1 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -2,20 +2,20 @@ import { AbstractState, computeArena, computePath, ConcreteState, getDescendants import { evalExpr } from "./actionlang_interpreter"; import { Environment, FlatEnvironment, ScopedEnvironment } from "./environment"; import { Action, EventTrigger, TransitionLabel } from "./label_ast"; -import { BigStepOutput, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types"; +import { BigStep, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types"; const initialEnv = new Map([ ["_timers", []], ["_log", (str: string) => console.log(str)], ]); -const initialScopedEnvironment = new ScopedEnvironment({env: initialEnv, children: {}}); -// const intiialFlatEnvironment = new FlatEnvironment(initialEnv); +// const initialScopedEnvironment = new ScopedEnvironment({env: initialEnv, children: {}}); +const intiialFlatEnvironment = new FlatEnvironment(initialEnv); -export function initialize(ast: Statechart): BigStepOutput { +export function initialize(ast: Statechart): BigStep { let history = new Map(); let enteredStates, environment, rest; ({enteredStates, environment, history, ...rest} = enterDefault(0, ast.root, { - environment: initialScopedEnvironment, + environment: intiialFlatEnvironment, history, ...initialRaised, })); @@ -256,8 +256,11 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_ const addEventParam = (event && event.kind === "input" && event.param !== undefined) ? (environment: Environment, label: TransitionLabel) => { const varName = (label.trigger as EventTrigger).paramName as string; - const result = environment.newVar(varName, event.param); - return result; + if (varName) { + const result = environment.newVar(varName, event.param); + return result; + } + return environment; } : (environment: Environment) => environment; // console.log('attemptSrcState', stateDescription(sourceState), arenasFired); @@ -342,15 +345,15 @@ export function fairStep(simtime: number, event: RT_Event, statechart: Statechar return {arenasFired, environment, ...config}; } -export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStepOutput { +export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStep { let raised = initialRaised; ({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised})); - return handleInternalEvents(simtime, statechart, {mode, environment, history, ...raised}); + return {inputEvent: event, ...handleInternalEvents(simtime, statechart, {mode, environment, history, ...raised})}; } -export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents): BigStepOutput { +export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents) { while (internalEvents.length > 0) { const [nextEvent, ...remainingEvents] = internalEvents; ({internalEvents, ...rest} = fairStep(simtime, @@ -389,11 +392,12 @@ export function fire(simtime: number, t: Transition, ts: Map"), label); for (const action of label.actions) { - environment = addEventParam(environment.enterScope(""), label); + console.log('environment after adding event param:', environment); ({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid])); - environment = environment.dropScope(); } + environment = environment.dropScope(); const tgtPath = computePath({ancestor: arena, descendant: t.tgt}); const state = tgtPath[0] as ConcreteState; // first state to enter diff --git a/src/statecharts/parser.ts b/src/statecharts/parser.ts index d20ea61..3bcaa20 100644 --- a/src/statecharts/parser.ts +++ b/src/statecharts/parser.ts @@ -5,7 +5,7 @@ import { Action, EventTrigger, Expression, ParsedText } from "./label_ast"; import { parse as parseLabel, SyntaxError } from "./label_parser"; import { Connections } from "./detect_connections"; import { HISTORY_RADIUS } from "../App/parameters"; -import { VisualEditorState } from "@/App/VisualEditor/VisualEditor"; +import { ConcreteSyntax, VisualEditorState } from "@/App/VisualEditor/VisualEditor"; import { memoize } from "@/util/util"; export type TraceableError = { @@ -34,7 +34,7 @@ function addEvent(events: EventTrigger[], e: EventTrigger, textUid: string) { } } -export function parseStatechart(state: VisualEditorState, conns: Connections): [Statechart, TraceableError[]] { +export function parseStatechart(state: ConcreteSyntax, conns: Connections): [Statechart, TraceableError[]] { const errors: TraceableError[] = []; // implicitly, the root is always an Or-state diff --git a/src/statecharts/runtime_types.ts b/src/statecharts/runtime_types.ts index 19f06b8..033b39e 100644 --- a/src/statecharts/runtime_types.ts +++ b/src/statecharts/runtime_types.ts @@ -27,16 +27,14 @@ export type RT_Statechart = { history: RT_History; // history-uid -> set of states } -export type BigStepOutput = RT_Statechart & { +export type BigStep = RT_Statechart & { + inputEvent?: RT_Event, outputEvents: RaisedEvent[], + + // we also record the transitions that fired, to highlight them in the UI: firedTransitions: string[], }; -// export type BigStep = { -// inputEvent: string | null, // null if initialization -// simtime: number, -// } & BigStepOutput; - // internal or output event export type RaisedEvent = { name: string, diff --git a/src/statecharts/time.ts b/src/statecharts/time.ts index c7c6bff..6c09cba 100644 --- a/src/statecharts/time.ts +++ b/src/statecharts/time.ts @@ -58,3 +58,12 @@ export function setPaused(currentMode: TimeMode, wallclktime: number): TimePause simtime: getSimTime(currentMode, wallclktime), }; } + +export function timeTravel(currentMode: TimeMode, simtime: number, wallclktime: number): TimeMode { + if (currentMode.kind === "paused") { + return {kind: "paused", simtime}; + } + else { + return {kind: "realtime", scale: currentMode.scale, since: {simtime, wallclktime}}; + } +} \ No newline at end of file diff --git a/src/statecharts/timed_reactive.ts b/src/statecharts/timed_reactive.ts index 0039569..2afad51 100644 --- a/src/statecharts/timed_reactive.ts +++ b/src/statecharts/timed_reactive.ts @@ -1,21 +1,24 @@ import { Statechart } from "./abstract_syntax"; -import { handleInputEvent, initialize } from "./interpreter"; -import { BigStepOutput, InputEvent, RaisedEvent, RT_Statechart, Timers } from "./runtime_types"; +import { handleInputEvent, initialize, RuntimeError } from "./interpreter"; +import { BigStep, InputEvent, RaisedEvent, RT_Statechart, Timers } from "./runtime_types"; // an abstract interface for timed reactive discrete event systems somewhat similar but not equal to DEVS // differences from DEVS: // - extTransition can have output events // - time is kept as absolute simulated time (since beginning of simulation), not relative to the last transition export type TimedReactive = { - initial: () => RT_Config, + initial: () => [RaisedEvent[], RT_Config], timeAdvance: (c: RT_Config) => number, intTransition: (c: RT_Config) => [RaisedEvent[], RT_Config], extTransition: (simtime: number, c: RT_Config, e: InputEvent) => [RaisedEvent[], RT_Config], } -export function statechartExecution(ast: Statechart): TimedReactive { +export function statechartExecution(ast: Statechart): TimedReactive { return { - initial: () => initialize(ast), + initial: () => { + const bigstep = initialize(ast); + return [bigstep.outputEvents, bigstep]; + }, timeAdvance: (c: RT_Statechart) => (c.environment.get("_timers") as Timers)[0]?.[0] || Infinity, intTransition: (c: RT_Statechart) => { const timers = c.environment.get("_timers") as Timers; @@ -33,13 +36,6 @@ export function statechartExecution(ast: Statechart): TimedReactive = { - initial: () => null, - timeAdvance: () => Infinity, - intTransition: () => { throw new Error("dummy never makes intTransition"); }, - extTransition: () => [[], null], -}; - export type EventDestination = ModelDestination | OutputDestination; export type ModelDestination = { @@ -53,13 +49,35 @@ export type OutputDestination = { eventName: string, }; -export function exposeStatechartInputs(ast: Statechart, model: string): Conns { +// export type NowhereDestination = { +// kind: "nowhere", +// }; + +export function exposeStatechartInputsOutputs(ast: Statechart, model: string): Conns { return { - inputEvents: Object.fromEntries(ast.inputEvents.map(e => [e.event, {kind: "model", model, eventName: e.event}])), - outputEvents: {}, + // all the coupled execution's input events become input events for the statechart + inputEvents: exposeStatechartInputs(ast, model), + outputEvents: exposeStatechartOutputs(ast, model), } } +export function exposeStatechartInputs(ast: Statechart, model: string, tfm = (s: string) => s): {[eventName: string]: ModelDestination} { + return Object.fromEntries(ast.inputEvents.map(e => [tfm(e.event), {kind: "model", model, eventName: e.event}])); +} + +export function exposeStatechartOutputs(ast: Statechart, model: string): {[modelName: string]: {[eventName: string]: EventDestination}} { + return { + // all the statechart's output events become output events of our coupled execution + [model]: Object.fromEntries([...ast.outputEvents].map(e => [e, {kind: "output", model, eventName: e}])), + }; +} + +// export function hideStatechartOutputs(ast: Statechart, model: string) { +// return { +// [model]: Object.fromEntries([...ast.outputEvents].map(e => [e, {kind: "nowhere" as const}])), +// } +// } + export type Conns = { // inputs coming from outside are routed to the right models inputEvents: {[eventName: string]: ModelDestination}, @@ -85,10 +103,12 @@ export function coupledExecution(models: {[name const destination = conns.outputEvents[model]?.[event.name]; if (destination === undefined) { // ignore + console.log(`${model}.${event.name} goes nowhere`); return processOutputs(simtime, rest, model, c); } if (destination.kind === "model") { // output event is input for another model + console.log(`${model}.${event.name} goes to ${destination.model}.${destination.eventName}`); const inputEvent = { kind: "input" as const, name: destination.eventName, @@ -100,11 +120,13 @@ export function coupledExecution(models: {[name const [restOutputEvents, newConfig2] = processOutputs(simtime, rest, model, newConfig); return [[...outputEvents, ...restOutputEvents], newConfig2]; } - else { + else if (destination.kind === "output") { // kind === "output" + console.log(`${model}.${event.name} becomes ^${destination.eventName}`); const [outputEvents, newConfig] = processOutputs(simtime, rest, model, c); return [[event, ...outputEvents], newConfig]; } + throw new Error("unreachable"); } else { return [[], c]; @@ -112,13 +134,33 @@ export function coupledExecution(models: {[name } return { - initial: () => Object.fromEntries(Object.entries(models).map(([name, model]) => { - return [name, model.initial()]; - })) as T, + initial: () => { + // 1. initialize every model + const allOutputs = []; + let state = {} as T; + for (const [modelName, model] of Object.entries(models)) { + const [outputEvents, modelState] = model.initial(); + for (const o of outputEvents) { + allOutputs.push([modelName, o]); + } + // @ts-ignore + state[modelName] = modelState; + } + console.log({state}); + // 2. handle all output events (models' outputs may be inputs for each other) + let finalOutputs = []; + for (const [modelName, outputEvents] of allOutputs) { + let newOutputs; + [newOutputs, state] = processOutputs(0, outputEvents, modelName, state); + finalOutputs.push(...newOutputs); + } + return [finalOutputs, state]; + }, timeAdvance: (c) => { return Object.entries(models).reduce((acc, [name, {timeAdvance}]) => Math.min(timeAdvance(c[name]), acc), Infinity); }, intTransition: (c) => { + // find earliest internal transition among all models: const [when, name] = Object.entries(models).reduce(([earliestSoFar, earliestModel], [name, {timeAdvance}]) => { const when = timeAdvance(c[name]); if (when < earliestSoFar) { @@ -133,8 +175,9 @@ export function coupledExecution(models: {[name throw new Error("cannot make intTransition - timeAdvance is infinity"); }, extTransition: (simtime, c, e) => { + console.log(e); const {model, eventName} = conns.inputEvents[e.name]; - // console.log('input event', e, 'goes to', model); + console.log('input event', e.name, 'goes to', `${model}.${eventName}`); const inputEvent: InputEvent = { kind: "input", name: eventName, @@ -144,3 +187,32 @@ export function coupledExecution(models: {[name }, } } + + +// Example of a coupled execution: + +// const clock1: TimedReactive<{nextTick: number}> = { +// initial: () => ({nextTick: 1}), +// timeAdvance: (c) => c.nextTick, +// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}], +// extTransition: (simtime, c, e) => [[], (c)], +// } + +// const clock2: TimedReactive<{nextTick: number}> = { +// initial: () => ({nextTick: 0.5}), +// timeAdvance: (c) => c.nextTick, +// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}], +// extTransition: (simtime, c, e) => [[], (c)], +// } + +// const coupled = coupledExecution({clock1, clock2}, {inputEvents: {}, outputEvents: { +// clock1: {tick: {kind:"output", eventName: 'tick'}}, +// clock2: {tick: {kind:"output", eventName: 'tick'}}, +// }}) + +// let state = coupled.initial(); +// for (let i=0; i<10; i++) { +// const nextWakeup = coupled.timeAdvance(state); +// console.log({state, nextWakeup}); +// [[], state] = coupled.intTransition(state); +// } diff --git a/todo.txt b/todo.txt index a82e787..f51944e 100644 --- a/todo.txt +++ b/todo.txt @@ -52,7 +52,6 @@ TODO - hovering over error in bottom panel should highlight that rror in the SC - highlight selected shapes while making a selection - - highlight fired transitions - highlight about-to-fire transitions - when there is a runtime error, e.g.,