re-organize project structure a bit + add icons

This commit is contained in:
Joeri Exelmans 2025-10-13 17:14:21 +02:00
parent 3cb3ef91d2
commit 5e7b944978
24 changed files with 514 additions and 249 deletions

View file

@ -1,5 +1,6 @@
.svgCanvas {
cursor: crosshair;
background-color: #eee;
}
text, text.highlight {
@ -102,6 +103,7 @@ text.highlight {
.rountangle.or {
stroke-dasharray: 7 6;
fill: #eee;
}
.arrow {

View file

@ -4,13 +4,13 @@ import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclid
import "./VisualEditor.css";
import { getBBoxInSvgCoords } from "./svg_helper";
import { VisualEditorState, Rountangle, emptyState, Arrow, ArrowPart, RountanglePart, findNearestRountangleSide, findNearestArrow, Text, findRountangle } from "./editor_types";
import { parseStatechart } from "./parser";
import { VisualEditorState, Rountangle, emptyState, Arrow, ArrowPart, RountanglePart, findNearestRountangleSide, findNearestArrow, Text, findRountangle } from "../statecharts/concrete_syntax";
import { parseStatechart } from "../statecharts/parser";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
import * as lz4 from "@nick/lz4";
import { RT_Statechart } from "./runtime_types";
import { Statechart } from "./ast";
import { BigStep, RT_Statechart } from "../statecharts/runtime_types";
import { Statechart } from "../statecharts/abstract_syntax";
type DraggingState = {
@ -50,15 +50,13 @@ export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [
];
type VisualEditorProps = {
ast: Statechart,
setAST: Dispatch<SetStateAction<Statechart>>,
rt: RT_Statechart|null,
setRT: Dispatch<SetStateAction<RT_Statechart|null>>,
rt: BigStep|undefined,
errors: [string,string][],
setErrors: Dispatch<SetStateAction<[string,string][]>>,
};
export function VisualEditor({ast, setAST, rt, setRT, errors, setErrors}: VisualEditorProps) {
export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) {
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
const state = historyState.current;
@ -145,7 +143,7 @@ export function VisualEditor({ast, setAST, rt, setRT, errors, setErrors}: Visual
window.location.hash = "#"+compressedStateString;
const [statechart, errors] = parseStatechart(state);
console.log('statechart: ', statechart, 'errors:', errors);
// console.log('statechart: ', statechart, 'errors:', errors);
setErrors(errors);
setAST(statechart);
}, 100);

View file

@ -1,46 +0,0 @@
// Just a simple recursive interpreter for the action language
import { Expression } from "./label_ast";
import { Environment } from "./runtime_types";
const UNARY_OPERATOR_MAP: Map<string, (x: any) => any> = new Map([
["!", x => !x],
["-", x => -x as any],
]);
const BINARY_OPERATOR_MAP: Map<string, (a: any, b: any) => any> = new Map([
["+", (a, b) => a + b],
["-", (a, b) => a - b],
["*", (a, b) => a * b],
["/", (a, b) => a / b],
["&&", (a, b) => a && b],
["||", (a, b) => a || b],
["==", (a, b) => a == b],
["<=", (a, b) => a <= b],
[">=", (a, b) => a >= b],
["<", (a, b) => a < b],
[">", (a, b) => a > b],
]);
export function evalExpr(expr: Expression, environment: Environment): any {
if (expr.kind === "literal") {
return expr.value;
}
else if (expr.kind === "ref") {
const found = environment.get(expr.variable);
if (found === undefined) {
throw new Error(`variable '${expr.variable}' does not exist in environment`);
}
return found;
}
else if (expr.kind === "unaryExpr") {
const arg = evalExpr(expr.expr, environment);
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
}
else if (expr.kind === "binaryExpr") {
const lhs = evalExpr(expr.lhs, environment);
const rhs = evalExpr(expr.rhs, environment);
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
}
throw new Error("should never reach here");
}

View file

@ -1,133 +0,0 @@
import { Action, TransitionLabel } 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 AndState = {
kind: "and";
} & AbstractState;
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;
export type ConcreteState = AndState | OrState;
export type Transition = {
uid: string;
src: ConcreteState;
tgt: ConcreteState;
label: TransitionLabel[];
}
export type Statechart = {
root: OrState;
transitions: Map<string, Transition[]>; // key: source state uid
variables: Set<string>;
inputEvents: Set<string>;
internalEvents: Set<string>;
outputEvents: Set<string>;
uid2State: Map<string, ConcreteState>;
}
const emptyRoot: OrState = {
uid: "root",
kind: "or",
depth: 0,
initial: [],
children:[],
comments: [],
entryActions: [],
exitActions: [],
timers: [],
};
export const emptyStatechart: Statechart = {
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
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];
}
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: OrState,
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};
}
}
export function getDescendants(state: ConcreteState): Set<string> {
const result = new Set([state.uid]);
for (const child of state.children) {
for (const descendant of getDescendants(child)) {
// will include child itself:
result.add(descendant);
}
}
return result;
}
// the 'description' of a state is a human-readable string that (hopefully) identifies the state.
// if the state contains a comment, we take the 'first' (= visually topmost) comment
// otherwise we fall back to the state's UID.
export function stateDescription(state: ConcreteState) {
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
return description;
}

