pseudo-states appear to be working + variables only exist within the scope where they are created
This commit is contained in:
parent
db1479bfc4
commit
d4930eb13d
22 changed files with 742 additions and 569 deletions
|
|
@ -1,33 +1,43 @@
|
|||
import { Action, EventTrigger, ParsedText, TransitionLabel } from "./label_ast";
|
||||
import { Action, EventTrigger, ParsedText } from "./label_ast";
|
||||
|
||||
export type AbstractState = {
|
||||
uid: string;
|
||||
parent?: ConcreteState;
|
||||
children: ConcreteState[];
|
||||
comments: [string, string][]; // array of tuple (text-uid, text-text)
|
||||
entryActions: Action[];
|
||||
exitActions: Action[];
|
||||
depth: number;
|
||||
timers: number[]; // list of timeouts (e.g., the state having an outgoing transition with trigger "after 4s" would appear as the number 4000 in this list)
|
||||
}
|
||||
|
||||
export type StableState = {
|
||||
kind: "and" | "or";
|
||||
children: ConcreteState[];
|
||||
timers: number[]; // list of timeouts (e.g., the state having an outgoing transition with trigger "after 4s" would appear as the number 4000 in this list)
|
||||
} & AbstractState;
|
||||
|
||||
export type AndState = {
|
||||
kind: "and";
|
||||
} & AbstractState;
|
||||
} & StableState;
|
||||
|
||||
export type OrState = {
|
||||
kind: "or";
|
||||
// array of tuples: (uid of Arrow indicating initial state, initial state)
|
||||
// in a valid AST, there must be one initial state, but we allow the user to draw crazy shit
|
||||
initial: [string, ConcreteState][];
|
||||
} & AbstractState;
|
||||
} & StableState;
|
||||
|
||||
export type PseudoState = {
|
||||
kind: "pseudo";
|
||||
uid: string;
|
||||
comments: [string, string][];
|
||||
};
|
||||
|
||||
export type ConcreteState = AndState | OrState;
|
||||
|
||||
export type Transition = {
|
||||
uid: string;
|
||||
src: ConcreteState;
|
||||
tgt: ConcreteState;
|
||||
uid: string; // uid of arrow in concrete syntax
|
||||
src: ConcreteState | PseudoState;
|
||||
tgt: ConcreteState | PseudoState;
|
||||
label: ParsedText[];
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +51,7 @@ export type Statechart = {
|
|||
internalEvents: EventTrigger[];
|
||||
outputEvents: Set<string>;
|
||||
|
||||
uid2State: Map<string, ConcreteState>;
|
||||
uid2State: Map<string, ConcreteState|PseudoState>;
|
||||
}
|
||||
|
||||
const emptyRoot: OrState = {
|
||||
|
|
@ -87,6 +97,57 @@ export function isOverlapping(a: ConcreteState, b: ConcreteState): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function computeLCA(a: ConcreteState, b: ConcreteState): ConcreteState {
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a.depth > b.depth) {
|
||||
return computeLCA(a.parent!, b);
|
||||
}
|
||||
return computeLCA(a, b.parent!);
|
||||
}
|
||||
|
||||
export function computeLCA2(states: ConcreteState[]): ConcreteState {
|
||||
if (states.length === 0) {
|
||||
throw new Error("cannot compute LCA of empty set of states");
|
||||
}
|
||||
if (states.length === 1) {
|
||||
return states[0];
|
||||
}
|
||||
// 2 states or more
|
||||
return states.reduce((acc, cur) => computeLCA(acc, cur));
|
||||
}
|
||||
|
||||
export function getPossibleTargets(t: Transition, ts: Map<string, Transition[]>): ConcreteState[] {
|
||||
if (t.tgt.kind !== "pseudo") {
|
||||
return [t.tgt];
|
||||
}
|
||||
const pseudoOutgoing = ts.get(t.tgt.uid) || [];
|
||||
return pseudoOutgoing.flatMap(t => getPossibleTargets(t, ts));
|
||||
}
|
||||
|
||||
export function computeArena2(t: Transition, ts: Map<string, Transition[]>): OrState {
|
||||
const tgts = getPossibleTargets(t, ts);
|
||||
let lca = computeLCA2([t.src as ConcreteState, ...tgts]);
|
||||
while (lca.kind !== "or") {
|
||||
lca = lca.parent!;
|
||||
}
|
||||
return lca as OrState;
|
||||
}
|
||||
|
||||
// Assuming ancestor is already entered, what states to enter in order to enter descendants?
|
||||
// E.g.
|
||||
// root > A > B > C > D
|
||||
// computePath({ancestor: A, descendant: A}) = []
|
||||
// computePath({ancestor: A, descendant: C}) = [B, C]
|
||||
export function computePath({ancestor, descendant}: {ancestor: ConcreteState, descendant: ConcreteState}): ConcreteState[] {
|
||||
if (ancestor === descendant) {
|
||||
return [];
|
||||
}
|
||||
return [...computePath({ancestor, descendant: descendant.parent!}), 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}): {
|
||||
|
|
@ -98,7 +159,7 @@ export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState
|
|||
const path = isAncestorOf({descendant: src, ancestor: tgt});
|
||||
if (path) {
|
||||
if (tgt.kind === "or") {
|
||||
return {arena: tgt, srcPath: path, tgtPath: [tgt]};
|
||||
return {arena: tgt as OrState, srcPath: path, tgtPath: [tgt]};
|
||||
}
|
||||
}
|
||||
// keep looking
|
||||
|
|
@ -126,8 +187,7 @@ export function getDescendants(state: ConcreteState): Set<string> {
|
|||
// 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) {
|
||||
export function stateDescription(state: ConcreteState | PseudoState) {
|
||||
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
|
||||
return description;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,5 +42,10 @@ export function evalExpr(expr: Expression, environment: Environment): any {
|
|||
const rhs = evalExpr(expr.rhs, environment);
|
||||
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
|
||||
}
|
||||
else if (expr.kind === "call") {
|
||||
const fn = evalExpr(expr.fn, environment);
|
||||
const param = evalExpr(expr.param, environment);
|
||||
return fn(param);
|
||||
}
|
||||
throw new Error("should never reach here");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ import { sides } from "../VisualEditor/VisualEditor";
|
|||
|
||||
export type Rountangle = {
|
||||
uid: string;
|
||||
kind: "and" | "or" | "pseudo";
|
||||
kind: "and" | "or";
|
||||
} & Rect2D;
|
||||
|
||||
export type Diamond = {
|
||||
uid: string;
|
||||
} & Rect2D;
|
||||
|
||||
export type Text = {
|
||||
|
|
@ -21,6 +25,7 @@ export type VisualEditorState = {
|
|||
rountangles: Rountangle[];
|
||||
texts: Text[];
|
||||
arrows: Arrow[];
|
||||
diamonds: Diamond[];
|
||||
nextID: number;
|
||||
};
|
||||
|
||||
|
|
@ -28,8 +33,8 @@ export type VisualEditorState = {
|
|||
export type RountanglePart = "left" | "top" | "right" | "bottom";
|
||||
export type ArrowPart = "start" | "end";
|
||||
|
||||
export const emptyState = {
|
||||
rountangles: [], texts: [], arrows: [], nextID: 0,
|
||||
export const emptyState: VisualEditorState = {
|
||||
rountangles: [], texts: [], arrows: [], diamonds: [], nextID: 0,
|
||||
};
|
||||
|
||||
export const onOffStateMachine = {
|
||||
|
|
@ -45,7 +50,7 @@ export const onOffStateMachine = {
|
|||
};
|
||||
|
||||
// used to find which rountangle an arrow connects to (src/tgt)
|
||||
export function findNearestRountangleSide(arrow: Line2D, arrowPart: "start" | "end", candidates: Rountangle[]): {uid: string, part: RountanglePart} | undefined {
|
||||
export function findNearestSide(arrow: Line2D, arrowPart: "start" | "end", candidates: (Rountangle|Diamond)[]): {uid: string, part: RountanglePart} | undefined {
|
||||
let best = Infinity;
|
||||
let bestSide: undefined | {uid: string, part: RountanglePart};
|
||||
for (const rountangle of candidates) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { computeArena2, computePath, ConcreteState, getDescendants, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
||||
import { evalExpr } from "./actionlang_interpreter";
|
||||
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
||||
import { Action, AfterTrigger, EventTrigger } from "./label_ast";
|
||||
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event, TimerElapseEvent } from "./runtime_types";
|
||||
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
||||
import { BigStepOutput, Environment, initialRaised, Mode, RaisedEvents, RT_Event, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
|
||||
|
||||
export function initialize(ast: Statechart): BigStepOutput {
|
||||
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
|
||||
environment: new Environment(),
|
||||
environment: new Environment([new Map([["_timers", []]])]),
|
||||
...initialRaised,
|
||||
});
|
||||
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
|
||||
|
|
@ -18,12 +18,15 @@ type ActionScope = {
|
|||
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
||||
|
||||
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
||||
// console.log('enter', stateDescription(state), '...');
|
||||
let {environment, ...rest} = actionScope;
|
||||
environment = environment.pushScope();
|
||||
for (const action of state.entryActions) {
|
||||
(actionScope = execAction(action, actionScope));
|
||||
({environment, ...rest} = execAction(action, {environment, ...rest}));
|
||||
}
|
||||
// schedule timers
|
||||
// we store timers in the environment (dirty!)
|
||||
let environment = actionScope.environment.transform<Timers>("_timers", oldTimers => {
|
||||
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||
const newTimers = [
|
||||
...oldTimers,
|
||||
...state.timers.map(timeOffset => {
|
||||
|
|
@ -35,20 +38,21 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
|
|||
return newTimers;
|
||||
}, []);
|
||||
// new nested scope
|
||||
environment = environment.pushScope();
|
||||
return {...actionScope, environment};
|
||||
return {environment, ...rest};
|
||||
}
|
||||
|
||||
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
||||
// console.log('exit', stateDescription(state), '...');
|
||||
for (const action of state.exitActions) {
|
||||
(actionScope = execAction(action, actionScope));
|
||||
}
|
||||
let environment = actionScope.environment.popScope();
|
||||
let environment = actionScope.environment;
|
||||
// cancel timers
|
||||
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||
// remove all timers of 'state':
|
||||
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
|
||||
}, []);
|
||||
environment = environment.popScope();
|
||||
return {...actionScope, environment};
|
||||
}
|
||||
|
||||
|
|
@ -193,42 +197,42 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
|
|||
throw new Error("should never reach here");
|
||||
}
|
||||
|
||||
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||
const arenasFired = new Set<OrState>();
|
||||
for (const state of activeParent.children) {
|
||||
if (mode.has(state.uid)) {
|
||||
const outgoing = statechart.transitions.get(state.uid) || [];
|
||||
const labels = outgoing.flatMap(t =>
|
||||
t.label
|
||||
.filter(l => l.kind === "transitionLabel")
|
||||
.map(l => [t,l] as [Transition, TransitionLabel]));
|
||||
let triggered;
|
||||
if (event.kind === "input") {
|
||||
// get transitions triggered by event
|
||||
triggered = outgoing.filter(transition => {
|
||||
const trigger = transition.label[0].trigger;
|
||||
if (trigger.kind === "event") {
|
||||
return trigger.event === event.name;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
triggered = labels.filter(([_t,l]) =>
|
||||
l.trigger.kind === "event" && l.trigger.event === event.name);
|
||||
}
|
||||
else {
|
||||
// get transitions triggered by timeout
|
||||
triggered = outgoing.filter(transition => {
|
||||
const trigger = transition.label[0].trigger;
|
||||
if (trigger.kind === "after") {
|
||||
return trigger.durationMs === event.timeDurMs;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
triggered = labels.filter(([_t,l]) =>
|
||||
l.trigger.kind === "after" && l.trigger.durationMs === event.timeDurMs);
|
||||
}
|
||||
// eval guard
|
||||
const enabled = triggered.filter(transition =>
|
||||
evalExpr(transition.label[0].guard, environment)
|
||||
);
|
||||
const guardEnvironment = environment.set("inState", (stateLabel: string) => {
|
||||
for (const [uid, state] of statechart.uid2State.entries()) {
|
||||
if (stateDescription(state) === stateLabel) {
|
||||
return (mode.has(uid));
|
||||
}
|
||||
}
|
||||
});
|
||||
const enabled = triggered.filter(([t,l]) =>
|
||||
evalExpr(l.guard, guardEnvironment));
|
||||
if (enabled.length > 0) {
|
||||
if (enabled.length > 1) {
|
||||
console.warn('nondeterminism!!!!');
|
||||
}
|
||||
const t = enabled[0];
|
||||
const {arena, srcPath, tgtPath} = computeArena(t);
|
||||
const [t,l] = enabled[0]; // just pick one transition
|
||||
const arena = computeArena2(t, statechart.transitions);
|
||||
let overlapping = false;
|
||||
for (const alreadyFired of arenasFired) {
|
||||
if (isOverlapping(arena, alreadyFired)) {
|
||||
|
|
@ -236,20 +240,18 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
|||
}
|
||||
}
|
||||
if (!overlapping) {
|
||||
let oldValue;
|
||||
if (event.kind === "input" && event.param !== undefined) {
|
||||
// input events may have a parameter
|
||||
// add event parameter to environment in new scope
|
||||
environment = environment.pushScope();
|
||||
environment = environment.newVar(
|
||||
(t.label[0].trigger as EventTrigger).paramName as string,
|
||||
(l.trigger as EventTrigger).paramName as string,
|
||||
event.param,
|
||||
);
|
||||
}
|
||||
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
|
||||
if (event.kind === "input" && event.param) {
|
||||
({mode, environment, ...raised} = fireTransition2(simtime, t, statechart.transitions, l, arena, {mode, environment, ...raised}));
|
||||
if (event.kind === "input" && event.param !== undefined) {
|
||||
environment = environment.popScope();
|
||||
// console.log('restored environment:', environment);
|
||||
}
|
||||
arenasFired.add(arena);
|
||||
}
|
||||
|
|
@ -288,27 +290,54 @@ function transitionDescription(t: Transition) {
|
|||
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
|
||||
}
|
||||
|
||||
export function fireTransition(simtime: number, t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||
export function fireTransition2(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||
console.log('fire', transitionDescription(t));
|
||||
|
||||
console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
|
||||
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse();
|
||||
|
||||
// exit src
|
||||
console.log('exit src...');
|
||||
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
|
||||
// exit src and other states up to arena
|
||||
({environment, ...raised} = exitPath(simtime, srcPath, {environment, enteredStates: mode, ...raised}));
|
||||
const toExit = getDescendants(arena);
|
||||
toExit.delete(arena.uid); // do not exit the arena itself
|
||||
const exitedMode = mode.difference(toExit);
|
||||
const exitedMode = mode.difference(toExit); // active states after exiting the states we need to exit
|
||||
|
||||
// console.log({exitedMode});
|
||||
|
||||
return fireSecondHalfOfTransition(simtime, t, ts, label, arena, {mode: exitedMode, environment, ...raised});
|
||||
}
|
||||
|
||||
// assuming we've already exited the source state of the transition, now enter the target state
|
||||
// IF however, the target is a pseudo-state, DON'T enter it (pseudo-states are NOT states), instead fire the first pseudo-outgoing transition.
|
||||
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||
// exec transition actions
|
||||
for (const action of t.label[0].actions) {
|
||||
for (const action of label.actions) {
|
||||
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
||||
}
|
||||
|
||||
// enter tgt
|
||||
console.log('enter tgt...');
|
||||
let enteredStates;
|
||||
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
|
||||
const enteredMode = exitedMode.union(enteredStates);
|
||||
if (t.tgt.kind === "pseudo") {
|
||||
const outgoing = ts.get(t.tgt.uid) || [];
|
||||
for (const nextT of outgoing) {
|
||||
for (const nextLabel of nextT.label) {
|
||||
if (nextLabel.kind === "transitionLabel") {
|
||||
if (evalExpr(nextLabel.guard, environment)) {
|
||||
console.log('fire', transitionDescription(nextT));
|
||||
// found ourselves an enabled transition
|
||||
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, ...raised});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("stuck in pseudo-state!!")
|
||||
}
|
||||
else {
|
||||
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
||||
// enter tgt
|
||||
let enteredStates;
|
||||
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath, {environment, ...raised}));
|
||||
const enteredMode = mode.union(enteredStates);
|
||||
|
||||
return {mode: enteredMode, environment, ...raised};
|
||||
// console.log({enteredMode});
|
||||
|
||||
return {mode: enteredMode, environment, ...raised};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ export type ParserError = {
|
|||
uid: string; // uid of the text node
|
||||
}
|
||||
|
||||
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
||||
export type Trigger = TriggerLess | EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
||||
|
||||
export type TriggerLess = {
|
||||
kind: "triggerless";
|
||||
}
|
||||
|
||||
export type EventTrigger = {
|
||||
kind: "event";
|
||||
|
|
@ -55,7 +59,7 @@ export type RaiseEvent = {
|
|||
}
|
||||
|
||||
|
||||
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal;
|
||||
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal | FunctionCall;
|
||||
|
||||
export type BinaryExpression = {
|
||||
kind: "binaryExpr";
|
||||
|
|
|
|||
|
|
@ -211,9 +211,9 @@ function peg$parse(input, options) {
|
|||
const peg$e13 = peg$classExpectation([["0", "9"]], false, false, false);
|
||||
const peg$e14 = peg$literalExpectation("==", false);
|
||||
const peg$e15 = peg$literalExpectation("!=", false);
|
||||
const peg$e16 = peg$classExpectation(["<", ">"], false, false, false);
|
||||
const peg$e17 = peg$literalExpectation("<=", false);
|
||||
const peg$e18 = peg$literalExpectation(">=", false);
|
||||
const peg$e16 = peg$literalExpectation("<=", false);
|
||||
const peg$e17 = peg$literalExpectation(">=", false);
|
||||
const peg$e18 = peg$classExpectation(["<", ">"], false, false, false);
|
||||
const peg$e19 = peg$classExpectation(["+", "-"], false, false, false);
|
||||
const peg$e20 = peg$classExpectation(["*", "/"], false, false, false);
|
||||
const peg$e21 = peg$literalExpectation("true", false);
|
||||
|
|
@ -229,7 +229,7 @@ function peg$parse(input, options) {
|
|||
function peg$f0(trigger, guard, actions) {
|
||||
return {
|
||||
kind: "transitionLabel",
|
||||
trigger,
|
||||
trigger: trigger ? trigger : {kind: "triggerless"},
|
||||
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||
actions: actions ? actions[2] : [],
|
||||
};
|
||||
|
|
@ -502,9 +502,9 @@ function peg$parse(input, options) {
|
|||
function peg$parsestart() {
|
||||
let s0;
|
||||
|
||||
s0 = peg$parsetlabel();
|
||||
s0 = peg$parsecomment();
|
||||
if (s0 === peg$FAILED) {
|
||||
s0 = peg$parsecomment();
|
||||
s0 = peg$parsetlabel();
|
||||
}
|
||||
|
||||
return s0;
|
||||
|
|
@ -516,35 +516,33 @@ function peg$parse(input, options) {
|
|||
s0 = peg$currPos;
|
||||
s1 = peg$parse_();
|
||||
s2 = peg$parsetrigger();
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parse_();
|
||||
s4 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s5 = peg$c0;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
||||
}
|
||||
if (s5 !== peg$FAILED) {
|
||||
s6 = peg$parse_();
|
||||
s7 = peg$parsecompare();
|
||||
if (s7 !== peg$FAILED) {
|
||||
s8 = peg$parse_();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s9 = peg$c1;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s9 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
||||
}
|
||||
if (s9 !== peg$FAILED) {
|
||||
s5 = [s5, s6, s7, s8, s9];
|
||||
s4 = s5;
|
||||
} else {
|
||||
peg$currPos = s4;
|
||||
s4 = peg$FAILED;
|
||||
}
|
||||
if (s2 === peg$FAILED) {
|
||||
s2 = null;
|
||||
}
|
||||
s3 = peg$parse_();
|
||||
s4 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 91) {
|
||||
s5 = peg$c0;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
||||
}
|
||||
if (s5 !== peg$FAILED) {
|
||||
s6 = peg$parse_();
|
||||
s7 = peg$parsecompare();
|
||||
if (s7 !== peg$FAILED) {
|
||||
s8 = peg$parse_();
|
||||
if (input.charCodeAt(peg$currPos) === 93) {
|
||||
s9 = peg$c1;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s9 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
||||
}
|
||||
if (s9 !== peg$FAILED) {
|
||||
s5 = [s5, s6, s7, s8, s9];
|
||||
s4 = s5;
|
||||
} else {
|
||||
peg$currPos = s4;
|
||||
s4 = peg$FAILED;
|
||||
|
|
@ -553,42 +551,42 @@ function peg$parse(input, options) {
|
|||
peg$currPos = s4;
|
||||
s4 = peg$FAILED;
|
||||
}
|
||||
if (s4 === peg$FAILED) {
|
||||
s4 = null;
|
||||
}
|
||||
s5 = peg$parse_();
|
||||
s6 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 47) {
|
||||
s7 = peg$c2;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s7 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e2); }
|
||||
}
|
||||
if (s7 !== peg$FAILED) {
|
||||
s8 = peg$parse_();
|
||||
s9 = peg$parseactions();
|
||||
if (s9 !== peg$FAILED) {
|
||||
s7 = [s7, s8, s9];
|
||||
s6 = s7;
|
||||
} else {
|
||||
peg$currPos = s6;
|
||||
s6 = peg$FAILED;
|
||||
}
|
||||
} else {
|
||||
peg$currPos = s4;
|
||||
s4 = peg$FAILED;
|
||||
}
|
||||
if (s4 === peg$FAILED) {
|
||||
s4 = null;
|
||||
}
|
||||
s5 = peg$parse_();
|
||||
s6 = peg$currPos;
|
||||
if (input.charCodeAt(peg$currPos) === 47) {
|
||||
s7 = peg$c2;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s7 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e2); }
|
||||
}
|
||||
if (s7 !== peg$FAILED) {
|
||||
s8 = peg$parse_();
|
||||
s9 = peg$parseactions();
|
||||
if (s9 !== peg$FAILED) {
|
||||
s7 = [s7, s8, s9];
|
||||
s6 = s7;
|
||||
} else {
|
||||
peg$currPos = s6;
|
||||
s6 = peg$FAILED;
|
||||
}
|
||||
if (s6 === peg$FAILED) {
|
||||
s6 = null;
|
||||
}
|
||||
s7 = peg$parse_();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f0(s2, s4, s6);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
peg$currPos = s6;
|
||||
s6 = peg$FAILED;
|
||||
}
|
||||
if (s6 === peg$FAILED) {
|
||||
s6 = null;
|
||||
}
|
||||
s7 = peg$parse_();
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f0(s2, s4, s6);
|
||||
|
||||
return s0;
|
||||
}
|
||||
|
|
@ -1002,25 +1000,25 @@ function peg$parse(input, options) {
|
|||
if (peg$silentFails === 0) { peg$fail(peg$e15); }
|
||||
}
|
||||
if (s5 === peg$FAILED) {
|
||||
s5 = input.charAt(peg$currPos);
|
||||
if (peg$r2.test(s5)) {
|
||||
peg$currPos++;
|
||||
if (input.substr(peg$currPos, 2) === peg$c14) {
|
||||
s5 = peg$c14;
|
||||
peg$currPos += 2;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e16); }
|
||||
}
|
||||
if (s5 === peg$FAILED) {
|
||||
if (input.substr(peg$currPos, 2) === peg$c14) {
|
||||
s5 = peg$c14;
|
||||
if (input.substr(peg$currPos, 2) === peg$c15) {
|
||||
s5 = peg$c15;
|
||||
peg$currPos += 2;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||
}
|
||||
if (s5 === peg$FAILED) {
|
||||
if (input.substr(peg$currPos, 2) === peg$c15) {
|
||||
s5 = peg$c15;
|
||||
peg$currPos += 2;
|
||||
s5 = input.charAt(peg$currPos);
|
||||
if (peg$r2.test(s5)) {
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||
|
|
@ -1474,8 +1472,16 @@ function peg$parse(input, options) {
|
|||
}
|
||||
if (s1 !== peg$FAILED) {
|
||||
s2 = peg$parse_();
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = [];
|
||||
s3 = [];
|
||||
if (input.length > peg$currPos) {
|
||||
s4 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s4 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
while (s4 !== peg$FAILED) {
|
||||
s3.push(s4);
|
||||
if (input.length > peg$currPos) {
|
||||
s4 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
|
|
@ -1483,54 +1489,36 @@ function peg$parse(input, options) {
|
|||
s4 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
while (s4 !== peg$FAILED) {
|
||||
s3.push(s4);
|
||||
if (input.length > peg$currPos) {
|
||||
s4 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s4 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
}
|
||||
s4 = peg$parse_();
|
||||
if (s4 !== peg$FAILED) {
|
||||
if (input.charCodeAt(peg$currPos) === 10) {
|
||||
s5 = peg$c21;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
}
|
||||
if (s5 === peg$FAILED) {
|
||||
s5 = peg$currPos;
|
||||
peg$silentFails++;
|
||||
if (input.length > peg$currPos) {
|
||||
s6 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
peg$silentFails--;
|
||||
if (s6 === peg$FAILED) {
|
||||
s5 = undefined;
|
||||
} else {
|
||||
peg$currPos = s5;
|
||||
s5 = peg$FAILED;
|
||||
}
|
||||
}
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
}
|
||||
}
|
||||
s4 = peg$parse_();
|
||||
if (input.charCodeAt(peg$currPos) === 10) {
|
||||
s5 = peg$c21;
|
||||
peg$currPos++;
|
||||
} else {
|
||||
s5 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||
}
|
||||
if (s5 === peg$FAILED) {
|
||||
s5 = peg$currPos;
|
||||
peg$silentFails++;
|
||||
if (input.length > peg$currPos) {
|
||||
s6 = input.charAt(peg$currPos);
|
||||
peg$currPos++;
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
s6 = peg$FAILED;
|
||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||
}
|
||||
peg$silentFails--;
|
||||
if (s6 === peg$FAILED) {
|
||||
s5 = undefined;
|
||||
} else {
|
||||
peg$currPos = s5;
|
||||
s5 = peg$FAILED;
|
||||
}
|
||||
}
|
||||
if (s5 !== peg$FAILED) {
|
||||
peg$savedPos = s0;
|
||||
s0 = peg$f22(s3);
|
||||
} else {
|
||||
peg$currPos = s0;
|
||||
s0 = peg$FAILED;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ConcreteState, OrState, Statechart, Transition } from "./abstract_syntax";
|
||||
import { findNearestArrow, findNearestRountangleSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
|
||||
import { AbstractState, ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
|
||||
import { findNearestArrow, findNearestSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
|
||||
import { isEntirelyWithin } from "../VisualEditor/geometry";
|
||||
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
|||
timers: [],
|
||||
}
|
||||
|
||||
const uid2State = new Map<string, ConcreteState>([["root", root]]);
|
||||
const uid2State = new Map<string, ConcreteState|PseudoState>([["root", root]]);
|
||||
|
||||
// we will always look for the smallest parent rountangle
|
||||
const parentCandidates: Rountangle[] = [{
|
||||
|
|
@ -59,37 +59,59 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
|||
|
||||
// step 1: figure out state hierarchy
|
||||
|
||||
// we assume that the rountangles are sorted from big to small:
|
||||
// IMPORTANT ASSUMPTION: state.rountangles is sorted from big to small surface area:
|
||||
for (const rt of state.rountangles) {
|
||||
// @ts-ignore
|
||||
const state: ConcreteState = {
|
||||
const common = {
|
||||
kind: rt.kind,
|
||||
uid: rt.uid,
|
||||
children: [],
|
||||
comments: [],
|
||||
entryActions: [],
|
||||
exitActions: [],
|
||||
timers: [],
|
||||
};
|
||||
if (state.kind === "or") {
|
||||
(state as unknown as OrState).initial = [];
|
||||
|
||||
let state;
|
||||
if (rt.kind === "or") {
|
||||
state = {
|
||||
...common,
|
||||
initial: [],
|
||||
children: [],
|
||||
timers: [],
|
||||
};
|
||||
}
|
||||
else if (rt.kind === "and") {
|
||||
state = {
|
||||
...common,
|
||||
children: [],
|
||||
timers: [],
|
||||
};
|
||||
}
|
||||
uid2State.set(rt.uid, (state));
|
||||
|
||||
// iterate in reverse:
|
||||
for (let i=parentCandidates.length-1; i>=0; i--) {
|
||||
const candidate = parentCandidates[i];
|
||||
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
|
||||
// found our parent :)
|
||||
const parentState = uid2State.get(candidate.uid)!;
|
||||
// found our parent
|
||||
const parentState = uid2State.get(candidate.uid)! as ConcreteState;
|
||||
parentState.children.push(state as unknown as ConcreteState);
|
||||
parentCandidates.push(rt);
|
||||
parentLinks.set(rt.uid, candidate.uid);
|
||||
state.parent = parentState;
|
||||
state.depth = parentState.depth+1;
|
||||
state = {
|
||||
...state,
|
||||
parent: parentState,
|
||||
depth: parentState.depth + 1,
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
uid2State.set(rt.uid, state as ConcreteState);
|
||||
}
|
||||
|
||||
for (const d of state.diamonds) {
|
||||
uid2State.set(d.uid, {
|
||||
kind: "pseudo",
|
||||
uid: d.uid,
|
||||
comments: [],
|
||||
});
|
||||
}
|
||||
|
||||
// step 2: figure out transitions
|
||||
|
|
@ -98,27 +120,37 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
|||
const uid2Transition = new Map<string, Transition>();
|
||||
|
||||
for (const arr of state.arrows) {
|
||||
const srcUID = findNearestRountangleSide(arr, "start", state.rountangles)?.uid;
|
||||
const tgtUID = findNearestRountangleSide(arr, "end", state.rountangles)?.uid;
|
||||
const sides = [...state.rountangles, ...state.diamonds];
|
||||
const srcUID = findNearestSide(arr, "start", sides)?.uid;
|
||||
const tgtUID = findNearestSide(arr, "end", sides)?.uid;
|
||||
if (!srcUID) {
|
||||
if (!tgtUID) {
|
||||
// dangling edge - todo: display error...
|
||||
// dangling edge
|
||||
errors.push({shapeUid: arr.uid, message: "dangling"});
|
||||
}
|
||||
else {
|
||||
// target but no source, so we treat is as an 'initial' marking
|
||||
const initialState = uid2State.get(tgtUID)!;
|
||||
const ofState = uid2State.get(parentLinks.get(tgtUID)!)!;
|
||||
if (ofState.kind === "or") {
|
||||
ofState.initial.push([arr.uid, initialState]);
|
||||
}
|
||||
else {
|
||||
// and states do not have an 'initial' state - todo: display error...
|
||||
const tgtState = uid2State.get(tgtUID)!;
|
||||
if (tgtState.kind === "pseudo") {
|
||||
// maybe allow this in the future?
|
||||
errors.push({
|
||||
shapeUid: arr.uid,
|
||||
message: "AND-state cannot have an initial state",
|
||||
message: "pseudo-state cannot be initial state",
|
||||
});
|
||||
}
|
||||
else {
|
||||
const ofState = uid2State.get(parentLinks.get(tgtUID)!)!;
|
||||
if (ofState.kind === "or") {
|
||||
ofState.initial.push([arr.uid, tgtState]);
|
||||
}
|
||||
else {
|
||||
// and states do not have an 'initial' state
|
||||
errors.push({
|
||||
shapeUid: arr.uid,
|
||||
message: "AND-state cannot have an initial state",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -194,26 +226,42 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
|||
if (belongsToArrow) {
|
||||
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
||||
if (belongsToTransition) {
|
||||
const {src} = belongsToTransition;
|
||||
belongsToTransition.label.push(parsed);
|
||||
if (parsed.kind === "transitionLabel") {
|
||||
// collect events
|
||||
// triggers
|
||||
if (parsed.trigger.kind === "event") {
|
||||
const {event} = parsed.trigger;
|
||||
if (event.startsWith("_")) {
|
||||
errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid));
|
||||
if (src.kind === "pseudo") {
|
||||
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"});
|
||||
}
|
||||
else {
|
||||
errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid));
|
||||
const {event} = parsed.trigger;
|
||||
if (event.startsWith("_")) {
|
||||
errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid));
|
||||
}
|
||||
else {
|
||||
errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (parsed.trigger.kind === "after") {
|
||||
belongsToTransition.src.timers.push(parsed.trigger.durationMs);
|
||||
belongsToTransition.src.timers.sort();
|
||||
if (src.kind === "pseudo") {
|
||||
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"});
|
||||
}
|
||||
else {
|
||||
src.timers.push(parsed.trigger.durationMs);
|
||||
src.timers.sort();
|
||||
}
|
||||
}
|
||||
else if (["entry", "exit"].includes(parsed.trigger.kind)) {
|
||||
errors.push({shapeUid: text.uid, message: "entry/exit trigger not allowed on transitions"});
|
||||
}
|
||||
else if (parsed.trigger.kind === "triggerless") {
|
||||
if (src.kind !== "pseudo") {
|
||||
errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"});
|
||||
}
|
||||
}
|
||||
|
||||
// // raise-actions
|
||||
// for (const action of parsed.actions) {
|
||||
|
|
@ -240,7 +288,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
|||
// text does not belong to transition...
|
||||
// so it belongs to a rountangle (a state)
|
||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
||||
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! : root;
|
||||
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! as ConcreteState : root;
|
||||
if (parsed.kind === "transitionLabel") {
|
||||
// labels belonging to a rountangle (= a state) must by entry/exit actions
|
||||
// if we cannot find a containing state, then it belong to the root
|
||||
|
|
@ -257,7 +305,6 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
|||
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
else if (parsed.kind === "comment") {
|
||||
// just append comments to their respective states
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
start = tlabel / comment
|
||||
start = comment / tlabel
|
||||
|
||||
tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
||||
tlabel = _ trigger:trigger? _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
||||
return {
|
||||
kind: "transitionLabel",
|
||||
trigger,
|
||||
trigger: trigger ? trigger : {kind: "triggerless"},
|
||||
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||
actions: actions ? actions[2] : [],
|
||||
};
|
||||
|
|
@ -57,7 +57,7 @@ number = [0-9]+ {
|
|||
|
||||
expr = compare
|
||||
|
||||
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
|
||||
compare = sum:sum rest:((_ ("==" / "!=" / "<=" / ">=" / "<" / ">") _) compare)? {
|
||||
if (rest === null) {
|
||||
return sum;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue