From b50f52496a2376e1576e44f7c6f0b6fa384971e0 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 27 Oct 2025 19:59:06 +0100 Subject: [PATCH] coupled simulation + fix nested scopes --- src/App/App.tsx | 208 ++++++++++++---------- src/App/RTHistory.tsx | 19 +- src/App/TopPanel/TopPanel.tsx | 2 +- src/statecharts/actionlang_interpreter.ts | 2 +- src/statecharts/environment.ts | 42 ++++- src/statecharts/interpreter.ts | 36 ++-- src/statecharts/runtime_types.ts | 8 +- src/statecharts/timed_reactive.ts | 146 +++++++++++++++ 8 files changed, 339 insertions(+), 124 deletions(-) diff --git a/src/App/App.tsx b/src/App/App.tsx index 672c532..c3a0df0 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,7 +1,7 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter"; -import { BigStep, RT_Event } from "../statecharts/runtime_types"; +import { BigStepOutput, RT_Event, RT_Statechart } from "../statecharts/runtime_types"; import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; @@ -22,6 +22,33 @@ import { usePersistentState } from "./persistent_state"; import { RTHistory } from "./RTHistory"; import { detectConnections } from "@/statecharts/detect_connections"; 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); +// } export type EditHistory = { current: VisualEditorState, @@ -35,15 +62,23 @@ const plants: [string, Plant][] = [ ["microwave", MicrowavePlant], ] -export type BigStepError = { - inputEvent: string, +export type TraceItemError = { + cause: string, // event name, or simtime: number, error: RuntimeError, } -export type TraceItem = { kind: "error" } & BigStepError | { kind: "bigstep", plantState: any } & BigStep; +type CoupledState = { + sc: BigStepOutput, + plant: any, +}; + +export type TraceItem = + { kind: "error" } & TraceItemError +| { kind: "bigstep", simtime: number, cause: string, state: CoupledState }; export type TraceState = { + // executor: TimedReactive, trace: [TraceItem, ...TraceItem[]], // non-empty idx: number, }; // <-- null if there is no trace @@ -52,22 +87,22 @@ function current(ts: TraceState) { return ts.trace[ts.idx]!; } -function getPlantState(plant: Plant, trace: TraceItem[], idx: number): T | null { - if (idx === -1) { - return plant.initial; - } - let plantState = getPlantState(plant, trace, idx-1); - if (plantState !== null) { - const currentConfig = trace[idx]; - if (currentConfig.kind === "bigstep") { - for (const o of currentConfig.outputEvents) { - plantState = plant.reduce(o, plantState); - } - } - return plantState; - } - return null; -} +// function getPlantState(plant: Plant, trace: TraceItem[], idx: number): T | null { +// if (idx === -1) { +// return plant.initial; +// } +// let plantState = getPlantState(plant, trace, idx-1); +// if (plantState !== null) { +// const currentConfig = trace[idx]; +// if (currentConfig.kind === "bigstep") { +// for (const o of currentConfig.outputEvents) { +// plantState = plant.reduce(o, plantState); +// } +// } +// return plantState; +// } +// return null; +// } export function App() { const [insertMode, setInsertMode] = useState("and"); @@ -150,12 +185,13 @@ export function App() { 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 allErrors = syntaxErrors && [ + const syntaxErrors = parsed && parsed[1] || []; + const currentTraceItem = trace && trace.trace[trace.idx]; + const allErrors = [ ...syntaxErrors, - ...(trace && trace.trace[trace.idx].kind === "error") ? [{ - message: trace.trace[trace.idx].error.message, - shapeUid: trace.trace[trace.idx].error.highlight[0], + ...(currentTraceItem && currentTraceItem.kind === "error") ? [{ + message: currentTraceItem.error.message, + shapeUid: currentTraceItem.error.highlight[0], }] : [], ] @@ -204,19 +240,29 @@ export function App() { } }, [refRightSideBar.current]); + const cE = useMemo(() => ast && coupledExecution({sc: statechartExecution(ast), plant: dummyExecution}, exposeStatechartInputs(ast, "sc")), [ast]); + const onInit = useCallback(() => { - if (ast === null) return; - const timestampedEvent = {simtime: 0, inputEvent: ""}; - let config; + if (cE === null) return; + const metadata = {simtime: 0, cause: ""}; try { - config = initialize(ast); - const item = {kind: "bigstep", ...timestampedEvent, ...config}; - const plantState = getPlantState(plant, [item], 0); - setTrace({trace: [{...item, plantState}], idx: 0}); + const state = cE.initial(); // may throw if initialing the statechart results in a RuntimeError + setTrace({ + trace: [{kind: "bigstep", ...metadata, state}], + 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) { - setTrace({trace: [{kind: "error", ...timestampedEvent, error}], idx: 0}); + setTrace({ + trace: [{kind: "error", ...metadata, error}], + idx: 0, + }); + // setTrace({trace: [{kind: "error", ...timestampedEvent, error}], idx: 0}); } else { throw error; // probably a bug in the interpreter @@ -231,7 +277,7 @@ export function App() { } }); scrollDownSidebar(); - }, [ast, scrollDownSidebar, setTime, setTrace]); + }, [cE, scrollDownSidebar]); const onClear = useCallback(() => { setTrace(null); @@ -240,36 +286,41 @@ export function App() { // raise input event, producing a new runtime configuration (or a runtime error) const onRaise = (inputEvent: string, param: any) => { - if (ast === null) return; - if (trace !== null && ast.inputEvents.some(e => e.event === inputEvent)) { - const config = current(trace); - if (config.kind === "bigstep") { + 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())); - produceNextConfig(simtime, {kind: "input", name: inputEvent, param}, config); + appendNewConfig(simtime, inputEvent, () => { + const [_, newState] = cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param}); + return newState; + }); } } }; + // timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout) useEffect(() => { let timeout: NodeJS.Timeout | undefined; - if (trace !== null) { - const config = current(trace); - if (config.kind === "bigstep") { - const timers = config.environment.get("_timers") || []; - if (timers.length > 0) { - const [nextInterrupt, timeElapsedEvent] = timers[0]; - const raiseTimeEvent = () => { - produceNextConfig(nextInterrupt, timeElapsedEvent, config); - } - // depending on whether paused or realtime, raise immediately or in the future: - if (time.kind === "realtime") { - const wallclkDelay = getWallClkDelay(time, nextInterrupt, Math.round(performance.now())); + if (currentTraceItem !== null && cE !== null) { + if (currentTraceItem.kind === "bigstep") { + const nextTimeout = cE?.timeAdvance(currentTraceItem.state); + + const raiseTimeEvent = () => { + appendNewConfig(nextTimeout, "", () => { + const [_, newState] = cE.intTransition(currentTraceItem.state); + return newState; + }); + } + + 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 (nextInterrupt <= time.simtime) { - raiseTimeEvent(); - } + } + else if (time.kind === "paused") { + if (nextTimeout <= time.simtime) { + raiseTimeEvent(); } } } @@ -278,31 +329,24 @@ export function App() { if (timeout) clearTimeout(timeout); } }, [time, trace]); // <-- todo: is this really efficient? - function produceNextConfig(simtime: number, event: RT_Event, config: TraceItem) { - if (ast === null) return; - const timedEvent = { - simtime, - inputEvent: event.kind === "timer" ? "" : event.name, - }; + function appendNewConfig(simtime: number, cause: string, computeNewState: () => CoupledState) { let newItem: TraceItem; + const metadata = {simtime, cause} try { - const nextConfig = handleInputEvent(simtime, event, ast, config as BigStep); // may throw - let plantState = config.plantState; - for (const o of nextConfig.outputEvents) { - plantState = plant.reduce(o, plantState); - } - newItem = {kind: "bigstep", plantState, ...timedEvent, ...nextConfig}; + const state = computeNewState(); // may throw RuntimeError + newItem = {kind: "bigstep", ...metadata, state}; } catch (error) { if (error instanceof RuntimeError) { - newItem = {kind: "error", ...timedEvent, error}; + newItem = {kind: "error", ...metadata, error}; + // also pause the simulation, for dramatic effect: + setTime({kind: "paused", simtime}); } else { throw error; } } - // @ts-ignore setTrace(trace => ({ trace: [ @@ -348,25 +392,9 @@ export function App() { }; }, []); - let highlightActive: Set; - let highlightTransitions: string[]; - if (trace === null) { - highlightActive = new Set(); - highlightTransitions = []; - } - else { - const item = current(trace); - if (item.kind === "bigstep") { - highlightActive = item.mode; - highlightTransitions = item.firedTransitions; - } - else { - highlightActive = new Set(); - highlightTransitions = []; - } - } - - // const plantState = trace && getPlantState(plant, trace.trace, trace.idx); + const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem; + const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set(); + const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || []; const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true); @@ -453,14 +481,14 @@ export function App() { )} - {trace !== null && trace.trace[trace.idx].plantState && + {/* {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, ) - }
} + }} */}
setShowExecutionTrace(e.newState === "open")}>execution trace
diff --git a/src/App/RTHistory.tsx b/src/App/RTHistory.tsx index 7b80560..656c983 100644 --- a/src/App/RTHistory.tsx +++ b/src/App/RTHistory.tsx @@ -1,6 +1,6 @@ -import { Dispatch, memo, Ref, SetStateAction, useCallback } from "react"; +import { Dispatch, memo, SetStateAction, useCallback } from "react"; import { Statechart, stateDescription } from "../statecharts/abstract_syntax"; -import { BigStep, Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types"; +import { Mode, RaisedEvent } from "../statecharts/runtime_types"; import { formatTime } from "../util/util"; import { TimeMode } from "../statecharts/time"; import { TraceItem, TraceState } from "./App"; @@ -33,19 +33,19 @@ export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) { 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}) { if (item.kind === "bigstep") { // @ts-ignore - const newStates = item.mode.difference(prevItem?.mode || new Set()); + 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.inputEvent || ""}
+
{item.cause}
- - {item.outputEvents.length>0 && <>^ - {item.outputEvents.map((e:RaisedEvent) => {e.name})} + + {item.state.sc.outputEvents.length>0 && <>^ + {item.state.sc.outputEvents.map((e:RaisedEvent) => {e.name})} }
; } @@ -57,7 +57,7 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt
{formatTime(item.simtime)}   -
{item.inputEvent}
+
{item.cause}
{item.error.message} @@ -71,8 +71,7 @@ function ShowEnvironment(props: {environment: Environment}) { return
{ [...props.environment.entries()] .filter(([variable]) => !variable.startsWith('_')) - // we strip the first 5 characters from 'variable' (remove "root.") - .map(([variable,value]) => `${variable.slice(5)}: ${value}`).join(', ') + .map(([variable,value]) => `${variable.split('.').at(-1)}: ${JSON.stringify(value)}`).join(', ') }
; } diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index bd693e3..e29c609 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -96,7 +96,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on }, [setTime, setTimescale]); // timestamp of next timed transition, in simulated time - const timers: Timers = config?.kind === "bigstep" && config.environment.get("_timers") || []; + const timers: Timers = config?.kind === "bigstep" && config.state.sc.environment.get("_timers") || []; const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0]; const onSkip = useCallback(() => { diff --git a/src/statecharts/actionlang_interpreter.ts b/src/statecharts/actionlang_interpreter.ts index 1142a5b..c9a4dfa 100644 --- a/src/statecharts/actionlang_interpreter.ts +++ b/src/statecharts/actionlang_interpreter.ts @@ -1,8 +1,8 @@ // Just a simple recursive interpreter for the action language +import { Environment } from "./environment"; import { RuntimeError } from "./interpreter"; import { Expression } from "./label_ast"; -import { Environment } from "./runtime_types"; const UNARY_OPERATOR_MAP: Map any> = new Map([ ["!", x => !x], diff --git a/src/statecharts/environment.ts b/src/statecharts/environment.ts index cead7e5..36456b5 100644 --- a/src/statecharts/environment.ts +++ b/src/statecharts/environment.ts @@ -3,6 +3,7 @@ import { getST, iterST, ScopeTree, updateST, writeST } from "./scope_tree"; export type Environment = { enterScope(scopeId: string): Environment; exitScope(): Environment; + dropScope(): Environment; // force creation of a new variable in the current scope, even if a variable with the same name already exists in a surrounding scope newVar(key: string, value: any): Environment; @@ -32,6 +33,9 @@ export class FlatEnvironment { exitScope(): FlatEnvironment { return this; } + dropScope(): FlatEnvironment { + return this; + } newVar(key: string, value: any) { return this.set(key, value); @@ -43,7 +47,7 @@ export class FlatEnvironment { return this.env.get(key); } - entries(): Iterator<[string, any]> { + entries(): IterableIterator<[string, any]> { return this.env.entries(); } } @@ -60,18 +64,51 @@ export class ScopedEnvironment { } enterScope(scopeId: string): ScopedEnvironment { + // console.log('enter scope', scopeId, new ScopedEnvironment( + // this.scopeTree, + // [...this.current, scopeId], + // )); return new ScopedEnvironment( this.scopeTree, [...this.current, scopeId], ); } exitScope() { + // console.log('exit scope', this.current.at(-1), new ScopedEnvironment( + // this.scopeTree, + // this.current.slice(0, -1), + // )); return new ScopedEnvironment( this.scopeTree, this.current.slice(0, -1), ); } + // like exitScope, but also gets rid of everything that was in the scope + dropScope() { + function dropPath({children, env}: ScopeTree, [first, ...restOfPath]: string[]): ScopeTree { + const { [first]: toDrop, ...rest} = children; + if (restOfPath.length === 0) { + return { + children: rest, + env, + }; + } + return { + children: { + [first]: dropPath(toDrop, restOfPath), + ...rest, + }, + env, + } + } + const after = dropPath(this.scopeTree, this.current); + return new ScopedEnvironment( + after, + this.current.slice(0, -1), + ) + } + newVar(key: string, value: any): ScopedEnvironment { return new ScopedEnvironment( writeST(key, value, this.current, this.scopeTree), @@ -96,7 +133,8 @@ export class ScopedEnvironment { return getST(this.current, key, this.scopeTree); } - *entries(): Iterator<[string, any]> { + *entries(): IterableIterator<[string, any]> { yield* iterST(this.scopeTree); } } + diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index 16839f3..28c53cd 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -2,7 +2,7 @@ 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, InputEvent } from "./runtime_types"; +import { BigStepOutput, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types"; const initialEnv = new Map([ ["_timers", []], @@ -253,6 +253,13 @@ function allowedToFire(arena: OrState, alreadyFiredArenas: OrState[]) { } function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_Event|undefined, statechart: Statechart, {environment, mode, arenasFired, ...rest}: RT_Statechart & RaisedEvents): (RT_Statechart & RaisedEvents) | undefined { + 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; + } + : (environment: Environment) => environment; // console.log('attemptSrcState', stateDescription(sourceState), arenasFired); const outgoing = statechart.transitions.get(sourceState.uid) || []; const labels = outgoing.flatMap(t => @@ -284,7 +291,7 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_ } }; const guardEnvironment = environment.set("inState", inState); - const enabled = triggered.filter(([t,l]) => evalExpr(l.guard, guardEnvironment, [t.uid])); + const enabled = triggered.filter(([t,l]) => evalExpr(l.guard, addEventParam(guardEnvironment, l), [t.uid])); if (enabled.length > 0) { if (enabled.length > 1) { throw new NonDeterminismError(`Non-determinism: state '${stateDescription(sourceState)}' has multiple (${enabled.length}) enabled outgoing transitions: ${enabled.map(([t]) => transitionDescription(t)).join(', ')}`, [...enabled.map(([t]) => t.uid), sourceState.uid]); @@ -292,15 +299,8 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_ const [toFire, label] = enabled[0]; const arena = computeArena(toFire.src, toFire.tgt); if (allowedToFire(arena, arenasFired)) { - environment = environment.enterScope(""); - // if there's an event parameter, add it to environment - if (event && event.kind === "input" && event.param !== undefined) { - const varName = (label.trigger as EventTrigger).paramName as string; - environment = environment.set(varName, event.param); - } - ({mode, environment, ...rest} = fire(simtime, toFire, statechart.transitions, label, arena, {mode, environment, ...rest})); + ({mode, environment, ...rest} = fire(simtime, toFire, statechart.transitions, label, arena, {mode, environment, ...rest}, addEventParam)); rest = {...rest, firedTransitions: [...rest.firedTransitions, toFire.uid]} - environment.exitScope(); arenasFired = [...arenasFired, arena]; // if there is any pseudo-state in the modal configuration, immediately fire any enabled outgoing transitions of that state: @@ -322,22 +322,24 @@ 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, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { +export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, environment, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { + environment = environment.enterScope(activeParent.uid); // console.log('fairStep', arenasFired); for (const state of activeParent.children) { if (config.mode.has(state.uid)) { - const didFire = attemptSrcState(simtime, state, event, statechart, {...config, arenasFired}); + const didFire = attemptSrcState(simtime, state, event, statechart, {...config, environment, arenasFired}); if (didFire) { - ({arenasFired, ...config} = didFire); + ({arenasFired, environment, ...config} = didFire); } else { // no enabled outgoing transitions, try the children: // console.log('attempt children'); - ({arenasFired, ...config} = fairStep(simtime, event, statechart, state, {...config, arenasFired})); + ({arenasFired, environment, ...config} = fairStep(simtime, event, statechart, state, {...config, environment, arenasFired})); } } } - return {arenasFired, ...config}; + environment = environment.exitScope(); + 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 { @@ -369,7 +371,7 @@ function resolveHistory(tgt: AbstractState, history: RT_History): Set { } } -export function fire(simtime: number, t: Transition, ts: Map, label: TransitionLabel, arena: OrState, {mode, environment, history, ...rest}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { +export function fire(simtime: number, t: Transition, ts: Map, label: TransitionLabel, arena: OrState, {mode, environment, history, ...rest}: RT_Statechart & RaisedEvents, addEventParam: (env: Environment, label: TransitionLabel) => Environment): RT_Statechart & RaisedEvents { // console.log('will now fire', transitionDescription(t), 'arena', arena); const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}) as ConcreteState[]; @@ -388,7 +390,9 @@ export function fire(simtime: number, t: Transition, ts: Map"), label); ({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid])); + environment = environment.dropScope(); } const tgtPath = computePath({ancestor: arena, descendant: t.tgt}); diff --git a/src/statecharts/runtime_types.ts b/src/statecharts/runtime_types.ts index 3d65d4a..19f06b8 100644 --- a/src/statecharts/runtime_types.ts +++ b/src/statecharts/runtime_types.ts @@ -32,10 +32,10 @@ export type BigStepOutput = RT_Statechart & { firedTransitions: string[], }; -export type BigStep = { - inputEvent: string | null, // null if initialization - simtime: number, -} & BigStepOutput; +// export type BigStep = { +// inputEvent: string | null, // null if initialization +// simtime: number, +// } & BigStepOutput; // internal or output event export type RaisedEvent = { diff --git a/src/statecharts/timed_reactive.ts b/src/statecharts/timed_reactive.ts index e69de29..0039569 100644 --- a/src/statecharts/timed_reactive.ts +++ b/src/statecharts/timed_reactive.ts @@ -0,0 +1,146 @@ +import { Statechart } from "./abstract_syntax"; +import { handleInputEvent, initialize } from "./interpreter"; +import { BigStepOutput, 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, + 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 { + return { + initial: () => initialize(ast), + 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; + if (timers.length === 0) { + throw new Error("cannot make intTransition - timeAdvance is infinity") + } + const [when, timerElapseEvent] = timers[0]; + const {outputEvents, ...rest} = handleInputEvent(when, timerElapseEvent, ast, c); + return [outputEvents, {outputEvents, ...rest}]; + }, + extTransition: (simtime: number, c: RT_Statechart, e: InputEvent) => { + const {outputEvents, ...rest} = handleInputEvent(simtime, e, ast, c); + return [outputEvents, {outputEvents, ...rest}]; + } + } +} + +export const dummyExecution: TimedReactive = { + initial: () => null, + timeAdvance: () => Infinity, + intTransition: () => { throw new Error("dummy never makes intTransition"); }, + extTransition: () => [[], null], +}; + +export type EventDestination = ModelDestination | OutputDestination; + +export type ModelDestination = { + kind: "model", + model: string, + eventName: string, +}; + +export type OutputDestination = { + kind: "output", + eventName: string, +}; + +export function exposeStatechartInputs(ast: Statechart, model: string): Conns { + return { + inputEvents: Object.fromEntries(ast.inputEvents.map(e => [e.event, {kind: "model", model, eventName: e.event}])), + outputEvents: {}, + } +} + +export type Conns = { + // inputs coming from outside are routed to the right models + inputEvents: {[eventName: string]: ModelDestination}, + + // outputs coming from the models are routed to other models or to outside + outputEvents: {[modelName: string]: {[eventName: string]: EventDestination}}, +} + +export function coupledExecution(models: {[name in keyof T]: TimedReactive}, conns: Conns): TimedReactive { + + function makeModelExtTransition(simtime: number, c: T, model: string, e: InputEvent) { + const [outputEvents, newConfig] = models[model].extTransition(simtime, c[model], e); + return processOutputs(simtime, outputEvents, model, { + ...c, + [model]: newConfig, + }); + } + + // one model's output events are possibly input events for another model. + function processOutputs(simtime: number, events: RaisedEvent[], model: string, c: T): [RaisedEvent[], T] { + if (events.length > 0) { + const [event, ...rest] = events; + const destination = conns.outputEvents[model]?.[event.name]; + if (destination === undefined) { + // ignore + return processOutputs(simtime, rest, model, c); + } + if (destination.kind === "model") { + // output event is input for another model + const inputEvent = { + kind: "input" as const, + name: destination.eventName, + param: event.param, + }; + const [outputEvents, newConfig] = makeModelExtTransition(simtime, c, destination.model, inputEvent); + + // proceed with 'rest': + const [restOutputEvents, newConfig2] = processOutputs(simtime, rest, model, newConfig); + return [[...outputEvents, ...restOutputEvents], newConfig2]; + } + else { + // kind === "output" + const [outputEvents, newConfig] = processOutputs(simtime, rest, model, c); + return [[event, ...outputEvents], newConfig]; + } + } + else { + return [[], c]; + } + } + + return { + initial: () => Object.fromEntries(Object.entries(models).map(([name, model]) => { + return [name, model.initial()]; + })) as T, + timeAdvance: (c) => { + return Object.entries(models).reduce((acc, [name, {timeAdvance}]) => Math.min(timeAdvance(c[name]), acc), Infinity); + }, + intTransition: (c) => { + const [when, name] = Object.entries(models).reduce(([earliestSoFar, earliestModel], [name, {timeAdvance}]) => { + const when = timeAdvance(c[name]); + if (when < earliestSoFar) { + return [when, name] as [number, string]; + } + return [earliestSoFar, earliestModel]; + }, [Infinity, null] as [number, string | null]); + if (name !== null) { + const [outputEvents, newConfig] = models[name].intTransition(c[name]); + return processOutputs(when, outputEvents, name, {...c, [name]: newConfig}); + } + throw new Error("cannot make intTransition - timeAdvance is infinity"); + }, + extTransition: (simtime, c, e) => { + const {model, eventName} = conns.inputEvents[e.name]; + // console.log('input event', e, 'goes to', model); + const inputEvent: InputEvent = { + kind: "input", + name: eventName, + param: e.param, + }; + return makeModelExtTransition(simtime, c, model, inputEvent); + }, + } +}