View file

@ -1,117 +0,0 @@
import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, isEntirelyWithin } from "./geometry";
import { ARROW_SNAP_THRESHOLD, TEXT_SNAP_THRESHOLD } from "./parameters";
import { sides } from "./VisualEditor";
export type Rountangle = {
uid: string;
kind: "and" | "or";
} & Rect2D;
export type Text = {
uid: string;
topLeft: Vec2D;
text: string;
};
export type Arrow = {
uid: string;
} & Line2D;
export type VisualEditorState = {
rountangles: Rountangle[];
texts: Text[];
arrows: Arrow[];
nextID: number;
};
// independently moveable parts of our shapes:
export type RountanglePart = "left" | "top" | "right" | "bottom";
export type ArrowPart = "start" | "end";
export const emptyState = {
rountangles: [], texts: [], arrows: [], nextID: 0,
};
export const onOffStateMachine = {
rountangles: [
{ uid: "0", topLeft: { x: 100, y: 100 }, size: { x: 100, y: 100 }, kind: "and" },
{ uid: "1", topLeft: { x: 100, y: 300 }, size: { x: 100, y: 100 }, kind: "and" },
],
texts: [],
arrows: [
{ uid: "2", start: { x: 150, y: 200 }, end: { x: 160, y: 300 } },
],
nextID: 3,
};
// 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 {
let best = Infinity;
let bestSide: undefined | {uid: string, part: RountanglePart};
for (const rountangle of candidates) {
for (const [side, getSide] of sides) {
const asLine = getSide(rountangle);
const intersection = intersectLines(arrow, asLine);
if (intersection !== null) {
const bbox = lineBBox(asLine, ARROW_SNAP_THRESHOLD);
const dist = euclideanDistance(arrow[arrowPart], intersection);
if (isWithin(arrow[arrowPart], bbox) && dist < best) {
best = dist;
bestSide = { uid: rountangle.uid, part: side };
}
}
}
}
return bestSide;
}
export function point2LineDistance(point: Vec2D, {start, end}: Line2D): number {
const A = point.x - start.x;
const B = point.y - start.y;
const C = end.x - start.x;
const D = end.y - start.y;
const dot = A * C + B * D;
const lenSq = C * C + D * D;
let t = lenSq ? dot / lenSq : -1;
if (t < 0) t = 0;
else if (t > 1) t = 1;
const closestX = start.x + t * C;
const closestY = start.y + t * D;
const dx = point.x - closestX;
const dy = point.y - closestY;
const distance = Math.hypot(dx, dy);
return distance;
}
// used to find which arrow a text label belongs to (if any)
// author: ChatGPT
export function findNearestArrow(point: Vec2D, candidates: Arrow[]): Arrow | undefined {
let best;
let bestDistance = Infinity
for (const arrow of candidates) {
const distance = point2LineDistance(point, arrow);
if (distance < TEXT_SNAP_THRESHOLD && distance < bestDistance) {
bestDistance = distance;
best = arrow;
}
}
return best;
}
// precondition: candidates are sorted from big to small
export function findRountangle(point: Vec2D, candidates: Rountangle[]): Rountangle | undefined {
for (let i=candidates.length-1; i>=0; i--) {
if (isWithin(point, candidates[i])) {
return candidates[i];
}
}
}

View file

