simple execution works

This commit is contained in:
Joeri Exelmans 2025-10-08 16:49:19 +02:00
parent 2b73da9387
commit 3f2db4457f
9 changed files with 412 additions and 52 deletions

View file

@ -30,7 +30,7 @@ text.highlight {
}
.rountangle {
fill: rgba(255, 255, 255, 255);
fill: white;
/* fill: none; */
stroke: black;
stroke-width: 2px;
@ -58,6 +58,10 @@ text.highlight {
.rountangle.error {
stroke: rgb(230,0,0);
}
.rountangle.active {
fill: rgb(255, 196, 0);
fill-opacity: 0.6;
}
.selected:hover {
cursor: grab;

View file

@ -1,4 +1,4 @@
import { MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react";
import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react";
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
import "./VisualEditor.css";
@ -9,7 +9,9 @@ import { parseStatechart } from "./parser";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
import * as lz4 from "@nick/lz4";
import { initialize } from "./interpreter";
import { getActiveStates, initialize } from "./interpreter";
import { RT_Statechart } from "./runtime_types";
import { emptyStatechart, Statechart } from "./ast";
type DraggingState = {
@ -48,7 +50,16 @@ export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [
["bottom", getBottomSide],
];
export function VisualEditor() {
type VisualEditorProps = {
ast: Statechart,
setAST: Dispatch<SetStateAction<Statechart>>,
rt: RT_Statechart|null,
setRT: Dispatch<SetStateAction<RT_Statechart|null>>,
errors: [string,string][],
setErrors: Dispatch<SetStateAction<[string,string][]>>,
};
export function VisualEditor({ast, setAST, rt, setRT, errors, setErrors}: VisualEditorProps) {
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
const state = historyState.current;
@ -110,8 +121,6 @@ export function VisualEditor() {
// not null while the user is making a selection
const [selectingState, setSelectingState] = useState<SelectingState>(null);
const [errors, setErrors] = useState<[string,string][]>([]);
const refSVG = useRef<SVGSVGElement>(null);
useEffect(() => {
@ -139,9 +148,7 @@ export function VisualEditor() {
const [statechart, errors] = parseStatechart(state);
console.log('statechart: ', statechart, 'errors:', errors);
setErrors(errors);
const rt = initialize(statechart);
console.log('runtime:', rt);
setAST(statechart);
}, 100);
return () => clearTimeout(timeout);
}, [state]);
@ -489,6 +496,8 @@ export function VisualEditor() {
}
}
const active = getActiveStates(rt?.mode || {});
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]);
return <svg width="4000px" height="4000px"
@ -518,6 +527,7 @@ export function VisualEditor() {
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[])]}
errors={errors.filter(([uid,msg])=>uid===rountangle.uid).map(err=>err[1])}
active={active.has(rountangle.uid)}
/>)}
{state.arrows.map(arrow => {
@ -631,7 +641,7 @@ function rountangleMinSize(size: Vec2D): Vec2D {
};
}
export function RountangleSVG(props: {rountangle: Rountangle, selected: string[], highlight: RountanglePart[], errors: string[]}) {
export function RountangleSVG(props: {rountangle: Rountangle, selected: string[], highlight: RountanglePart[], errors: string[], active: boolean}) {
const {topLeft, size, uid} = props.rountangle;
// always draw a rountangle with a minimum size
// during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it
@ -642,6 +652,7 @@ export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]
+(props.selected.length===4?" selected":"")
+((props.rountangle.kind==="or")?" or":"")
+(props.errors.length>0?" error":"")
+(props.active?" active":"")
}
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
x={0}

View file

@ -2,10 +2,12 @@ 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;
}
export type AndState = {
@ -37,4 +39,61 @@ export type Statechart = {
inputEvents: Set<string>;
internalEvents: Set<string>;
outputEvents: Set<string>;
uid2State: Map<string, ConcreteState>;
}
export const emptyStatechart: Statechart = {
root: {
uid: "root",
kind: "or",
initial: [],
children:[],
comments: [],
entryActions: [],
exitActions: [],
},
transitions: new Map(),
variables: new Set(),
inputEvents: new Set(),
internalEvents: new Set(),
outputEvents: new Set(),
};
// reflexive, transitive relation
export function isAncestorOf({ancestor, descendant}: {ancestor: ConcreteState, descendant: ConcreteState}): ConcreteState[] | false {
if (ancestor.uid === descendant.uid) {
return [descendant];
}
if (ancestor.depth >= descendant.depth) {
return false;
}
const pathToParent = isAncestorOf({ancestor, descendant: descendant.parent!});
return pathToParent && [...pathToParent, descendant];
}
// the arena of a transition is the lowest common ancestor state that is an OR-state
// see "Deconstructing the Semantics of Big-Step Modelling Languages" by Shahram Esmaeilsabzali, 2009
export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): {
arena: ConcreteState,
srcPath: ConcreteState[],
tgtPath: ConcreteState[],
} {
if (src.depth >= tgt.depth) {
const path = isAncestorOf({descendant: src, ancestor: tgt});
if (path) {
if (tgt.kind === "or") {
return {arena: tgt, srcPath: path, tgtPath: [tgt]};
}
}
// keep looking
const {arena, srcPath, tgtPath} = computeArena({src, tgt: tgt.parent!});
return {arena, srcPath, tgtPath: [...tgtPath, tgt]};
}
else {
// same, but swap src and tgt
const {arena, srcPath, tgtPath} = computeArena({src: tgt, tgt: src});
return {arena, srcPath: tgtPath, tgtPath: srcPath};
}
throw new Error("should never reach here");
}

View file

@ -1,11 +1,11 @@
import { ConcreteState, Statechart } from "./ast";
import { computeArena, ConcreteState, isAncestorOf, Statechart, Transition } from "./ast";
import { Action, Expression } from "./label_ast";
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised } from "./runtime_types";
export function initialize(ast: Statechart): RT_Statechart {
const {mode, environment, raised} = enter(ast.root, {
const {mode, environment, ...raised} = enterDefault(ast.root, {
environment: new Map(),
raised: initialRaised,
...initialRaised,
});
return {
mode,
@ -17,59 +17,102 @@ export function initialize(ast: Statechart): RT_Statechart {
type ActionScope = {
environment: Environment,
raised: RaisedEvents,
};
} & RaisedEvents;
type EnteredScope = { mode: Mode } & ActionScope;
export function enter(state: ConcreteState, rt: ActionScope): EnteredScope {
let {environment, raised} = rt;
export function enterDefault(state: ConcreteState, rt: ActionScope): EnteredScope {
let actionScope = rt;
// execute entry actions
for (const action of state.entryActions) {
({environment, raised} = execAction(action, {environment, raised}));
(actionScope = execAction(action, actionScope));
}
// enter children...
const mode: {[uid:string]: Mode} = {};
if (state.kind === "and") {
// enter every child
for (const child of state.children) {
let childMode;
({mode: childMode, environment, raised} = enter(child, {environment, raised}));
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
mode[child.uid] = childMode;
}
}
else if (state.kind === "or") {
const mode: {[uid:string]: Mode} = {};
// same as AND-state, but we only enter the initial state(s)
for (const [_, child] of state.initial) {
let childMode;
({mode: childMode, environment, raised} = enter(child, {environment, raised}));
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
mode[child.uid] = childMode;
}
}
return { mode, environment, raised };
return { mode, ...actionScope };
}
export function exit(state: ConcreteState, rt: EnteredScope): ActionScope {
let {mode, environment, raised} = rt;
export function enterPath(path: ConcreteState[], rt: ActionScope): EnteredScope {
let actionScope = rt;
const [state, ...rest] = path;
// execute entry actions
for (const action of state.entryActions) {
(actionScope = execAction(action, actionScope));
}
// enter children...
const mode: {[uid:string]: Mode} = {};
if (state.kind === "and") {
// enter every child
for (const child of state.children) {
let childMode;
if (rest.length > 0 && child.uid === rest[0].uid) {
({mode: childMode, ...actionScope} = enterPath(rest, actionScope));
}
else {
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
}
mode[child.uid] = childMode;
}
}
else if (state.kind === "or") {
if (rest.length > 0) {
let childMode;
({mode: childMode, ...actionScope} = enterPath(rest, actionScope));
mode[rest[0].uid] = childMode;
}
else {
// same as AND-state, but we only enter the initial state(s)
for (const [_, child] of state.initial) {
let childMode;
({mode: childMode, ...actionScope} = enterDefault(child, actionScope));
mode[child.uid] = childMode;
}
}
}
return { mode, ...actionScope };
}
export function exitCurrent(state: ConcreteState, rt: EnteredScope): ActionScope {
let {mode, ...actionScope} = rt;
// exit all active children...
for (const [childUid, childMode] of Object.entries(mode)) {
const child = state.children.find(child => child.uid === childUid);
if (child) {
({environment, raised} = exit(child, {mode: childMode, environment, raised}));
(actionScope = exitCurrent(child, {mode: childMode, ...actionScope}));
}
}
// execute exit actions
for (const action of state.exitActions) {
({environment, raised} = execAction(action, {environment, raised}));
(actionScope = execAction(action, actionScope));
}
return {environment, raised};
return actionScope;
}
export function execAction(action: Action, rt: ActionScope): ActionScope {
@ -87,19 +130,14 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
// append to internal events
return {
...rt,
raised: {
...rt.raised,
internalEvents: [...rt.raised.internalEvents, action.event]},
internalEvents: [...rt.internalEvents, action.event],
};
}
else {
// append to output events
return {
...rt,
raised: {
...rt.raised,
outputEvents: [...rt.raised.outputEvents, action.event],
},
outputEvents: [...rt.outputEvents, action.event],
}
}
}
@ -142,10 +180,81 @@ export function evalExpr(expr: Expression, environment: Environment): any {
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
}
else if (expr.kind === "binaryExpr") {
const argA = evalExpr(expr.lhs, environment);
const argB = evalExpr(expr.rhs, environment);
return BINARY_OPERATOR_MAP.get(expr.operator)!(argA,argB);
const lhs = evalExpr(expr.lhs, environment);
const rhs = evalExpr(expr.rhs, environment);
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs,rhs);
}
throw new Error("should never reach here");
}
export function getActiveStates(mode: Mode): Set<string> {
return new Set([].concat(
...Object.entries(mode).map(([childUid, childMode]) =>
[childUid, ...getActiveStates(childMode)])
));
}
export function raiseEvent(event: string, statechart: Statechart, sourceState: ConcreteState, rt: RT_Statechart): RT_Statechart[] {
const activeStates = sourceState.children.filter(child => rt.mode.hasOwnProperty(child.uid))
for (const state of activeStates) {
const outgoing = statechart.transitions.get(state.uid) || [];
const enabled = outgoing.filter(transition => transition.label[0].trigger.kind === "event" && transition.label[0].trigger.event === event);
const enabledGuard = enabled.filter(transition =>
evalExpr(transition.label[0].guard, rt.environment)
);
if (enabledGuard.length > 0) {
const newRts = enabledGuard.map(t => fireTransition(t, statechart, rt));
return newRts;
}
else {
// no enabled outgoing transitions, try the children:
return raiseEvent(event, statechart, state, rt);
}
}
return [];
}
function setModeDeep(oldMode: Mode, pathToState: ConcreteState[], newMode: Mode): Mode {
if (pathToState.length === 0) {
return newMode;
}
const [next, ...rest] = pathToState;
return {
...oldMode,
[next.uid]: setModeDeep(oldMode[next.uid], rest, newMode),
}
}
function unsetModeDeep(oldMode: Mode, pathToState: ConcreteState[]): Mode {
if (pathToState.length === 0) {
return {};
}
if (pathToState.length === 1) {
const keyToDelete = pathToState[0].uid;
const newMode = {...oldMode}; // shallow copy
delete newMode[keyToDelete];
return newMode;
}
const [next, ...rest] = pathToState;
return {
...oldMode,
[next.uid]: unsetModeDeep(oldMode[next.uid], rest),
}
}
export function fireTransition(t: Transition, statechart: Statechart, rt: RT_Statechart): RT_Statechart {
const {arena, srcPath, tgtPath} = computeArena(t);
const pathToArena = isAncestorOf({ancestor: statechart.root, descendant: arena}) as ConcreteState[];
console.log('fire ', t.src.comments[0][1], '->', t.tgt.comments[0][1], {srcPath, tgtPath});
let {environment, ...raised} = exitCurrent(srcPath[1], rt);
const exitedMode = unsetModeDeep(rt.mode, [...pathToArena.slice(1), ...srcPath.slice(1)]);
for (const action of t.label[0].actions) {
({environment, ...raised} = execAction(action, {environment, ...raised}));
}
let deepMode;
({mode: deepMode, environment, ...raised} = enterPath(tgtPath.slice(1), {environment, ...raised}));
// console.log('entered path:', tgtPath.slice(1), {deepMode});
const enteredMode = setModeDeep(exitedMode, [...pathToArena.slice(1), ...tgtPath.slice(1)], deepMode);
// console.log('pathToArena:', pathToArena, 'newMode:', enteredMode);
return {mode: enteredMode, environment, inputEvents: rt.inputEvents, ...raised};
}

View file

@ -18,6 +18,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
comments: [],
entryActions: [],
exitActions: [],
depth: 0,
}
const uid2State = new Map<string, ConcreteState>([["root", root]]);
@ -54,10 +55,12 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
const candidate = parentCandidates[i];
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
// found our parent :)
const parentState = uid2State.get(candidate.uid);
parentState!.children.push(state);
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;
}
}