diff --git a/src/App/App.tsx b/src/App/App.tsx index 1a4198a..8dac4b1 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,19 +1,13 @@ 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 { 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 "./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)}; @@ -51,34 +45,22 @@ export function AST(props: {root: ConcreteState, transitions: Map{props.root.kind}: {description} {props.root.entryActions.length>0 && -
- entry actions - {props.root.entryActions.map(action => -
- )} -
+ props.root.entryActions.map(action => +
 entry /
+ ) } {props.root.exitActions.length>0 && -
- exit actions - {props.root.exitActions.map(action => - - )} -
+ props.root.exitActions.map(action => +
 exit /
+ ) } {props.root.children.length>0 && -
- children - {props.root.children.map(child => + props.root.children.map(child => - )} -
+ ) } {outgoing.length>0 && -
- outgoing - {outgoing.map(transition => <> 
)} -
+ outgoing.map(transition => <> 
) } } @@ -104,14 +86,15 @@ export function App() { 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]); - } + const nextConfig = handleEvent(event, ast, ast.root, rt); + setRT(nextConfig); + // 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]); + // } } } @@ -126,9 +109,9 @@ export function App() {   - + setPaused(e.target.checked)}/> - + setPaused(!e.target.checked)}/> diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css index 8ce73d5..a525fc7 100644 --- a/src/VisualEditor/VisualEditor.css +++ b/src/VisualEditor/VisualEditor.css @@ -60,7 +60,7 @@ text.highlight { } .rountangle.active { fill: rgb(255, 196, 0); - fill-opacity: 0.6; + fill-opacity: 0.2; } .selected:hover { diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 8185bee..7d41632 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -9,9 +9,8 @@ import { parseStatechart } from "./parser"; import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters"; import * as lz4 from "@nick/lz4"; -import { getActiveStates, initialize } from "./interpreter"; import { RT_Statechart } from "./runtime_types"; -import { emptyStatechart, Statechart } from "./ast"; +import { Statechart } from "./ast"; 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"]})), ]); } + + 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]); diff --git a/src/VisualEditor/actionlang_interpreter.ts b/src/VisualEditor/actionlang_interpreter.ts new file mode 100644 index 0000000..cca5b31 --- /dev/null +++ b/src/VisualEditor/actionlang_interpreter.ts @@ -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 any> = new Map([ + ["!", x => !x], + ["-", x => -x as any], +]); +const BINARY_OPERATOR_MAP: Map 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"); +} diff --git a/src/VisualEditor/ast.ts b/src/VisualEditor/ast.ts index f605f54..37469e3 100644 --- a/src/VisualEditor/ast.ts +++ b/src/VisualEditor/ast.ts @@ -43,21 +43,25 @@ export type Statechart = { uid2State: Map; } +const emptyRoot: OrState = { + uid: "root", + kind: "or", + depth: 0, + initial: [], + children:[], + comments: [], + entryActions: [], + exitActions: [], +}; + export const emptyStatechart: Statechart = { - root: { - uid: "root", - kind: "or", - initial: [], - children:[], - comments: [], - entryActions: [], - exitActions: [], - }, + root: emptyRoot, transitions: new Map(), variables: new Set(), inputEvents: new Set(), internalEvents: new Set(), outputEvents: new Set(), + uid2State: new Map([["root", emptyRoot]]), }; // reflexive, transitive relation @@ -72,10 +76,19 @@ export function isAncestorOf({ancestor, descendant}: {ancestor: ConcreteState, d 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 // see "Deconstructing the Semantics of Big-Step Modelling Languages" by Shahram Esmaeilsabzali, 2009 export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): { - arena: ConcreteState, + arena: OrState, srcPath: 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}); return {arena, srcPath: tgtPath, tgtPath: srcPath}; } - throw new Error("should never reach here"); } + +export function getDescendants(state: ConcreteState): Set { + 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; +} + diff --git a/src/VisualEditor/interpreter.ts b/src/VisualEditor/interpreter.ts index ba0fb15..3f2b645 100644 --- a/src/VisualEditor/interpreter.ts +++ b/src/VisualEditor/interpreter.ts @@ -1,16 +1,17 @@ -import { computeArena, ConcreteState, isAncestorOf, Statechart, Transition } from "./ast"; -import { Action, Expression } from "./label_ast"; +import { act } from "react"; +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"; export function initialize(ast: Statechart): RT_Statechart { - const {mode, environment, ...raised} = enterDefault(ast.root, { + const {enteredStates, environment, ...raised} = enterDefault(ast.root, { environment: new Map(), ...initialRaised, }); return { - mode, + mode: enteredStates, environment, - inputEvents: [], ...raised, }; } @@ -19,7 +20,7 @@ type ActionScope = { environment: Environment, } & RaisedEvents; -type EnteredScope = { mode: Mode } & ActionScope; +type EnteredScope = { enteredStates: Mode } & ActionScope; export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScope { let actionScope = rt; @@ -30,25 +31,29 @@ export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScop } // enter children... - const mode: {[uid:string]: Mode} = {}; + let enteredStates = new Set([state.uid]); if (state.kind === "and") { // enter every child for (const child of state.children) { - let childMode; - ({mode: childMode, ...actionScope} = enterDefault(child, actionScope)); - mode[child.uid] = childMode; + let enteredChildren; + ({enteredStates: enteredChildren, ...actionScope} = enterDefault(child, actionScope)); + enteredStates = enteredStates.union(enteredChildren); } } else if (state.kind === "or") { // 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; + if (state.initial.length > 0) { + if (state.initial.length > 1) { + console.warn(state.uid + ': multiple initial states, only entering one of them'); + } + 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 { @@ -62,48 +67,47 @@ export function enterPath(path: ConcreteState[], rt: ActionScope): EnteredScope } // enter children... - - const mode: {[uid:string]: Mode} = {}; + let enteredStates = new Set([state.uid]); if (state.kind === "and") { // enter every child for (const child of state.children) { - let childMode; + let enteredChildren; if (rest.length > 0 && child.uid === rest[0].uid) { - ({mode: childMode, ...actionScope} = enterPath(rest, actionScope)); + ({enteredStates: enteredChildren, ...actionScope} = enterPath(rest, actionScope)); } 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") { if (rest.length > 0) { - let childMode; - ({mode: childMode, ...actionScope} = enterPath(rest, actionScope)); - mode[rest[0].uid] = childMode; + let enteredChildren; + ({enteredStates: enteredChildren, ...actionScope} = enterPath(rest, actionScope)); + enteredStates = enteredStates.union(enteredChildren); } 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; + let enteredChildren; + ({enteredStates: enteredChildren, ...actionScope} = enterDefault(child, actionScope)); + 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 { - let {mode, ...actionScope} = rt; + let {enteredStates, ...actionScope} = rt; // exit all active children... - for (const [childUid, childMode] of Object.entries(mode)) { - const child = state.children.find(child => child.uid === childUid); - if (child) { - (actionScope = exitCurrent(child, {mode: childMode, ...actionScope})); + for (const child of state.children) { + if (enteredStates.has(child.uid)) { + actionScope = exitCurrent(child, {enteredStates, ...actionScope}); } } @@ -115,6 +119,27 @@ export function exitCurrent(state: ConcreteState, rt: EnteredScope): 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 { if (action.kind === "assignment") { 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"); } - -const UNARY_OPERATOR_MAP: Mapany> = new Map([ - ["!", x => !x], - ["-", x => -x as any], -]); - -const BINARY_OPERATOR_MAP: Mapany> = 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", (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"); -} - -export function getActiveStates(mode: Mode): Set { - 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); +export function handleEvent(event: string, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart): RT_Statechart { + const arenasFired = new Set(); + for (const state of activeParent.children) { + 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 enabled = triggered.filter(transition => + evalExpr(transition.label[0].guard, environment) + ); + if (enabled.length > 0) { + if (enabled.length > 1) { + console.warn('nondeterminism!!!!'); + } + const t = enabled[0]; + console.log('enabled:', transitionDescription(t)); + const {arena, srcPath, tgtPath} = computeArena(t); + let overlapping = false; + for (const alreadyFired of arenasFired) { + if (isOverlapping(arena, alreadyFired)) { + overlapping = true; + } + } + if (!overlapping) { + console.log('^ firing'); + ({mode, environment, ...raised} = fireTransition(t, arena, srcPath, tgtPath, {mode, environment, ...raised})); + arenasFired.add(arena); + } + else { + console.log('skip (overlapping arenas)'); + } + } + else { + // no enabled outgoing transitions, try the children: + ({environment, mode, ...raised} = handleEvent(event, statechart, state, {environment, mode, ...raised})); + } } } - return []; + return {environment, mode, ...raised}; } -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 transitionDescription(t: Transition) { + return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt); } -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, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart): {mode: Mode, environment: Environment} & RaisedEvents { -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)]); + console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath}); + + // exit src + ({environment, ...raised} = exitPath(srcPath.slice(1), {environment, enteredStates: mode, ...raised})); + const toExit = getDescendants(arena); + 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) { ({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}; + + // enter tgt + let enteredStates; + ({enteredStates, environment, ...raised} = enterPath(tgtPath.slice(1), {environment, ...raised})); + const enteredMode = exitedMode.union(enteredStates); + + console.log('enteredMode', enteredMode); + + return {mode: enteredMode, environment, ...raised}; } diff --git a/src/VisualEditor/parser.ts b/src/VisualEditor/parser.ts index a9d08eb..9718543 100644 --- a/src/VisualEditor/parser.ts +++ b/src/VisualEditor/parser.ts @@ -225,6 +225,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, inputEvents, internalEvents, outputEvents, + uid2State, }, errorShapes]; } diff --git a/src/VisualEditor/runtime_types.ts b/src/VisualEditor/runtime_types.ts index 093c340..0dc9e19 100644 --- a/src/VisualEditor/runtime_types.ts +++ b/src/VisualEditor/runtime_types.ts @@ -2,7 +2,11 @@ // 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 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; // set of active states + export type Environment = ReadonlyMap; // variable name -> value @@ -10,8 +14,6 @@ export type RT_Statechart = { mode: Mode; environment: Environment; // history: // TODO - - inputEvents: string[]; } & RaisedEvents; export type RaisedEvents = {