@ -1,4 +1,4 @@
import { RountanglePart } from "./editor_types";
import { RountanglePart } from "../statecharts/concrete_syntax";
export type Vec2D = {
x: number;

View file

@ -1,287 +0,0 @@
import { evalExpr } from "./actionlang_interpreter";
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./ast";
import { Action } from "./label_ast";
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, TimerElapseEvent, Timers } from "./runtime_types";
export function initialize(ast: Statechart): BigStepOutput {
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
environment: new Map(),
...initialRaised,
});
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
}
type ActionScope = {
environment: Environment,
} & RaisedEvents;
type EnteredScope = { enteredStates: Mode } & ActionScope;
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
for (const action of state.entryActions) {
(actionScope = execAction(action, actionScope));
}
// schedule timers
// we store timers in the environment (dirty!)
const environment = new Map(actionScope.environment);
const timers: Timers = [...(environment.get("_timers") || [])];
for (const timeOffset of state.timers) {
const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled
timers.push([futureSimTime, {state: state.uid, timeDurMs: timeOffset}]);
}
timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first
environment.set("_timers", timers);
return {...actionScope, environment};
}
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
for (const action of state.exitActions) {
(actionScope = execAction(action, actionScope));
}
// cancel timers
const environment = new Map(actionScope.environment);
const timers: Timers = environment.get("_timers") || [];
const filtered = timers.filter(([_, {state: s}]) => s !== state.uid);
environment.set("_timers", filtered);
return {...actionScope, environment};
}
export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope {
let actionScope = rt;
// execute entry actions
actionScope = entryActions(simtime, state, actionScope);
// enter children...
let enteredStates = new Set([state.uid]);
if (state.kind === "and") {
// enter every child
for (const child of state.children) {
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
}
else if (state.kind === "or") {
// same as AND-state, but we only enter the initial state(s)
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(simtime, state.initial[0][1], actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
console.warn(state.uid + ': no initial state');
}
return {enteredStates, ...actionScope};
}
export function enterPath(simtime: number, path: ConcreteState[], rt: ActionScope): EnteredScope {
let actionScope = rt;
const [state, ...rest] = path;
// execute entry actions
actionScope = entryActions(simtime, state, actionScope);
// enter children...
let enteredStates = new Set([state.uid]);
if (state.kind === "and") {
// enter every child
for (const child of state.children) {
let enteredChildren;
if (rest.length > 0 && child.uid === rest[0].uid) {
({enteredStates: enteredChildren, ...actionScope} = enterPath(simtime, rest, actionScope));
}
else {
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
}
enteredStates = enteredStates.union(enteredChildren);
}
}
else if (state.kind === "or") {
if (rest.length > 0) {
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterPath(simtime, 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 enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
}
}
return { enteredStates, ...actionScope };
}
// exit the given state and all its active descendants
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
let {enteredStates, ...actionScope} = rt;
// exit all active children...
for (const child of state.children) {
if (enteredStates.has(child.uid)) {
actionScope = exitCurrent(simtime, child, {enteredStates, ...actionScope});
}
}
// execute exit actions
actionScope = exitActions(simtime, state, actionScope);
return actionScope;
}
export function exitPath(simtime: number, 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(simtime, state, {enteredStates: toExit, ...actionScope});
if (rest.length > 0) {
actionScope = exitPath(simtime, rest, {enteredStates, ...actionScope});
}
// execute exit actions
actionScope = exitActions(simtime, state, actionScope);
return actionScope;
}
export function execAction(action: Action, rt: ActionScope): ActionScope {
if (action.kind === "assignment") {
const rhs = evalExpr(action.rhs, rt.environment);
const newEnvironment = new Map(rt.environment);
newEnvironment.set(action.lhs, rhs);
return {
...rt,
environment: newEnvironment,
};
}
else if (action.kind === "raise") {
if (action.event.startsWith('_')) {
// append to internal events
return {
...rt,
internalEvents: [...rt.internalEvents, action.event],
};
}
else {
// append to output events
return {
...rt,
outputEvents: [...rt.outputEvents, action.event],
}
}
}
throw new Error("should never reach here");
}
export function handleEvent(simtime: number, event: string | TimerElapseEvent, statechart: Statechart, activeParent: ConcreteState, {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) || [];
let triggered;
if (typeof event === 'string') {
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "event") {
return trigger.event === event;
}
return false;
});
}
else {
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "after") {
return trigger.durationMs === event.timeDurMs;
}
return false;
});
}
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(simtime, 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(simtime, event, statechart, state, {environment, mode, ...raised}));
}
}
}
return {environment, mode, ...raised};
}
export function handleInputEvent(simtime: number, event: string, statechart: Statechart, {mode, environment}: {mode: Mode, environment: Environment}): BigStepOutput {
let raised = initialRaised;
({mode, environment, ...raised} = handleEvent(simtime, event, statechart, statechart.root, {mode, environment, ...raised}));
return handleInternalEvents(simtime, statechart, {mode, environment, ...raised});
}
export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput {
while (raised.internalEvents.length > 0) {
const [internalEvent, ...rest] = raised.internalEvents;
({mode, environment, ...raised} = handleEvent(simtime, internalEvent, statechart, statechart.root, {mode, environment, internalEvents: rest, outputEvents: raised.outputEvents}));
}
return {mode, environment, outputEvents: raised.outputEvents};
}
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 {
// console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
// exit src
({environment, ...raised} = exitPath(simtime, 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);
// exec transition actions
for (const action of t.label[0].actions) {
({environment, ...raised} = execAction(action, {environment, ...raised}));
}
// enter tgt
let enteredStates;
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
const enteredMode = exitedMode.union(enteredStates);
return {mode: enteredMode, environment, ...raised};
}

View file

@ -1,74 +0,0 @@
export type ParsedText = TransitionLabel | Comment;
export type TransitionLabel = {
kind: "transitionLabel";
uid: string; // uid of the text node
trigger: Trigger;
guard: Expression;
actions: Action[];
}
export type Comment = {
kind: "comment";
uid: string; // uid of the text node
text: string;
}
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
export type EventTrigger = {
kind: "event";
event: string;
}
export type AfterTrigger = {
kind: "after";
durationMs: number;
}
export type EntryTrigger = {
kind: "entry";
}
export type ExitTrigger = {
kind: "exit";
}
export type Action = Assignment | RaiseEvent;
export type Assignment = {
kind: "assignment";
lhs: string;
rhs: Expression;
}
export type RaiseEvent = {
kind: "raise";
event: string;
}
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal;
export type BinaryExpression = {
kind: "binaryExpr";
operator: "+" | "-" | "*" | "/" | "&&" | "||";
lhs: Expression;
rhs: Expression;
}
export type UnaryExpression = {
kind: "unaryExpr";
operator: "!" | "-";
expr: Expression;
}
export type VarRef = {
kind: "ref";
variable: string;
}
export type Literal = {
kind: "literal";
value: any;
}

File diff suppressed because it is too large Load diff

View file

@ -1,257 +0,0 @@
import { ConcreteState, OrState, Statechart, Transition } from "./ast";
import { findNearestArrow, findNearestRountangleSide, findRountangle, Rountangle, VisualEditorState } from "./editor_types";
import { isEntirelyWithin } from "./geometry";
import { Action, Expression, ParsedText } from "./label_ast";
import { parse as parseLabel, SyntaxError } from "./label_parser";
export function parseStatechart(state: VisualEditorState): [Statechart, [string,string][]] {
const errorShapes: [string, string][] = [];
// implicitly, the root is always an Or-state
const root: OrState = {
kind: "or",
uid: "root",
children: [],
initial: [],
comments: [],
entryActions: [],
exitActions: [],
depth: 0,
timers: [],
}
const uid2State = new Map<string, ConcreteState>([["root", root]]);
// we will always look for the smallest parent rountangle
const parentCandidates: Rountangle[] = [{
kind: "or",
uid: root.uid,
topLeft: {x: -Infinity, y: -Infinity},
size: {x: Infinity, y: Infinity},
}];
const parentLinks = new Map<string, string>();
// step 1: figure out state hierarchy
// we assume that the rountangles are sorted from big to small:
for (const rt of state.rountangles) {
const state: ConcreteState = {
kind: rt.kind,
uid: rt.uid,
children: [],
comments: [],
entryActions: [],
exitActions: [],
timers: [],
}
if (state.kind === "or") {
state.initial = [];
}
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)!;
parentState.children.push(state);
parentCandidates.push(rt);
parentLinks.set(rt.uid, candidate.uid);
state.parent = parentState;
state.depth = parentState.depth+1;
break;
}
}
}
// step 2: figure out transitions
const transitions = new Map<string, Transition[]>();
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;
if (!srcUID) {
if (!tgtUID) {
// dangling edge - todo: display error...
errorShapes.push([arr.uid, "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...
errorShapes.push([arr.uid, "AND-state cannot have an initial state"]);
}
}
}
else {
if (!tgtUID) {
errorShapes.push([arr.uid, "no target"]);
}
else {
// add transition
const transition: Transition = {
uid: arr.uid,
src: uid2State.get(srcUID)!,
tgt: uid2State.get(tgtUID)!,
label: [],
};
const existingTransitions = transitions.get(srcUID) || [];
existingTransitions.push(transition);
transitions.set(srcUID, existingTransitions);
uid2Transition.set(arr.uid, transition);
}
}
}
for (const state of uid2State.values()) {
if (state.kind === "or") {
if (state.initial.length > 1) {
errorShapes.push(...state.initial.map(([uid,childState])=>[uid,"multiple initial states"] as [string, string]));
}
else if (state.initial.length === 0) {
errorShapes.push([state.uid, "no initial state"]);
}
}
}
let variables = new Set<string>();
const inputEvents = new Set<string>();
const outputEvents = new Set<string>();
const internalEvents = new Set<string>();
// step 3: figure out labels
const textsSorted = state.texts.toSorted((a,b) => a.topLeft.y - b.topLeft.y);
for (const text of textsSorted) {
let parsed: ParsedText;
try {
parsed = parseLabel(text.text); // may throw
} catch (e) {
if (e instanceof SyntaxError) {
errorShapes.push([text.uid, e]);
continue;
}
else {
throw e;
}
}
parsed.uid = text.uid;
if (parsed.kind === "transitionLabel") {
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
if (belongsToArrow) {
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
if (belongsToTransition) {
// parse as transition label
belongsToTransition.label.push(parsed);
// collect events
if (parsed.trigger.kind === "event") {
const {event} = parsed.trigger;
if (event.startsWith("_")) {
internalEvents.add(event);
}
else {
inputEvents.add(event);
}
}
else if (parsed.trigger.kind === "after") {
belongsToTransition.src.timers.push(parsed.trigger.durationMs);
belongsToTransition.src.timers.sort();
}
for (const action of parsed.actions) {
if (action.kind === "raise") {
const {event} = action;
if (event.startsWith("_")) {
internalEvents.add(event);
}
else {
outputEvents.add(event);
}
}
}
// collect variables
variables = variables
.union(findVariables(parsed.guard));
for (const action of parsed.actions) {
variables = variables.union(findVariablesAction(action));
}
}
continue;
}
}
// 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;
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
if (parsed.trigger.kind === "entry") {
belongsToState.entryActions.push(...parsed.actions);
}
else if(parsed.trigger.kind === "exit") {
belongsToState.exitActions.push(...parsed.actions);
}
else {
errorShapes.push([text.uid, {
message: "states can only have entry/exit triggers",
location: {start: {offset: 0}, end: {offset: text.text.length}},
}]);
}
}
else if (parsed.kind === "comment") {
// just append comments to their respective states
belongsToState.comments.push([text.uid, parsed.text]);
}
}
for (const transition of uid2Transition.values()) {
if (transition.label.length === 0) {
errorShapes.push([transition.uid, "no label"]);
}
else if (transition.label.length > 1) {
errorShapes.push([transition.uid, "multiple labels"]);
}
}
return [{
root,
transitions,
variables,
inputEvents,
internalEvents,
outputEvents,
uid2State,
}, errorShapes];
}
function findVariables(expr: Expression): Set<string> {
if (expr.kind === "ref") {
return new Set([expr.variable]);
}
else if (expr.kind === "unaryExpr") {
return findVariables(expr.expr);
}
else if (expr.kind === "binaryExpr") {
return findVariables(expr.lhs).union(findVariables(expr.rhs));
}
return new Set();
}
function findVariablesAction(action: Action): Set<string> {
if (action.kind === "assignment") {
return new Set([action.lhs, ...findVariables(action.rhs)]);
}
return new Set();
}

