diff --git a/src/App/AST.css b/src/App/AST.css new file mode 100644 index 0000000..ad90394 --- /dev/null +++ b/src/App/AST.css @@ -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; +} \ No newline at end of file diff --git a/src/App/AST.tsx b/src/App/AST.tsx index 7b7ea3e..2de6e2b 100644 --- a/src/App/AST.tsx +++ b/src/App/AST.tsx @@ -1,8 +1,11 @@ import { ConcreteState, stateDescription, Transition } from "../statecharts/abstract_syntax"; import { Action, Expression } from "../statecharts/label_ast"; +import { RT_Statechart } from "../statecharts/runtime_types"; + +import "./AST.css"; export function ShowTransition(props: {transition: Transition}) { - return <>➔ {stateDescription(props.transition.tgt)}; + return <>➝ {stateDescription(props.transition.tgt)}; } export function ShowExpr(props: {expr: Expression}) { @@ -29,11 +32,11 @@ export function ShowAction(props: {action: Action}) { } } -export function AST(props: {root: ConcreteState, transitions: Map}) { +export function AST(props: {root: ConcreteState, transitions: Map, rt: RT_Statechart | undefined}) { const description = stateDescription(props.root); const outgoing = props.transitions.get(props.root.uid) || []; - return
+ return
{props.root.kind}: {description} {props.root.entryActions.length>0 && @@ -48,7 +51,7 @@ export function AST(props: {root: ConcreteState, transitions: Map0 && props.root.children.map(child => - + ) } {outgoing.length>0 && diff --git a/src/App/App.tsx b/src/App/App.tsx index fb3c5bf..692feb2 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -13,10 +13,11 @@ import { Box, Stack } from "@mui/material"; import { TopPanel } from "./TopPanel"; import { RTHistory } from "./RTHistory"; import { AST } from "./AST"; +import { TraceableError } from "../statecharts/parser"; export function App() { const [ast, setAST] = useState(emptyStatechart); - const [errors, setErrors] = useState<[string,string][]>([]); + const [errors, setErrors] = useState([]); const [rt, setRT] = useState([]); const [rtIdx, setRTIdx] = useState(); @@ -40,7 +41,7 @@ export function App() { function onRaise(inputEvent: string) { if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.has(inputEvent)) { 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); } } @@ -108,7 +109,7 @@ export function App() { paddingRight: 1, paddingLeft: 1, }}> - +
diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css index 10003be..fa10a2f 100644 --- a/src/VisualEditor/VisualEditor.css +++ b/src/VisualEditor/VisualEditor.css @@ -3,6 +3,10 @@ background-color: #eee; } +.svgCanvas.active { + background-color: rgb(255, 140, 0, 0.2); +} + text, text.highlight { 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; */ diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index c5ba308..a38e153 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -1,17 +1,16 @@ +import * as lz4 from "@nick/lz4"; 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 { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters"; +import { getBBoxInSvgCoords } from "./svg_helper"; 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 = { lastMousePos: Vec2D; @@ -52,8 +51,8 @@ export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [ type VisualEditorProps = { setAST: Dispatch>, rt: BigStep|undefined, - errors: [string,string][], - setErrors: Dispatch>, + errors: TraceableError[], + setErrors: Dispatch>, }; export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) { @@ -204,6 +203,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) nextID: state.nextID+1, } } + throw new Error("unreachable"); // shut up typescript }); setDragging({ 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 let allPartsInSelection = true; 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; break; } } if (!allPartsInSelection) { - setSelection([{uid, parts}]); + setSelection([{uid, parts}] as Selection); } // start dragging @@ -513,10 +513,10 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) 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 e.preventDefault()} ref={refSVG} @@ -540,22 +540,26 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) key={rountangle.uid} rountangle={rountangle} 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])} + highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]} + errors={errors + .filter(({shapeUid}) => shapeUid === rountangle.uid) + .map(({message}) => message)} active={active.has(rountangle.uid)} />)} {state.arrows.map(arrow => { const sides = arrow2SideMap.get(arrow.uid); let arc = "no" as ArcDirection; - if (sides && sides[0]?.uid === sides[1]?.uid && sides[0].uid !== undefined) { - arc = arcDirection(sides[0]?.part, sides[1]?.part); + if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) { + arc = arcDirection(sides[0]!.part, sides[1]!.part); } return 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)} arc={arc} />; @@ -563,7 +567,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) )} {state.texts.map(txt => { - const err = errors.find(([uid]) => txt.uid === uid)?.[1]; + const err = errors.find(({shapeUid}) => txt.uid === shapeUid); const commonProps = { "data-uid": txt.uid, "data-parts": "text", @@ -574,7 +578,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) } let textNode; if (err) { - const {start,end} = err.location; + const {start,end} = err.data; textNode = <> {txt.text.slice(0, start.offset)} @@ -620,7 +624,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) {selectingState && } - {showHelp ? <> + {/* {showHelp ? <> Left mouse button: Select/Drag. @@ -641,7 +645,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) [H] Show/hide this help. - : [H] To show help.} + : [H] To show help.} */} ; } diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index 5b406fa..5c5a7db 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -1,7 +1,7 @@ import { evalExpr } from "./actionlang_interpreter"; import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax"; -import { Action } from "./label_ast"; -import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, TimerElapseEvent, Timers } from "./runtime_types"; +import { Action, EventTrigger } from "./label_ast"; +import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event } from "./runtime_types"; export function initialize(ast: Statechart): BigStepOutput { 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") || [])]; 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.push([futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}]); } timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first environment.set("_timers", timers); @@ -167,40 +167,46 @@ export function execAction(action: Action, rt: ActionScope): ActionScope { }; } else if (action.kind === "raise") { + const raisedEvent = { + name: action.event, + param: action.param && evalExpr(action.param, rt.environment), + }; if (action.event.startsWith('_')) { // append to internal events return { ...rt, - internalEvents: [...rt.internalEvents, action.event], + internalEvents: [...rt.internalEvents, raisedEvent], }; } else { // append to output events return { ...rt, - outputEvents: [...rt.outputEvents, action.event], + outputEvents: [...rt.outputEvents, raisedEvent], } } } 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(); for (const state of activeParent.children) { if (mode.has(state.uid)) { const outgoing = statechart.transitions.get(state.uid) || []; let triggered; - if (typeof event === 'string') { + if (event.kind === "input") { + // get transitions triggered by event triggered = outgoing.filter(transition => { const trigger = transition.label[0].trigger; if (trigger.kind === "event") { - return trigger.event === event; + return trigger.event === event.name; } return false; }); } else { + // get transitions triggered by timeout triggered = outgoing.filter(transition => { const trigger = transition.label[0].trigger; if (trigger.kind === "after") { @@ -209,6 +215,7 @@ export function handleEvent(simtime: number, event: string | TimerElapseEvent, s return false; }); } + // eval guard const enabled = triggered.filter(transition => evalExpr(transition.label[0].guard, environment) ); @@ -227,7 +234,24 @@ export function handleEvent(simtime: number, event: string | TimerElapseEvent, s } if (!overlapping) { 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})); + 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); } else { @@ -243,7 +267,7 @@ export function handleEvent(simtime: number, event: string | TimerElapseEvent, s 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; ({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 { 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})); + ({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}; } diff --git a/src/statecharts/label_ast.ts b/src/statecharts/label_ast.ts index 42e2919..7c2f43c 100644 --- a/src/statecharts/label_ast.ts +++ b/src/statecharts/label_ast.ts @@ -19,6 +19,7 @@ export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger; export type EventTrigger = { kind: "event"; event: string; + paramName?: string; } export type AfterTrigger = { @@ -45,6 +46,7 @@ export type Assignment = { export type RaiseEvent = { kind: "raise"; event: string; + param?: Expression; } diff --git a/src/statecharts/label_parser.js b/src/statecharts/label_parser.js index fac7cda..b92129b 100644 --- a/src/statecharts/label_parser.js +++ b/src/statecharts/label_parser.js @@ -167,19 +167,19 @@ function peg$parse(input, options) { const peg$c0 = "["; const peg$c1 = "]"; const peg$c2 = "/"; - const peg$c3 = "after"; - const peg$c4 = "entry"; - const peg$c5 = "exit"; - const peg$c6 = "ms"; - const peg$c7 = "s"; - const peg$c8 = ";"; - const peg$c9 = "="; - const peg$c10 = "=="; - const peg$c11 = "!="; - const peg$c12 = "<="; - const peg$c13 = ">="; - const peg$c14 = "("; - const peg$c15 = ")"; + const peg$c3 = "("; + const peg$c4 = ")"; + const peg$c5 = "after"; + const peg$c6 = "entry"; + const peg$c7 = "exit"; + const peg$c8 = "ms"; + const peg$c9 = "s"; + const peg$c10 = ";"; + const peg$c11 = "="; + const peg$c12 = "=="; + const peg$c13 = "!="; + const peg$c14 = "<="; + const peg$c15 = ">="; const peg$c16 = "true"; const peg$c17 = "false"; const peg$c18 = "^"; @@ -196,24 +196,24 @@ function peg$parse(input, options) { const peg$e0 = peg$literalExpectation("[", false); const peg$e1 = peg$literalExpectation("]", false); const peg$e2 = peg$literalExpectation("/", false); - const peg$e3 = peg$literalExpectation("after", false); - const peg$e4 = peg$literalExpectation("entry", false); - const peg$e5 = peg$literalExpectation("exit", false); - const peg$e6 = peg$literalExpectation("ms", false); - const peg$e7 = peg$literalExpectation("s", false); - const peg$e8 = peg$literalExpectation(";", false); - const peg$e9 = peg$literalExpectation("=", false); - const peg$e10 = peg$classExpectation([["0", "9"], ["A", "Z"], "_", ["a", "z"]], false, false, false); - const peg$e11 = peg$classExpectation([["0", "9"]], false, false, false); - const peg$e12 = peg$literalExpectation("==", false); - const peg$e13 = peg$literalExpectation("!=", false); - const peg$e14 = peg$classExpectation(["<", ">"], false, false, false); - const peg$e15 = peg$literalExpectation("<=", false); - const peg$e16 = peg$literalExpectation(">=", false); - const peg$e17 = peg$classExpectation(["+", "-"], false, false, false); - const peg$e18 = peg$classExpectation(["*", "/"], false, false, false); - const peg$e19 = peg$literalExpectation("(", false); - const peg$e20 = peg$literalExpectation(")", false); + const peg$e3 = peg$literalExpectation("(", false); + const peg$e4 = peg$literalExpectation(")", false); + const peg$e5 = peg$literalExpectation("after", false); + const peg$e6 = peg$literalExpectation("entry", false); + const peg$e7 = peg$literalExpectation("exit", false); + const peg$e8 = peg$literalExpectation("ms", false); + const peg$e9 = peg$literalExpectation("s", false); + const peg$e10 = peg$literalExpectation(";", false); + const peg$e11 = peg$literalExpectation("=", false); + const peg$e12 = peg$classExpectation([["0", "9"], ["A", "Z"], "_", ["a", "z"]], false, false, false); + const peg$e13 = peg$classExpectation([["0", "9"]], false, false, false); + const peg$e14 = peg$literalExpectation("==", false); + const peg$e15 = peg$literalExpectation("!=", false); + const peg$e16 = peg$classExpectation(["<", ">"], false, false, false); + const peg$e17 = peg$literalExpectation("<=", false); + const peg$e18 = peg$literalExpectation(">=", false); + const peg$e19 = peg$classExpectation(["+", "-"], false, false, false); + const peg$e20 = peg$classExpectation(["*", "/"], false, false, false); const peg$e21 = peg$literalExpectation("true", false); const peg$e22 = peg$literalExpectation("false", false); const peg$e23 = peg$literalExpectation("^", false); @@ -230,8 +230,8 @@ function peg$parse(input, options) { actions: actions ? actions[2] : [], }; } - function peg$f1(event) { - return {kind: "event", event}; + function peg$f1(event, param) { + return {kind: "event", event, param: param ? param[1] : undefined}; } function peg$f2(dur) { return {kind: "after", durationMs: dur}; @@ -305,8 +305,8 @@ function peg$parse(input, options) { function peg$f17() { return text() === "true"; } - function peg$f18(event) { - return {kind: "raise", event}; + function peg$f18(event, param) { + return {kind: "raise", event, param: param ? param[1] : undefined}; } function peg$f19() { return null; } function peg$f20(text) { @@ -597,15 +597,53 @@ function peg$parse(input, options) { } function peg$parseeventTrigger() { - let s0, s1; + let s0, s1, s2, s3, s4, s5; s0 = peg$currPos; s1 = peg$parseidentifier(); 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; - s1 = peg$f1(s1); + s0 = peg$f1(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; } - s0 = s1; return s0; } @@ -614,12 +652,12 @@ function peg$parse(input, options) { let s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 5) === peg$c3) { - s1 = peg$c3; + if (input.substr(peg$currPos, 5) === peg$c5) { + s1 = peg$c5; peg$currPos += 5; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e3); } + if (peg$silentFails === 0) { peg$fail(peg$e5); } } if (s1 !== peg$FAILED) { s2 = peg$parse_(); @@ -643,12 +681,12 @@ function peg$parse(input, options) { let s0, s1; s0 = peg$currPos; - if (input.substr(peg$currPos, 5) === peg$c4) { - s1 = peg$c4; + if (input.substr(peg$currPos, 5) === peg$c6) { + s1 = peg$c6; peg$currPos += 5; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e4); } + if (peg$silentFails === 0) { peg$fail(peg$e6); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -663,12 +701,12 @@ function peg$parse(input, options) { let s0, s1; s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c5) { - s1 = peg$c5; + if (input.substr(peg$currPos, 4) === peg$c7) { + s1 = peg$c7; peg$currPos += 4; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e5); } + if (peg$silentFails === 0) { peg$fail(peg$e7); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -705,21 +743,21 @@ function peg$parse(input, options) { function peg$parsetimeUnit() { let s0, s1; - if (input.substr(peg$currPos, 2) === peg$c6) { - s0 = peg$c6; + if (input.substr(peg$currPos, 2) === peg$c8) { + s0 = peg$c8; peg$currPos += 2; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e6); } + if (peg$silentFails === 0) { peg$fail(peg$e8); } } if (s0 === peg$FAILED) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 115) { - s1 = peg$c7; + s1 = peg$c9; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e7); } + if (peg$silentFails === 0) { peg$fail(peg$e9); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; @@ -741,11 +779,11 @@ function peg$parse(input, options) { s3 = peg$currPos; s4 = peg$parse_(); if (input.charCodeAt(peg$currPos) === 59) { - s5 = peg$c8; + s5 = peg$c10; peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e8); } + if (peg$silentFails === 0) { peg$fail(peg$e10); } } if (s5 !== peg$FAILED) { s6 = peg$parse_(); @@ -766,11 +804,11 @@ function peg$parse(input, options) { s3 = peg$currPos; s4 = peg$parse_(); if (input.charCodeAt(peg$currPos) === 59) { - s5 = peg$c8; + s5 = peg$c10; peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e8); } + if (peg$silentFails === 0) { peg$fail(peg$e10); } } if (s5 !== peg$FAILED) { s6 = peg$parse_(); @@ -789,11 +827,11 @@ function peg$parse(input, options) { } s3 = peg$parse_(); if (input.charCodeAt(peg$currPos) === 59) { - s4 = peg$c8; + s4 = peg$c10; peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e8); } + if (peg$silentFails === 0) { peg$fail(peg$e10); } } if (s4 === peg$FAILED) { s4 = null; @@ -827,11 +865,11 @@ function peg$parse(input, options) { if (s1 !== peg$FAILED) { s2 = peg$parse_(); if (input.charCodeAt(peg$currPos) === 61) { - s3 = peg$c9; + s3 = peg$c11; peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e9); } + if (peg$silentFails === 0) { peg$fail(peg$e11); } } if (s3 !== peg$FAILED) { s4 = peg$parse_(); @@ -865,7 +903,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e10); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -875,7 +913,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e10); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } } } else { @@ -900,7 +938,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e11); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -910,7 +948,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e11); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } } } else { @@ -934,20 +972,20 @@ function peg$parse(input, options) { s2 = peg$currPos; s3 = peg$currPos; s4 = peg$parse_(); - if (input.substr(peg$currPos, 2) === peg$c10) { - s5 = peg$c10; + if (input.substr(peg$currPos, 2) === peg$c12) { + s5 = peg$c12; peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e12); } + if (peg$silentFails === 0) { peg$fail(peg$e14); } } if (s5 === peg$FAILED) { - if (input.substr(peg$currPos, 2) === peg$c11) { - s5 = peg$c11; + if (input.substr(peg$currPos, 2) === peg$c13) { + s5 = peg$c13; peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e13); } + if (peg$silentFails === 0) { peg$fail(peg$e15); } } if (s5 === peg$FAILED) { s5 = input.charAt(peg$currPos); @@ -955,23 +993,23 @@ function peg$parse(input, options) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e14); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } if (s5 === peg$FAILED) { - if (input.substr(peg$currPos, 2) === peg$c12) { - s5 = peg$c12; + if (input.substr(peg$currPos, 2) === peg$c14) { + s5 = peg$c14; peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e17); } } if (s5 === peg$FAILED) { - if (input.substr(peg$currPos, 2) === peg$c13) { - s5 = peg$c13; + if (input.substr(peg$currPos, 2) === peg$c15) { + s5 = peg$c15; peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e16); } + if (peg$silentFails === 0) { peg$fail(peg$e18); } } } } @@ -1025,7 +1063,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e17); } + if (peg$silentFails === 0) { peg$fail(peg$e19); } } if (s5 !== peg$FAILED) { s6 = peg$parse_(); @@ -1075,7 +1113,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e18); } + if (peg$silentFails === 0) { peg$fail(peg$e20); } } if (s5 !== peg$FAILED) { s6 = peg$parse_(); @@ -1130,11 +1168,11 @@ function peg$parse(input, options) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 40) { - s1 = peg$c14; + s1 = peg$c3; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e19); } + if (peg$silentFails === 0) { peg$fail(peg$e3); } } if (s1 !== peg$FAILED) { s2 = peg$parse_(); @@ -1142,11 +1180,11 @@ function peg$parse(input, options) { if (s3 !== peg$FAILED) { s4 = peg$parse_(); if (input.charCodeAt(peg$currPos) === 41) { - s5 = peg$c15; + s5 = peg$c4; peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e20); } + if (peg$silentFails === 0) { peg$fail(peg$e4); } } if (s5 !== peg$FAILED) { peg$savedPos = s0; @@ -1228,7 +1266,7 @@ function peg$parse(input, options) { } function peg$parseraise() { - let s0, s1, s2, s3; + let s0, s1, s2, s3, s4, s5, s6, s7; s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 94) { @@ -1242,8 +1280,44 @@ function peg$parse(input, options) { s2 = peg$parse_(); s3 = peg$parseidentifier(); 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; - s0 = peg$f18(s3); + s0 = peg$f18(s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; diff --git a/src/statecharts/parser.ts b/src/statecharts/parser.ts index f968e23..67d3a32 100644 --- a/src/statecharts/parser.ts +++ b/src/statecharts/parser.ts @@ -5,8 +5,14 @@ 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][] = []; +export type TraceableError = { + shapeUid: string; + message: string; + data?: any; +} + +export function parseStatechart(state: VisualEditorState): [Statechart, TraceableError[]] { + const errors: TraceableError[] = []; // implicitly, the root is always an Or-state 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: for (const rt of state.rountangles) { + // @ts-ignore const state: ConcreteState = { kind: rt.kind, uid: rt.uid, @@ -45,11 +52,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, entryActions: [], exitActions: [], timers: [], - } + }; 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: 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)) { // found our parent :) const parentState = uid2State.get(candidate.uid)!; - parentState.children.push(state); + parentState.children.push(state as unknown as ConcreteState); parentCandidates.push(rt); parentLinks.set(rt.uid, candidate.uid); state.parent = parentState; @@ -78,7 +85,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, if (!srcUID) { if (!tgtUID) { // dangling edge - todo: display error... - errorShapes.push([arr.uid, "dangling"]); + errors.push({shapeUid: arr.uid, message: "dangling"}); } else { // target but no source, so we treat is as an 'initial' marking @@ -89,13 +96,19 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, } else { // 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 { if (!tgtUID) { - errorShapes.push([arr.uid, "no target"]); + errors.push({ + shapeUid: arr.uid, + message: "no target", + }); } else { // add transition @@ -116,10 +129,16 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, 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])); + errors.push(...state.initial.map(([uid,childState]) => ({ + shapeUid: uid, + message: "multiple initial states", + }))); } 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 } catch (e) { if (e instanceof SyntaxError) { - errorShapes.push([text.uid, e]); + errors.push({ + shapeUid: text.uid, + message: e.message, + data: e, + }); continue; } else { @@ -202,10 +225,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, belongsToState.exitActions.push(...parsed.actions); } else { - errorShapes.push([text.uid, { + errors.push({ + shapeUid: text.uid, 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()) { 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) { - 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, outputEvents, uid2State, - }, errorShapes]; + }, errors]; } function findVariables(expr: Expression): Set { diff --git a/src/statecharts/runtime_types.ts b/src/statecharts/runtime_types.ts index 7c974d5..31fca1a 100644 --- a/src/statecharts/runtime_types.ts +++ b/src/statecharts/runtime_types.ts @@ -1,9 +1,22 @@ 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; // set of active states - export type Environment = ReadonlyMap; // variable name -> value export type RT_Statechart = { @@ -13,7 +26,7 @@ export type RT_Statechart = { } export type BigStepOutput = RT_Statechart & { - outputEvents: string[], + outputEvents: RaisedEvent[], }; export type BigStep = { @@ -21,10 +34,16 @@ export type BigStep = { simtime: number, } & BigStepOutput; +// internal or output event +export type RaisedEvent = { + name: string, + param?: any, +} + export type RaisedEvents = { - internalEvents: string[]; - outputEvents: string[]; + internalEvents: RaisedEvent[]; + outputEvents: RaisedEvent[]; }; // export type Timers = Map; // transition uid -> timestamp @@ -34,6 +53,4 @@ export const initialRaised: RaisedEvents = { outputEvents: [], }; -export type TimerElapseEvent = { state: string; timeDurMs: number; }; export type Timers = [number, TimerElapseEvent][]; - diff --git a/src/statecharts/transition_label.grammar b/src/statecharts/transition_label.grammar index ed317e1..7545969 100644 --- a/src/statecharts/transition_label.grammar +++ b/src/statecharts/transition_label.grammar @@ -11,8 +11,8 @@ tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger -eventTrigger = event:identifier { - return {kind: "event", event}; +eventTrigger = event:identifier param:("(" identifier ")")? { + return {kind: "event", event, param: param ? param[1] : undefined}; } afterTrigger = "after" _ dur:durationMs { @@ -111,8 +111,8 @@ boolean = ("true" / "false") { return text() === "true"; } -raise = "^" _ event:identifier { - return {kind: "raise", event}; +raise = "^" _ event:identifier param:("(" expr ")")? { + return {kind: "raise", event, param: param ? param[1] : undefined}; } _ "whitespace"