cleanup code a bit

This commit is contained in:
Joeri Exelmans 2025-10-14 13:29:13 +02:00
parent 5e7b944978
commit b14b9e205c
11 changed files with 327 additions and 155 deletions

11
src/App/AST.css Normal file
View file

@ -0,0 +1,11 @@
details.active {
background-color: rgba(255, 140, 0, 0.2);
}
details {
border: 1px black solid;
/* border-radius: 5px; */
background-color: white;
margin-bottom: 2px;
padding-right: 2px;
}

View file

@ -1,8 +1,11 @@
import { ConcreteState, stateDescription, Transition } from "../statecharts/abstract_syntax"; import { ConcreteState, stateDescription, Transition } from "../statecharts/abstract_syntax";
import { Action, Expression } from "../statecharts/label_ast"; import { Action, Expression } from "../statecharts/label_ast";
import { RT_Statechart } from "../statecharts/runtime_types";
import "./AST.css";
export function ShowTransition(props: {transition: Transition}) { export function ShowTransition(props: {transition: Transition}) {
return <>&#10132; {stateDescription(props.transition.tgt)}</>; return <> {stateDescription(props.transition.tgt)}</>;
} }
export function ShowExpr(props: {expr: Expression}) { export function ShowExpr(props: {expr: Expression}) {
@ -29,11 +32,11 @@ export function ShowAction(props: {action: Action}) {
} }
} }
export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>}) { export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined}) {
const description = stateDescription(props.root); const description = stateDescription(props.root);
const outgoing = props.transitions.get(props.root.uid) || []; const outgoing = props.transitions.get(props.root.uid) || [];
return <details open={true}> return <details open={true} className={props.rt?.mode.has(props.root.uid) ? "active" : ""}>
<summary>{props.root.kind}: {description}</summary> <summary>{props.root.kind}: {description}</summary>
{props.root.entryActions.length>0 && {props.root.entryActions.length>0 &&
@ -48,7 +51,7 @@ export function AST(props: {root: ConcreteState, transitions: Map<string, Transi
} }
{props.root.children.length>0 && {props.root.children.length>0 &&
props.root.children.map(child => props.root.children.map(child =>
<AST root={child} transitions={props.transitions} /> <AST root={child} transitions={props.transitions} rt={props.rt} />
) )
} }
{outgoing.length>0 && {outgoing.length>0 &&

View file

@ -13,10 +13,11 @@ import { Box, Stack } from "@mui/material";
import { TopPanel } from "./TopPanel"; import { TopPanel } from "./TopPanel";
import { RTHistory } from "./RTHistory"; import { RTHistory } from "./RTHistory";
import { AST } from "./AST"; import { AST } from "./AST";
import { TraceableError } from "../statecharts/parser";
export function App() { export function App() {
const [ast, setAST] = useState<Statechart>(emptyStatechart); const [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<[string,string][]>([]); const [errors, setErrors] = useState<TraceableError[]>([]);
const [rt, setRT] = useState<BigStep[]>([]); const [rt, setRT] = useState<BigStep[]>([]);
const [rtIdx, setRTIdx] = useState<number|undefined>(); const [rtIdx, setRTIdx] = useState<number|undefined>();
@ -40,7 +41,7 @@ export function App() {
function onRaise(inputEvent: string) { function onRaise(inputEvent: string) {
if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.has(inputEvent)) { if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.has(inputEvent)) {
const simtime = getSimTime(time, performance.now()); const simtime = getSimTime(time, performance.now());
const nextConfig = handleInputEvent(simtime, inputEvent, ast, rt[rtIdx]!); const nextConfig = handleInputEvent(simtime, {kind: "input", name: inputEvent}, ast, rt[rtIdx]!);
appendNewConfig(inputEvent, simtime, nextConfig); appendNewConfig(inputEvent, simtime, nextConfig);
} }
} }
@ -108,7 +109,7 @@ export function App() {
paddingRight: 1, paddingRight: 1,
paddingLeft: 1, paddingLeft: 1,
}}> }}>
<AST {...ast}/> <AST {...{...ast, rt: rt.at(rtIdx!)}}/>
<hr/> <hr/>
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx}}/> <RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx}}/>
</Box> </Box>

View file