View file

@ -1,39 +0,0 @@
export type Timestamp = number; // milliseconds since begin of simulation
export type Event = string;
export type Mode = Set<string>; // set of active states
export type Environment = ReadonlyMap<string, any>; // variable name -> value
export type RT_Statechart = {
mode: Mode;
environment: Environment;
// history: // TODO
}
export type BigStepOutput = RT_Statechart & {
outputEvents: string[],
};
export type BigStep = {
inputEvent: string | null, // null if initialization
simtime: number,
} & BigStepOutput;
export type RaisedEvents = {
internalEvents: string[];
outputEvents: string[];
};
// export type Timers = Map<string, number>; // transition uid -> timestamp
export const initialRaised: RaisedEvents = {
internalEvents: [],
outputEvents: [],
};
export type TimerElapseEvent = { state: string; timeDurMs: number; };
export type Timers = [number, TimerElapseEvent][];

View file

@ -1,77 +0,0 @@
export type TimeMode = TimePaused | TimeRealTime;
// When the simulation is paused, we only need to know the current simulated time.
export type TimePaused = {
kind: "paused",
simtime: number, // the current simulated time
}
// When the simulation is running in real time, we need to know the time when the simulation was set to real time (both in simulated and wall-clock time), and the time scale. This allows us to compute the simulated time of every future event.
// Such a 'future event' may be:
// - raising an input event
// - changing of the time scale parameter
// - pausing the simulation
export type TimeRealTime = {
kind: "realtime",
since: {
simtime: number, // the simulated time at which the time was set to realtime
wallclktime: number, // the wall-clock time at which the time was set to realtime
}
scale: number, // time scale relative to wall-clock time
}
// given a wall-clock time, how does it translate to simtime?
export function getSimTime(currentMode: TimeMode, wallclktime: number): number {
if (currentMode.kind === "paused") {
return currentMode.simtime;
}
else {
const elapsedWallclk = wallclktime - currentMode.since.wallclktime;
return currentMode.since.simtime + currentMode.scale * elapsedWallclk;
}
}
// given a simulated real time clock, how long will it take in wall-clock time duration until 'simtime' is the current time?
export function getWallClkDelay(realtime: TimeRealTime, simtime: number, wallclktime: number): number {
const currentSimTime = getSimTime(realtime, wallclktime);
const simtimeDelay = simtime - currentSimTime;
return Math.max(0, simtimeDelay / realtime.scale);
}
// given a current simulated clock (paused or real time), switch to real time with given time scale
export function setRealtime(currentMode: TimeMode, scale: number, wallclktime: number): TimeRealTime {
if (currentMode.kind === "paused") {
return {
kind: "realtime",
scale,
since: {
simtime: currentMode.simtime,
wallclktime,
},
};
}
else {
return {
kind: "realtime",
scale,
since: {
simtime: getSimTime(currentMode, wallclktime),
wallclktime,
},
};
}
}
// given a current simulated clock (paused or real time), switch to paused
export function setPaused(currentMode: TimeMode, wallclktime: number): TimePaused {
if (currentMode.kind === "paused") {
return currentMode; // no change
}
else {
return {
kind: "paused",
simtime: getSimTime(currentMode, wallclktime),
};
}
}

