coupled simulation + fix nested scopes

This commit is contained in:
Joeri Exelmans 2025-10-27 19:59:06 +01:00
parent 7b6ce420c0
commit b50f52496a
8 changed files with 339 additions and 124 deletions

View file

@ -1,7 +1,7 @@
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter"; 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 { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
@ -22,6 +22,33 @@ import { usePersistentState } from "./persistent_state";
import { RTHistory } from "./RTHistory"; import { RTHistory } from "./RTHistory";
import { detectConnections } from "@/statecharts/detect_connections"; import { detectConnections } from "@/statecharts/detect_connections";
import { MicrowavePlant } from "./Plant/Microwave/Microwave"; 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 = { export type EditHistory = {
current: VisualEditorState, current: VisualEditorState,
@ -35,15 +62,23 @@ const plants: [string, Plant<any>][] = [
["microwave", MicrowavePlant], ["microwave", MicrowavePlant],
] ]
export type BigStepError = { export type TraceItemError = {
inputEvent: string, cause: string, // event name, <init> or <timer>
simtime: number, simtime: number,
error: RuntimeError, 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 = { export type TraceState = {
// executor: TimedReactive<CoupledState>,
trace: [TraceItem, ...TraceItem[]], // non-empty trace: [TraceItem, ...TraceItem[]], // non-empty
idx: number, idx: number,
}; // <-- null if there is no trace }; // <-- null if there is no trace
@ -52,22 +87,22 @@ function current(ts: TraceState) {
return ts.trace[ts.idx]!; return ts.trace[ts.idx]!;
} }
function getPlantState<T>(plant: Plant<T>, trace: TraceItem[], idx: number): T | null { // function getPlantState<T>(plant: Plant<T>, trace: TraceItem[], idx: number): T | null {
if (idx === -1) { // if (idx === -1) {
return plant.initial; // return plant.initial;
} // }
let plantState = getPlantState(plant, trace, idx-1); // let plantState = getPlantState(plant, trace, idx-1);
if (plantState !== null) { // if (plantState !== null) {
const currentConfig = trace[idx]; // const currentConfig = trace[idx];
if (currentConfig.kind === "bigstep") { // if (currentConfig.kind === "bigstep") {
for (const o of currentConfig.outputEvents) { // for (const o of currentConfig.outputEvents) {
plantState = plant.reduce(o, plantState); // plantState = plant.reduce(o, plantState);
} // }
} // }
return plantState; // return plantState;
} // }
return null; // return null;
} // }
export function App() { export function App() {
const [insertMode, setInsertMode] = useState<InsertMode>("and"); const [insertMode, setInsertMode] = useState<InsertMode>("and");
@ -150,12 +185,13 @@ export function App() {
const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]); const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]);
const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]); const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]);
const ast = parsed && parsed[0]; const ast = parsed && parsed[0];
const syntaxErrors = parsed && parsed[1]; const syntaxErrors = parsed && parsed[1] || [];
const allErrors = syntaxErrors && [ const currentTraceItem = trace && trace.trace[trace.idx];
const allErrors = [
...syntaxErrors, ...syntaxErrors,
...(trace && trace.trace[trace.idx].kind === "error") ? [{ ...(currentTraceItem && currentTraceItem.kind === "error") ? [{
message: trace.trace[trace.idx].error.message, message: currentTraceItem.error.message,
shapeUid: trace.trace[trace.idx].error.highlight[0], shapeUid: currentTraceItem.error.highlight[0],
}] : [], }] : [],
] ]
@ -204,19 +240,29 @@ export function App() {
} }
}, [refRightSideBar.current]); }, [refRightSideBar.current]);
const cE = useMemo(() => ast && coupledExecution({sc: statechartExecution(ast), plant: dummyExecution}, exposeStatechartInputs(ast, "sc")), [ast]);
const onInit = useCallback(() => { const onInit = useCallback(() => {
if (ast === null) return; if (cE === null) return;
const timestampedEvent = {simtime: 0, inputEvent: "<init>"}; const metadata = {simtime: 0, cause: "<init>"};
let config;
try { try {
config = initialize(ast); const state = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
const item = {kind: "bigstep", ...timestampedEvent, ...config}; setTrace({
const plantState = getPlantState(plant, [item], 0); trace: [{kind: "bigstep", ...metadata, state}],
setTrace({trace: [{...item, plantState}], idx: 0}); 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) { catch (error) {
if (error instanceof RuntimeError) { 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 { else {
throw error; // probably a bug in the interpreter throw error; // probably a bug in the interpreter
@ -231,7 +277,7 @@ export function App() {
} }
}); });
scrollDownSidebar(); scrollDownSidebar();
}, [ast, scrollDownSidebar, setTime, setTrace]); }, [cE, scrollDownSidebar]);
const onClear = useCallback(() => { const onClear = useCallback(() => {
setTrace(null); setTrace(null);
@ -240,36 +286,41 @@ export function App() {
// raise input event, producing a new runtime configuration (or a runtime error) // raise input event, producing a new runtime configuration (or a runtime error)
const onRaise = (inputEvent: string, param: any) => { const onRaise = (inputEvent: string, param: any) => {
if (ast === null) return; if (cE === null) return;
if (trace !== null && ast.inputEvents.some(e => e.event === inputEvent)) { if (currentTraceItem !== null /*&& ast.inputEvents.some(e => e.event === inputEvent)*/) {
const config = current(trace); if (currentTraceItem.kind === "bigstep") {
if (config.kind === "bigstep") {
const simtime = getSimTime(time, Math.round(performance.now())); 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) // timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
useEffect(() => { useEffect(() => {
let timeout: NodeJS.Timeout | undefined; let timeout: NodeJS.Timeout | undefined;
if (trace !== null) { if (currentTraceItem !== null && cE !== null) {
const config = current(trace); if (currentTraceItem.kind === "bigstep") {
if (config.kind === "bigstep") { const nextTimeout = cE?.timeAdvance(currentTraceItem.state);
const timers = config.environment.get("_timers") || [];
if (timers.length > 0) {
const [nextInterrupt, timeElapsedEvent] = timers[0];
const raiseTimeEvent = () => { const raiseTimeEvent = () => {
produceNextConfig(nextInterrupt, timeElapsedEvent, config); appendNewConfig(nextTimeout, "<timer>", () => {
const [_, newState] = cE.intTransition(currentTraceItem.state);
return newState;
});
} }
// depending on whether paused or realtime, raise immediately or in the future:
if (time.kind === "realtime") { if (time.kind === "realtime") {
const wallclkDelay = getWallClkDelay(time, nextInterrupt, Math.round(performance.now())); const wallclkDelay = getWallClkDelay(time, nextTimeout, Math.round(performance.now()));
if (wallclkDelay !== Infinity) {
timeout = setTimeout(raiseTimeEvent, wallclkDelay); 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); if (timeout) clearTimeout(timeout);
} }
}, [time, trace]); // <-- todo: is this really efficient? }, [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" ? "<timer>" : event.name,
};
function appendNewConfig(simtime: number, cause: string, computeNewState: () => CoupledState) {
let newItem: TraceItem; let newItem: TraceItem;
const metadata = {simtime, cause}
try { try {
const nextConfig = handleInputEvent(simtime, event, ast, config as BigStep); // may throw const state = computeNewState(); // may throw RuntimeError
let plantState = config.plantState; newItem = {kind: "bigstep", ...metadata, state};
for (const o of nextConfig.outputEvents) {
plantState = plant.reduce(o, plantState);
}
newItem = {kind: "bigstep", plantState, ...timedEvent, ...nextConfig};
} }
catch (error) { catch (error) {
if (error instanceof RuntimeError) { 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 { else {
throw error; throw error;
} }
} }
// @ts-ignore // @ts-ignore
setTrace(trace => ({ setTrace(trace => ({
trace: [ trace: [
@ -348,25 +392,9 @@ export function App() {
}; };
}, []); }, []);
let highlightActive: Set<string>; const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem;
let highlightTransitions: string[]; const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set();
if (trace === null) { const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || [];
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 [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true); const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true);
@ -453,14 +481,14 @@ export function App() {
<option>{plantName}</option> <option>{plantName}</option>
)} )}
</select> </select>
{trace !== null && trace.trace[trace.idx].plantState && {/* {trace !== null && trace.trace[trace.idx].plantState &&
<div>{ <div>{
plant.render( plant.render(
trace.trace[trace.idx].plantState, trace.trace[trace.idx].plantState,
event => onRaise(event.name, event.param), event => onRaise(event.name, event.param),
time.kind === "paused" ? 0 : time.scale, time.kind === "paused" ? 0 : time.scale,
) )
}</div>} }</div>} */}
</PersistentDetails> </PersistentDetails>
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary></details> <details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary></details>
</div> </div>

View file

@ -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 { 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 { formatTime } from "../util/util";
import { TimeMode } from "../statecharts/time"; import { TimeMode } from "../statecharts/time";
import { TraceItem, TraceState } from "./App"; 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}) { 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") { if (item.kind === "bigstep") {
// @ts-ignore // @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 <div return <div
className={"runtimeState" + (active ? " active" : "")} className={"runtimeState" + (active ? " active" : "")}
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}> onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
<div> <div>
{formatTime(item.simtime)} {formatTime(item.simtime)}
&emsp; &emsp;
<div className="inputEvent">{item.inputEvent || "<init>"}</div> <div className="inputEvent">{item.cause}</div>
</div> </div>
<ShowMode mode={newStates} statechart={ast}/> <ShowMode mode={newStates} statechart={ast}/>
<ShowEnvironment environment={item.environment}/> <ShowEnvironment environment={item.state.sc.environment}/>
{item.outputEvents.length>0 && <>^ {item.state.sc.outputEvents.length>0 && <>^
{item.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)} {item.state.sc.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
</>} </>}
</div>; </div>;
} }
@ -57,7 +57,7 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt
<div> <div>
{formatTime(item.simtime)} {formatTime(item.simtime)}
&emsp; &emsp;
<div className="inputEvent">{item.inputEvent}</div> <div className="inputEvent">{item.cause}</div>
</div> </div>
<div> <div>
{item.error.message} {item.error.message}
@ -71,8 +71,7 @@ function ShowEnvironment(props: {environment: Environment}) {
return <div>{ return <div>{
[...props.environment.entries()] [...props.environment.entries()]
.filter(([variable]) => !variable.startsWith('_')) .filter(([variable]) => !variable.startsWith('_'))
// we strip the first 5 characters from 'variable' (remove "root.") .map(([variable,value]) => `${variable.split('.').at(-1)}: ${JSON.stringify(value)}`).join(', ')
.map(([variable,value]) => `${variable.slice(5)}: ${value}`).join(', ')
}</div>; }</div>;
} }

View file

@ -96,7 +96,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
}, [setTime, setTimescale]); }, [setTime, setTimescale]);
// timestamp of next timed transition, in simulated time // 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 nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0];
const onSkip = useCallback(() => { const onSkip = useCallback(() => {

View file

@ -1,8 +1,8 @@
// Just a simple recursive interpreter for the action language // Just a simple recursive interpreter for the action language
import { Environment } from "./environment";
import { RuntimeError } from "./interpreter"; import { RuntimeError } from "./interpreter";
import { Expression } from "./label_ast"; import { Expression } from "./label_ast";
import { Environment } from "./runtime_types";
const UNARY_OPERATOR_MAP: Map<string, (x: any) => any> = new Map([ const UNARY_OPERATOR_MAP: Map<string, (x: any) => any> = new Map([
["!", x => !x], ["!", x => !x],

View file

@ -3,6 +3,7 @@ import { getST, iterST, ScopeTree, updateST, writeST } from "./scope_tree";
export type Environment = { export type Environment = {
enterScope(scopeId: string): Environment; enterScope(scopeId: string): Environment;
exitScope(): 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 // 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; newVar(key: string, value: any): Environment;
@ -32,6 +33,9 @@ export class FlatEnvironment {
exitScope(): FlatEnvironment { exitScope(): FlatEnvironment {
return this; return this;
} }
dropScope(): FlatEnvironment {
return this;
}
newVar(key: string, value: any) { newVar(key: string, value: any) {
return this.set(key, value); return this.set(key, value);
@ -43,7 +47,7 @@ export class FlatEnvironment {
return this.env.get(key); return this.env.get(key);
} }
entries(): Iterator<[string, any]> { entries(): IterableIterator<[string, any]> {
return this.env.entries(); return this.env.entries();
} }
} }
@ -60,18 +64,51 @@ export class ScopedEnvironment {
} }
enterScope(scopeId: string): ScopedEnvironment { enterScope(scopeId: string): ScopedEnvironment {
// console.log('enter scope', scopeId, new ScopedEnvironment(
// this.scopeTree,
// [...this.current, scopeId],
// ));
return new ScopedEnvironment( return new ScopedEnvironment(
this.scopeTree, this.scopeTree,
[...this.current, scopeId], [...this.current, scopeId],
); );
} }
exitScope() { exitScope() {
// console.log('exit scope', this.current.at(-1), new ScopedEnvironment(
// this.scopeTree,
// this.current.slice(0, -1),
// ));
return new ScopedEnvironment( return new ScopedEnvironment(
this.scopeTree, this.scopeTree,
this.current.slice(0, -1), 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 { newVar(key: string, value: any): ScopedEnvironment {
return new ScopedEnvironment( return new ScopedEnvironment(
writeST(key, value, this.current, this.scopeTree), writeST(key, value, this.current, this.scopeTree),
@ -96,7 +133,8 @@ export class ScopedEnvironment {
return getST(this.current, key, this.scopeTree); return getST(this.current, key, this.scopeTree);
} }
*entries(): Iterator<[string, any]> { *entries(): IterableIterator<[string, any]> {
yield* iterST(this.scopeTree); yield* iterST(this.scopeTree);
} }
} }

View file

@ -2,7 +2,7 @@ import { AbstractState, computeArena, computePath, ConcreteState, getDescendants
import { evalExpr } from "./actionlang_interpreter"; import { evalExpr } from "./actionlang_interpreter";
import { Environment, FlatEnvironment, ScopedEnvironment } from "./environment"; import { Environment, FlatEnvironment, ScopedEnvironment } from "./environment";
import { Action, EventTrigger, TransitionLabel } from "./label_ast"; 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<string, any>([ const initialEnv = new Map<string, any>([
["_timers", []], ["_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 { 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); // console.log('attemptSrcState', stateDescription(sourceState), arenasFired);
const outgoing = statechart.transitions.get(sourceState.uid) || []; const outgoing = statechart.transitions.get(sourceState.uid) || [];
const labels = outgoing.flatMap(t => const labels = outgoing.flatMap(t =>
@ -284,7 +291,7 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_
} }
}; };
const guardEnvironment = environment.set("inState", inState); 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 > 0) {
if (enabled.length > 1) { 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]); 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 [toFire, label] = enabled[0];
const arena = computeArena(toFire.src, toFire.tgt); const arena = computeArena(toFire.src, toFire.tgt);
if (allowedToFire(arena, arenasFired)) { if (allowedToFire(arena, arenasFired)) {
environment = environment.enterScope("<transition>"); ({mode, environment, ...rest} = fire(simtime, toFire, statechart.transitions, label, arena, {mode, environment, ...rest}, addEventParam));
// 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}));
rest = {...rest, firedTransitions: [...rest.firedTransitions, toFire.uid]} rest = {...rest, firedTransitions: [...rest.firedTransitions, toFire.uid]}
environment.exitScope();
arenasFired = [...arenasFired, arena]; arenasFired = [...arenasFired, arena];
// if there is any pseudo-state in the modal configuration, immediately fire any enabled outgoing transitions of that state: // 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. // 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); // console.log('fairStep', arenasFired);
for (const state of activeParent.children) { for (const state of activeParent.children) {
if (config.mode.has(state.uid)) { 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) { if (didFire) {
({arenasFired, ...config} = didFire); ({arenasFired, environment, ...config} = didFire);
} }
else { else {
// no enabled outgoing transitions, try the children: // no enabled outgoing transitions, try the children:
// console.log('attempt 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 { 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<string> {
} }
} }
export function fire(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...rest}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { export function fire(simtime: number, t: Transition, ts: Map<string, Transition[]>, 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); // console.log('will now fire', transitionDescription(t), 'arena', arena);
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}) as ConcreteState[]; 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<string, Transition[
// transition actions // transition actions
for (const action of label.actions) { for (const action of label.actions) {
environment = addEventParam(environment.enterScope("<transition>"), label);
({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid])); ({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid]));
environment = environment.dropScope();
} }
const tgtPath = computePath({ancestor: arena, descendant: t.tgt}); const tgtPath = computePath({ancestor: arena, descendant: t.tgt});

View file

@ -32,10 +32,10 @@ export type BigStepOutput = RT_Statechart & {
firedTransitions: string[], firedTransitions: string[],
}; };
export type BigStep = { // export type BigStep = {
inputEvent: string | null, // null if initialization // inputEvent: string | null, // null if initialization
simtime: number, // simtime: number,
} & BigStepOutput; // } & BigStepOutput;
// internal or output event // internal or output event
export type RaisedEvent = { export type RaisedEvent = {

View file

@ -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<RT_Config> = {
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<BigStepOutput> {
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<null> = {
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<T extends {[name: string]: any}>(models: {[name in keyof T]: TimedReactive<T[name]>}, conns: Conns): TimedReactive<T> {
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);
},
}
}