interpreter initializes statechart

This commit is contained in:
Joeri Exelmans 2025-10-07 17:47:07 +02:00
parent 692c052e11
commit b9327d2eb0
5 changed files with 171 additions and 44 deletions

View file

@ -36,7 +36,9 @@ text.highlight {
} }
.rountangle:hover { .rountangle:hover {
/* fill: lightgrey; */ /* stroke: darkcyan; */
/* stroke-opacity: 0.2; */
/* fill: #eee; */
/* stroke-width: 4px; */ /* stroke-width: 4px; */
/* cursor: grab; */ /* cursor: grab; */
} }
@ -65,15 +67,29 @@ text.highlight {
stroke-width: 16px; stroke-width: 16px;
} }
.lineHelper:hover { .lineHelper:hover {
stroke: rgba(0, 255, 0, 0.2); stroke: darkcyan;
stroke-opacity: 0.2;
cursor: grab; cursor: grab;
} }
.pathHelper {
fill: none;
stroke: rgba(0, 0, 0, 0);
stroke-width: 16px;
}
.pathHelper:hover {
stroke: darkcyan;
stroke-opacity: 0.2;
cursor: grab;
}
.circleHelper { .circleHelper {
fill: rgba(0, 0, 0, 0); fill: rgba(0, 0, 0, 0);
} }
.circleHelper:hover { .circleHelper:hover {
fill: rgba(0, 255, 0, 0.2); fill: darkcyan;
fill-opacity: 0.2;
cursor: grab; cursor: grab;
} }

View file

@ -9,6 +9,7 @@ import { parseStatechart } from "./parser";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters"; import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
import * as lz4 from "@nick/lz4"; import * as lz4 from "@nick/lz4";
import { initialize } from "./interpreter";
type DraggingState = { type DraggingState = {
@ -138,6 +139,9 @@ export function VisualEditor() {
const [statechart, errors] = parseStatechart(state); const [statechart, errors] = parseStatechart(state);
console.log('statechart: ', statechart, 'errors:', errors); console.log('statechart: ', statechart, 'errors:', errors);
setErrors(errors); setErrors(errors);
const rt = initialize(statechart);
console.log('runtime:', rt);
}, 100); }, 100);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [state]); }, [state]);
@ -183,7 +187,7 @@ export function VisualEditor() {
...state, ...state,
texts: [...state.texts, { texts: [...state.texts, {
uid: newID, uid: newID,
text: "Double-click to edit text", text: "// Double-click to edit",
topLeft: currentPointer, topLeft: currentPointer,
}], }],
nextID: state.nextID+1, nextID: state.nextID+1,
@ -538,7 +542,7 @@ export function VisualEditor() {
const commonProps = { const commonProps = {
"data-uid": txt.uid, "data-uid": txt.uid,
"data-parts": "text", "data-parts": "text",
textAnchor: "middle", textAnchor: "middle" as "middle",
className: className:
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"") (selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":""), +(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":""),
@ -763,12 +767,12 @@ export function ArrowSVG(props: {arrow: Arrow, selected: string[], errors: strin
{props.errors.length>0 && <text className="error" x={(start.x+end.x)/2+5} y={(start.y+end.y)/2} data-uid={uid} data-parts="start end">{props.errors.join(' ')}</text>} {props.errors.length>0 && <text className="error" x={(start.x+end.x)/2+5} y={(start.y+end.y)/2} data-uid={uid} data-parts="start end">{props.errors.join(' ')}</text>}
<line <path
className="lineHelper" className="pathHelper"
x1={start.x} // markerEnd='url(#arrowEnd)'
y1={start.y} d={`M ${start.x} ${start.y}
x2={end.x} ${arcOrLine}
y2={end.y} ${end.x} ${end.y}`}
data-uid={uid} data-uid={uid}
data-parts="start end" data-parts="start end"
/> />

View file

@ -1,27 +1,126 @@
import { ConcreteState, Statechart } from "./ast"; import { ConcreteState, Statechart } 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 { export function initialize(ast: Statechart): RT_Statechart {
const rt_root = recursiveEnter(ast.root) as RT_OrState; const {mode, environment, raised} = enter(ast.root, {
environment: new Map(),
raised: initialRaised,
});
return { return {
root: rt_root, mode,
variables: new Map(), environment,
inputEvents: [],
...raised,
}; };
} }
export function recursiveEnter(state: ConcreteState): RT_ConcreteState { type ActionScope = {
environment: Environment,
raised: RaisedEvents,
};
export function enter(state: ConcreteState, rt: ActionScope): ({mode: Mode} & ActionScope) {
let {environment, raised} = rt;
for (const action of state.entryActions) {
({environment, raised} = execAction(action, {environment, raised}));
}
if (state.kind === "and") { if (state.kind === "and") {
return { const mode: {[uid:string]: Mode} = {};
kind: "and", for (const child of state.children) {
children: state.children.map(child => recursiveEnter(child)), let childMode;
}; ({mode: childMode, environment, raised} = enter(child, {environment, raised}));
mode[child.uid] = childMode;
}
return { mode, environment, raised };
} }
else { else if (state.kind === "or") {
const currentState = state.initial[0][1]; const mode: {[uid:string]: Mode} = {};
return { // same as AND-state, but we only enter the initial state(s)
kind: "or", for (const [_, child] of state.initial) {
current: currentState.uid, let childMode;
current_rt: recursiveEnter(currentState), ({mode: childMode, environment, raised} = enter(child, {environment, raised}));
}; mode[child.uid] = childMode;
return { mode, environment, raised };
}
} }
throw new Error("should never reach here");
}
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,
raised: {
...rt.raised,
internalEvents: [...rt.raised.internalEvents, action.event]},
};
}
else {
// append to output events
return {
...rt,
raised: {
...rt.raised,
outputEvents: [...rt.raised.outputEvents, action.event],
},
}
}
}
throw new Error("should never reach here");
}
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 argA = evalExpr(expr.lhs, environment);
const argB = evalExpr(expr.rhs, environment);
return BINARY_OPERATOR_MAP.get(expr.operator)!(argA,argB);
}
throw new Error("should never reach here");
} }