@ -3,6 +3,10 @@
background-color: #eee; background-color: #eee;
} }
.svgCanvas.active {
background-color: rgb(255, 140, 0, 0.2);
}
text, text.highlight { text, text.highlight {
user-select: none; user-select: none;
/* text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; */ /* text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; */

View file

@ -1,17 +1,16 @@
import * as lz4 from "@nick/lz4";
import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react"; import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react";
import { Statechart } from "../statecharts/abstract_syntax";
import { Arrow, ArrowPart, Rountangle, RountanglePart, VisualEditorState, emptyState, findNearestArrow, findNearestRountangleSide, findRountangle } from "../statecharts/concrete_syntax";
import { parseStatechart, TraceableError } from "../statecharts/parser";
import { BigStep } from "../statecharts/runtime_types";
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry"; import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
import { getBBoxInSvgCoords } from "./svg_helper";
import "./VisualEditor.css"; import "./VisualEditor.css";
import { getBBoxInSvgCoords } from "./svg_helper";
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 { BigStep, RT_Statechart } from "../statecharts/runtime_types";
import { Statechart } from "../statecharts/abstract_syntax";
type DraggingState = { type DraggingState = {
lastMousePos: Vec2D; lastMousePos: Vec2D;
@ -52,8 +51,8 @@ export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [
type VisualEditorProps = { type VisualEditorProps = {
setAST: Dispatch<SetStateAction<Statechart>>, setAST: Dispatch<SetStateAction<Statechart>>,
rt: BigStep|undefined, rt: BigStep|undefined,
errors: [string,string][], errors: TraceableError[],
setErrors: Dispatch<SetStateAction<[string,string][]>>, setErrors: Dispatch<SetStateAction<TraceableError[]>>,
}; };
export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) { export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) {
@ -204,6 +203,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
nextID: state.nextID+1, nextID: state.nextID+1,
} }
} }
throw new Error("unreachable"); // shut up typescript
}); });
setDragging({ setDragging({
lastMousePos: currentPointer, lastMousePos: currentPointer,
@ -221,13 +221,13 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
// if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on // if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on
let allPartsInSelection = true; let allPartsInSelection = true;
for (const part of parts) { for (const part of parts) {
if (!(selection.find(s => s.uid === uid)?.parts || []).includes(part)) { if (!(selection.find(s => s.uid === uid)?.parts || [] as string[]).includes(part)) {
allPartsInSelection = false; allPartsInSelection = false;
break; break;
} }
} }
if (!allPartsInSelection) { if (!allPartsInSelection) {
setSelection([{uid, parts}]); setSelection([{uid, parts}] as Selection);
} }
// start dragging // start dragging
@ -513,10 +513,10 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
const active = rt?.mode || new Set(); const active = rt?.mode || new Set();
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]); const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
return <svg width="4000px" height="4000px" return <svg width="4000px" height="4000px"
className="svgCanvas" className={"svgCanvas"+(active.has("root")?" active":"")}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onContextMenu={e => e.preventDefault()} onContextMenu={e => e.preventDefault()}
ref={refSVG} ref={refSVG}
@ -540,22 +540,26 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
key={rountangle.uid} key={rountangle.uid}
rountangle={rountangle} rountangle={rountangle}
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []} selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[])]} highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
errors={errors.filter(([uid,msg])=>uid===rountangle.uid).map(err=>err[1])} errors={errors
.filter(({shapeUid}) => shapeUid === rountangle.uid)
.map(({message}) => message)}
active={active.has(rountangle.uid)} active={active.has(rountangle.uid)}
/>)} />)}
{state.arrows.map(arrow => { {state.arrows.map(arrow => {
const sides = arrow2SideMap.get(arrow.uid); const sides = arrow2SideMap.get(arrow.uid);
let arc = "no" as ArcDirection; let arc = "no" as ArcDirection;
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0].uid !== undefined) { if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
arc = arcDirection(sides[0]?.part, sides[1]?.part); arc = arcDirection(sides[0]!.part, sides[1]!.part);
} }
return <ArrowSVG return <ArrowSVG
key={arrow.uid} key={arrow.uid}
arrow={arrow} arrow={arrow}
selected={selection.find(a => a.uid === arrow.uid)?.parts || []} selected={selection.find(a => a.uid === arrow.uid)?.parts || []}
errors={errors.filter(([uid,msg])=>uid===arrow.uid).map(err=>err[1])} errors={errors
.filter(({shapeUid}) => shapeUid === arrow.uid)
.map(({message}) => message)}
highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)} highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
arc={arc} arc={arc}
/>; />;
@ -563,7 +567,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
)} )}
{state.texts.map(txt => { {state.texts.map(txt => {
const err = errors.find(([uid]) => txt.uid === uid)?.[1]; const err = errors.find(({shapeUid}) => txt.uid === shapeUid);
const commonProps = { const commonProps = {
"data-uid": txt.uid, "data-uid": txt.uid,
"data-parts": "text", "data-parts": "text",
@ -574,7 +578,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
} }
let textNode; let textNode;
if (err) { if (err) {
const {start,end} = err.location; const {start,end} = err.data;
textNode = <><text {...commonProps}> textNode = <><text {...commonProps}>
{txt.text.slice(0, start.offset)} {txt.text.slice(0, start.offset)}
<tspan className="error" data-uid={txt.uid} data-parts="text"> <tspan className="error" data-uid={txt.uid} data-parts="text">
@ -620,7 +624,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
{selectingState && <Selecting {...selectingState} />} {selectingState && <Selecting {...selectingState} />}
{showHelp ? <> {/* {showHelp ? <>
<text x={5} y={20}> <text x={5} y={20}>
Left mouse button: Select/Drag. Left mouse button: Select/Drag.
</text> </text>
@ -641,7 +645,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps)
<text x={5} y={140}> <text x={5} y={140}>
[H] Show/hide this help. [H] Show/hide this help.
</text> </text>
</> : <text x={5} y={20}>[H] To show help.</text>} </> : <text x={5} y={20}>[H] To show help.</text>} */}
</svg>; </svg>;
} }

View file

@ -1,7 +1,7 @@
import { evalExpr } from "./actionlang_interpreter"; import { evalExpr } from "./actionlang_interpreter";
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax"; import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
import { Action } from "./label_ast"; import { Action, EventTrigger } from "./label_ast";
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, TimerElapseEvent, Timers } from "./runtime_types"; import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event } from "./runtime_types";
export function initialize(ast: Statechart): BigStepOutput { export function initialize(ast: Statechart): BigStepOutput {
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, { let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
@ -27,7 +27,7 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
const timers: Timers = [...(environment.get("_timers") || [])]; const timers: Timers = [...(environment.get("_timers") || [])];
for (const timeOffset of state.timers) { for (const timeOffset of state.timers) {
const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled
timers.push([futureSimTime, {state: state.uid, timeDurMs: timeOffset}]); timers.push([futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}]);
} }
timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first
environment.set("_timers", timers); environment.set("_timers", timers);
@ -167,40 +167,46 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
}; };
} }
else if (action.kind === "raise") { else if (action.kind === "raise") {
const raisedEvent = {
name: action.event,
param: action.param && evalExpr(action.param, rt.environment),
};
if (action.event.startsWith('_')) { if (action.event.startsWith('_')) {
// append to internal events // append to internal events
return { return {
...rt, ...rt,
internalEvents: [...rt.internalEvents, action.event], internalEvents: [...rt.internalEvents, raisedEvent],
}; };
} }
else { else {
// append to output events // append to output events
return { return {
...rt, ...rt,
outputEvents: [...rt.outputEvents, action.event], outputEvents: [...rt.outputEvents, raisedEvent],
} }
} }
} }
throw new Error("should never reach here"); 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 { export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
const arenasFired = new Set<OrState>(); const arenasFired = new Set<OrState>();
for (const state of activeParent.children) { for (const state of activeParent.children) {
if (mode.has(state.uid)) { if (mode.has(state.uid)) {
const outgoing = statechart.transitions.get(state.uid) || []; const outgoing = statechart.transitions.get(state.uid) || [];
let triggered; let triggered;
if (typeof event === 'string') { if (event.kind === "input") {
// get transitions triggered by event
triggered = outgoing.filter(transition => { triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger; const trigger = transition.label[0].trigger;
if (trigger.kind === "event") { if (trigger.kind === "event") {
return trigger.event === event; return trigger.event === event.name;
} }
return false; return false;
}); });
} }
else { else {
// get transitions triggered by timeout
triggered = outgoing.filter(transition => { triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger; const trigger = transition.label[0].trigger;
if (trigger.kind === "after") { if (trigger.kind === "after") {
@ -209,6 +215,7 @@ export function handleEvent(simtime: number, event: string | TimerElapseEvent, s
return false; return false;
}); });
} }
// eval guard
const enabled = triggered.filter(transition => const enabled = triggered.filter(transition =>
evalExpr(transition.label[0].guard, environment) evalExpr(transition.label[0].guard, environment)
); );
@ -227,7 +234,24 @@ export function handleEvent(simtime: number, event: string | TimerElapseEvent, s
} }
if (!overlapping) { if (!overlapping) {
console.log('^ firing'); console.log('^ firing');
let oldValue;
if (event.kind === "input" && event.param !== undefined) {
// input events may have a parameter
// *temporarily* add event to environment (dirty!)
oldValue = environment.get(event.param.name);
environment = new Map([
...environment,
[(t.label[0].trigger as EventTrigger).paramName as string, event.param.value],
]);
}
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised})); ({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
if (event.kind === "input" && event.param) {
// restore original value of variable that had same name as input parameter
environment = new Map([
...environment,
[(t.label[0].trigger as EventTrigger).paramName as string, oldValue],
]);
}
arenasFired.add(arena); arenasFired.add(arena);
} }
else { else {
@ -243,7 +267,7 @@ export function handleEvent(simtime: number, event: string | TimerElapseEvent, s
return {environment, mode, ...raised}; return {environment, mode, ...raised};
} }
export function handleInputEvent(simtime: number, event: string, statechart: Statechart, {mode, environment}: {mode: Mode, environment: Environment}): BigStepOutput { export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment}: {mode: Mode, environment: Environment}): BigStepOutput {
let raised = initialRaised; let raised = initialRaised;
({mode, environment, ...raised} = handleEvent(simtime, event, statechart, statechart.root, {mode, environment, ...raised})); ({mode, environment, ...raised} = handleEvent(simtime, event, statechart, statechart.root, {mode, environment, ...raised}));
@ -254,7 +278,9 @@ export function handleInputEvent(simtime: number, event: string, statechart: Sta
export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput { export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput {
while (raised.internalEvents.length > 0) { while (raised.internalEvents.length > 0) {
const [internalEvent, ...rest] = raised.internalEvents; const [internalEvent, ...rest] = raised.internalEvents;
({mode, environment, ...raised} = handleEvent(simtime, internalEvent, statechart, statechart.root, {mode, environment, internalEvents: rest, outputEvents: raised.outputEvents})); ({mode, environment, ...raised} = handleEvent(simtime,
{kind: "input", ...internalEvent}, // internal event becomes input event
statechart, statechart.root, {mode, environment, internalEvents: rest, outputEvents: raised.outputEvents}));
} }
return {mode, environment, outputEvents: raised.outputEvents}; return {mode, environment, outputEvents: raised.outputEvents};
} }

View file

@ -19,6 +19,7 @@ export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
export type EventTrigger = { export type EventTrigger = {
kind: "event"; kind: "event";
event: string; event: string;
paramName?: string;
} }
export type AfterTrigger = { export type AfterTrigger = {
@ -45,6 +46,7 @@ export type Assignment = {
export type RaiseEvent = { export type RaiseEvent = {
kind: "raise"; kind: "raise";
event: string; event: string;
param?: Expression;
} }

View file

@ -167,19 +167,19 @@ function peg$parse(input, options) {
const peg$c0 = "["; const peg$c0 = "[";
const peg$c1 = "]"; const peg$c1 = "]";
const peg$c2 = "/"; const peg$c2 = "/";
const peg$c3 = "after"; const peg$c3 = "(";
const peg$c4 = "entry"; const peg$c4 = ")";
const peg$c5 = "exit"; const peg$c5 = "after";
const peg$c6 = "ms"; const peg$c6 = "entry";
const peg$c7 = "s"; const peg$c7 = "exit";
const peg$c8 = ";"; const peg$c8 = "ms";
const peg$c9 = "="; const peg$c9 = "s";
const peg$c10 = "=="; const peg$c10 = ";";
const peg$c11 = "!="; const peg$c11 = "=";
const peg$c12 = "<="; const peg$c12 = "==";
const peg$c13 = ">="; const peg$c13 = "!=";
const peg$c14 = "("; const peg$c14 = "<=";
const peg$c15 = ")"; const peg$c15 = ">=";
const peg$c16 = "true"; const peg$c16 = "true";
const peg$c17 = "false"; const peg$c17 = "false";
const peg$c18 = "^"; const peg$c18 = "^";
@ -196,24 +196,24 @@ function peg$parse(input, options) {
const peg$e0 = peg$literalExpectation("[", false); const peg$e0 = peg$literalExpectation("[", false);
const peg$e1 = peg$literalExpectation("]", false); const peg$e1 = peg$literalExpectation("]", false);
const peg$e2 = peg$literalExpectation("/", false); const peg$e2 = peg$literalExpectation("/", false);
const peg$e3 = peg$literalExpectation("after", false); const peg$e3 = peg$literalExpectation("(", false);
const peg$e4 = peg$literalExpectation("entry", false); const peg$e4 = peg$literalExpectation(")", false);
const peg$e5 = peg$literalExpectation("exit", false); const peg$e5 = peg$literalExpectation("after", false);
const peg$e6 = peg$literalExpectation("ms", false); const peg$e6 = peg$literalExpectation("entry", false);
const peg$e7 = peg$literalExpectation("s", false); const peg$e7 = peg$literalExpectation("exit", false);
const peg$e8 = peg$literalExpectation(";", false); const peg$e8 = peg$literalExpectation("ms", false);
const peg$e9 = peg$literalExpectation("=", false); const peg$e9 = peg$literalExpectation("s", false);
const peg$e10 = peg$classExpectation([["0", "9"], ["A", "Z"], "_", ["a", "z"]], false, false, false); const peg$e10 = peg$literalExpectation(";", false);
const peg$e11 = peg$classExpectation([["0", "9"]], false, false, false); const peg$e11 = peg$literalExpectation("=", false);
const peg$e12 = peg$literalExpectation("==", false); const peg$e12 = peg$classExpectation([["0", "9"], ["A", "Z"], "_", ["a", "z"]], false, false, false);
const peg$e13 = peg$literalExpectation("!=", false); const peg$e13 = peg$classExpectation([["0", "9"]], false, false, false);
const peg$e14 = peg$classExpectation(["<", ">"], false, false, false); const peg$e14 = peg$literalExpectation("==", false);
const peg$e15 = peg$literalExpectation("<=", false); const peg$e15 = peg$literalExpectation("!=", false);
const peg$e16 = peg$literalExpectation(">=", false); const peg$e16 = peg$classExpectation(["<", ">"], false, false, false);
const peg$e17 = peg$classExpectation(["+", "-"], false, false, false); const peg$e17 = peg$literalExpectation("<=", false);
const peg$e18 = peg$classExpectation(["*", "/"], false, false, false); const peg$e18 = peg$literalExpectation(">=", false);
const peg$e19 = peg$literalExpectation("(", false); const peg$e19 = peg$classExpectation(["+", "-"], false, false, false);
const peg$e20 = peg$literalExpectation(")", false); const peg$e20 = peg$classExpectation(["*", "/"], false, false, false);
const peg$e21 = peg$literalExpectation("true", false); const peg$e21 = peg$literalExpectation("true", false);
const peg$e22 = peg$literalExpectation("false", false); const peg$e22 = peg$literalExpectation("false", false);
const peg$e23 = peg$literalExpectation("^", false); const peg$e23 = peg$literalExpectation("^", false);
@ -230,8 +230,8 @@ function peg$parse(input, options) {
actions: actions ? actions[2] : [], actions: actions ? actions[2] : [],
}; };
} }
function peg$f1(event) { function peg$f1(event, param) {
return {kind: "event", event}; return {kind: "event", event, param: param ? param[1] : undefined};
} }
function peg$f2(dur) { function peg$f2(dur) {
return {kind: "after", durationMs: dur}; return {kind: "after", durationMs: dur};
@ -305,8 +305,8 @@ function peg$parse(input, options) {
function peg$f17() { function peg$f17() {
return text() === "true"; return text() === "true";
} }
function peg$f18(event) { function peg$f18(event, param) {
return {kind: "raise", event}; return {kind: "raise", event, param: param ? param[1] : undefined};
} }
function peg$f19() { return null; } function peg$f19() { return null; }
function peg$f20(text) { function peg$f20(text) {
@ -597,15 +597,53 @@ function peg$parse(input, options) {
} }
function peg$parseeventTrigger() { function peg$parseeventTrigger() {
let s0, s1; let s0, s1, s2, s3, s4, s5;
s0 = peg$currPos; s0 = peg$currPos;
s1 = peg$parseidentifier(); s1 = peg$parseidentifier();
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s3 = peg$c3;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e3); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parseidentifier();
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 41) {
s5 = peg$c4;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e4); }
}
if (s5 !== peg$FAILED) {
s3 = [s3, s4, s5];
s2 = s3;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 === peg$FAILED) {
s2 = null;
}
peg$savedPos = s0; peg$savedPos = s0;
s1 = peg$f1(s1); s0 = peg$f1(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
} }
s0 = s1;
return s0; return s0;
} }
@ -614,12 +652,12 @@ function peg$parse(input, options) {
let s0, s1, s2, s3; let s0, s1, s2, s3;
s0 = peg$currPos; s0 = peg$currPos;
if (input.substr(peg$currPos, 5) === peg$c3) { if (input.substr(peg$currPos, 5) === peg$c5) {
s1 = peg$c3; s1 = peg$c5;
peg$currPos += 5; peg$currPos += 5;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e3); } if (peg$silentFails === 0) { peg$fail(peg$e5); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parse_(); s2 = peg$parse_();
@ -643,12 +681,12 @@ function peg$parse(input, options) {
let s0, s1; let s0, s1;
s0 = peg$currPos; s0 = peg$currPos;
if (input.substr(peg$currPos, 5) === peg$c4) { if (input.substr(peg$currPos, 5) === peg$c6) {
s1 = peg$c4; s1 = peg$c6;
peg$currPos += 5; peg$currPos += 5;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e4); } if (peg$silentFails === 0) { peg$fail(peg$e6); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
@ -663,12 +701,12 @@ function peg$parse(input, options) {
let s0, s1; let s0, s1;
s0 = peg$currPos; s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c5) { if (input.substr(peg$currPos, 4) === peg$c7) {
s1 = peg$c5; s1 = peg$c7;
peg$currPos += 4; peg$currPos += 4;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e5); } if (peg$silentFails === 0) { peg$fail(peg$e7); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
@ -705,21 +743,21 @@ function peg$parse(input, options) {
function peg$parsetimeUnit() { function peg$parsetimeUnit() {
let s0, s1; let s0, s1;
if (input.substr(peg$currPos, 2) === peg$c6) { if (input.substr(peg$currPos, 2) === peg$c8) {
s0 = peg$c6; s0 = peg$c8;
peg$currPos += 2; peg$currPos += 2;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e6); } if (peg$silentFails === 0) { peg$fail(peg$e8); }
} }
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
s0 = peg$currPos; s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 115) { if (input.charCodeAt(peg$currPos) === 115) {
s1 = peg$c7; s1 = peg$c9;
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e7); } if (peg$silentFails === 0) { peg$fail(peg$e9); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
@ -741,11 +779,11 @@ function peg$parse(input, options) {
s3 = peg$currPos; s3 = peg$currPos;
s4 = peg$parse_(); s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 59) { if (input.charCodeAt(peg$currPos) === 59) {
s5 = peg$c8; s5 = peg$c10;
peg$currPos++; peg$currPos++;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e8); } if (peg$silentFails === 0) { peg$fail(peg$e10); }
} }
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
s6 = peg$parse_(); s6 = peg$parse_();
@ -766,11 +804,11 @@ function peg$parse(input, options) {
s3 = peg$currPos; s3 = peg$currPos;
s4 = peg$parse_(); s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 59) { if (input.charCodeAt(peg$currPos) === 59) {
s5 = peg$c8; s5 = peg$c10;
peg$currPos++; peg$currPos++;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e8); } if (peg$silentFails === 0) { peg$fail(peg$e10); }
} }
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
s6 = peg$parse_(); s6 = peg$parse_();
@ -789,11 +827,11 @@ function peg$parse(input, options) {
} }
s3 = peg$parse_(); s3 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 59) { if (input.charCodeAt(peg$currPos) === 59) {
s4 = peg$c8; s4 = peg$c10;
peg$currPos++; peg$currPos++;
} else { } else {
s4 = peg$FAILED; s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e8); } if (peg$silentFails === 0) { peg$fail(peg$e10); }
} }
if (s4 === peg$FAILED) { if (s4 === peg$FAILED) {
s4 = null; s4 = null;
@ -827,11 +865,11 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parse_(); s2 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 61) { if (input.charCodeAt(peg$currPos) === 61) {
s3 = peg$c9; s3 = peg$c11;
peg$currPos++; peg$currPos++;
} else { } else {
s3 = peg$FAILED; s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e9); } if (peg$silentFails === 0) { peg$fail(peg$e11); }
} }
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
s4 = peg$parse_(); s4 = peg$parse_();
@ -865,7 +903,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e10); } if (peg$silentFails === 0) { peg$fail(peg$e12); }
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) {
@ -875,7 +913,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e10); } if (peg$silentFails === 0) { peg$fail(peg$e12); }
} }
} }
} else { } else {
@ -900,7 +938,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e11); } if (peg$silentFails === 0) { peg$fail(peg$e13); }
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) {
@ -910,7 +948,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e11); } if (peg$silentFails === 0) { peg$fail(peg$e13); }
} }
} }
} else { } else {
@ -934,20 +972,20 @@ function peg$parse(input, options) {
s2 = peg$currPos; s2 = peg$currPos;
s3 = peg$currPos; s3 = peg$currPos;
s4 = peg$parse_(); s4 = peg$parse_();
if (input.substr(peg$currPos, 2) === peg$c10) { if (input.substr(peg$currPos, 2) === peg$c12) {
s5 = peg$c10; s5 = peg$c12;
peg$currPos += 2; peg$currPos += 2;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e12); } if (peg$silentFails === 0) { peg$fail(peg$e14); }
} }
if (s5 === peg$FAILED) { if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c11) { if (input.substr(peg$currPos, 2) === peg$c13) {
s5 = peg$c11; s5 = peg$c13;
peg$currPos += 2; peg$currPos += 2;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e13); } if (peg$silentFails === 0) { peg$fail(peg$e15); }
} }
if (s5 === peg$FAILED) { if (s5 === peg$FAILED) {
s5 = input.charAt(peg$currPos); s5 = input.charAt(peg$currPos);
@ -955,23 +993,23 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e14); } if (peg$silentFails === 0) { peg$fail(peg$e16); }
} }
if (s5 === peg$FAILED) { if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c12) { if (input.substr(peg$currPos, 2) === peg$c14) {
s5 = peg$c12; s5 = peg$c14;
peg$currPos += 2; peg$currPos += 2;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e15); } if (peg$silentFails === 0) { peg$fail(peg$e17); }
} }
if (s5 === peg$FAILED) { if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c13) { if (input.substr(peg$currPos, 2) === peg$c15) {
s5 = peg$c13; s5 = peg$c15;
peg$currPos += 2; peg$currPos += 2;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e16); } if (peg$silentFails === 0) { peg$fail(peg$e18); }
} }
} }
} }
@ -1025,7 +1063,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); } if (peg$silentFails === 0) { peg$fail(peg$e19); }
} }
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
s6 = peg$parse_(); s6 = peg$parse_();
@ -1075,7 +1113,7 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e18); } if (peg$silentFails === 0) { peg$fail(peg$e20); }
} }
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
s6 = peg$parse_(); s6 = peg$parse_();
@ -1130,11 +1168,11 @@ function peg$parse(input, options) {
s0 = peg$currPos; s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) { if (input.charCodeAt(peg$currPos) === 40) {
s1 = peg$c14; s1 = peg$c3;
peg$currPos++; peg$currPos++;
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e19); } if (peg$silentFails === 0) { peg$fail(peg$e3); }
} }
if (s1 !== peg$FAILED) { if (s1 !== peg$FAILED) {
s2 = peg$parse_(); s2 = peg$parse_();
@ -1142,11 +1180,11 @@ function peg$parse(input, options) {
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
s4 = peg$parse_(); s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 41) { if (input.charCodeAt(peg$currPos) === 41) {
s5 = peg$c15; s5 = peg$c4;
peg$currPos++; peg$currPos++;
} else { } else {
s5 = peg$FAILED; s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e20); } if (peg$silentFails === 0) { peg$fail(peg$e4); }
} }
if (s5 !== peg$FAILED) { if (s5 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
@ -1228,7 +1266,7 @@ function peg$parse(input, options) {
} }
function peg$parseraise() { function peg$parseraise() {
let s0, s1, s2, s3; let s0, s1, s2, s3, s4, s5, s6, s7;
s0 = peg$currPos; s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 94) { if (input.charCodeAt(peg$currPos) === 94) {
@ -1242,8 +1280,44 @@ function peg$parse(input, options) {
s2 = peg$parse_(); s2 = peg$parse_();
s3 = peg$parseidentifier(); s3 = peg$parseidentifier();
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
s4 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s5 = peg$c3;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e3); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parsecompare();
if (s6 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 41) {
s7 = peg$c4;
peg$currPos++;
} else {
s7 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e4); }
}
if (s7 !== peg$FAILED) {
s5 = [s5, s6, s7];
s4 = s5;
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
if (s4 === peg$FAILED) {
s4 = null;
}
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f18(s3); s0 = peg$f18(s3, s4);
} else { } else {
peg$currPos = s0; peg$currPos = s0;
s0 = peg$FAILED; s0 = peg$FAILED;

View file

@ -5,8 +5,14 @@ import { Action, Expression, ParsedText } from "./label_ast";
import { parse as parseLabel, SyntaxError } from "./label_parser"; import { parse as parseLabel, SyntaxError } from "./label_parser";
export function parseStatechart(state: VisualEditorState): [Statechart, [string,string][]] { export type TraceableError = {
const errorShapes: [string, string][] = []; shapeUid: string;
message: string;
data?: any;
}
export function parseStatechart(state: VisualEditorState): [Statechart, TraceableError[]] {
const errors: TraceableError[] = [];
// implicitly, the root is always an Or-state // implicitly, the root is always an Or-state
const root: OrState = { const root: OrState = {
@ -37,6 +43,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
// we assume that the rountangles are sorted from big to small: // we assume that the rountangles are sorted from big to small:
for (const rt of state.rountangles) { for (const rt of state.rountangles) {
// @ts-ignore
const state: ConcreteState = { const state: ConcreteState = {
kind: rt.kind, kind: rt.kind,
uid: rt.uid, uid: rt.uid,
@ -45,11 +52,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
entryActions: [], entryActions: [],
exitActions: [], exitActions: [],
timers: [], timers: [],
} };
if (state.kind === "or") { if (state.kind === "or") {
state.initial = []; (state as unknown as OrState).initial = [];
} }
uid2State.set(rt.uid, state); uid2State.set(rt.uid, (state));
// iterate in reverse: // iterate in reverse:
for (let i=parentCandidates.length-1; i>=0; i--) { for (let i=parentCandidates.length-1; i>=0; i--) {
@ -57,7 +64,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) { if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
// found our parent :) // found our parent :)
const parentState = uid2State.get(candidate.uid)!; const parentState = uid2State.get(candidate.uid)!;
parentState.children.push(state); parentState.children.push(state as unknown as ConcreteState);
parentCandidates.push(rt); parentCandidates.push(rt);
parentLinks.set(rt.uid, candidate.uid); parentLinks.set(rt.uid, candidate.uid);
state.parent = parentState; state.parent = parentState;
@ -78,7 +85,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
if (!srcUID) { if (!srcUID) {
if (!tgtUID) { if (!tgtUID) {
// dangling edge - todo: display error... // dangling edge - todo: display error...
errorShapes.push([arr.uid, "dangling"]); errors.push({shapeUid: arr.uid, message: "dangling"});
} }
else { else {
// target but no source, so we treat is as an 'initial' marking // target but no source, so we treat is as an 'initial' marking
@ -89,13 +96,19 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
} }
else { else {
// and states do not have an 'initial' state - todo: display error... // and states do not have an 'initial' state - todo: display error...
errorShapes.push([arr.uid, "AND-state cannot have an initial state"]); errors.push({
shapeUid: arr.uid,
message: "AND-state cannot have an initial state",
});
} }
} }
} }
else { else {
if (!tgtUID) { if (!tgtUID) {
errorShapes.push([arr.uid, "no target"]); errors.push({
shapeUid: arr.uid,
message: "no target",
});
} }
else { else {
// add transition // add transition
@ -116,10 +129,16 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
for (const state of uid2State.values()) { for (const state of uid2State.values()) {
if (state.kind === "or") { if (state.kind === "or") {
if (state.initial.length > 1) { if (state.initial.length > 1) {
errorShapes.push(...state.initial.map(([uid,childState])=>[uid,"multiple initial states"] as [string, string])); errors.push(...state.initial.map(([uid,childState]) => ({
shapeUid: uid,
message: "multiple initial states",
})));
} }
else if (state.initial.length === 0) { else if (state.initial.length === 0) {
errorShapes.push([state.uid, "no initial state"]); errors.push({
shapeUid: state.uid,
message: "no initial state",
});
} }
} }
} }
@ -138,7 +157,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
parsed = parseLabel(text.text); // may throw parsed = parseLabel(text.text); // may throw
} catch (e) { } catch (e) {
if (e instanceof SyntaxError) { if (e instanceof SyntaxError) {
errorShapes.push([text.uid, e]); errors.push({
shapeUid: text.uid,
message: e.message,
data: e,
});
continue; continue;
} }
else { else {
@ -202,10 +225,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
belongsToState.exitActions.push(...parsed.actions); belongsToState.exitActions.push(...parsed.actions);
} }
else { else {
errorShapes.push([text.uid, { errors.push({
shapeUid: 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}}, data: {start: {offset: 0}, end: {offset: text.text.length}},
}]); });
} }
} }
@ -217,10 +241,16 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
for (const transition of uid2Transition.values()) { for (const transition of uid2Transition.values()) {
if (transition.label.length === 0) { if (transition.label.length === 0) {
errorShapes.push([transition.uid, "no label"]); errors.push({
shapeUid: transition.uid,
message: "no label",
});
} }
else if (transition.label.length > 1) { else if (transition.label.length > 1) {
errorShapes.push([transition.uid, "multiple labels"]); errors.push({
shapeUid: transition.uid,
message: "multiple labels",
});
} }
} }
@ -232,7 +262,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
internalEvents, internalEvents,
outputEvents, outputEvents,
uid2State, uid2State,
}, errorShapes]; }, errors];
} }
function findVariables(expr: Expression): Set<string> { function findVariables(expr: Expression): Set<string> {

View file

@ -1,9 +1,22 @@
export type Timestamp = number; // milliseconds since begin of simulation export type Timestamp = number; // milliseconds since begin of simulation
export type Event = string;
export type RT_Event = InputEvent | TimerElapseEvent;
export type InputEvent = {
kind: "input",
name: string,
param?: any,
}
export type TimerElapseEvent = {
kind: "timer",
state: string,
timeDurMs: number,
}
export type Mode = Set<string>; // set of active states export type Mode = Set<string>; // set of active states
export type Environment = ReadonlyMap<string, any>; // variable name -> value export type Environment = ReadonlyMap<string, any>; // variable name -> value
export type RT_Statechart = { export type RT_Statechart = {
@ -13,7 +26,7 @@ export type RT_Statechart = {
} }
export type BigStepOutput = RT_Statechart & { export type BigStepOutput = RT_Statechart & {
outputEvents: string[], outputEvents: RaisedEvent[],
}; };
export type BigStep = { export type BigStep = {
@ -21,10 +34,16 @@ export type BigStep = {
simtime: number, simtime: number,
} & BigStepOutput; } & BigStepOutput;
// internal or output event
export type RaisedEvent = {
name: string,
param?: any,
}
export type RaisedEvents = { export type RaisedEvents = {
internalEvents: string[]; internalEvents: RaisedEvent[];
outputEvents: string[]; outputEvents: RaisedEvent[];
}; };
// export type Timers = Map<string, number>; // transition uid -> timestamp // export type Timers = Map<string, number>; // transition uid -> timestamp
@ -34,6 +53,4 @@ export const initialRaised: RaisedEvents = {
outputEvents: [], outputEvents: [],
}; };
export type TimerElapseEvent = { state: string; timeDurMs: number; };
export type Timers = [number, TimerElapseEvent][]; export type Timers = [number, TimerElapseEvent][];

View file

@ -11,8 +11,8 @@ tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions
trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger
eventTrigger = event:identifier { eventTrigger = event:identifier param:("(" identifier ")")? {
return {kind: "event", event}; return {kind: "event", event, param: param ? param[1] : undefined};
} }
afterTrigger = "after" _ dur:durationMs { afterTrigger = "after" _ dur:durationMs {
@ -111,8 +111,8 @@ boolean = ("true" / "false") {
return text() === "true"; return text() === "true";
} }
raise = "^" _ event:identifier { raise = "^" _ event:identifier param:("(" expr ")")? {
return {kind: "raise", event}; return {kind: "raise", event, param: param ? param[1] : undefined};
} }
_ "whitespace" _ "whitespace"