View file

@ -1,128 +0,0 @@
start = tlabel / comment
tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
return {
kind: "transitionLabel",
trigger,
guard: guard ? guard[2] : {kind: "literal", value: true},
actions: actions ? actions[2] : [],
};
}
trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger
eventTrigger = event:identifier {
return {kind: "event", event};
}
afterTrigger = "after" _ dur:durationMs {
return {kind: "after", durationMs: dur};
}
entryTrigger = "entry" {
return {kind: "entry"};
}
exitTrigger = "exit" {
return {kind: "exit"};
}
durationMs = num:number _ u:timeUnit {
return num * (u === "s" ? 1000 : 1);
}
timeUnit = "ms" / "s" {
return text();
}
guard = expr
actions = head:action tail:(_ ";" _ action)* _ ";"? {
return [head, ...tail.map(t => t[3])];
}
action = assignment / raise
assignment = lhs:identifier _ "=" _ rhs:expr {
return {kind: "assignment", lhs, rhs};
}
identifier = ("_" / [a-zA-Z0-9])+ {
return text();
}
number = [0-9]+ {
return parseInt(text());
}
expr = compare
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
if (rest === null) {
return sum;
}
return {
kind: "binaryExpr",
operator: rest[0][1],
lhs: sum,
rhs: rest[1],
};
}
sum = prod:product rest:((_ ("+" / "-") _) sum)? {
if (rest === null) {
return prod;
}
return {
kind: "binaryExpr",
operator: rest[0][1],
lhs: prod,
rhs: rest[1],
};
}
product = atom:atom rest:((_ ("*" / "/") _) product)? {
if (rest === null) {
return atom;
}
return {
kind: "binaryExpr",
operator: rest[0][1],
lhs: atom,
rhs: rest[1],
};
}
atom = nested / literal / ref
nested = "(" _ expr:expr _ ")" {
return expr;
}
literal = value:(number / boolean) {
return {kind: "literal", value}
}
ref = variable:identifier {
return {kind: "ref", variable}
}
boolean = ("true" / "false") {
return text() === "true";
}
raise = "^" _ event:identifier {
return {kind: "raise", event};
}
_ "whitespace"
= (comment / [ \t\n\r])*
{ return null; }
comment = "//" _ text:.* _ ('\n' / !.) {
return {
kind: "comment",
text: text.join(''),
};
}