View file

@ -196,7 +196,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
errorShapes.push([text.uid, { errorShapes.push([text.uid, {
message: "states can only have entry/exit triggers", message: "states can only have entry/exit triggers",
location: {start: {offset: 0}, end: {offset: text.text.length}}, location: {start: {offset: 0}, end: {offset: text.text.length}},
} as unknown as string]); }]);
} }
} }
@ -235,9 +235,7 @@ function findVariables(expr: Expression): Set<string> {
else if (expr.kind === "binaryExpr") { else if (expr.kind === "binaryExpr") {
return findVariables(expr.lhs).union(findVariables(expr.rhs)); return findVariables(expr.lhs).union(findVariables(expr.rhs));
} }
else if (expr.kind === "literal") { return new Set();
return new Set();
}
} }
function findVariablesAction(action: Action): Set<string> { function findVariablesAction(action: Action): Set<string> {

View file

@ -1,18 +1,28 @@
// modal configuration: maps child-uid to modal configuration of the child
// for OR-states, only the modal configuration of the current state is kept
// for AND-states, the modal configuration of every child is kept
// for basic states (= empty AND-states), the modal configuration is just an empty object
export type Mode = {[uid:string]: Mode};
type RT_ConcreteState = RT_OrState | RT_AndState; export type Environment = ReadonlyMap<string, any>; // variable name -> value
type RT_OrState = { export type RT_Statechart = {
kind: "or"; mode: Mode;
current: string; environment: Environment;
current_rt: RT_ConcreteState; // keep the runtime configuration only of the current state // history: // TODO
inputEvents: string[];
} & RaisedEvents;
export type RaisedEvents = {
internalEvents: string[];
outputEvents: string[];
};
export const initialRaised: RaisedEvents = {
internalEvents: [],
outputEvents: [],
} }
type RT_AndState = { // export type RT_Events = {
kind: "and"; // };
children: RT_ConcreteState[]; // keep the runtime configuration of every child
}
type RT_Statechart = {
root: RT_OrState;
variables: Map<string, any>;
}