simple execution works
This commit is contained in:
parent
2b73da9387
commit
3f2db4457f
9 changed files with 412 additions and 52 deletions
11
src/App.tsx
11
src/App.tsx
|
|
@ -1,11 +0,0 @@
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
import { VisualEditor } from "./VisualEditor/VisualEditor";
|
|
||||||
|
|
||||||
export function App() {
|
|
||||||
return (
|
|
||||||
<VisualEditor/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
31
src/App/App.css
Normal file
31
src/App/App.css
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
.layoutVertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
height: 1.5rem;
|
||||||
|
background-color: lightgrey;
|
||||||
|
}
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
flex: 0 0 content;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
padding-left :10;
|
||||||
|
}
|
||||||
|
/* details:not(:has(details)) > summary::marker {
|
||||||
|
color: white;
|
||||||
|
} */
|
||||||
154
src/App/App.tsx
Normal file
154
src/App/App.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { ConcreteState, emptyStatechart, Statechart, Transition } from "../VisualEditor/ast";
|
||||||
|
|
||||||
|
import { VisualEditor } from "../VisualEditor/VisualEditor";
|
||||||
|
import { RT_Statechart } from "../VisualEditor/runtime_types";
|
||||||
|
import { initialize, raiseEvent } from "../VisualEditor/interpreter";
|
||||||
|
|
||||||
|
import "../index.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}) {
|
||||||
|
return <>➔ {stateDescription(props.transition.tgt)}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShowExpr(props: {expr: Expression}) {
|
||||||
|
if (props.expr.kind === "literal") {
|
||||||
|
return <>{props.expr.value}</>;
|
||||||
|
}
|
||||||
|
else if (props.expr.kind === "ref") {
|
||||||
|
return <>{props.expr.variable}</>;
|
||||||
|
}
|
||||||
|
else if (props.expr.kind === "unaryExpr") {
|
||||||
|
return <>{props.expr.operator}<ShowExpr expr={props.expr.expr}/></>;
|
||||||
|
}
|
||||||
|
else if (props.expr.kind === "binaryExpr") {
|
||||||
|
return <><ShowExpr expr={props.expr.lhs}/>{props.expr.operator}<ShowExpr expr={props.expr.rhs}/></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShowAction(props: {action: Action}) {
|
||||||
|
if (props.action.kind === "raise") {
|
||||||
|
return <>raise {props.action.event}</>;
|
||||||
|
}
|
||||||
|
else if (props.action.kind === "assignment") {
|
||||||
|
return <>{props.action.lhs} = <ShowExpr expr={props.action.rhs}/>;</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>}) {
|
||||||
|
const description = stateDescription(props.root);
|
||||||
|
const outgoing = props.transitions.get(props.root.uid) || [];
|
||||||
|
|
||||||
|
return <details open={true}>
|
||||||
|
<summary>{props.root.kind}: {description}</summary>
|
||||||
|
|
||||||
|
{props.root.entryActions.length>0 &&
|
||||||
|
<details open={true}>
|
||||||
|
<summary>entry actions</summary>
|
||||||
|
{props.root.entryActions.map(action =>
|
||||||
|
<div> <ShowAction action={action}/></div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
{props.root.exitActions.length>0 &&
|
||||||
|
<details open={true}>
|
||||||
|
<summary>exit actions</summary>
|
||||||
|
{props.root.exitActions.map(action =>
|
||||||
|
<ShowAction action={action}/>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
{props.root.children.length>0 &&
|
||||||
|
<details open={true}>
|
||||||
|
<summary>children</summary>
|
||||||
|
{props.root.children.map(child =>
|
||||||
|
<AST root={child} transitions={props.transitions} />
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
{outgoing.length>0 &&
|
||||||
|
<details open={true}>
|
||||||
|
<summary>outgoing</summary>
|
||||||
|
{outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [ast, setAST] = useState<Statechart>(emptyStatechart);
|
||||||
|
const [errors, setErrors] = useState<[string,string][]>([]);
|
||||||
|
const [rt, setRT] = useState<RT_Statechart|null>(null);
|
||||||
|
|
||||||
|
const [paused, setPaused] = useState(true);
|
||||||
|
const [timescale, setTimescale] = useState(1);
|
||||||
|
|
||||||
|
function restart() {
|
||||||
|
const rt = initialize(ast);
|
||||||
|
console.log('runtime: ', rt);
|
||||||
|
setRT(rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
setRT(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function raise(event: string) {
|
||||||
|
if (rt && ast.inputEvents.has(event)) {
|
||||||
|
const nextConfigs = raiseEvent(event, ast, ast.root, rt)
|
||||||
|
console.log({nextConfigs});
|
||||||
|
if (nextConfigs.length > 0) {
|
||||||
|
if (nextConfigs.length > 1) {
|
||||||
|
console.warn('non-determinism, blindly selecting first next run-time state!');
|
||||||
|
}
|
||||||
|
setRT(nextConfigs[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="layoutVertical">
|
||||||
|
<div className="panel">
|
||||||
|
<button onClick={restart}>(re)start</button>
|
||||||
|
<select disabled={rt===null} value="raise event..." onChange={e => raise(e.target.value)}>
|
||||||
|
<option value="">raise event...</option>
|
||||||
|
{[...ast.inputEvents].map(event =>
|
||||||
|
<option value={event}>{event}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<button onClick={stop} >stop</button>
|
||||||
|
 
|
||||||
|
<input type="radio" name="paused" id="radio-paused" checked={paused} disabled={rt===null}/>
|
||||||
|
<label htmlFor="radio-paused">paused</label>
|
||||||
|
<input type="radio" name="realtime" id="radio-realtime" checked={!paused} disabled={rt===null}/>
|
||||||
|
<label htmlFor="radio-realtime">real-time</label>
|
||||||
|
 
|
||||||
|
<label htmlFor="number-timescale">timescale</label>
|
||||||
|
<input type="number" id="number-timescale" disabled={rt===null} value={timescale} style={{width:40}}/>
|
||||||
|
</div>
|
||||||
|
<div className="layout">
|
||||||
|
<main className="content">
|
||||||
|
<VisualEditor {...{ast, setAST, rt, setRT, errors, setErrors}}/>
|
||||||
|
</main>
|
||||||
|
<aside className="sidebar">
|
||||||
|
<AST {...ast}/>
|
||||||
|
<hr/>
|
||||||
|
{rt &&
|
||||||
|
[...rt.environment.entries()].map(([variable,value]) => <>
|
||||||
|
{variable}: {value}<br/>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -30,7 +30,7 @@ text.highlight {
|
||||||
}
|
}
|
||||||
|
|
||||||
.rountangle {
|
.rountangle {
|
||||||
fill: rgba(255, 255, 255, 255);
|
fill: white;
|
||||||
/* fill: none; */
|
/* fill: none; */
|
||||||
stroke: black;
|
stroke: black;
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
|
|
@ -58,6 +58,10 @@ text.highlight {
|
||||||
.rountangle.error {
|
.rountangle.error {
|
||||||
stroke: rgb(230,0,0);
|
stroke: rgb(230,0,0);
|
||||||
}
|
}
|
||||||
|
.rountangle.active {
|
||||||
|
fill: rgb(255, 196, 0);
|
||||||
|
fill-opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.selected:hover {
|
.selected:hover {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react";
|
import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react";
|
||||||
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
|
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
|
||||||
|
|
||||||
import "./VisualEditor.css";
|
import "./VisualEditor.css";
|
||||||
|
|
@ -9,7 +9,9 @@ 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 { initialize } from "./interpreter";
|
import { getActiveStates, initialize } from "./interpreter";
|
||||||
|
import { RT_Statechart } from "./runtime_types";
|
||||||
|
import { emptyStatechart, Statechart } from "./ast";
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
type DraggingState = {
|
||||||
|
|
@ -48,7 +50,16 @@ export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [
|
||||||
["bottom", getBottomSide],
|
["bottom", getBottomSide],
|
||||||
];
|
];
|
||||||
|
|
||||||
export function VisualEditor() {
|
type VisualEditorProps = {
|
||||||
|
ast: Statechart,
|
||||||
|
setAST: Dispatch<SetStateAction<Statechart>>,
|
||||||
|
rt: RT_Statechart|null,
|
||||||
|
setRT: Dispatch<SetStateAction<RT_Statechart|null>>,
|
||||||
|
errors: [string,string][],
|
||||||
|
setErrors: Dispatch<SetStateAction<[string,string][]>>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function VisualEditor({ast, setAST, rt, setRT, errors, setErrors}: VisualEditorProps) {
|
||||||
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
|
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
|
||||||
|
|
||||||
const state = historyState.current;
|
const state = historyState.current;
|
||||||
|
|
@ -110,8 +121,6 @@ export function VisualEditor() {
|
||||||
// not null while the user is making a selection
|
// not null while the user is making a selection
|
||||||
const [selectingState, setSelectingState] = useState<SelectingState>(null);
|
const [selectingState, setSelectingState] = useState<SelectingState>(null);
|
||||||
|
|
||||||
const [errors, setErrors] = useState<[string,string][]>([]);
|
|
||||||
|
|
||||||
const refSVG = useRef<SVGSVGElement>(null);
|
const refSVG = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -139,9 +148,7 @@ export function VisualEditor() {
|
||||||
const [statechart, errors] = parseStatechart(state);
|
const [statechart, errors] = parseStatechart(state);
|
||||||
console.log('statechart: ', statechart, 'errors:', errors);
|
console.log('statechart: ', statechart, 'errors:', errors);
|
||||||
setErrors(errors);
|
setErrors(errors);
|
||||||
|
setAST(statechart);
|
||||||
const rt = initialize(statechart);
|
|
||||||
console.log('runtime:', rt);
|
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
@ -489,6 +496,8 @@ export function VisualEditor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const active = getActiveStates(rt?.mode || {});
|
||||||
|
|
||||||
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]);
|
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]);
|
||||||
|
|
||||||
return <svg width="4000px" height="4000px"
|
return <svg width="4000px" height="4000px"
|
||||||
|
|
@ -518,6 +527,7 @@ export function VisualEditor() {
|
||||||
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
||||||
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[])]}
|
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[])]}
|
||||||
errors={errors.filter(([uid,msg])=>uid===rountangle.uid).map(err=>err[1])}
|
errors={errors.filter(([uid,msg])=>uid===rountangle.uid).map(err=>err[1])}
|
||||||
|
active={active.has(rountangle.uid)}
|
||||||
/>)}
|
/>)}
|
||||||
|
|
||||||
{state.arrows.map(arrow => {
|
{state.arrows.map(arrow => {
|
||||||
|
|
@ -631,7 +641,7 @@ function rountangleMinSize(size: Vec2D): Vec2D {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RountangleSVG(props: {rountangle: Rountangle, selected: string[], highlight: RountanglePart[], errors: string[]}) {
|
export function RountangleSVG(props: {rountangle: Rountangle, selected: string[], highlight: RountanglePart[], errors: string[], active: boolean}) {
|
||||||
const {topLeft, size, uid} = props.rountangle;
|
const {topLeft, size, uid} = props.rountangle;
|
||||||
// always draw a rountangle with a minimum size
|
// always draw a rountangle with a minimum size
|
||||||
// during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it
|
// during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it
|
||||||
|
|
@ -642,6 +652,7 @@ export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]
|
||||||
+(props.selected.length===4?" selected":"")
|
+(props.selected.length===4?" selected":"")
|
||||||
+((props.rountangle.kind==="or")?" or":"")
|
+((props.rountangle.kind==="or")?" or":"")
|
||||||
+(props.errors.length>0?" error":"")
|
+(props.errors.length>0?" error":"")
|
||||||
|
+(props.active?" active":"")
|
||||||
}
|
}
|
||||||
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
|
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
|
||||||
x={0}
|
x={0}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import { Action, TransitionLabel } from "./label_ast";
|
||||||
|
|
||||||
export type AbstractState = {
|
export type AbstractState = {
|
||||||
uid: string;
|
uid: string;
|
||||||
|
parent?: ConcreteState;
|
||||||
children: ConcreteState[];
|
children: ConcreteState[];
|
||||||
comments: [string, string][]; // array of tuple (text-uid, text-text)
|
comments: [string, string][]; // array of tuple (text-uid, text-text)
|
||||||
entryActions: Action[];
|
entryActions: Action[];
|
||||||
exitActions: Action[];
|
exitActions: Action[];
|
||||||
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AndState = {
|
export type AndState = {
|
||||||
|
|
@ -37,4 +39,61 @@ export type Statechart = {
|
||||||
inputEvents: Set<string>;
|
inputEvents: Set<string>;
|
||||||
internalEvents: Set<string>;
|
internalEvents: Set<string>;
|
||||||
outputEvents: Set<string>;
|
outputEvents: Set<string>;
|
||||||
|
|
||||||
|
uid2State: Map<string, ConcreteState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emptyStatechart: Statechart = {
|
||||||
|
root: {
|
||||||
|
uid: "root",
|
||||||
|
kind: "or",
|
||||||
|
initial: [],
|
||||||
|
children:[],
|
||||||
|
comments: [],
|
||||||
|
entryActions: [],
|
||||||
|
exitActions: [],
|
||||||
|
},
|
||||||
|
transitions: new Map(),
|
||||||
|
variables: new Set(),
|
||||||
|
inputEvents: new Set(),
|
||||||
|
internalEvents: new Set(),
|
||||||
|
outputEvents: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// reflexive, transitive relation
|
||||||
|
export function isAncestorOf({ancestor, descendant}: {ancestor: ConcreteState, descendant: ConcreteState}): ConcreteState[] | false {
|
||||||
|
if (ancestor.uid === descendant.uid) {
|
||||||
|
return [descendant];
|
||||||
|
}
|
||||||
|
if (ancestor.depth >= descendant.depth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pathToParent = isAncestorOf({ancestor, descendant: descendant.parent!});
|
||||||
|
return pathToParent && [...pathToParent, descendant];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): {
|
||||||
|
arena: ConcreteState,
|
||||||
|
srcPath: ConcreteState[],
|
||||||
|
tgtPath: ConcreteState[],
|
||||||
|
} {
|
||||||
|
if (src.depth >= tgt.depth) {
|
||||||
|
const path = isAncestorOf({descendant: src, ancestor: tgt});
|
||||||
|
if (path) {
|
||||||
|
if (tgt.kind === "or") {
|
||||||
|
return {arena: tgt, srcPath: path, tgtPath: [tgt]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// keep looking
|
||||||
|
const {arena, srcPath, tgtPath} = computeArena({src, tgt: tgt.parent!});
|
||||||
|
return {arena, srcPath, tgtPath: [...tgtPath, tgt]};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// same, but swap src and tgt
|
||||||
|
const {arena, srcPath, tgtPath} = computeArena({src: tgt, tgt: src});
|
||||||
|
return {arena, srcPath: tgtPath, tgtPath: srcPath};
|
||||||
|
}
|
||||||
|
throw new Error("should never reach here");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { ConcreteState, Statechart } from "./ast";
|
import { computeArena, ConcreteState, isAncestorOf, Statechart, Transition } from "./ast";
|
||||||
import { Action, Expression } from "./label_ast";
|
import { Action, Expression } 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} = enter(ast.root, {
|
const {mode, environment, ...raised} = enterDefault(ast.root, {
|
||||||
environment: new Map(),
|
environment: new Map(),
|
||||||
raised: initialRaised,
|
...initialRaised,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -17,59 +17,102 @@ export function initialize(ast: Statechart): RT_Statechart {
|
||||||
|
|
||||||
type ActionScope = {
|
type ActionScope = {
|
||||||
environment: Environment,
|
environment: Environment,
|
||||||
raised: RaisedEvents,
|
} & RaisedEvents;
|
||||||
};
|
|
||||||
|
|
||||||
type EnteredScope = { mode: Mode } & ActionScope;
|
type EnteredScope = { mode: Mode } & ActionScope;
|
||||||
|
|
||||||
export function enter(state: ConcreteState, rt: ActionScope): EnteredScope {
|
export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScope {
|
||||||
let {environment, raised} = rt;
|
let actionScope = rt;
|
||||||
|
|
||||||
// execute entry actions
|
// execute entry actions
|
||||||
for (const action of state.entryActions) {
|
for (const action of state.entryActions) {
|
||||||
({environment, raised} = execAction(action, {environment, raised}));
|
(actionScope = execAction(action, actionScope));
|
||||||
}
|
}
|
||||||
|
|
||||||
// enter children...
|
// enter children...
|
||||||
const mode: {[uid:string]: Mode} = {};
|
const mode: {[uid:string]: Mode} = {};
|
||||||
if (state.kind === "and") {
|
if (state.kind === "and") {
|
||||||
|
// enter every child
|
||||||
for (const child of state.children) {
|
for (const child of state.children) {
|
||||||
let childMode;
|
let childMode;
|
||||||
({mode: childMode, environment, raised} = enter(child, {environment, raised}));
|
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
||||||
mode[child.uid] = childMode;
|
mode[child.uid] = childMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (state.kind === "or") {
|
else if (state.kind === "or") {
|
||||||
const mode: {[uid:string]: Mode} = {};
|
|
||||||
// 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 childMode;
|
||||||
({mode: childMode, environment, raised} = enter(child, {environment, raised}));
|
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
||||||
mode[child.uid] = childMode;
|
mode[child.uid] = childMode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mode, environment, raised };
|
return { mode, ...actionScope };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exit(state: ConcreteState, rt: EnteredScope): ActionScope {
|
export function enterPath(path: ConcreteState[], rt: ActionScope): EnteredScope {
|
||||||
let {mode, environment, raised} = rt;
|
let actionScope = rt;
|
||||||
|
|
||||||
|
const [state, ...rest] = path;
|
||||||
|
|
||||||
|
// execute entry actions
|
||||||
|
for (const action of state.entryActions) {
|
||||||
|
(actionScope = execAction(action, actionScope));
|
||||||
|
}
|
||||||
|
|
||||||
|
// enter children...
|
||||||
|
|
||||||
|
const mode: {[uid:string]: Mode} = {};
|
||||||
|
if (state.kind === "and") {
|
||||||
|
// enter every child
|
||||||
|
for (const child of state.children) {
|
||||||
|
let childMode;
|
||||||
|
if (rest.length > 0 && child.uid === rest[0].uid) {
|
||||||
|
({mode: childMode, ...actionScope} = enterPath(rest, actionScope));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
||||||
|
}
|
||||||
|
mode[child.uid] = childMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (state.kind === "or") {
|
||||||
|
if (rest.length > 0) {
|
||||||
|
let childMode;
|
||||||
|
({mode: childMode, ...actionScope} = enterPath(rest, actionScope));
|
||||||
|
mode[rest[0].uid] = childMode;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// same as AND-state, but we only enter the initial state(s)
|
||||||
|
for (const [_, child] of state.initial) {
|
||||||
|
let childMode;
|
||||||
|
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
|
||||||
|
mode[child.uid] = childMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mode, ...actionScope };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitCurrent(state: ConcreteState, rt: EnteredScope): ActionScope {
|
||||||
|
let {mode, ...actionScope} = rt;
|
||||||
|
|
||||||
// exit all active children...
|
// exit all active children...
|
||||||
for (const [childUid, childMode] of Object.entries(mode)) {
|
for (const [childUid, childMode] of Object.entries(mode)) {
|
||||||
const child = state.children.find(child => child.uid === childUid);
|
const child = state.children.find(child => child.uid === childUid);
|
||||||
if (child) {
|
if (child) {
|
||||||
({environment, raised} = exit(child, {mode: childMode, environment, raised}));
|
(actionScope = exitCurrent(child, {mode: childMode, ...actionScope}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute exit actions
|
// execute exit actions
|
||||||
for (const action of state.exitActions) {
|
for (const action of state.exitActions) {
|
||||||
({environment, raised} = execAction(action, {environment, raised}));
|
(actionScope = execAction(action, actionScope));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {environment, raised};
|
return actionScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
|
|
@ -87,19 +130,14 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
// append to internal events
|
// append to internal events
|
||||||
return {
|
return {
|
||||||
...rt,
|
...rt,
|
||||||
raised: {
|
internalEvents: [...rt.internalEvents, action.event],
|
||||||
...rt.raised,
|
|
||||||
internalEvents: [...rt.raised.internalEvents, action.event]},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// append to output events
|
// append to output events
|
||||||
return {
|
return {
|
||||||
...rt,
|
...rt,
|
||||||
raised: {
|
outputEvents: [...rt.outputEvents, action.event],
|
||||||
...rt.raised,
|
|
||||||
outputEvents: [...rt.raised.outputEvents, action.event],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,10 +180,81 @@ export function evalExpr(expr: Expression, environment: Environment): any {
|
||||||
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
|
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
|
||||||
}
|
}
|
||||||
else if (expr.kind === "binaryExpr") {
|
else if (expr.kind === "binaryExpr") {
|
||||||
const argA = evalExpr(expr.lhs, environment);
|
const lhs = evalExpr(expr.lhs, environment);
|
||||||
const argB = evalExpr(expr.rhs, environment);
|
const rhs = evalExpr(expr.rhs, environment);
|
||||||
return BINARY_OPERATOR_MAP.get(expr.operator)!(argA,argB);
|
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs,rhs);
|
||||||
}
|
}
|
||||||
throw new Error("should never reach here");
|
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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setModeDeep(oldMode: Mode, pathToState: ConcreteState[], newMode: Mode): Mode {
|
||||||
|
if (pathToState.length === 0) {
|
||||||
|
return newMode;
|
||||||
|
}
|
||||||
|
const [next, ...rest] = pathToState;
|
||||||
|
return {
|
||||||
|
...oldMode,
|
||||||
|
[next.uid]: setModeDeep(oldMode[next.uid], rest, newMode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetModeDeep(oldMode: Mode, pathToState: ConcreteState[]): Mode {
|
||||||
|
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 {
|
||||||
|
const {arena, srcPath, tgtPath} = computeArena(t);
|
||||||
|
const pathToArena = isAncestorOf({ancestor: statechart.root, descendant: arena}) as ConcreteState[];
|
||||||
|
console.log('fire ', t.src.comments[0][1], '->', t.tgt.comments[0][1], {srcPath, tgtPath});
|
||||||
|
let {environment, ...raised} = exitCurrent(srcPath[1], rt);
|
||||||
|
const exitedMode = unsetModeDeep(rt.mode, [...pathToArena.slice(1), ...srcPath.slice(1)]);
|
||||||
|
for (const action of t.label[0].actions) {
|
||||||
|
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
||||||
|
}
|
||||||
|
let deepMode;
|
||||||
|
({mode: deepMode, environment, ...raised} = enterPath(tgtPath.slice(1), {environment, ...raised}));
|
||||||
|
// console.log('entered path:', tgtPath.slice(1), {deepMode});
|
||||||
|
const enteredMode = setModeDeep(exitedMode, [...pathToArena.slice(1), ...tgtPath.slice(1)], deepMode);
|
||||||
|
// console.log('pathToArena:', pathToArena, 'newMode:', enteredMode);
|
||||||
|
return {mode: enteredMode, environment, inputEvents: rt.inputEvents, ...raised};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
comments: [],
|
comments: [],
|
||||||
entryActions: [],
|
entryActions: [],
|
||||||
exitActions: [],
|
exitActions: [],
|
||||||
|
depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid2State = new Map<string, ConcreteState>([["root", root]]);
|
const uid2State = new Map<string, ConcreteState>([["root", root]]);
|
||||||
|
|
@ -54,10 +55,12 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
const candidate = parentCandidates[i];
|
const candidate = parentCandidates[i];
|
||||||
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
|
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
|
||||||
// found our parent :)
|
// found our parent :)
|
||||||
const parentState = uid2State.get(candidate.uid);
|
const parentState = uid2State.get(candidate.uid)!;
|
||||||
parentState!.children.push(state);
|
parentState.children.push(state);
|
||||||
parentCandidates.push(rt);
|
parentCandidates.push(rt);
|
||||||
parentLinks.set(rt.uid, candidate.uid);
|
parentLinks.set(rt.uid, candidate.uid);
|
||||||
|
state.parent = parentState;
|
||||||
|
state.depth = parentState.depth+1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { App } from "./App";
|
import { App } from "./App/App";
|
||||||
|
|
||||||
const elem = document.getElementById("root")!;
|
const elem = document.getElementById("root")!;
|
||||||
const app = (
|
const app = (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue