Compare commits

..

No commits in common. "d4930eb13de7c96a7d9353c7058d4ad53246a675" and "ec49c47b3997e47c6fc6d020791efd1f8024b396" have entirely different histories.

22 changed files with 608 additions and 781 deletions

View file

@ -1,4 +1,4 @@
import { ConcreteState, PseudoState, stateDescription, Transition } from "../statecharts/abstract_syntax";
import { ConcreteState, stateDescription, Transition } from "../statecharts/abstract_syntax";
import { Action, Expression } from "../statecharts/label_ast";
import { RT_Statechart } from "../statecharts/runtime_types";
@ -32,7 +32,7 @@ export function ShowAction(props: {action: Action}) {
}
}
export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined}) {
export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined}) {
const description = stateDescription(props.root);
const outgoing = props.transitions.get(props.root.uid) || [];
@ -49,9 +49,9 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
<div>&emsp;exit / <ShowAction action={action}/></div>
)
}
{props.root.kind !== "pseudo" && props.root.children.length>0 &&
{props.root.children.length>0 &&
props.root.children.map(child =>
<ShowAST root={child} transitions={props.transitions} rt={props.rt} />
<AST root={child} transitions={props.transitions} rt={props.rt} />
)
}
{outgoing.length>0 &&

View file

@ -1,3 +1,28 @@
/* .layoutVertical {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
}
.panel {
height: 1.5rem;
background-color: lightgrey;
}
.layout {
display: flex;
width: 100%;
height: calc(100vh - 1.5rem);
}
.sidebar {
flex: 0 0 content;
padding-right: 4px;
}
.content {
flex: 1 1 auto;
overflow: auto;
} */
details {
padding-left: 20;
/* margin-left: 30; */
@ -44,9 +69,3 @@ summary {
.toolbar > input {
height: 20px;
}
button.active {
border: solid blue 2px;
background-color: rgba(0,0,255,0.2);
color: black;
}

View file

@ -12,10 +12,9 @@ import "./App.css";
import { Box, Stack } from "@mui/material";
import { TopPanel } from "./TopPanel";
import { RTHistory } from "./RTHistory";
import { ShowAST } from "./ShowAST";
import { AST } from "./AST";
import { TraceableError } from "../statecharts/parser";
import { getKeyHandler } from "./shortcut_handler";
import { BottomPanel } from "./BottomPanel";
export function App() {
const [mode, setMode] = useState<InsertMode>("and");
@ -107,7 +106,7 @@ export function App() {
{...{ast, time, setTime, onInit, onClear, onRaise, mode, setMode}}
/>
</Box>
<Stack direction="row" sx={{height:'calc(100vh - 64px)'}}>
<Stack direction="row" sx={{height:'calc(100vh - 32px)'}}>
{/* main */}
<Box sx={{flexGrow:1, overflow:'auto'}}>
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode}}/>
@ -118,17 +117,14 @@ export function App() {
borderLeft: 1,
borderColor: "divider",
flex: '0 0 content',
// paddingRight: 1,
// paddingLeft: 1,
paddingRight: 1,
paddingLeft: 1,
}}>
<ShowAST {...{...ast, rt: rt.at(rtIdx!)}}/>
<AST {...{...ast, rt: rt.at(rtIdx!)}}/>
<br/>
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx}}/>
</Box>
</Stack>
<Box>
<BottomPanel {...{errors}}/>
</Box>
</Stack>;
}

View file

@ -1,3 +0,0 @@
.errorStatus {
color: rgb(230,0,0);
}

View file

@ -1,10 +0,0 @@
import { TraceableError } from "../statecharts/parser";
import "./BottomPanel.css";
export function BottomPanel(props: {errors: TraceableError[]}) {
return <div className="toolbar">
<div className="errorStatus">{
props.errors.length>0 && <>{props.errors.length} errors {props.errors.map(({message})=>message).join(',')}</>}</div>
</div>;
}

View file

@ -10,11 +10,10 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import BoltIcon from '@mui/icons-material/Bolt';
import SkipNextIcon from '@mui/icons-material/SkipNext';
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import StopIcon from '@mui/icons-material/Stop';
import { formatTime } from "./util";
import { InsertMode } from "../VisualEditor/VisualEditor";
import { DiamondShape } from "../VisualEditor/RountangleSVG";
export type TopPanelProps = {
rt?: BigStep,
@ -34,21 +33,16 @@ function RountangleIcon(props: {kind: string}) {
x={1} y={1}
width={18} height={18}
className={`rountangle ${props.kind}`}
style={{...(props.kind === "or" ? {strokeDasharray: '3 2'}: {}), strokeWidth: 1.2}}
style={props.kind === "or" ? {strokeDasharray: '3 2'}: {}}
/>
</svg>;
}
function PseudoStateIcon(props: {}) {
const w=20, h=20;
return <svg width={w} height={h}>
<polygon
points={`
${w/2} ${1},
${w-1} ${h/2},
${w/2} ${h-1},
${1} ${h/2},
`} fill="white" stroke="black" strokeWidth={1.2}/>
return <svg width={20} height={20}>
<g transform="translate(2,1)">
<DiamondShape geometry={{topLeft:{x:0,y:0}, size:{x:16,y:18}}} extraAttrs={{className: 'rountangle pseudo'}}/>
</g>
</svg>;
}
@ -107,29 +101,28 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
return <>
<div className="toolbar">
{([
["and", "AND-states", <RountangleIcon kind="and"/>],
["or", "OR-states", <RountangleIcon kind="or"/>],
["pseudo", "pseudo-states", <PseudoStateIcon/>],
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>],
["text", "text", <>&nbsp;T&nbsp;</>],
] as [InsertMode, string, ReactElement][]).map(([m, hint, buttonTxt]) =>
<button
title={"insert "+hint}
disabled={mode===m}
className={mode===m ? "active":""}
onClick={() => setMode(m)}
>{buttonTxt}</button>)}
{([
["and", "AND-states", <RountangleIcon kind="and"/>],
["or", "OR-states", <RountangleIcon kind="or"/>],
["pseudo", "pseudo-states", <PseudoStateIcon/>],
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>],
["text", "text", <>T</>],
] as [InsertMode, string, ReactElement][]).map(([m, hint, buttonTxt]) =>
<button
title={"insert "+hint}
disabled={mode===m}
onClick={() => setMode(m)}
>{buttonTxt}</button>)}
</div>
&emsp;
<div className="toolbar">
<button title="(re)initialize simulation" onClick={onInit} ><CachedIcon fontSize="small"/><PlayArrowIcon fontSize="small"/></button>
<button title="clear the simulation" onClick={onClear} disabled={!rt}><ClearIcon fontSize="small"/></button>
&emsp;
<button title="(re)initialize simulation" onClick={onInit} ><PlayArrowIcon fontSize="small"/><CachedIcon fontSize="small"/></button>
<button title="clear the simulation" onClick={onClear} disabled={!rt}><StopIcon fontSize="small"/></button>
&emsp;
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} className={(rt && time.kind==="paused") ? "active":""} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button>
<button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} className={(rt && time.kind==="realtime") ? "active":""} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button>
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button>
<button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button>
{/* <ToggleButtonGroup value={time.kind} exclusive onChange={(_,newValue) => onChangePaused(newValue==="paused", performance.now())} size="small">
<ToggleButton disableRipple value="paused" disabled={!rt}><PauseIcon/></ToggleButton>
@ -162,15 +155,12 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
return {kind: "realtime", scale: time.scale, since: {simtime: nextTimedTransition[0], wallclktime: now}};
}
});
}}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button>
&emsp;
}}><SkipNextIcon fontSize="small"/></button>
{ast.inputEvents &&
<>
{ast.inputEvents.map(({event, paramName}) =>
<><button title={`raise input event '${event}'`} disabled={!rt} onClick={() => {
// @ts-ignore
<>&emsp;<button title={`raise input event '${event}'`} disabled={!rt} onClick={() => {
const param = document.getElementById(`input-${event}-param`)?.value;
let paramParsed;
try {
@ -186,7 +176,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
}}>
<BoltIcon fontSize="small"/>
{event}
</button>{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>}&nbsp;</>)}
</button>{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>}</>)}
</>
}

View file

@ -22,16 +22,11 @@ export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: stri
data-uid={uid}
data-parts="start end" />
{props.errors.length > 0 && <text
className="error"
x={(start.x + end.x) / 2 + 5}
y={(start.y + end.y) / 2}
textAnchor="middle"
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>}
<path
className="pathHelper"
// markerEnd='url(#arrowEnd)'
d={`M ${start.x} ${start.y}
${arcOrLine}
${end.x} ${end.y}`}

View file

@ -1,37 +0,0 @@
import { Diamond, RountanglePart } from "@/statecharts/concrete_syntax";
import { rountangleMinSize } from "./VisualEditor";
import { Rect2D, Vec2D } from "./geometry";
import { RectHelper } from "./RectHelpers";
export function DiamondShape(props: {size: Vec2D, extraAttrs: object}) {
const minSize = rountangleMinSize(props.size);
return <polygon
points={`
${minSize.x/2} ${0},
${minSize.x} ${minSize.y/2},
${minSize.x/2} ${minSize.y},
${0} ${minSize.y/2}
`}
fill="white"
stroke="black"
strokeWidth={2}
{...props.extraAttrs}
/>;
}
export function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
const minSize = rountangleMinSize(props.diamond.size);
const extraAttrs = {
className: ''
+ (props.selected.length === 4 ? " selected" : "")
+ (props.errors.length > 0 ? " error" : "")
+ (props.active ? " active" : ""),
"data-uid": props.diamond.uid,
"data-parts": "left top right bottom",
};
return <g transform={`translate(${props.diamond.topLeft.x} ${props.diamond.topLeft.y})`}>
<DiamondShape size={minSize} extraAttrs={extraAttrs}/>
<RectHelper uid={props.diamond.uid} size={minSize} highlight={props.highlight} selected={props.selected} />
</g>;
}

View file

@ -1,3 +0,0 @@
export function ShallowHistorySVG() {
}

View file

@ -1,80 +0,0 @@
import { RountanglePart } from "../statecharts/concrete_syntax";
import { Vec2D } from "./geometry";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters";
export function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) {
return <>
<line
className={"lineHelper"
+ (props.selected.includes("top") ? " selected" : "")
+ (props.highlight.includes("top") ? " highlight" : "")}
x1={0}
y1={0}
x2={props.size.x}
y2={0}
data-uid={props.uid}
data-parts="top" />
<line
className={"lineHelper"
+ (props.selected.includes("right") ? " selected" : "")
+ (props.highlight.includes("right") ? " highlight" : "")}
x1={props.size.x}
y1={0}
x2={props.size.x}
y2={props.size.y}
data-uid={props.uid}
data-parts="right" />
<line
className={"lineHelper"
+ (props.selected.includes("bottom") ? " selected" : "")
+ (props.highlight.includes("bottom") ? " highlight" : "")}
x1={0}
y1={props.size.y}
x2={props.size.x}
y2={props.size.y}
data-uid={props.uid}
data-parts="bottom" />
<line
className={"lineHelper"
+ (props.selected.includes("left") ? " selected" : "")
+ (props.highlight.includes("left") ? " highlight" : "")}
x1={0}
y1={0}
x2={0}
y2={props.size.y}
data-uid={props.uid}
data-parts="left" />
<circle
className="circleHelper corner"
cx={CORNER_HELPER_OFFSET}
cy={CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={props.uid}
data-parts="top left" />
<circle
className="circleHelper corner"
cx={props.size.x - CORNER_HELPER_OFFSET}
cy={CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={props.uid}
data-parts="top right" />
<circle
className="circleHelper corner"
cx={props.size.x - CORNER_HELPER_OFFSET}
cy={props.size.y - CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={props.uid}
data-parts="bottom right" />
<circle
className="circleHelper corner"
cx={CORNER_HELPER_OFFSET}
cy={props.size.y - CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={props.uid}
data-parts="bottom left" />
<text x={10} y={20}
className="uid"
data-uid={props.uid}>{props.uid}</text>
</>;
}

View file

@ -1,8 +1,20 @@
import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax";
import { ROUNTANGLE_RADIUS } from "./parameters";
import { RectHelper } from "./RectHelpers";
import { Rect2D } from "./geometry";
import { ROUNTANGLE_RADIUS, CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters";
import { rountangleMinSize } from "./VisualEditor";
export function DiamondShape(props: {geometry: Rect2D, extraAttrs: object}) {
const {geometry} = props;
return <polygon
points={`
${geometry.size.x/2} ${0},
${geometry.size.x} ${geometry.size.y/2},
${geometry.size.x/2} ${geometry.size.y},
${0} ${geometry.size.y/2}
`}
{...props.extraAttrs}
/>;
}
export function RountangleSVG(props: { rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
const { topLeft, size, uid } = props.rountangle;
@ -19,20 +31,94 @@ export function RountangleSVG(props: { rountangle: Rountangle; selected: string[
"data-parts": "left top right bottom",
};
return <g transform={`translate(${topLeft.x} ${topLeft.y})`}>
<rect
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
x={0}
y={0}
width={minSize.x}
height={minSize.y}
{...extraAttrs}
/>
{props.rountangle.kind === "pseudo" ?
<DiamondShape geometry={props.rountangle} extraAttrs={extraAttrs}/>
: <rect
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
x={0}
y={0}
width={minSize.x}
height={minSize.y}
{...extraAttrs}
/>
}
{(props.errors.length > 0) &&
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.errors.join(' ')}</text>}
<RectHelper uid={uid} size={minSize}
selected={props.selected}
highlight={props.highlight} />
<line
className={"lineHelper"
+ (props.selected.includes("top") ? " selected" : "")
+ (props.highlight.includes("top") ? " highlight" : "")}
x1={0}
y1={0}
x2={minSize.x}
y2={0}
data-uid={uid}
data-parts="top" />
<line
className={"lineHelper"
+ (props.selected.includes("right") ? " selected" : "")
+ (props.highlight.includes("right") ? " highlight" : "")}
x1={minSize.x}
y1={0}
x2={minSize.x}
y2={minSize.y}
data-uid={uid}
data-parts="right" />
<line
className={"lineHelper"
+ (props.selected.includes("bottom") ? " selected" : "")
+ (props.highlight.includes("bottom") ? " highlight" : "")}
x1={0}
y1={minSize.y}
x2={minSize.x}
y2={minSize.y}
data-uid={uid}
data-parts="bottom" />
<line
className={"lineHelper"
+ (props.selected.includes("left") ? " selected" : "")
+ (props.highlight.includes("left") ? " highlight" : "")}
x1={0}
y1={0}
x2={0}
y2={minSize.y}
data-uid={uid}
data-parts="left" />
<circle
className="circleHelper corner"
cx={CORNER_HELPER_OFFSET}
cy={CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={uid}
data-parts="top left" />
<circle
className="circleHelper corner"
cx={minSize.x - CORNER_HELPER_OFFSET}
cy={CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={uid}
data-parts="top right" />
<circle
className="circleHelper corner"
cx={minSize.x - CORNER_HELPER_OFFSET}
cy={minSize.y - CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={uid}
data-parts="bottom right" />
<circle
className="circleHelper corner"
cx={CORNER_HELPER_OFFSET}
cy={minSize.y - CORNER_HELPER_OFFSET}
r={CORNER_HELPER_RADIUS}
data-uid={uid}
data-parts="bottom left" />
<text x={10} y={20}
className="uid"
data-uid={uid}>{uid}</text>
</g>;
}

View file

@ -1,41 +0,0 @@
import { TraceableError } from "..//statecharts/parser";
import {Text} from "../statecharts/concrete_syntax";
export function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (newText: string) => void}) {
const commonProps = {
"data-uid": props.text.uid,
"data-parts": "text",
textAnchor: "middle" as "middle",
className:
(props.selected ? "selected":"")
+(props.highlight ? " highlight":""),
}
let textNode;
if (props.error?.data?.location) {
const {start,end} = props.error.data.location;
textNode = <><text {...commonProps}>
{props.text.text.slice(0, start.offset)}
<tspan className="error" data-uid={props.text.uid} data-parts="text">
{props.text.text.slice(start.offset, end.offset)}
{start.offset === end.offset && <>_</>}
</tspan>
{props.text.text.slice(end.offset)}
</text>
<text className="error errorHover" y={20} textAnchor="middle">{props.error.message}</text></>;
}
else {
textNode = <text {...commonProps}>{props.text.text}</text>;
}
return <g
key={props.text.uid}
transform={`translate(${props.text.topLeft.x} ${props.text.topLeft.y})`}
onDoubleClick={() => {
const newText = prompt("", props.text.text);
if (newText) {
props.onEdit(newText);
}
}}
>{textNode}</g>;
}

View file

@ -3,10 +3,6 @@
background-color: #eee;
}
.svgCanvas.dragging {
cursor: grabbing !important;
}
.svgCanvas.active {
background-color: rgb(255, 140, 0, 0.2);
}
@ -53,6 +49,12 @@ text.highlight {
/* cursor: grab; */
}
.rountangle.dragging {
/* fill: lightgrey; */
/* stroke-width: 4px; */
cursor: grabbing;
}
.rountangle.selected {
fill: rgba(0, 0, 255, 0.2);
/* stroke: blue;
@ -68,7 +70,7 @@ text.highlight {
stroke-width: 3px;
}
.selected:hover:not(:active) {
.selected:hover {
cursor: grab;
}
@ -76,10 +78,10 @@ text.highlight {
stroke: rgba(0, 0, 0, 0);
stroke-width: 16px;
}
.lineHelper:hover:not(:active) {
.lineHelper:hover {
stroke: blue;
stroke-opacity: 0.2;
cursor: grab;
/* cursor: grab; */
}
.pathHelper {
@ -87,7 +89,7 @@ text.highlight {
stroke: rgba(0, 0, 0, 0);
stroke-width: 16px;
}
.pathHelper:hover:not(:active) {
.pathHelper:hover {
stroke: blue;
stroke-opacity: 0.2;
cursor: grab;
@ -97,10 +99,10 @@ text.highlight {
.circleHelper {
fill: rgba(0, 0, 0, 0);
}
.circleHelper:hover:not(:active) {
.circleHelper:hover {
fill: blue;
fill-opacity: 0.2;
cursor: grab;
/* cursor: grab; */
}
.rountangle.or {
@ -132,6 +134,9 @@ text.highlight {
cursor: grab;
}
line.selected, circle.selected {
fill: rgba(0, 0, 255, 0.2);
/* stroke-dasharray: 7 6; */
@ -143,9 +148,9 @@ text.selected, text.selected:hover {
fill: blue;
font-weight: 600;
}
text:hover:not(:active) {
text:hover {
fill: blue;
cursor: grab;
/* cursor: grab; */
}
.highlight {

View file

@ -2,7 +2,7 @@ import * as lz4 from "@nick/lz4";
import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react";
import { Statechart } from "../statecharts/abstract_syntax";
import { Arrow, ArrowPart, Diamond, Rountangle, RountanglePart, Text, VisualEditorState, emptyState, findNearestArrow, findNearestSide, findRountangle } from "../statecharts/concrete_syntax";
import { Arrow, ArrowPart, Rountangle, RountanglePart, Text, 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, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
@ -12,8 +12,6 @@ import { getBBoxInSvgCoords } from "./svg_helper";
import "./VisualEditor.css";
import { ArrowSVG } from "./ArrowSVG";
import { RountangleSVG } from "./RountangleSVG";
import { TextSVG } from "./TextSVG";
import { DiamondSVG } from "./DiamondSVG";
type DraggingState = {
@ -117,6 +115,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
}
const [dragging, setDragging] = useState<DraggingState>(null);
const [showHelp, setShowHelp] = useState<boolean>(false);
// uid's of selected rountangles
const [selection, setSelection] = useState<Selection>([]);
@ -148,20 +147,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
const compressedStateString = compressedStateBuffer.toBase64();
window.location.hash = "#"+compressedStateString;
// const [statechart, errors] = parseStatechart(state);
// setErrors(errors);
// setAST(statechart);
}, 200);
const [statechart, errors] = parseStatechart(state);
// console.log('statechart: ', statechart, 'errors:', errors);
setErrors(errors);
setAST(statechart);
}, 100);
return () => clearTimeout(timeout);
}, [state]);
useEffect(() => {
const [statechart, errors] = parseStatechart(state);
setErrors(errors);
setAST(statechart);
}, [state])
function getCurrentPointer(e: {pageX: number, pageY: number}) {
const bbox = refSVG.current!.getBoundingClientRect();
return {
@ -178,7 +171,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
// ignore selection, middle mouse button always inserts
setState(state => {
const newID = state.nextID.toString();
if (mode === "and" || mode === "or") {
if (mode === "and" || mode === "or" || mode === "pseudo") {
// insert rountangle
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
return {
@ -192,18 +185,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
nextID: state.nextID+1,
};
}
else if (mode === "pseudo") {
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
return {
...state,
diamonds: [...state.diamonds, {
uid: newID,
topLeft: currentPointer,
size: MIN_ROUNTANGLE_SIZE,
}],
nextID: state.nextID+1,
};
}
else if (mode === "transition") {
setSelection([{uid: newID, parts: ["end"]}]);
return {
@ -238,9 +219,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
if (e.button === 0) {
// left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape.
// @ts-ignore
const uid = e.target?.dataset.uid;
// @ts-ignore
const parts: string[] = e.target?.dataset.parts?.split(' ') || [];
if (uid) {
checkPoint();
@ -253,25 +232,26 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
break;
}
}
if (!allPartsInSelection) {
setSelection([{uid, parts}] as Selection);
// if (!allPartsInSelection) {
// setSelection([{uid, parts}] as Selection);
// }
if (allPartsInSelection) {
// start dragging
setDragging({
lastMousePos: currentPointer,
});
return;
}
// start dragging
setDragging({
lastMousePos: currentPointer,
});
return;
}
// otherwise, just start making a selection
setDragging(null);
setSelectingState({
topLeft: currentPointer,
size: {x: 0, y: 0},
});
setSelection([]);
}
// otherwise, just start making a selection
setDragging(null);
setSelectingState({
topLeft: currentPointer,
size: {x: 0, y: 0},
});
setSelection([]);
};
const onMouseMove = (e: {pageX: number, pageY: number}) => {
@ -286,7 +266,8 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
return r;
}
return {
...r,
uid: r.uid,
kind: r.kind,
...transformRect(r, parts, pointerDelta),
};
})
@ -297,7 +278,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
return a;
}
return {
...a,
uid: a.uid,
...transformLine(a, parts, pointerDelta),
}
}),
@ -307,20 +288,11 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
return t;
}
return {
...t,
uid: t.uid,
text: t.text,
topLeft: addV2D(t.topLeft, pointerDelta),
}
}),
diamonds: state.diamonds.map(d => {
const parts = selection.find(selected => selected.uid === d.uid)?.parts || [];
if (parts.length === 0) {
return d;
}
return {
...d,
...transformRect(d, parts, pointerDelta),
}
})
}));
setDragging({lastMousePos: currentPointer});
}
@ -335,7 +307,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
}
};
const onMouseUp = (e: {pageX: number, pageY: number}) => {
const onMouseUp = (e) => {
if (dragging) {
setDragging(null);
// do not persist sizes smaller than 40x40
@ -346,37 +318,53 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
...r,
size: rountangleMinSize(r.size),
})),
diamonds: state.diamonds.map(d => ({
...d,
size: rountangleMinSize(d.size),
}))
};
});
}
if (selectingState) {
// we were making a selection
const normalizedSS = normalizeRect(selectingState);
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
const shapesInSelection = shapes.filter(el => {
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
return isEntirelyWithin(bbox, normalizedSS);
}).filter(el => !el.classList.contains("corner"));
const uidToParts = new Map();
for (const shape of shapesInSelection) {
const uid = shape.dataset.uid;
if (selectingState.size.x === 0 && selectingState.size.y === 0) {
const uid = e.target?.dataset.uid;
const parts: string[] = e.target?.dataset.parts?.split(' ') || [];
if (uid) {
const parts: Set<string> = uidToParts.get(uid) || new Set();
for (const part of shape.dataset.parts?.split(' ') || []) {
parts.add(part);
checkPoint();
// @ts-ignore
setSelection(() => ([{uid, parts}]));
// 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 || [] as string[]).includes(part)) {
allPartsInSelection = false;
break;
}
}
uidToParts.set(uid, parts);
}
}
setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({
uid,
parts: [...parts],
})));
else {
const normalizedSS = normalizeRect(selectingState);
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
const shapesInSelection = shapes.filter(el => {
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
return isEntirelyWithin(bbox, normalizedSS);
}).filter(el => !el.classList.contains("corner"));
const uidToParts = new Map();
for (const shape of shapesInSelection) {
const uid = shape.dataset.uid;
if (uid) {
const parts: Set<string> = uidToParts.get(uid) || new Set();
for (const part of shape.dataset.parts?.split(' ') || []) {
parts.add(part);
}
uidToParts.set(uid, parts);
}
}
setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({
uid,
parts: [...parts],
})));
}
}
setSelectingState(null); // no longer making a selection
};
@ -387,7 +375,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
arrows: state.arrows.filter(a => !selection.some(as => as.uid === a.uid)),
texts: state.texts.filter(t => !selection.some(ts => ts.uid === t.uid)),
diamonds: state.diamonds.filter(d => !selection.some(ds => ds.uid === d.uid)),
}));
setSelection([]);
}
@ -420,17 +407,74 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
return selection;
});
}
// if (e.key === "p") {
// // selected states become pseudo-states
// setSelection(selection => {
// setState(state => ({
// ...state,
// rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r),
// }));
// return selection;
// });
// }
if (e.key === "p") {
// selected states become pseudo-states
setSelection(selection => {
setState(state => ({
...state,
rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r),
}));
return selection;
});
}
if (e.key === "h") {
setShowHelp(showHelp => !showHelp);
}
if (e.ctrlKey) {
// if (e.key === "c") {
// if (selection.length > 0) {
// e.preventDefault();
// setClipboard(new Set(selection.map(shape => shape.uid)));
// console.log('set clipboard', new Set(selection.map(shape => shape.uid)));
// }
// }
// if (e.key === "v") {
// console.log('paste shortcut..', clipboard);
// if (clipboard.size > 0) {
// console.log('pasting...a');
// e.preventDefault();
// checkPoint();
// const offset = {x: 40, y: 40};
// const rountanglesToCopy = state.rountangles.filter(r => clipboard.has(r.uid));
// const arrowsToCopy = state.arrows.filter(a => clipboard.has(a.uid));
// const textsToCopy = state.texts.filter(t => clipboard.has(t.uid));
// let nextUid = state.nextID;
// const rountanglesCopied: Rountangle[] = rountanglesToCopy.map(r => ({
// ...r,
// uid: (nextUid++).toString(),
// topLeft: addV2D(r.topLeft, offset),
// }));
// const arrowsCopied: Arrow[] = arrowsToCopy.map(a => ({
// ...a,
// uid: (nextUid++).toString(),
// start: addV2D(a.start, offset),
// end: addV2D(a.end, offset),
// }));
// const textsCopied: Text[] = textsToCopy.map(t => ({
// ...t,
// uid: (nextUid++).toString(),
// topLeft: addV2D(t.topLeft, offset),
// }));
// setState(state => ({
// ...state,
// rountangles: [...state.rountangles, ...rountanglesCopied],
// arrows: [...state.arrows, ...arrowsCopied],
// texts: [...state.texts, ...textsCopied],
// nextID: nextUid,
// }));
// setClipboard(new Set([
// ...rountanglesCopied.map(r => r.uid),
// ...arrowsCopied.map(a => a.uid),
// ...textsCopied.map(t => t.uid),
// ]));
// // @ts-ignore
// setSelection([
// ...rountanglesCopied.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
// ...arrowsCopied.map(a => ({uid: a.uid, parts: ["start", "end"]})),
// ...textsCopied.map(t => ({uid: t.uid, parts: ["text"]})),
// ]);
// }
// }
if (e.key === "z") {
e.preventDefault();
undo();
@ -449,6 +493,11 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
]);
}
if (e.key === "c") {
// e.preventDefault();
// setClipboard()
}
}
};
@ -471,12 +520,9 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
const arrow2TextMap = new Map<string,string[]>();
const text2RountangleMap = new Map<string, string>();
const rountangle2TextMap = new Map<string, string[]>();
// arrow <-> (rountangle | diamond)
for (const arrow of state.arrows) {
const sides = [...state.rountangles, ...state.diamonds];
const startSide = findNearestSide(arrow, "start", sides);
const endSide = findNearestSide(arrow, "end", sides);
const startSide = findNearestRountangleSide(arrow, "start", state.rountangles);
const endSide = findNearestRountangleSide(arrow, "end", state.rountangles);
if (startSide || endSide) {
arrow2SideMap.set(arrow.uid, [startSide, endSide]);
}
@ -491,7 +537,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns);
}
}
// text <-> arrow
for (const text of state.texts) {
const nearestArrow = findNearestArrow(text.topLeft, state.arrows);
if (nearestArrow) {
@ -502,7 +547,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
arrow2TextMap.set(nearestArrow.uid, textsOfArrow);
}
else {
// text <-> rountangle
// no arrow, then the text belongs to the rountangle it is in
const rountangle = findRountangle(text.topLeft, state.rountangles);
if (rountangle) {
text2RountangleMap.set(text.uid, rountangle.uid);
@ -627,50 +672,20 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
deleteShapes(selection);
e.preventDefault();
}
}
function onEditText(text: Text, newText: string) {
if (newText === "") {
// delete text node
setState(state => ({
...state,
texts: state.texts.filter(t => t.uid !== text.uid),
}));
}
else {
setState(state => ({
...state,
texts: state.texts.map(t => {
if (t.uid === text.uid) {
return {
...text,
text: newText,
}
}
else {
return t;
}
}),
}));
}
}
const active = rt?.mode || new Set();
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
return <svg width="4000px" height="4000px"
className={"svgCanvas"+(active.has("root")?" active":"")+(dragging!==null?" dragging":"")}
className={"svgCanvas"+(active.has("root")?" active":"")}
onMouseDown={onMouseDown}
onContextMenu={e => e.preventDefault()}
ref={refSVG}
// @ts-ignore
onCopy={onCopy}
// @ts-ignore
onPaste={onPaste}
// @ts-ignore
onCut={onCut}
>
<defs>
@ -679,15 +694,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
viewBox="0 0 10 10"
refX="5"
refY="5"
markerWidth="12"
markerHeight="12"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse">
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
{(rootErrors.length>0) && <text className="error" x={5} y={50}>{rootErrors.join(' ')}</text>}
{state.rountangles.map(rountangle => <RountangleSVG
key={rountangle.uid}
@ -700,18 +714,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
active={active.has(rountangle.uid)}
/>)}
{state.diamonds.map(diamond => <>
<DiamondSVG
key={diamond.uid}
diamond={diamond}
selected={selection.find(r => r.uid === diamond.uid)?.parts || []}
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
errors={errors
.filter(({shapeUid}) => shapeUid === diamond.uid)
.map(({message}) => message)}
active={active.has(diamond.uid)}/>
</>)}
{state.arrows.map(arrow => {
const sides = arrow2SideMap.get(arrow.uid);
let arc = "no" as ArcDirection;
@ -732,14 +734,60 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
)}
{state.texts.map(txt => {
return <TextSVG
error={errors.find(({shapeUid}) => txt.uid === shapeUid)}
text={txt}
selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)}
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
onEdit={newText => onEditText(txt, newText)}
/>
})}
const err = errors.find(({shapeUid}) => txt.uid === shapeUid);
const commonProps = {
"data-uid": txt.uid,
"data-parts": "text",
textAnchor: "middle" as "middle",
className:
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":""),
}
let textNode;
if (err?.data?.location) {
const {start,end} = err.data.location;
textNode = <><text {...commonProps}>
{txt.text.slice(0, start.offset)}
<tspan className="error" data-uid={txt.uid} data-parts="text">
{txt.text.slice(start.offset, end.offset)}
{start.offset === end.offset && <>_</>}
</tspan>
{txt.text.slice(end.offset)}
</text>
<text className="error errorHover" y={20} textAnchor="middle">{err.message}</text></>;
}
else {
textNode = <text {...commonProps}>{txt.text}</text>;
}
return <g
key={txt.uid}
transform={`translate(${txt.topLeft.x} ${txt.topLeft.y})`}
onDoubleClick={() => {
const newText = prompt("", txt.text);
if (newText) {
setState(state => ({
...state,
texts: state.texts.map(t => {
if (t.uid === txt.uid) {
return {
...txt,
text: newText,
}
}
else {
return t;
}
}),
}));
}
else if (newText === "") {
setState(state => ({
...state,
texts: state.texts.filter(t => t.uid !== txt.uid),
}));
}
}}
>{textNode}</g>;})}
{selectingState && <Selecting {...selectingState} />}
</svg>;

