coupled simulation + fix nested scopes
This commit is contained in:
parent
7b6ce420c0
commit
b50f52496a
8 changed files with 339 additions and 124 deletions
|
|
@ -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<string, (x: any) => any> = new Map([
|
||||
["!", x => !x],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, any>([
|
||||
["_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("<transition>");
|
||||
// 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<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);
|
||||
|
||||
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
|
||||
for (const action of label.actions) {
|
||||
environment = addEventParam(environment.enterScope("<transition>"), label);
|
||||
({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid]));
|
||||
environment = environment.dropScope();
|
||||
}
|
||||
|
||||
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue