pseudo-states appear to be working + variables only exist within the scope where they are created

This commit is contained in:
Joeri Exelmans 2025-10-16 17:10:37 +02:00
parent db1479bfc4
commit d4930eb13d
22 changed files with 742 additions and 569 deletions

View file

@ -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;
}

View file

@ -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");
}

View file

@ -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) {

View file

@ -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};
}
}

View file

@ -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";

View file

@ -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;

View file

@ -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

View file

@ -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;
}