View file

@ -1,43 +1,33 @@
import { Action, EventTrigger, ParsedText } from "./label_ast";
import { Action, EventTrigger, ParsedText, 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 StableState = {
kind: "and" | "or";
children: ConcreteState[];
timers: number[]; // list of timeouts (e.g., the state having an outgoing transition with trigger "after 4s" would appear as the number 4000 in this list)
} & AbstractState;
}
export type AndState = {
kind: "and";
} & StableState;
} & AbstractState;
export type OrState = {
kind: "or";
// array of tuples: (uid of Arrow indicating initial state, initial state)
// in a valid AST, there must be one initial state, but we allow the user to draw crazy shit
initial: [string, ConcreteState][];
} & StableState;
export type PseudoState = {
kind: "pseudo";
uid: string;
comments: [string, string][];
};
} & AbstractState;
export type ConcreteState = AndState | OrState;
export type Transition = {
uid: string; // uid of arrow in concrete syntax
src: ConcreteState | PseudoState;
tgt: ConcreteState | PseudoState;
uid: string;
src: ConcreteState;
tgt: ConcreteState;
label: ParsedText[];
}
@ -51,7 +41,7 @@ export type Statechart = {
internalEvents: EventTrigger[];
outputEvents: Set<string>;
uid2State: Map<string, ConcreteState|PseudoState>;
uid2State: Map<string, ConcreteState>;
}
const emptyRoot: OrState = {
@ -97,57 +87,6 @@ export function isOverlapping(a: ConcreteState, b: ConcreteState): boolean {
}
}
export function computeLCA(a: ConcreteState, b: ConcreteState): ConcreteState {
if (a === b) {
return a;
}
if (a.depth > b.depth) {
return computeLCA(a.parent!, b);
}
return computeLCA(a, b.parent!);
}
export function computeLCA2(states: ConcreteState[]): ConcreteState {
if (states.length === 0) {
throw new Error("cannot compute LCA of empty set of states");
}
if (states.length === 1) {
return states[0];
}
// 2 states or more
return states.reduce((acc, cur) => computeLCA(acc, cur));
}
export function getPossibleTargets(t: Transition, ts: Map<string, Transition[]>): ConcreteState[] {
if (t.tgt.kind !== "pseudo") {
return [t.tgt];
}
const pseudoOutgoing = ts.get(t.tgt.uid) || [];
return pseudoOutgoing.flatMap(t => getPossibleTargets(t, ts));
}
export function computeArena2(t: Transition, ts: Map<string, Transition[]>): OrState {
const tgts = getPossibleTargets(t, ts);
let lca = computeLCA2([t.src as ConcreteState, ...tgts]);
while (lca.kind !== "or") {
lca = lca.parent!;
}
return lca as OrState;
}
// Assuming ancestor is already entered, what states to enter in order to enter descendants?
// E.g.
// root > A > B > C > D
// computePath({ancestor: A, descendant: A}) = []
// computePath({ancestor: A, descendant: C}) = [B, C]
export function computePath({ancestor, descendant}: {ancestor: ConcreteState, descendant: ConcreteState}): ConcreteState[] {
if (ancestor === descendant) {
return [];
}
return [...computePath({ancestor, descendant: descendant.parent!}), 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}): {
@ -159,7 +98,7 @@ export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState
const path = isAncestorOf({descendant: src, ancestor: tgt});
if (path) {
if (tgt.kind === "or") {
return {arena: tgt as OrState, srcPath: path, tgtPath: [tgt]};
return {arena: tgt, srcPath: path, tgtPath: [tgt]};
}
}
// keep looking
@ -187,7 +126,8 @@ export function getDescendants(state: ConcreteState): Set<string> {
// the 'description' of a state is a human-readable string that (hopefully) identifies the state.
// if the state contains a comment, we take the 'first' (= visually topmost) comment
// otherwise we fall back to the state's UID.
export function stateDescription(state: ConcreteState | PseudoState) {
export function stateDescription(state: ConcreteState) {
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
return description;
}

View file

@ -42,10 +42,5 @@ export function evalExpr(expr: Expression, environment: Environment): any {
const rhs = evalExpr(expr.rhs, environment);
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
}
else if (expr.kind === "call") {
const fn = evalExpr(expr.fn, environment);
const param = evalExpr(expr.param, environment);
return fn(param);
}
throw new Error("should never reach here");
}

View file

@ -4,11 +4,7 @@ import { sides } from "../VisualEditor/VisualEditor";
export type Rountangle = {
uid: string;
kind: "and" | "or";
} & Rect2D;
export type Diamond = {
uid: string;
kind: "and" | "or" | "pseudo";
} & Rect2D;
export type Text = {
@ -25,7 +21,6 @@ export type VisualEditorState = {
rountangles: Rountangle[];
texts: Text[];
arrows: Arrow[];
diamonds: Diamond[];
nextID: number;
};
@ -33,8 +28,8 @@ export type VisualEditorState = {
export type RountanglePart = "left" | "top" | "right" | "bottom";
export type ArrowPart = "start" | "end";
export const emptyState: VisualEditorState = {
rountangles: [], texts: [], arrows: [], diamonds: [], nextID: 0,
export const emptyState = {
rountangles: [], texts: [], arrows: [], nextID: 0,
};
export const onOffStateMachine = {
@ -50,7 +45,7 @@ export const onOffStateMachine = {
};
// used to find which rountangle an arrow connects to (src/tgt)
export function findNearestSide(arrow: Line2D, arrowPart: "start" | "end", candidates: (Rountangle|Diamond)[]): {uid: string, part: RountanglePart} | undefined {
export function findNearestRountangleSide(arrow: Line2D, arrowPart: "start" | "end", candidates: Rountangle[]): {uid: string, part: RountanglePart} | undefined {
let best = Infinity;
let bestSide: undefined | {uid: string, part: RountanglePart};
for (const rountangle of candidates) {

View file

@ -1,11 +1,11 @@
import { computeArena2, computePath, ConcreteState, getDescendants, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition } from "./abstract_syntax";
import { evalExpr } from "./actionlang_interpreter";
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
import { BigStepOutput, Environment, initialRaised, Mode, RaisedEvents, RT_Event, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
import { Action, AfterTrigger, EventTrigger } from "./label_ast";
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event, TimerElapseEvent } from "./runtime_types";
export function initialize(ast: Statechart): BigStepOutput {
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
environment: new Environment([new Map([["_timers", []]])]),
environment: new Environment(),
...initialRaised,
});
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
@ -18,15 +18,12 @@ type ActionScope = {
type EnteredScope = { enteredStates: Mode } & ActionScope;
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
// console.log('enter', stateDescription(state), '...');
let {environment, ...rest} = actionScope;
environment = environment.pushScope();
for (const action of state.entryActions) {
({environment, ...rest} = execAction(action, {environment, ...rest}));
(actionScope = execAction(action, actionScope));
}
// schedule timers
// we store timers in the environment (dirty!)
environment = environment.transform<Timers>("_timers", oldTimers => {
let environment = actionScope.environment.transform<Timers>("_timers", oldTimers => {
const newTimers = [
...oldTimers,
...state.timers.map(timeOffset => {
@ -38,21 +35,20 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
return newTimers;
}, []);
// new nested scope
return {environment, ...rest};
environment = environment.pushScope();
return {...actionScope, environment};
}
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
// console.log('exit', stateDescription(state), '...');
for (const action of state.exitActions) {
(actionScope = execAction(action, actionScope));
}
let environment = actionScope.environment;
let environment = actionScope.environment.popScope();
// cancel timers
environment = environment.transform<Timers>("_timers", oldTimers => {
// remove all timers of 'state':
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
}, []);
environment = environment.popScope();
return {...actionScope, environment};
}
@ -197,42 +193,42 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
throw new Error("should never reach here");
}
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {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>();
for (const state of activeParent.children) {
if (mode.has(state.uid)) {
const outgoing = statechart.transitions.get(state.uid) || [];
const labels = outgoing.flatMap(t =>
t.label
.filter(l => l.kind === "transitionLabel")
.map(l => [t,l] as [Transition, TransitionLabel]));
let triggered;
if (event.kind === "input") {
// get transitions triggered by event
triggered = labels.filter(([_t,l]) =>
l.trigger.kind === "event" && l.trigger.event === event.name);
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "event") {
return trigger.event === event.name;
}
return false;
});
}
else {
// get transitions triggered by timeout
triggered = labels.filter(([_t,l]) =>
l.trigger.kind === "after" && l.trigger.durationMs === event.timeDurMs);
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "after") {
return trigger.durationMs === event.timeDurMs;
}
return false;
});
}
// eval guard
const guardEnvironment = environment.set("inState", (stateLabel: string) => {
for (const [uid, state] of statechart.uid2State.entries()) {
if (stateDescription(state) === stateLabel) {
return (mode.has(uid));
}
}
});
const enabled = triggered.filter(([t,l]) =>
evalExpr(l.guard, guardEnvironment));
const enabled = triggered.filter(transition =>
evalExpr(transition.label[0].guard, environment)
);
if (enabled.length > 0) {
if (enabled.length > 1) {
console.warn('nondeterminism!!!!');
}
const [t,l] = enabled[0]; // just pick one transition
const arena = computeArena2(t, statechart.transitions);
const t = enabled[0];
const {arena, srcPath, tgtPath} = computeArena(t);
let overlapping = false;
for (const alreadyFired of arenasFired) {
if (isOverlapping(arena, alreadyFired)) {
@ -240,18 +236,20 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
}
}
if (!overlapping) {
let oldValue;
if (event.kind === "input" && event.param !== undefined) {
// input events may have a parameter
// add event parameter to environment in new scope
environment = environment.pushScope();
environment = environment.newVar(
(l.trigger as EventTrigger).paramName as string,
(t.label[0].trigger as EventTrigger).paramName as string,
event.param,
);
}
({mode, environment, ...raised} = fireTransition2(simtime, t, statechart.transitions, l, arena, {mode, environment, ...raised}));
if (event.kind === "input" && event.param !== undefined) {
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
if (event.kind === "input" && event.param) {
environment = environment.popScope();
// console.log('restored environment:', environment);
}
arenasFired.add(arena);
}
@ -290,54 +288,27 @@ function transitionDescription(t: Transition) {
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
}
export function fireTransition2(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
console.log('fire', transitionDescription(t));
export function fireTransition(simtime: number, t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse();
console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
// exit src and other states up to arena
({environment, ...raised} = exitPath(simtime, srcPath, {environment, enteredStates: mode, ...raised}));
// exit src
console.log('exit src...');
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
const toExit = getDescendants(arena);
toExit.delete(arena.uid); // do not exit the arena itself
const exitedMode = mode.difference(toExit); // active states after exiting the states we need to exit
const exitedMode = mode.difference(toExit);
// console.log({exitedMode});
return fireSecondHalfOfTransition(simtime, t, ts, label, arena, {mode: exitedMode, environment, ...raised});
}
// assuming we've already exited the source state of the transition, now enter the target state
// IF however, the target is a pseudo-state, DON'T enter it (pseudo-states are NOT states), instead fire the first pseudo-outgoing transition.
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
// exec transition actions
for (const action of label.actions) {
for (const action of t.label[0].actions) {
({environment, ...raised} = execAction(action, {environment, ...raised}));
}
if (t.tgt.kind === "pseudo") {
const outgoing = ts.get(t.tgt.uid) || [];
for (const nextT of outgoing) {
for (const nextLabel of nextT.label) {
if (nextLabel.kind === "transitionLabel") {
if (evalExpr(nextLabel.guard, environment)) {
console.log('fire', transitionDescription(nextT));
// found ourselves an enabled transition
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, ...raised});
}
}
}
}
throw new Error("stuck in pseudo-state!!")
}
else {
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
// enter tgt
let enteredStates;
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath, {environment, ...raised}));
const enteredMode = mode.union(enteredStates);
// enter tgt
console.log('enter tgt...');
let enteredStates;
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
const enteredMode = exitedMode.union(enteredStates);
// console.log({enteredMode});
return {mode: enteredMode, environment, ...raised};
}
return {mode: enteredMode, environment, ...raised};
}

View file

@ -19,11 +19,7 @@ export type ParserError = {
uid: string; // uid of the text node
}
export type Trigger = TriggerLess | EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
export type TriggerLess = {
kind: "triggerless";
}
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
export type EventTrigger = {
kind: "event";
@ -59,7 +55,7 @@ export type RaiseEvent = {
}
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal | FunctionCall;
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal;
export type BinaryExpression = {
kind: "binaryExpr";

View file

@ -211,9 +211,9 @@ function peg$parse(input, options) {
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$literalExpectation("<=", false);
const peg$e17 = peg$literalExpectation(">=", false);
const peg$e18 = peg$classExpectation(["<", ">"], false, false, 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);
@ -229,7 +229,7 @@ function peg$parse(input, options) {
function peg$f0(trigger, guard, actions) {
return {
kind: "transitionLabel",
trigger: trigger ? trigger : {kind: "triggerless"},
trigger,
guard: guard ? guard[2] : {kind: "literal", value: true},
actions: actions ? actions[2] : [],
};
@ -502,9 +502,9 @@ function peg$parse(input, options) {
function peg$parsestart() {
let s0;
s0 = peg$parsecomment();
s0 = peg$parsetlabel();
if (s0 === peg$FAILED) {
s0 = peg$parsetlabel();
s0 = peg$parsecomment();
}
return s0;
@ -516,33 +516,35 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = peg$parse_();
s2 = peg$parsetrigger();
if (s2 === peg$FAILED) {
s2 = null;
}
s3 = peg$parse_();
s4 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 91) {
s5 = peg$c0;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e0); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
s7 = peg$parsecompare();
if (s7 !== peg$FAILED) {
s8 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 93) {
s9 = peg$c1;
peg$currPos++;
} else {
s9 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e1); }
}
if (s9 !== peg$FAILED) {
s5 = [s5, s6, s7, s8, s9];
s4 = s5;
if (s2 !== peg$FAILED) {
s3 = peg$parse_();
s4 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 91) {
s5 = peg$c0;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e0); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
s7 = peg$parsecompare();
if (s7 !== peg$FAILED) {
s8 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 93) {
s9 = peg$c1;
peg$currPos++;
} else {
s9 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e1); }
}
if (s9 !== peg$FAILED) {
s5 = [s5, s6, s7, s8, s9];
s4 = s5;
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
@ -551,42 +553,42 @@ function peg$parse(input, options) {
peg$currPos = s4;
s4 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
if (s4 === peg$FAILED) {
s4 = null;
}
s5 = peg$parse_();
s6 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 47) {
s7 = peg$c2;
peg$currPos++;
} else {
s7 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e2); }
}
if (s7 !== peg$FAILED) {
s8 = peg$parse_();
s9 = peg$parseactions();
if (s9 !== peg$FAILED) {
s7 = [s7, s8, s9];
s6 = s7;
if (s4 === peg$FAILED) {
s4 = null;
}
s5 = peg$parse_();
s6 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 47) {
s7 = peg$c2;
peg$currPos++;
} else {
s7 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e2); }
}
if (s7 !== peg$FAILED) {
s8 = peg$parse_();
s9 = peg$parseactions();
if (s9 !== peg$FAILED) {
s7 = [s7, s8, s9];
s6 = s7;
} else {
peg$currPos = s6;
s6 = peg$FAILED;
}
} else {
peg$currPos = s6;
s6 = peg$FAILED;
}
if (s6 === peg$FAILED) {
s6 = null;
}
s7 = peg$parse_();
peg$savedPos = s0;
s0 = peg$f0(s2, s4, s6);
} else {
peg$currPos = s6;
s6 = peg$FAILED;
peg$currPos = s0;
s0 = peg$FAILED;
}
if (s6 === peg$FAILED) {
s6 = null;
}
s7 = peg$parse_();
peg$savedPos = s0;
s0 = peg$f0(s2, s4, s6);
return s0;
}
@ -1000,25 +1002,25 @@ function peg$parse(input, options) {
if (peg$silentFails === 0) { peg$fail(peg$e15); }
}
if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c14) {
s5 = peg$c14;
peg$currPos += 2;
s5 = input.charAt(peg$currPos);
if (peg$r2.test(s5)) {
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e16); }
}
if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c15) {
s5 = peg$c15;
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$e17); }
}
if (s5 === peg$FAILED) {
s5 = input.charAt(peg$currPos);
if (peg$r2.test(s5)) {
peg$currPos++;
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$e18); }
@ -1472,16 +1474,8 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
s2 = peg$parse_();
s3 = [];
if (input.length > peg$currPos) {
s4 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
while (s4 !== peg$FAILED) {
s3.push(s4);
if (s2 !== peg$FAILED) {
s3 = [];
if (input.length > peg$currPos) {
s4 = input.charAt(peg$currPos);
peg$currPos++;
@ -1489,36 +1483,54 @@ function peg$parse(input, options) {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
}
s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 10) {
s5 = peg$c21;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); }
}
if (s5 === peg$FAILED) {
s5 = peg$currPos;
peg$silentFails++;
if (input.length > peg$currPos) {
s6 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
while (s4 !== peg$FAILED) {
s3.push(s4);
if (input.length > peg$currPos) {
s4 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
}
peg$silentFails--;
if (s6 === peg$FAILED) {
s5 = undefined;
s4 = peg$parse_();
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 10) {
s5 = peg$c21;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e29); }
}
if (s5 === peg$FAILED) {
s5 = peg$currPos;
peg$silentFails++;
if (input.length > peg$currPos) {
s6 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e28); }
}
peg$silentFails--;
if (s6 === peg$FAILED) {
s5 = undefined;
} else {
peg$currPos = s5;
s5 = peg$FAILED;
}
}
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s5;
s5 = peg$FAILED;
peg$currPos = s0;
s0 = peg$FAILED;
}
}
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f22(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;

View file

@ -1,5 +1,5 @@
import { AbstractState, ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
import { findNearestArrow, findNearestSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
import { ConcreteState, OrState, Statechart, Transition } from "./abstract_syntax";
import { findNearestArrow, findNearestRountangleSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
import { isEntirelyWithin } from "../VisualEditor/geometry";
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
@ -45,7 +45,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
timers: [],
}
const uid2State = new Map<string, ConcreteState|PseudoState>([["root", root]]);
const uid2State = new Map<string, ConcreteState>([["root", root]]);
// we will always look for the smallest parent rountangle
const parentCandidates: Rountangle[] = [{
@ -59,59 +59,37 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
// step 1: figure out state hierarchy
// IMPORTANT ASSUMPTION: state.rountangles is sorted from big to small surface area:
// we assume that the rountangles are sorted from big to small:
for (const rt of state.rountangles) {
const common = {
// @ts-ignore
const state: ConcreteState = {
kind: rt.kind,
uid: rt.uid,
children: [],
comments: [],
entryActions: [],
exitActions: [],
timers: [],
};
let state;
if (rt.kind === "or") {
state = {
...common,
initial: [],
children: [],
timers: [],
};
}
else if (rt.kind === "and") {
state = {
...common,
children: [],
timers: [],
};
if (state.kind === "or") {
(state as unknown as OrState).initial = [];
}
uid2State.set(rt.uid, (state));
// iterate in reverse:
for (let i=parentCandidates.length-1; i>=0; i--) {
const candidate = parentCandidates[i];
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
// found our parent
const parentState = uid2State.get(candidate.uid)! as ConcreteState;
// found our parent :)
const parentState = uid2State.get(candidate.uid)!;
parentState.children.push(state as unknown as ConcreteState);
parentCandidates.push(rt);
parentLinks.set(rt.uid, candidate.uid);
state = {
...state,
parent: parentState,
depth: parentState.depth + 1,
}
state.parent = parentState;
state.depth = parentState.depth+1;
break;
}
}
uid2State.set(rt.uid, state as ConcreteState);
}
for (const d of state.diamonds) {
uid2State.set(d.uid, {
kind: "pseudo",
uid: d.uid,
comments: [],
});
}
// step 2: figure out transitions
@ -120,36 +98,26 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
const uid2Transition = new Map<string, Transition>();
for (const arr of state.arrows) {
const sides = [...state.rountangles, ...state.diamonds];
const srcUID = findNearestSide(arr, "start", sides)?.uid;
const tgtUID = findNearestSide(arr, "end", sides)?.uid;
const srcUID = findNearestRountangleSide(arr, "start", state.rountangles)?.uid;
const tgtUID = findNearestRountangleSide(arr, "end", state.rountangles)?.uid;
if (!srcUID) {
if (!tgtUID) {
// dangling edge
// dangling edge - todo: display error...
errors.push({shapeUid: arr.uid, message: "dangling"});
}
else {
// target but no source, so we treat is as an 'initial' marking
const tgtState = uid2State.get(tgtUID)!;
if (tgtState.kind === "pseudo") {
// maybe allow this in the future?
errors.push({
shapeUid: arr.uid,
message: "pseudo-state cannot be initial state",
});
const initialState = uid2State.get(tgtUID)!;
const ofState = uid2State.get(parentLinks.get(tgtUID)!)!;
if (ofState.kind === "or") {
ofState.initial.push([arr.uid, initialState]);
}
else {
const ofState = uid2State.get(parentLinks.get(tgtUID)!)!;
if (ofState.kind === "or") {
ofState.initial.push([arr.uid, tgtState]);
}
else {
// and states do not have an 'initial' state
errors.push({
shapeUid: arr.uid,
message: "AND-state cannot have an initial state",
});
}
// and states do not have an 'initial' state - todo: display error...
errors.push({
shapeUid: arr.uid,
message: "AND-state cannot have an initial state",
});
}
}
}
@ -226,42 +194,26 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
if (belongsToArrow) {
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
if (belongsToTransition) {
const {src} = belongsToTransition;
belongsToTransition.label.push(parsed);
if (parsed.kind === "transitionLabel") {
// collect events
// triggers
if (parsed.trigger.kind === "event") {
if (src.kind === "pseudo") {
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"});
const {event} = parsed.trigger;
if (event.startsWith("_")) {
errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid));
}
else {
const {event} = parsed.trigger;
if (event.startsWith("_")) {
errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid));
}
else {
errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid));
}
errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid));
}
}
else if (parsed.trigger.kind === "after") {
if (src.kind === "pseudo") {
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"});
}
else {
src.timers.push(parsed.trigger.durationMs);
src.timers.sort();
}
belongsToTransition.src.timers.push(parsed.trigger.durationMs);
belongsToTransition.src.timers.sort();
}
else if (["entry", "exit"].includes(parsed.trigger.kind)) {
errors.push({shapeUid: text.uid, message: "entry/exit trigger not allowed on transitions"});
}
else if (parsed.trigger.kind === "triggerless") {
if (src.kind !== "pseudo") {
errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"});
}
}
// // raise-actions
// for (const action of parsed.actions) {
@ -288,7 +240,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
// text does not belong to transition...
// so it belongs to a rountangle (a state)
const rountangle = findRountangle(text.topLeft, state.rountangles);
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! as ConcreteState : root;
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! : root;
if (parsed.kind === "transitionLabel") {
// labels belonging to a rountangle (= a state) must by entry/exit actions
// if we cannot find a containing state, then it belong to the root
@ -305,6 +257,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
data: {start: {offset: 0}, end: {offset: text.text.length}},
});
}
}
else if (parsed.kind === "comment") {
// just append comments to their respective states

View file

@ -1,9 +1,9 @@
start = comment / tlabel
start = tlabel / comment
tlabel = _ trigger:trigger? _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
return {
kind: "transitionLabel",
trigger: trigger ? trigger : {kind: "triggerless"},
trigger,
guard: guard ? guard[2] : {kind: "literal", value: true},
actions: actions ? actions[2] : [],
};
@ -57,7 +57,7 @@ number = [0-9]+ {
expr = compare
compare = sum:sum rest:((_ ("==" / "!=" / "<=" / ">=" / "<" / ">") _) compare)? {
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
if (rest === null) {
return sum;
}