raising events appears to work. nondeterminism currently ignored
This commit is contained in:
parent
3f2db4457f
commit
09a87d3025
8 changed files with 243 additions and 194 deletions
|
|
@ -1,19 +1,13 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { ConcreteState, emptyStatechart, Statechart, Transition } from "../VisualEditor/ast";
|
import { ConcreteState, emptyStatechart, Statechart, stateDescription, Transition } from "../VisualEditor/ast";
|
||||||
|
|
||||||
import { VisualEditor } from "../VisualEditor/VisualEditor";
|
import { VisualEditor } from "../VisualEditor/VisualEditor";
|
||||||
import { RT_Statechart } from "../VisualEditor/runtime_types";
|
import { RT_Statechart } from "../VisualEditor/runtime_types";
|
||||||
import { initialize, raiseEvent } from "../VisualEditor/interpreter";
|
import { initialize, handleEvent } from "../VisualEditor/interpreter";
|
||||||
|
import { Action, Expression } from "../VisualEditor/label_ast";
|
||||||
|
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { Action, Expression } from "../VisualEditor/label_ast";
|
|
||||||
|
|
||||||
export function stateDescription(state: ConcreteState) {
|
|
||||||
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShowTransition(props: {transition: Transition}) {
|
export function ShowTransition(props: {transition: Transition}) {
|
||||||
return <>➔ {stateDescription(props.transition.tgt)}</>;
|
return <>➔ {stateDescription(props.transition.tgt)}</>;
|
||||||
|
|
@ -51,34 +45,22 @@ export function AST(props: {root: ConcreteState, transitions: Map<string, Transi
|
||||||
<summary>{props.root.kind}: {description}</summary>
|
<summary>{props.root.kind}: {description}</summary>
|
||||||
|
|
||||||
{props.root.entryActions.length>0 &&
|
{props.root.entryActions.length>0 &&
|
||||||
<details open={true}>
|
props.root.entryActions.map(action =>
|
||||||
<summary>entry actions</summary>
|
<div> entry / <ShowAction action={action}/></div>
|
||||||
{props.root.entryActions.map(action =>
|
)
|
||||||
<div> <ShowAction action={action}/></div>
|
|
||||||
)}
|
|
||||||
</details>
|
|
||||||
}
|
}
|
||||||
{props.root.exitActions.length>0 &&
|
{props.root.exitActions.length>0 &&
|
||||||
<details open={true}>
|
props.root.exitActions.map(action =>
|
||||||
<summary>exit actions</summary>
|
<div> exit / <ShowAction action={action}/></div>
|
||||||
{props.root.exitActions.map(action =>
|
)
|
||||||
<ShowAction action={action}/>
|
|
||||||
)}
|
|
||||||
</details>
|
|
||||||
}
|
}
|
||||||
{props.root.children.length>0 &&
|
{props.root.children.length>0 &&
|
||||||
<details open={true}>
|
props.root.children.map(child =>
|
||||||
<summary>children</summary>
|
|
||||||
{props.root.children.map(child =>
|
|
||||||
<AST root={child} transitions={props.transitions} />
|
<AST root={child} transitions={props.transitions} />
|
||||||
)}
|
)
|
||||||
</details>
|
|
||||||
}
|
}
|
||||||
{outgoing.length>0 &&
|
{outgoing.length>0 &&
|
||||||
<details open={true}>
|
outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)
|
||||||
<summary>outgoing</summary>
|
|
||||||
{outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)}
|
|
||||||
</details>
|
|
||||||
}
|
}
|
||||||
</details>
|
</details>
|
||||||
}
|
}
|
||||||
|
|
@ -104,14 +86,15 @@ export function App() {
|
||||||
|
|
||||||
function raise(event: string) {
|
function raise(event: string) {
|
||||||
if (rt && ast.inputEvents.has(event)) {
|
if (rt && ast.inputEvents.has(event)) {
|
||||||
const nextConfigs = raiseEvent(event, ast, ast.root, rt)
|
const nextConfig = handleEvent(event, ast, ast.root, rt);
|
||||||
console.log({nextConfigs});
|
setRT(nextConfig);
|
||||||
if (nextConfigs.length > 0) {
|
// console.log({nextConfigs});
|
||||||
if (nextConfigs.length > 1) {
|
// if (nextConfigs.length > 0) {
|
||||||
console.warn('non-determinism, blindly selecting first next run-time state!');
|
// if (nextConfigs.length > 1) {
|
||||||
}
|
// console.warn('non-determinism, blindly selecting first next run-time state!');
|
||||||
setRT(nextConfigs[0]);
|
// }
|
||||||
}
|
// setRT(nextConfigs[0]);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,9 +109,9 @@ export function App() {
|
||||||
</select>
|
</select>
|
||||||
<button onClick={stop} >stop</button>
|
<button onClick={stop} >stop</button>
|
||||||
 
|
 
|
||||||
<input type="radio" name="paused" id="radio-paused" checked={paused} disabled={rt===null}/>
|
<input type="radio" name="paused" id="radio-paused" checked={paused} disabled={rt===null} onChange={e => setPaused(e.target.checked)}/>
|
||||||
<label htmlFor="radio-paused">paused</label>
|
<label htmlFor="radio-paused">paused</label>
|
||||||
<input type="radio" name="realtime" id="radio-realtime" checked={!paused} disabled={rt===null}/>
|
<input type="radio" name="realtime" id="radio-realtime" checked={!paused} disabled={rt===null} onChange={e => setPaused(!e.target.checked)}/>
|
||||||
<label htmlFor="radio-realtime">real-time</label>
|
<label htmlFor="radio-realtime">real-time</label>
|
||||||
 
|
 
|
||||||
<label htmlFor="number-timescale">timescale</label>
|
<label htmlFor="number-timescale">timescale</label>
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ text.highlight {
|
||||||
}
|
}
|
||||||
.rountangle.active {
|
.rountangle.active {
|
||||||
fill: rgb(255, 196, 0);
|
fill: rgb(255, 196, 0);
|
||||||
fill-opacity: 0.6;
|
fill-opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected:hover {
|
.selected:hover {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ import { parseStatechart } from "./parser";
|
||||||
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
|
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
|
||||||
|
|
||||||
import * as lz4 from "@nick/lz4";
|
import * as lz4 from "@nick/lz4";
|
||||||
import { getActiveStates, initialize } from "./interpreter";
|
|
||||||
import { RT_Statechart } from "./runtime_types";
|
import { RT_Statechart } from "./runtime_types";
|
||||||
import { emptyStatechart, Statechart } from "./ast";
|
import { Statechart } from "./ast";
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
type DraggingState = {
|
||||||
|
|
@ -401,6 +400,11 @@ export function VisualEditor({ast, setAST, rt, setRT, errors, setErrors}: Visual
|
||||||
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === "c") {
|
||||||
|
// e.preventDefault();
|
||||||
|
// setClipboard()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -496,7 +500,7 @@ export function VisualEditor({ast, setAST, rt, setRT, errors, setErrors}: Visual
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = getActiveStates(rt?.mode || {});
|
const active = rt?.mode || new Set();
|
||||||
|
|
||||||
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]);
|
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]);
|
||||||
|
|
||||||
|
|
|
||||||
46
src/VisualEditor/actionlang_interpreter.ts
Normal file
46
src/VisualEditor/actionlang_interpreter.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Just a simple recursive interpreter for the action language
|
||||||
|
|
||||||
|
import { Expression } from "./label_ast";
|
||||||
|
import { Environment } from "./runtime_types";
|
||||||
|
|
||||||
|
|
||||||
|
const UNARY_OPERATOR_MAP: Map<string, (x: any) => any> = new Map([
|
||||||
|
["!", x => !x],
|
||||||
|
["-", x => -x as any],
|
||||||
|
]);
|
||||||
|
const BINARY_OPERATOR_MAP: Map<string, (a: any, b: any) => any> = new Map([
|
||||||
|
["+", (a, b) => a + b],
|
||||||
|
["-", (a, b) => a - b],
|
||||||
|
["*", (a, b) => a * b],
|
||||||
|
["/", (a, b) => a / b],
|
||||||
|
["&&", (a, b) => a && b],
|
||||||
|
["||", (a, b) => a || b],
|
||||||
|
["==", (a, b) => a == b],
|
||||||
|
["<=", (a, b) => a <= b],
|
||||||
|
[">=", (a, b) => a >= b],
|
||||||
|
["<", (a, b) => a < b],
|
||||||
|
[">", (a, b) => a > b],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function evalExpr(expr: Expression, environment: Environment): any {
|
||||||
|
if (expr.kind === "literal") {
|
||||||
|
return expr.value;
|
||||||
|
}
|
||||||
|
else if (expr.kind === "ref") {
|
||||||
|
const found = environment.get(expr.variable);
|
||||||
|
if (found === undefined) {
|
||||||
|
throw new Error(`variable '${expr.variable}' does not exist in environment`);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
else if (expr.kind === "unaryExpr") {
|
||||||
|
const arg = evalExpr(expr.expr, environment);
|
||||||
|
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
|
||||||
|
}
|
||||||
|
else if (expr.kind === "binaryExpr") {
|
||||||
|
const lhs = evalExpr(expr.lhs, environment);
|
||||||
|
const rhs = evalExpr(expr.rhs, environment);
|
||||||
|
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
|
||||||
|
}
|
||||||
|
throw new Error("should never reach here");
|
||||||
|
}
|
||||||
|
|
@ -43,21 +43,25 @@ export type Statechart = {
|
||||||
uid2State: Map<string, ConcreteState>;
|
uid2State: Map<string, ConcreteState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyRoot: OrState = {
|
||||||
|
uid: "root",
|
||||||
|
kind: "or",
|
||||||
|
depth: 0,
|
||||||
|
initial: [],
|
||||||
|
children:[],
|
||||||
|
comments: [],
|
||||||
|
entryActions: [],
|
||||||
|
exitActions: [],
|
||||||
|
};
|
||||||
|
|
||||||
export const emptyStatechart: Statechart = {
|
export const emptyStatechart: Statechart = {
|
||||||
root: {
|
root: emptyRoot,
|
||||||
uid: "root",
|
|
||||||
kind: "or",
|
|
||||||
initial: [],
|
|
||||||
children:[],
|
|
||||||
comments: [],
|
|
||||||
entryActions: [],
|
|
||||||
exitActions: [],
|
|
||||||
},
|
|
||||||
transitions: new Map(),
|
transitions: new Map(),
|
||||||
variables: new Set(),
|
variables: new Set(),
|
||||||
inputEvents: new Set(),
|
inputEvents: new Set(),
|
||||||
internalEvents: new Set(),
|
internalEvents: new Set(),
|
||||||
outputEvents: new Set(),
|
outputEvents: new Set(),
|
||||||
|
uid2State: new Map([["root", emptyRoot]]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// reflexive, transitive relation
|
// reflexive, transitive relation
|
||||||
|
|
@ -72,10 +76,19 @@ export function isAncestorOf({ancestor, descendant}: {ancestor: ConcreteState, d
|
||||||
return pathToParent && [...pathToParent, descendant];
|
return pathToParent && [...pathToParent, descendant];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOverlapping(a: ConcreteState, b: ConcreteState): boolean {
|
||||||
|
if (a.depth < b.depth) {
|
||||||
|
return Boolean(isAncestorOf({ancestor: a, descendant: b}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Boolean(isAncestorOf({ancestor: b, descendant: a}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// the arena of a transition is the lowest common ancestor state that is an OR-state
|
// the arena of a transition is the lowest common ancestor state that is an OR-state
|
||||||
// see "Deconstructing the Semantics of Big-Step Modelling Languages" by Shahram Esmaeilsabzali, 2009
|
// see "Deconstructing the Semantics of Big-Step Modelling Languages" by Shahram Esmaeilsabzali, 2009
|
||||||
export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): {
|
export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): {
|
||||||
arena: ConcreteState,
|
arena: OrState,
|
||||||
srcPath: ConcreteState[],
|
srcPath: ConcreteState[],
|
||||||
tgtPath: ConcreteState[],
|
tgtPath: ConcreteState[],
|
||||||
} {
|
} {
|
||||||
|
|
@ -95,5 +108,24 @@ export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState
|
||||||
const {arena, srcPath, tgtPath} = computeArena({src: tgt, tgt: src});
|
const {arena, srcPath, tgtPath} = computeArena({src: tgt, tgt: src});
|
||||||
return {arena, srcPath: tgtPath, tgtPath: srcPath};
|
return {arena, srcPath: tgtPath, tgtPath: srcPath};
|
||||||
}
|
}
|
||||||
throw new Error("should never reach here");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDescendants(state: ConcreteState): Set<string> {
|
||||||
|
const result = new Set([state.uid]);
|
||||||
|
for (const child of state.children) {
|
||||||
|
for (const descendant of getDescendants(child)) {
|
||||||
|
// will include child itself:
|
||||||
|
result.add(descendant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the 'description' of a state is a human-readable string that (hopefully) identifies the state.
|
||||||
|
// if the state contains a comment, we take the 'first' (= visually topmost) comment
|
||||||
|
// otherwise we fall back to the state's UID.
|
||||||
|
export function stateDescription(state: ConcreteState) {
|
||||||
|
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { computeArena, ConcreteState, isAncestorOf, Statechart, Transition } from "./ast";
|
import { act } from "react";
|
||||||
import { Action, Expression } from "./label_ast";
|
import { evalExpr } from "./actionlang_interpreter";
|
||||||
|
import { computeArena, ConcreteState, getDescendants, isAncestorOf, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./ast";
|
||||||
|
import { Action } from "./label_ast";
|
||||||
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised } from "./runtime_types";
|
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised } from "./runtime_types";
|
||||||
|
|
||||||
export function initialize(ast: Statechart): RT_Statechart {
|
export function initialize(ast: Statechart): RT_Statechart {
|
||||||
const {mode, environment, ...raised} = enterDefault(ast.root, {
|
const {enteredStates, environment, ...raised} = enterDefault(ast.root, {
|
||||||
environment: new Map(),
|
environment: new Map(),
|
||||||
...initialRaised,
|
...initialRaised,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
mode,
|
mode: enteredStates,
|
||||||
environment,
|
environment,
|
||||||
inputEvents: [],
|
|
||||||
...raised,
|
...raised,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +20,7 @@ type ActionScope = {
|
||||||
environment: Environment,
|
environment: Environment,
|
||||||
} & RaisedEvents;
|
} & RaisedEvents;
|
||||||
|
|
||||||
type EnteredScope = { mode: Mode } & ActionScope;
|
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
||||||
|
|
||||||
export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScope {
|
export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScope {
|
||||||
let actionScope = rt;
|
let actionScope = rt;
|
||||||
|
|
@ -30,25 +31,29 @@ export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScop
|
||||||
}
|
}
|
||||||
|
|
||||||
// enter children...
|
// enter children...
|
||||||
const mode: {[uid:string]: Mode} = {};
|
let enteredStates = new Set([state.uid]);
|
||||||
if (state.kind === "and") {
|
if (state.kind === "and") {
|
||||||
// enter every child
|
// enter every child
|
||||||
for (const child of state.children) {
|
for (const child of state.children) {
|
||||||
let childMode;
|
let enteredChildren;
|
||||||
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(child, actionScope));
|
||||||
mode[child.uid] = childMode;
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (state.kind === "or") {
|
else if (state.kind === "or") {
|
||||||
// same as AND-state, but we only enter the initial state(s)
|
// same as AND-state, but we only enter the initial state(s)
|
||||||
for (const [_, child] of state.initial) {
|
if (state.initial.length > 0) {
|
||||||
let childMode;
|
if (state.initial.length > 1) {
|
||||||
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
console.warn(state.uid + ': multiple initial states, only entering one of them');
|
||||||
mode[child.uid] = childMode;
|
}
|
||||||
|
let enteredChildren;
|
||||||
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(state.initial[0][1], actionScope));
|
||||||
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
|
console.warn(state.uid + ': no initial state');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mode, ...actionScope };
|
return {enteredStates, ...actionScope};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enterPath(path: ConcreteState[], rt: ActionScope): EnteredScope {
|
export function enterPath(path: ConcreteState[], rt: ActionScope): EnteredScope {
|
||||||
|
|
@ -62,48 +67,47 @@ export function enterPath(path: ConcreteState[], rt: ActionScope): EnteredScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// enter children...
|
// enter children...
|
||||||
|
let enteredStates = new Set([state.uid]);
|
||||||
const mode: {[uid:string]: Mode} = {};
|
|
||||||
if (state.kind === "and") {
|
if (state.kind === "and") {
|
||||||
// enter every child
|
// enter every child
|
||||||
for (const child of state.children) {
|
for (const child of state.children) {
|
||||||
let childMode;
|
let enteredChildren;
|
||||||
if (rest.length > 0 && child.uid === rest[0].uid) {
|
if (rest.length > 0 && child.uid === rest[0].uid) {
|
||||||
({mode: childMode, ...actionScope} = enterPath(rest, actionScope));
|
({enteredStates: enteredChildren, ...actionScope} = enterPath(rest, actionScope));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(child, actionScope));
|
||||||
}
|
}
|
||||||
mode[child.uid] = childMode;
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (state.kind === "or") {
|
else if (state.kind === "or") {
|
||||||
if (rest.length > 0) {
|
if (rest.length > 0) {
|
||||||
let childMode;
|
let enteredChildren;
|
||||||
({mode: childMode, ...actionScope} = enterPath(rest, actionScope));
|
({enteredStates: enteredChildren, ...actionScope} = enterPath(rest, actionScope));
|
||||||
mode[rest[0].uid] = childMode;
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// same as AND-state, but we only enter the initial state(s)
|
// same as AND-state, but we only enter the initial state(s)
|
||||||
for (const [_, child] of state.initial) {
|
for (const [_, child] of state.initial) {
|
||||||
let childMode;
|
let enteredChildren;
|
||||||
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(child, actionScope));
|
||||||
mode[child.uid] = childMode;
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mode, ...actionScope };
|
return { enteredStates, ...actionScope };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exit the given state and all its active descendants
|
||||||
export function exitCurrent(state: ConcreteState, rt: EnteredScope): ActionScope {
|
export function exitCurrent(state: ConcreteState, rt: EnteredScope): ActionScope {
|
||||||
let {mode, ...actionScope} = rt;
|
let {enteredStates, ...actionScope} = rt;
|
||||||
|
|
||||||
// exit all active children...
|
// exit all active children...
|
||||||
for (const [childUid, childMode] of Object.entries(mode)) {
|
for (const child of state.children) {
|
||||||
const child = state.children.find(child => child.uid === childUid);
|
if (enteredStates.has(child.uid)) {
|
||||||
if (child) {
|
actionScope = exitCurrent(child, {enteredStates, ...actionScope});
|
||||||
(actionScope = exitCurrent(child, {mode: childMode, ...actionScope}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +119,27 @@ export function exitCurrent(state: ConcreteState, rt: EnteredScope): ActionScope
|
||||||
return actionScope;
|
return actionScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exitPath(path: ConcreteState[], rt: EnteredScope): ActionScope {
|
||||||
|
let {enteredStates, ...actionScope} = rt;
|
||||||
|
|
||||||
|
const toExit = enteredStates.difference(new Set(path));
|
||||||
|
|
||||||
|
const [state, ...rest] = path;
|
||||||
|
|
||||||
|
// exit state and all its children, *except* states along the rest of the path
|
||||||
|
actionScope = exitCurrent(state, {enteredStates: toExit, ...actionScope});
|
||||||
|
if (rest.length > 0) {
|
||||||
|
actionScope = exitPath(rest, {enteredStates, ...actionScope});
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute exit actions
|
||||||
|
for (const action of state.exitActions) {
|
||||||
|
(actionScope = execAction(action, actionScope));
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionScope;
|
||||||
|
}
|
||||||
|
|
||||||
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
if (action.kind === "assignment") {
|
if (action.kind === "assignment") {
|
||||||
const rhs = evalExpr(action.rhs, rt.environment);
|
const rhs = evalExpr(action.rhs, rt.environment);
|
||||||
|
|
@ -144,117 +169,73 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
throw new Error("should never reach here");
|
throw new Error("should never reach here");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleEvent(event: string, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart): RT_Statechart {
|
||||||
const UNARY_OPERATOR_MAP: Map<string, (x:any)=>any> = new Map([
|
const arenasFired = new Set<OrState>();
|
||||||
["!", x => !x],
|
for (const state of activeParent.children) {
|
||||||
["-", x => -x as any],
|
if (mode.has(state.uid)) {
|
||||||
]);
|
const outgoing = statechart.transitions.get(state.uid) || [];
|
||||||
|
const triggered = outgoing.filter(transition => transition.label[0].trigger.kind === "event" && transition.label[0].trigger.event === event);
|
||||||
const BINARY_OPERATOR_MAP: Map<string, (a:any,b:any)=>any> = new Map([
|
const enabled = triggered.filter(transition =>
|
||||||
["+", (a,b) => a+b],
|
evalExpr(transition.label[0].guard, environment)
|
||||||
["-", (a,b) => a-b],
|
);
|
||||||
["*", (a,b) => a*b],
|
if (enabled.length > 0) {
|
||||||
["/", (a,b) => a/b],
|
if (enabled.length > 1) {
|
||||||
["&&", (a,b) => a&&b],
|
console.warn('nondeterminism!!!!');
|
||||||
["||", (a,b) => a||b],
|
}
|
||||||
["==", (a,b) => a==b],
|
const t = enabled[0];
|
||||||
["<=", (a,b) => a<=b],
|
console.log('enabled:', transitionDescription(t));
|
||||||
[">=", (a,b) => a>=b],
|
const {arena, srcPath, tgtPath} = computeArena(t);
|
||||||
["<", (a,b) => a<b],
|
let overlapping = false;
|
||||||
[">", (a,b) => a>b],
|
for (const alreadyFired of arenasFired) {
|
||||||
]);
|
if (isOverlapping(arena, alreadyFired)) {
|
||||||
|
overlapping = true;
|
||||||
export function evalExpr(expr: Expression, environment: Environment): any {
|
}
|
||||||
if (expr.kind === "literal") {
|
}
|
||||||
return expr.value;
|
if (!overlapping) {
|
||||||
}
|
console.log('^ firing');
|
||||||
else if (expr.kind === "ref") {
|
({mode, environment, ...raised} = fireTransition(t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
|
||||||
const found = environment.get(expr.variable);
|
arenasFired.add(arena);
|
||||||
if (found === undefined) {
|
}
|
||||||
throw new Error(`variable '${expr.variable}' does not exist in environment`)
|
else {
|
||||||
}
|
console.log('skip (overlapping arenas)');
|
||||||
return found;
|
}
|
||||||
}
|
}
|
||||||
else if (expr.kind === "unaryExpr") {
|
else {
|
||||||
const arg = evalExpr(expr.expr, environment);
|
// no enabled outgoing transitions, try the children:
|
||||||
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
|
({environment, mode, ...raised} = handleEvent(event, statechart, state, {environment, mode, ...raised}));
|
||||||
}
|
}
|
||||||
else if (expr.kind === "binaryExpr") {
|
|
||||||
const lhs = evalExpr(expr.lhs, environment);
|
|
||||||
const rhs = evalExpr(expr.rhs, environment);
|
|
||||||
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs,rhs);
|
|
||||||
}
|
|
||||||
throw new Error("should never reach here");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getActiveStates(mode: Mode): Set<string> {
|
|
||||||
return new Set([].concat(
|
|
||||||
...Object.entries(mode).map(([childUid, childMode]) =>
|
|
||||||
[childUid, ...getActiveStates(childMode)])
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function raiseEvent(event: string, statechart: Statechart, sourceState: ConcreteState, rt: RT_Statechart): RT_Statechart[] {
|
|
||||||
const activeStates = sourceState.children.filter(child => rt.mode.hasOwnProperty(child.uid))
|
|
||||||
for (const state of activeStates) {
|
|
||||||
const outgoing = statechart.transitions.get(state.uid) || [];
|
|
||||||
const enabled = outgoing.filter(transition => transition.label[0].trigger.kind === "event" && transition.label[0].trigger.event === event);
|
|
||||||
const enabledGuard = enabled.filter(transition =>
|
|
||||||
evalExpr(transition.label[0].guard, rt.environment)
|
|
||||||
);
|
|
||||||
if (enabledGuard.length > 0) {
|
|
||||||
const newRts = enabledGuard.map(t => fireTransition(t, statechart, rt));
|
|
||||||
return newRts;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// no enabled outgoing transitions, try the children:
|
|
||||||
return raiseEvent(event, statechart, state, rt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return {environment, mode, ...raised};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setModeDeep(oldMode: Mode, pathToState: ConcreteState[], newMode: Mode): Mode {
|
function transitionDescription(t: Transition) {
|
||||||
if (pathToState.length === 0) {
|
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
|
||||||
return newMode;
|
|
||||||
}
|
|
||||||
const [next, ...rest] = pathToState;
|
|
||||||
return {
|
|
||||||
...oldMode,
|
|
||||||
[next.uid]: setModeDeep(oldMode[next.uid], rest, newMode),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsetModeDeep(oldMode: Mode, pathToState: ConcreteState[]): Mode {
|
export function fireTransition(t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart): {mode: Mode, environment: Environment} & RaisedEvents {
|
||||||
if (pathToState.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (pathToState.length === 1) {
|
|
||||||
const keyToDelete = pathToState[0].uid;
|
|
||||||
const newMode = {...oldMode}; // shallow copy
|
|
||||||
delete newMode[keyToDelete];
|
|
||||||
return newMode;
|
|
||||||
}
|
|
||||||
const [next, ...rest] = pathToState;
|
|
||||||
return {
|
|
||||||
...oldMode,
|
|
||||||
[next.uid]: unsetModeDeep(oldMode[next.uid], rest),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fireTransition(t: Transition, statechart: Statechart, rt: RT_Statechart): RT_Statechart {
|
console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
|
||||||
const {arena, srcPath, tgtPath} = computeArena(t);
|
|
||||||
const pathToArena = isAncestorOf({ancestor: statechart.root, descendant: arena}) as ConcreteState[];
|
// exit src
|
||||||
console.log('fire ', t.src.comments[0][1], '->', t.tgt.comments[0][1], {srcPath, tgtPath});
|
({environment, ...raised} = exitPath(srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
|
||||||
let {environment, ...raised} = exitCurrent(srcPath[1], rt);
|
const toExit = getDescendants(arena);
|
||||||
const exitedMode = unsetModeDeep(rt.mode, [...pathToArena.slice(1), ...srcPath.slice(1)]);
|
toExit.delete(arena.uid); // do not exit the arena itself
|
||||||
|
const exitedMode = mode.difference(toExit);
|
||||||
|
|
||||||
|
console.log('exitedMode', exitedMode);
|
||||||
|
|
||||||
|
// exec transition actions
|
||||||
for (const action of t.label[0].actions) {
|
for (const action of t.label[0].actions) {
|
||||||
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
||||||
}
|
}
|
||||||
let deepMode;
|
|
||||||
({mode: deepMode, environment, ...raised} = enterPath(tgtPath.slice(1), {environment, ...raised}));
|
// enter tgt
|
||||||
// console.log('entered path:', tgtPath.slice(1), {deepMode});
|
let enteredStates;
|
||||||
const enteredMode = setModeDeep(exitedMode, [...pathToArena.slice(1), ...tgtPath.slice(1)], deepMode);
|
({enteredStates, environment, ...raised} = enterPath(tgtPath.slice(1), {environment, ...raised}));
|
||||||
// console.log('pathToArena:', pathToArena, 'newMode:', enteredMode);
|
const enteredMode = exitedMode.union(enteredStates);
|
||||||
return {mode: enteredMode, environment, inputEvents: rt.inputEvents, ...raised};
|
|
||||||
|
console.log('enteredMode', enteredMode);
|
||||||
|
|
||||||
|
return {mode: enteredMode, environment, ...raised};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
inputEvents,
|
inputEvents,
|
||||||
internalEvents,
|
internalEvents,
|
||||||
outputEvents,
|
outputEvents,
|
||||||
|
uid2State,
|
||||||
}, errorShapes];
|
}, errorShapes];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
// for OR-states, only the modal configuration of the current state is kept
|
// for OR-states, only the modal configuration of the current state is kept
|
||||||
// for AND-states, the modal configuration of every child is kept
|
// for AND-states, the modal configuration of every child is kept
|
||||||
// for basic states (= empty AND-states), the modal configuration is just an empty object
|
// for basic states (= empty AND-states), the modal configuration is just an empty object
|
||||||
export type Mode = {[uid:string]: Mode};
|
// export type Mode = {[uid:string]: Mode};
|
||||||
|
|
||||||
|
|
||||||
|
export type Mode = Set<string>; // set of active states
|
||||||
|
|
||||||
|
|
||||||
export type Environment = ReadonlyMap<string, any>; // variable name -> value
|
export type Environment = ReadonlyMap<string, any>; // variable name -> value
|
||||||
|
|
||||||
|
|
@ -10,8 +14,6 @@ export type RT_Statechart = {
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
// history: // TODO
|
// history: // TODO
|
||||||
|
|
||||||
inputEvents: string[];
|
|
||||||
} & RaisedEvents;
|
} & RaisedEvents;
|
||||||
|
|
||||||
export type RaisedEvents = {
|
export type RaisedEvents = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue