implement copy paste
This commit is contained in:
parent
39a229bf21
commit
ec49c47b39
14 changed files with 580 additions and 234 deletions
|
|
@ -14,6 +14,7 @@ import { TopPanel } from "./TopPanel";
|
||||||
import { RTHistory } from "./RTHistory";
|
import { RTHistory } from "./RTHistory";
|
||||||
import { AST } from "./AST";
|
import { AST } from "./AST";
|
||||||
import { TraceableError } from "../statecharts/parser";
|
import { TraceableError } from "../statecharts/parser";
|
||||||
|
import { getKeyHandler } from "./shortcut_handler";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [mode, setMode] = useState<InsertMode>("and");
|
const [mode, setMode] = useState<InsertMode>("and");
|
||||||
|
|
@ -83,6 +84,14 @@ export function App() {
|
||||||
|
|
||||||
}, [time, rtIdx]);
|
}, [time, rtIdx]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = getKeyHandler(setMode);
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <Stack sx={{height:'100vh'}}>
|
return <Stack sx={{height:'100vh'}}>
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<Box
|
<Box
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,11 @@ export function RTHistory({rt, rtIdx, ast, setRTIdx, setTime}: RTHistoryProps) {
|
||||||
|
|
||||||
|
|
||||||
function ShowEnvironment(props: {environment: Environment}) {
|
function ShowEnvironment(props: {environment: Environment}) {
|
||||||
return <div>{[...props.environment.entries()]
|
return <div>{
|
||||||
.filter(([variable]) => !variable.startsWith('_'))
|
[...props.environment.entries()]
|
||||||
.map(([variable,value]) =>
|
.filter(([variable]) => !variable.startsWith('_'))
|
||||||
`${variable}: ${value}`
|
.map(([variable,value]) => `${variable}: ${value}`).join(', ')
|
||||||
).join(', ')}</div>;
|
}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShowMode(props: {mode: Mode, statechart: Statechart}) {
|
function ShowMode(props: {mode: Mode, statechart: Statechart}) {
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
|
||||||
</div>
|
</div>
|
||||||
 
|
 
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<button title="(re)initialize simulation" onClick={onInit} ><CachedIcon fontSize="small"/></button>
|
<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>
|
<button title="clear the simulation" onClick={onClear} disabled={!rt}><ClearIcon fontSize="small"/></button>
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
|
||||||
22
src/App/shortcut_handler.ts
Normal file
22
src/App/shortcut_handler.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { InsertMode } from "../VisualEditor/VisualEditor";
|
||||||
|
|
||||||
|
export function getKeyHandler(setMode: Dispatch<SetStateAction<InsertMode>>) {
|
||||||
|
return function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "a") {
|
||||||
|
setMode("and");
|
||||||
|
}
|
||||||
|
if (e.key === "o") {
|
||||||
|
setMode("or");
|
||||||
|
}
|
||||||
|
if (e.key === "p") {
|
||||||
|
setMode("pseudo");
|
||||||
|
}
|
||||||
|
if (e.key === "t") {
|
||||||
|
setMode("transition");
|
||||||
|
}
|
||||||
|
if (e.key === "x") {
|
||||||
|
setMode("text");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -81,7 +81,7 @@ text.highlight {
|
||||||
.lineHelper:hover {
|
.lineHelper:hover {
|
||||||
stroke: blue;
|
stroke: blue;
|
||||||
stroke-opacity: 0.2;
|
stroke-opacity: 0.2;
|
||||||
cursor: grab;
|
/* cursor: grab; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.pathHelper {
|
.pathHelper {
|
||||||
|
|
@ -102,7 +102,7 @@ text.highlight {
|
||||||
.circleHelper:hover {
|
.circleHelper:hover {
|
||||||
fill: blue;
|
fill: blue;
|
||||||
fill-opacity: 0.2;
|
fill-opacity: 0.2;
|
||||||
cursor: grab;
|
/* cursor: grab; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.rountangle.or {
|
.rountangle.or {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import * as lz4 from "@nick/lz4";
|
||||||
import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react";
|
import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react";
|
||||||
|
|
||||||
import { Statechart } from "../statecharts/abstract_syntax";
|
import { Statechart } from "../statecharts/abstract_syntax";
|
||||||
import { ArrowPart, RountanglePart, VisualEditorState, emptyState, findNearestArrow, findNearestRountangleSide, 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 { parseStatechart, TraceableError } from "../statecharts/parser";
|
||||||
import { BigStep } from "../statecharts/runtime_types";
|
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";
|
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
|
||||||
|
|
@ -63,6 +63,8 @@ type VisualEditorProps = {
|
||||||
export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditorProps) {
|
export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditorProps) {
|
||||||
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
|
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
|
||||||
|
|
||||||
|
const [clipboard, setClipboard] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const state = historyState.current;
|
const state = historyState.current;
|
||||||
const setState = (s: SetStateAction<VisualEditorState>) => {
|
const setState = (s: SetStateAction<VisualEditorState>) => {
|
||||||
setHistoryState(historyState => {
|
setHistoryState(historyState => {
|
||||||
|
|
@ -164,7 +166,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
const currentPointer = getCurrentPointer(e);
|
const currentPointer = getCurrentPointer(e);
|
||||||
|
|
||||||
if (e.button === 1) {
|
if (e.button === 2) {
|
||||||
checkPoint();
|
checkPoint();
|
||||||
// ignore selection, middle mouse button always inserts
|
// ignore selection, middle mouse button always inserts
|
||||||
setState(state => {
|
setState(state => {
|
||||||
|
|
@ -230,25 +232,26 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!allPartsInSelection) {
|
// if (!allPartsInSelection) {
|
||||||
setSelection([{uid, parts}] as Selection);
|
// 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}) => {
|
const onMouseMove = (e: {pageX: number, pageY: number}) => {
|
||||||
|
|
@ -304,7 +307,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = (e) => {
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
setDragging(null);
|
setDragging(null);
|
||||||
// do not persist sizes smaller than 40x40
|
// do not persist sizes smaller than 40x40
|
||||||
|
|
@ -320,45 +323,68 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}
|
}
|
||||||
if (selectingState) {
|
if (selectingState) {
|
||||||
// we were making a selection
|
// we were making a selection
|
||||||
const normalizedSS = normalizeRect(selectingState);
|
if (selectingState.size.x === 0 && selectingState.size.y === 0) {
|
||||||
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
const uid = e.target?.dataset.uid;
|
||||||
const shapesInSelection = shapes.filter(el => {
|
const parts: string[] = e.target?.dataset.parts?.split(' ') || [];
|
||||||
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) {
|
if (uid) {
|
||||||
const parts: Set<string> = uidToParts.get(uid) || new Set();
|
checkPoint();
|
||||||
for (const part of shape.dataset.parts?.split(' ') || []) {
|
// @ts-ignore
|
||||||
parts.add(part);
|
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]) => ({
|
else {
|
||||||
kind: "rountangle",
|
const normalizedSS = normalizeRect(selectingState);
|
||||||
uid,
|
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
||||||
parts: [...parts],
|
const shapesInSelection = shapes.filter(el => {
|
||||||
})));
|
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
|
||||||
setSelectingState(null); // no longer making a selection
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function deleteShapes(selection: Selection) {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
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)),
|
||||||
|
}));
|
||||||
|
setSelection([]);
|
||||||
|
}
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Delete") {
|
if (e.key === "Delete") {
|
||||||
// delete selection
|
// delete selection
|
||||||
if (selection.length > 0) {
|
if (selection.length > 0) {
|
||||||
checkPoint();
|
checkPoint();
|
||||||
setState(state => ({
|
deleteShapes(selection);
|
||||||
...state,
|
|
||||||
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)),
|
|
||||||
}));
|
|
||||||
setSelection([]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.key === "o") {
|
if (e.key === "o") {
|
||||||
|
|
@ -394,17 +420,61 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
if (e.key === "h") {
|
if (e.key === "h") {
|
||||||
setShowHelp(showHelp => !showHelp);
|
setShowHelp(showHelp => !showHelp);
|
||||||
}
|
}
|
||||||
// if (e.key === "s") {
|
|
||||||
// setMode("state");
|
|
||||||
// }
|
|
||||||
// if (e.key === "t") {
|
|
||||||
// setMode("transition");
|
|
||||||
// }
|
|
||||||
// if (e.key === "x") {
|
|
||||||
// setMode("text");
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
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") {
|
if (e.key === "z") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
undo();
|
undo();
|
||||||
|
|
@ -441,7 +511,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
};
|
};
|
||||||
}, [selectingState, dragging]);
|
}, [selectingState, dragging, clipboard]);
|
||||||
|
|
||||||
// detect what is 'connected'
|
// detect what is 'connected'
|
||||||
const arrow2SideMap = new Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>();
|
const arrow2SideMap = new Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>();
|
||||||
|
|
@ -525,6 +595,86 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPaste(e: ClipboardEvent) {
|
||||||
|
const data = e.clipboardData?.getData("text/plain");
|
||||||
|
if (data) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(data);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// const offset = {x: 40, y: 40};
|
||||||
|
const offset = {x: 0, y: 0};
|
||||||
|
let nextID = state.nextID;
|
||||||
|
try {
|
||||||
|
const copiedRountangles: Rountangle[] = parsed.rountangles.map((r: Rountangle) => ({
|
||||||
|
...r,
|
||||||
|
uid: (nextID++).toString(),
|
||||||
|
topLeft: addV2D(r.topLeft, offset),
|
||||||
|
} as Rountangle));
|
||||||
|
const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({
|
||||||
|
...a,
|
||||||
|
uid: (nextID++).toString(),
|
||||||
|
start: addV2D(a.start, offset),
|
||||||
|
end: addV2D(a.end, offset),
|
||||||
|
} as Arrow));
|
||||||
|
const copiedTexts: Text[] = parsed.texts.map((t: Text) => ({
|
||||||
|
...t,
|
||||||
|
uid: (nextID++).toString(),
|
||||||
|
topLeft: addV2D(t.topLeft, offset),
|
||||||
|
} as Text));
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
rountangles: [...state.rountangles, ...copiedRountangles],
|
||||||
|
arrows: [...state.arrows, ...copiedArrows],
|
||||||
|
texts: [...state.texts, ...copiedTexts],
|
||||||
|
nextID: nextID,
|
||||||
|
}));
|
||||||
|
// @ts-ignore
|
||||||
|
const newSelection: Selection = [
|
||||||
|
...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
||||||
|
...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
||||||
|
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
||||||
|
];
|
||||||
|
setSelection(newSelection);
|
||||||
|
// copyInternal(newSelection, e); // doesn't work
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyInternal(selection: Selection, e: ClipboardEvent) {
|
||||||
|
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
||||||
|
const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid));
|
||||||
|
const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid));
|
||||||
|
const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid));
|
||||||
|
e.clipboardData?.setData("text/plain", JSON.stringify({
|
||||||
|
rountangles: rountanglesToCopy,
|
||||||
|
arrows: arrowsToCopy,
|
||||||
|
texts: textsToCopy,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCopy(e: ClipboardEvent) {
|
||||||
|
if (selection.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
copyInternal(selection, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCut(e: ClipboardEvent) {
|
||||||
|
if (selection.length > 0) {
|
||||||
|
copyInternal(selection, e);
|
||||||
|
deleteShapes(selection);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const active = rt?.mode || new Set();
|
const active = rt?.mode || new Set();
|
||||||
|
|
||||||
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
@ -534,6 +684,9 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
ref={refSVG}
|
ref={refSVG}
|
||||||
|
onCopy={onCopy}
|
||||||
|
onPaste={onPaste}
|
||||||
|
onCut={onCut}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<marker
|
<marker
|
||||||
|
|
@ -637,30 +790,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
>{textNode}</g>;})}
|
>{textNode}</g>;})}
|
||||||
|
|
||||||
{selectingState && <Selecting {...selectingState} />}
|
{selectingState && <Selecting {...selectingState} />}
|
||||||
|
|
||||||
{/* {showHelp ? <>
|
|
||||||
<text x={5} y={20}>
|
|
||||||
Left mouse button: Select/Drag.
|
|
||||||
</text>
|
|
||||||
<text x={5} y={40}>
|
|
||||||
Right mouse button: Select only.
|
|
||||||
</text>
|
|
||||||
<text x={5} y={60}>
|
|
||||||
Middle mouse button: Insert [S]tates / [T]ransitions / Te[X]t (current mode: {mode})</text>
|
|
||||||
<text x={5} y={80}>
|
|
||||||
[Del] Delete selection.
|
|
||||||
</text>
|
|
||||||
<text x={5} y={100}>
|
|
||||||
[O] Turn selected states into OR-states.
|
|
||||||
</text>
|
|
||||||
<text x={5} y={120}>
|
|
||||||
[A] Turn selected states into AND-states.
|
|
||||||
</text>
|
|
||||||
<text x={5} y={140}>
|
|
||||||
[H] Show/hide this help.
|
|
||||||
</text>
|
|
||||||
</> : <text x={5} y={20}>[H] To show help.</text>} */}
|
|
||||||
|
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
import { Action, EventTrigger, ParsedText, TransitionLabel } from "./label_ast";
|
||||||
|
|
||||||
export type AbstractState = {
|
export type AbstractState = {
|
||||||
uid: string;
|
uid: string;
|
||||||
|
|
@ -28,7 +28,7 @@ export type Transition = {
|
||||||
uid: string;
|
uid: string;
|
||||||
src: ConcreteState;
|
src: ConcreteState;
|
||||||
tgt: ConcreteState;
|
tgt: ConcreteState;
|
||||||
label: TransitionLabel[];
|
label: ParsedText[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Statechart = {
|
export type Statechart = {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { Expression } from "./label_ast";
|
import { Expression } from "./label_ast";
|
||||||
import { Environment } from "./runtime_types";
|
import { Environment } from "./runtime_types";
|
||||||
|
|
||||||
|
|
||||||
const UNARY_OPERATOR_MAP: Map<string, (x: any) => any> = new Map([
|
const UNARY_OPERATOR_MAP: Map<string, (x: any) => any> = new Map([
|
||||||
["!", x => !x],
|
["!", x => !x],
|
||||||
["-", x => -x as any],
|
["-", x => -x as any],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { evalExpr } from "./actionlang_interpreter";
|
import { evalExpr } from "./actionlang_interpreter";
|
||||||
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
||||||
import { Action, EventTrigger } from "./label_ast";
|
import { Action, AfterTrigger, EventTrigger } from "./label_ast";
|
||||||
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event } from "./runtime_types";
|
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event, TimerElapseEvent } from "./runtime_types";
|
||||||
|
|
||||||
export function initialize(ast: Statechart): BigStepOutput {
|
export function initialize(ast: Statechart): BigStepOutput {
|
||||||
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
|
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
|
||||||
environment: new Map(),
|
environment: new Environment(),
|
||||||
...initialRaised,
|
...initialRaised,
|
||||||
});
|
});
|
||||||
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
|
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
|
||||||
|
|
@ -23,14 +23,19 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
|
||||||
}
|
}
|
||||||
// schedule timers
|
// schedule timers
|
||||||
// we store timers in the environment (dirty!)
|
// we store timers in the environment (dirty!)
|
||||||
const environment = new Map(actionScope.environment);
|
let environment = actionScope.environment.transform<Timers>("_timers", oldTimers => {
|
||||||
const timers: Timers = [...(environment.get("_timers") || [])];
|
const newTimers = [
|
||||||
for (const timeOffset of state.timers) {
|
...oldTimers,
|
||||||
const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled
|
...state.timers.map(timeOffset => {
|
||||||
timers.push([futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}]);
|
const futureSimTime = simtime + timeOffset;
|
||||||
}
|
return [futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}] as [number, TimerElapseEvent];
|
||||||
timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first
|
}),
|
||||||
environment.set("_timers", timers);
|
];
|
||||||
|
newTimers.sort((a,b) => a[0] - b[0]);
|
||||||
|
return newTimers;
|
||||||
|
}, []);
|
||||||
|
// new nested scope
|
||||||
|
environment = environment.pushScope();
|
||||||
return {...actionScope, environment};
|
return {...actionScope, environment};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,11 +43,12 @@ export function exitActions(simtime: number, state: ConcreteState, actionScope:
|
||||||
for (const action of state.exitActions) {
|
for (const action of state.exitActions) {
|
||||||
(actionScope = execAction(action, actionScope));
|
(actionScope = execAction(action, actionScope));
|
||||||
}
|
}
|
||||||
|
let environment = actionScope.environment.popScope();
|
||||||
// cancel timers
|
// cancel timers
|
||||||
const environment = new Map(actionScope.environment);
|
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||||
const timers: Timers = environment.get("_timers") || [];
|
// remove all timers of 'state':
|
||||||
const filtered = timers.filter(([_, {state: s}]) => s !== state.uid);
|
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
|
||||||
environment.set("_timers", filtered);
|
}, []);
|
||||||
return {...actionScope, environment};
|
return {...actionScope, environment};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,15 +130,15 @@ export function enterPath(simtime: number, path: ConcreteState[], rt: ActionScop
|
||||||
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
||||||
let {enteredStates, ...actionScope} = rt;
|
let {enteredStates, ...actionScope} = rt;
|
||||||
|
|
||||||
// exit all active children...
|
if (enteredStates.has(state.uid)) {
|
||||||
for (const child of state.children) {
|
// exit all active children...
|
||||||
if (enteredStates.has(child.uid)) {
|
for (const child of state.children) {
|
||||||
actionScope = exitCurrent(simtime, child, {enteredStates, ...actionScope});
|
actionScope = exitCurrent(simtime, child, {enteredStates, ...actionScope});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// execute exit actions
|
// execute exit actions
|
||||||
actionScope = exitActions(simtime, state, actionScope);
|
actionScope = exitActions(simtime, state, actionScope);
|
||||||
|
}
|
||||||
|
|
||||||
return actionScope;
|
return actionScope;
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +146,7 @@ export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredSc
|
||||||
export function exitPath(simtime: number, path: ConcreteState[], rt: EnteredScope): ActionScope {
|
export function exitPath(simtime: number, path: ConcreteState[], rt: EnteredScope): ActionScope {
|
||||||
let {enteredStates, ...actionScope} = rt;
|
let {enteredStates, ...actionScope} = rt;
|
||||||
|
|
||||||
const toExit = enteredStates.difference(new Set(path));
|
const toExit = enteredStates.difference(new Set(path.map(s=>s.uid)));
|
||||||
|
|
||||||
const [state, ...rest] = path;
|
const [state, ...rest] = path;
|
||||||
|
|
||||||
|
|
@ -152,18 +158,16 @@ export function exitPath(simtime: number, path: ConcreteState[], rt: EnteredScop
|
||||||
|
|
||||||
// execute exit actions
|
// execute exit actions
|
||||||
actionScope = exitActions(simtime, state, actionScope);
|
actionScope = exitActions(simtime, state, actionScope);
|
||||||
|
|
||||||
return actionScope;
|
return actionScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
if (action.kind === "assignment") {
|
if (action.kind === "assignment") {
|
||||||
const rhs = evalExpr(action.rhs, rt.environment);
|
const rhs = evalExpr(action.rhs, rt.environment);
|
||||||
const newEnvironment = new Map(rt.environment);
|
const environment = rt.environment.set(action.lhs, rhs);
|
||||||
newEnvironment.set(action.lhs, rhs);
|
|
||||||
return {
|
return {
|
||||||
...rt,
|
...rt,
|
||||||
environment: newEnvironment,
|
environment,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (action.kind === "raise") {
|
else if (action.kind === "raise") {
|
||||||
|
|
@ -224,7 +228,6 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
||||||
console.warn('nondeterminism!!!!');
|
console.warn('nondeterminism!!!!');
|
||||||
}
|
}
|
||||||
const t = enabled[0];
|
const t = enabled[0];
|
||||||
console.log('enabled:', transitionDescription(t));
|
|
||||||
const {arena, srcPath, tgtPath} = computeArena(t);
|
const {arena, srcPath, tgtPath} = computeArena(t);
|
||||||
let overlapping = false;
|
let overlapping = false;
|
||||||
for (const alreadyFired of arenasFired) {
|
for (const alreadyFired of arenasFired) {
|
||||||
|
|
@ -233,30 +236,25 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!overlapping) {
|
if (!overlapping) {
|
||||||
console.log('^ firing');
|
|
||||||
let oldValue;
|
let oldValue;
|
||||||
if (event.kind === "input" && event.param !== undefined) {
|
if (event.kind === "input" && event.param !== undefined) {
|
||||||
// input events may have a parameter
|
// input events may have a parameter
|
||||||
// *temporarily* add event to environment (dirty!)
|
// add event parameter to environment in new scope
|
||||||
oldValue = environment.get(event.param);
|
environment = environment.pushScope();
|
||||||
environment = new Map([
|
environment = environment.newVar(
|
||||||
...environment,
|
(t.label[0].trigger as EventTrigger).paramName as string,
|
||||||
[(t.label[0].trigger as EventTrigger).paramName as string, event.param],
|
event.param,
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
|
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
|
||||||
if (event.kind === "input" && event.param) {
|
if (event.kind === "input" && event.param) {
|
||||||
// restore original value of variable that had same name as input parameter
|
environment = environment.popScope();
|
||||||
environment = new Map([
|
// console.log('restored environment:', environment);
|
||||||
...environment,
|
|
||||||
[(t.label[0].trigger as EventTrigger).paramName as string, oldValue],
|
|
||||||
]);
|
|
||||||
console.log('restored environment:', environment);
|
|
||||||
}
|
}
|
||||||
arenasFired.add(arena);
|
arenasFired.add(arena);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log('skip (overlapping arenas)');
|
// console.log('skip (overlapping arenas)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -292,9 +290,10 @@ function transitionDescription(t: Transition) {
|
||||||
|
|
||||||
export function fireTransition(simtime: number, t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function fireTransition(simtime: number, t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
|
|
||||||
// console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
|
console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
|
||||||
|
|
||||||
// exit src
|
// exit src
|
||||||
|
console.log('exit src...');
|
||||||
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
|
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
|
||||||
const toExit = getDescendants(arena);
|
const toExit = getDescendants(arena);
|
||||||
toExit.delete(arena.uid); // do not exit the arena itself
|
toExit.delete(arena.uid); // do not exit the arena itself
|
||||||
|
|
@ -306,6 +305,7 @@ export function fireTransition(simtime: number, t: Transition, arena: OrState, s
|
||||||
}
|
}
|
||||||
|
|
||||||
// enter tgt
|
// enter tgt
|
||||||
|
console.log('enter tgt...');
|
||||||
let enteredStates;
|
let enteredStates;
|
||||||
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
|
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
|
||||||
const enteredMode = exitedMode.union(enteredStates);
|
const enteredMode = exitedMode.union(enteredStates);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export type ParsedText = TransitionLabel | Comment;
|
export type ParsedText = TransitionLabel | Comment | ParserError;
|
||||||
|
|
||||||
export type TransitionLabel = {
|
export type TransitionLabel = {
|
||||||
kind: "transitionLabel";
|
kind: "transitionLabel";
|
||||||
|
|
@ -14,6 +14,11 @@ export type Comment = {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ParserError = {
|
||||||
|
kind: "parserError";
|
||||||
|
uid: string; // uid of the text node
|
||||||
|
}
|
||||||
|
|
||||||
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
||||||
|
|
||||||
export type EventTrigger = {
|
export type EventTrigger = {
|
||||||
|
|
@ -73,4 +78,10 @@ export type VarRef = {
|
||||||
export type Literal = {
|
export type Literal = {
|
||||||
kind: "literal";
|
kind: "literal";
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FunctionCall = {
|
||||||
|
kind: "call",
|
||||||
|
fn: VarRef,
|
||||||
|
param: Expression,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,16 +182,18 @@ function peg$parse(input, options) {
|
||||||
const peg$c15 = ">=";
|
const peg$c15 = ">=";
|
||||||
const peg$c16 = "true";
|
const peg$c16 = "true";
|
||||||
const peg$c17 = "false";
|
const peg$c17 = "false";
|
||||||
const peg$c18 = "^";
|
const peg$c18 = "\"";
|
||||||
const peg$c19 = "//";
|
const peg$c19 = "^";
|
||||||
const peg$c20 = "\n";
|
const peg$c20 = "//";
|
||||||
|
const peg$c21 = "\n";
|
||||||
|
|
||||||
const peg$r0 = /^[0-9A-Z_a-z]/;
|
const peg$r0 = /^[0-9A-Z_a-z]/;
|
||||||
const peg$r1 = /^[0-9]/;
|
const peg$r1 = /^[0-9]/;
|
||||||
const peg$r2 = /^[<>]/;
|
const peg$r2 = /^[<>]/;
|
||||||
const peg$r3 = /^[+\-]/;
|
const peg$r3 = /^[+\-]/;
|
||||||
const peg$r4 = /^[*\/]/;
|
const peg$r4 = /^[*\/]/;
|
||||||
const peg$r5 = /^[ \t\n\r]/;
|
const peg$r5 = /^[^"]/;
|
||||||
|
const peg$r6 = /^[ \t\n\r]/;
|
||||||
|
|
||||||
const peg$e0 = peg$literalExpectation("[", false);
|
const peg$e0 = peg$literalExpectation("[", false);
|
||||||
const peg$e1 = peg$literalExpectation("]", false);
|
const peg$e1 = peg$literalExpectation("]", false);
|
||||||
|
|
@ -216,11 +218,13 @@ function peg$parse(input, options) {
|
||||||
const peg$e20 = peg$classExpectation(["*", "/"], false, false, false);
|
const peg$e20 = peg$classExpectation(["*", "/"], false, false, false);
|
||||||
const peg$e21 = peg$literalExpectation("true", false);
|
const peg$e21 = peg$literalExpectation("true", false);
|
||||||
const peg$e22 = peg$literalExpectation("false", false);
|
const peg$e22 = peg$literalExpectation("false", false);
|
||||||
const peg$e23 = peg$literalExpectation("^", false);
|
const peg$e23 = peg$literalExpectation("\"", false);
|
||||||
const peg$e24 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
|
const peg$e24 = peg$classExpectation(["\""], true, false, false);
|
||||||
const peg$e25 = peg$literalExpectation("//", false);
|
const peg$e25 = peg$literalExpectation("^", false);
|
||||||
const peg$e26 = peg$anyExpectation();
|
const peg$e26 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
|
||||||
const peg$e27 = peg$literalExpectation("\n", false);
|
const peg$e27 = peg$literalExpectation("//", false);
|
||||||
|
const peg$e28 = peg$anyExpectation();
|
||||||
|
const peg$e29 = peg$literalExpectation("\n", false);
|
||||||
|
|
||||||
function peg$f0(trigger, guard, actions) {
|
function peg$f0(trigger, guard, actions) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -296,20 +300,30 @@ function peg$parse(input, options) {
|
||||||
function peg$f14(expr) {
|
function peg$f14(expr) {
|
||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
function peg$f15(value) {
|
function peg$f15(fn, param) {
|
||||||
|
return {
|
||||||
|
kind: "call",
|
||||||
|
fn,
|
||||||
|
param,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function peg$f16(value) {
|
||||||
return {kind: "literal", value}
|
return {kind: "literal", value}
|
||||||
}
|
}
|
||||||
function peg$f16(variable) {
|
function peg$f17(variable) {
|
||||||
return {kind: "ref", variable}
|
return {kind: "ref", variable}
|
||||||
}
|
}
|
||||||
function peg$f17() {
|
function peg$f18() {
|
||||||
return text() === "true";
|
return text() === "true";
|
||||||
}
|
}
|
||||||
function peg$f18(event, param) {
|
function peg$f19(str) {
|
||||||
|
return str.join('');
|
||||||
|
}
|
||||||
|
function peg$f20(event, param) {
|
||||||
return {kind: "raise", event, param: param ? param[1] : undefined};
|
return {kind: "raise", event, param: param ? param[1] : undefined};
|
||||||
}
|
}
|
||||||
function peg$f19() { return null; }
|
function peg$f21() { return null; }
|
||||||
function peg$f20(text) {
|
function peg$f22(text) {
|
||||||
return {
|
return {
|
||||||
kind: "comment",
|
kind: "comment",
|
||||||
text: text.join(''),
|
text: text.join(''),
|
||||||
|
|
@ -1152,11 +1166,14 @@ function peg$parse(input, options) {
|
||||||
function peg$parseatom() {
|
function peg$parseatom() {
|
||||||
let s0;
|
let s0;
|
||||||
|
|
||||||
s0 = peg$parsenested();
|
s0 = peg$parsefnCall();
|
||||||
if (s0 === peg$FAILED) {
|
if (s0 === peg$FAILED) {
|
||||||
s0 = peg$parseliteral();
|
s0 = peg$parsenested();
|
||||||
if (s0 === peg$FAILED) {
|
if (s0 === peg$FAILED) {
|
||||||
s0 = peg$parseref();
|
s0 = peg$parseliteral();
|
||||||
|
if (s0 === peg$FAILED) {
|
||||||
|
s0 = peg$parseref();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1205,6 +1222,28 @@ function peg$parse(input, options) {
|
||||||
return s0;
|
return s0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peg$parsefnCall() {
|
||||||
|
let s0, s1, s2;
|
||||||
|
|
||||||
|
s0 = peg$currPos;
|
||||||
|
s1 = peg$parseref();
|
||||||
|
if (s1 !== peg$FAILED) {
|
||||||
|
s2 = peg$parsenested();
|
||||||
|
if (s2 !== peg$FAILED) {
|
||||||
|
peg$savedPos = s0;
|
||||||
|
s0 = peg$f15(s1, s2);
|
||||||
|
} else {
|
||||||
|
peg$currPos = s0;
|
||||||
|
s0 = peg$FAILED;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
peg$currPos = s0;
|
||||||
|
s0 = peg$FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s0;
|
||||||
|
}
|
||||||
|
|
||||||
function peg$parseliteral() {
|
function peg$parseliteral() {
|
||||||
let s0, s1;
|
let s0, s1;
|
||||||
|
|
||||||
|
|
@ -1212,10 +1251,13 @@ function peg$parse(input, options) {
|
||||||
s1 = peg$parsenumber();
|
s1 = peg$parsenumber();
|
||||||
if (s1 === peg$FAILED) {
|
if (s1 === peg$FAILED) {
|
||||||
s1 = peg$parseboolean();
|
s1 = peg$parseboolean();
|
||||||
|
if (s1 === peg$FAILED) {
|
||||||
|
s1 = peg$parsestring();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f15(s1);
|
s1 = peg$f16(s1);
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
|
|
@ -1229,7 +1271,7 @@ function peg$parse(input, options) {
|
||||||
s1 = peg$parseidentifier();
|
s1 = peg$parseidentifier();
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f16(s1);
|
s1 = peg$f17(s1);
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
|
|
@ -1258,23 +1300,75 @@ function peg$parse(input, options) {
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f17();
|
s1 = peg$f18();
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
return s0;
|
return s0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peg$parsestring() {
|
||||||
|
let s0, s1, s2, s3;
|
||||||
|
|
||||||
|
s0 = peg$currPos;
|
||||||
|
if (input.charCodeAt(peg$currPos) === 34) {
|
||||||
|
s1 = peg$c18;
|
||||||
|
peg$currPos++;
|
||||||
|
} else {
|
||||||
|
s1 = peg$FAILED;
|
||||||
|
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||||
|
}
|
||||||
|
if (s1 !== peg$FAILED) {
|
||||||
|
s2 = [];
|
||||||
|
s3 = input.charAt(peg$currPos);
|
||||||
|
if (peg$r5.test(s3)) {
|
||||||
|
peg$currPos++;
|
||||||
|
} else {
|
||||||
|
s3 = peg$FAILED;
|
||||||
|
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||||
|
}
|
||||||
|
while (s3 !== peg$FAILED) {
|
||||||
|
s2.push(s3);
|
||||||
|
s3 = input.charAt(peg$currPos);
|
||||||
|
if (peg$r5.test(s3)) {
|
||||||
|
peg$currPos++;
|
||||||
|
} else {
|
||||||
|
s3 = peg$FAILED;
|
||||||
|
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (input.charCodeAt(peg$currPos) === 34) {
|
||||||
|
s3 = peg$c18;
|
||||||
|
peg$currPos++;
|
||||||
|
} else {
|
||||||
|
s3 = peg$FAILED;
|
||||||
|
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||||
|
}
|
||||||
|
if (s3 !== peg$FAILED) {
|
||||||
|
peg$savedPos = s0;
|
||||||
|
s0 = peg$f19(s2);
|
||||||
|
} else {
|
||||||
|
peg$currPos = s0;
|
||||||
|
s0 = peg$FAILED;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
peg$currPos = s0;
|
||||||
|
s0 = peg$FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s0;
|
||||||
|
}
|
||||||
|
|
||||||
function peg$parseraise() {
|
function peg$parseraise() {
|
||||||
let s0, s1, s2, s3, s4, s5, s6, s7;
|
let s0, s1, s2, s3, s4, s5, s6, s7;
|
||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 94) {
|
if (input.charCodeAt(peg$currPos) === 94) {
|
||||||
s1 = peg$c18;
|
s1 = peg$c19;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parse_();
|
s2 = peg$parse_();
|
||||||
|
|
@ -1317,7 +1411,7 @@ function peg$parse(input, options) {
|
||||||
s4 = null;
|
s4 = null;
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f18(s3, s4);
|
s0 = peg$f20(s3, s4);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
|
|
@ -1339,11 +1433,11 @@ function peg$parse(input, options) {
|
||||||
s2 = peg$parsecomment();
|
s2 = peg$parsecomment();
|
||||||
if (s2 === peg$FAILED) {
|
if (s2 === peg$FAILED) {
|
||||||
s2 = input.charAt(peg$currPos);
|
s2 = input.charAt(peg$currPos);
|
||||||
if (peg$r5.test(s2)) {
|
if (peg$r6.test(s2)) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s2 = peg$FAILED;
|
s2 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (s2 !== peg$FAILED) {
|
while (s2 !== peg$FAILED) {
|
||||||
|
|
@ -1351,16 +1445,16 @@ function peg$parse(input, options) {
|
||||||
s2 = peg$parsecomment();
|
s2 = peg$parsecomment();
|
||||||
if (s2 === peg$FAILED) {
|
if (s2 === peg$FAILED) {
|
||||||
s2 = input.charAt(peg$currPos);
|
s2 = input.charAt(peg$currPos);
|
||||||
if (peg$r5.test(s2)) {
|
if (peg$r6.test(s2)) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s2 = peg$FAILED;
|
s2 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f19();
|
s1 = peg$f21();
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
peg$silentFails--;
|
peg$silentFails--;
|
||||||
|
|
||||||
|
|
@ -1371,12 +1465,12 @@ function peg$parse(input, options) {
|
||||||
let s0, s1, s2, s3, s4, s5, s6;
|
let s0, s1, s2, s3, s4, s5, s6;
|
||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.substr(peg$currPos, 2) === peg$c19) {
|
if (input.substr(peg$currPos, 2) === peg$c20) {
|
||||||
s1 = peg$c19;
|
s1 = peg$c20;
|
||||||
peg$currPos += 2;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parse_();
|
s2 = peg$parse_();
|
||||||
|
|
@ -1387,7 +1481,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s4 = peg$FAILED;
|
s4 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
}
|
}
|
||||||
while (s4 !== peg$FAILED) {
|
while (s4 !== peg$FAILED) {
|
||||||
s3.push(s4);
|
s3.push(s4);
|
||||||
|
|
@ -1396,17 +1490,17 @@ function peg$parse(input, options) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s4 = peg$FAILED;
|
s4 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s4 = peg$parse_();
|
s4 = peg$parse_();
|
||||||
if (s4 !== peg$FAILED) {
|
if (s4 !== peg$FAILED) {
|
||||||
if (input.charCodeAt(peg$currPos) === 10) {
|
if (input.charCodeAt(peg$currPos) === 10) {
|
||||||
s5 = peg$c20;
|
s5 = peg$c21;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
s5 = peg$currPos;
|
s5 = peg$currPos;
|
||||||
|
|
@ -1416,7 +1510,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s6 = peg$FAILED;
|
s6 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
}
|
}
|
||||||
peg$silentFails--;
|
peg$silentFails--;
|
||||||
if (s6 === peg$FAILED) {
|
if (s6 === peg$FAILED) {
|
||||||
|
|
@ -1428,7 +1522,7 @@ function peg$parse(input, options) {
|
||||||
}
|
}
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f20(s3);
|
s0 = peg$f22(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
let parsed: ParsedText;
|
let parsed: ParsedText;
|
||||||
try {
|
try {
|
||||||
parsed = parseLabel(text.text); // may throw
|
parsed = parseLabel(text.text); // may throw
|
||||||
|
parsed.uid = text.uid;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
errors.push({
|
errors.push({
|
||||||
|
|
@ -180,21 +181,21 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
message: e.message,
|
message: e.message,
|
||||||
data: e,
|
data: e,
|
||||||
});
|
});
|
||||||
continue;
|
parsed = {
|
||||||
|
kind: "parserError",
|
||||||
|
uid: text.uid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parsed.uid = text.uid;
|
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
|
||||||
if (parsed.kind === "transitionLabel") {
|
if (belongsToArrow) {
|
||||||
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
|
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
||||||
if (belongsToArrow) {
|
if (belongsToTransition) {
|
||||||
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
belongsToTransition.label.push(parsed);
|
||||||
if (belongsToTransition) {
|
if (parsed.kind === "transitionLabel") {
|
||||||
// parse as transition label
|
|
||||||
belongsToTransition.label.push(parsed);
|
|
||||||
|
|
||||||
// collect events
|
// collect events
|
||||||
// triggers
|
// triggers
|
||||||
if (parsed.trigger.kind === "event") {
|
if (parsed.trigger.kind === "event") {
|
||||||
|
|
@ -210,6 +211,10 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
belongsToTransition.src.timers.push(parsed.trigger.durationMs);
|
belongsToTransition.src.timers.push(parsed.trigger.durationMs);
|
||||||
belongsToTransition.src.timers.sort();
|
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"});
|
||||||
|
}
|
||||||
|
|
||||||
// // raise-actions
|
// // raise-actions
|
||||||
// for (const action of parsed.actions) {
|
// for (const action of parsed.actions) {
|
||||||
// if (action.kind === "raise") {
|
// if (action.kind === "raise") {
|
||||||
|
|
@ -224,40 +229,40 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// collect variables
|
// collect variables
|
||||||
variables = variables
|
variables = variables.union(findVariables(parsed.guard));
|
||||||
.union(findVariables(parsed.guard));
|
|
||||||
for (const action of parsed.actions) {
|
for (const action of parsed.actions) {
|
||||||
variables = variables.union(findVariablesAction(action));
|
variables = variables.union(findVariablesAction(action));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// text does not belong to transition...
|
else {
|
||||||
// so it belongs to a rountangle (a state)
|
// text does not belong to transition...
|
||||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
// so it belongs to a rountangle (a state)
|
||||||
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! : root;
|
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
||||||
if (parsed.kind === "transitionLabel") {
|
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! : root;
|
||||||
// labels belonging to a rountangle (= a state) must by entry/exit actions
|
if (parsed.kind === "transitionLabel") {
|
||||||
// if we cannot find a containing state, then it belong to the root
|
// labels belonging to a rountangle (= a state) must by entry/exit actions
|
||||||
if (parsed.trigger.kind === "entry") {
|
// if we cannot find a containing state, then it belong to the root
|
||||||
belongsToState.entryActions.push(...parsed.actions);
|
if (parsed.trigger.kind === "entry") {
|
||||||
}
|
belongsToState.entryActions.push(...parsed.actions);
|
||||||
else if(parsed.trigger.kind === "exit") {
|
}
|
||||||
belongsToState.exitActions.push(...parsed.actions);
|
else if(parsed.trigger.kind === "exit") {
|
||||||
}
|
belongsToState.exitActions.push(...parsed.actions);
|
||||||
else {
|
}
|
||||||
errors.push({
|
else {
|
||||||
shapeUid: text.uid,
|
errors.push({
|
||||||
message: "states can only have entry/exit triggers",
|
shapeUid: text.uid,
|
||||||
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
message: "states can only have entry/exit triggers",
|
||||||
});
|
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (parsed.kind === "comment") {
|
else if (parsed.kind === "comment") {
|
||||||
// just append comments to their respective states
|
// just append comments to their respective states
|
||||||
belongsToState.comments.push([text.uid, parsed.text]);
|
belongsToState.comments.push([text.uid, parsed.text]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,94 @@ export type TimerElapseEvent = {
|
||||||
|
|
||||||
export type Mode = Set<string>; // set of active states
|
export type Mode = Set<string>; // set of active states
|
||||||
|
|
||||||
export type Environment = ReadonlyMap<string, any>; // variable name -> value
|
// export type Environment = ReadonlyMap<string, any>; // variable name -> value
|
||||||
|
|
||||||
// export class Environment {
|
export class Environment {
|
||||||
// env: Map<string, any>[];
|
scopes: ReadonlyMap<string, any>[]; // array of nested scopes - scope at the back of the array is used first
|
||||||
// constructor(env = [new Map()]) {
|
|
||||||
// this.env = env;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// with(key: string, value: any): Environment {
|
constructor(env = [new Map()] as ReadonlyMap<string, any>[]) {
|
||||||
// for (let i=0; i<this.env.length; i++) {
|
this.scopes = env;
|
||||||
// if (this.env[i].has(key)) {
|
}
|
||||||
// return new Environment(this.env.with(i, new Map([
|
|
||||||
// ...this.env[i].entries(),
|
pushScope(): Environment {
|
||||||
// [key, value],
|
return new Environment([...this.scopes, new Map<string, any>()]);
|
||||||
// ])));
|
}
|
||||||
// }
|
|
||||||
// }
|
popScope(): Environment {
|
||||||
// return new Environment(this.env.with(-1, new Map([
|
return new Environment(this.scopes.slice(0, -1));
|
||||||
// ...this.env[this.env.length-1].entries(),
|
}
|
||||||
// [key, value],
|
|
||||||
// ])));
|
// force creation of a new variable in the current scope, even if a variable with the same name already exists in a surrounding scope
|
||||||
// }
|
newVar(key: string, value: any): Environment {
|
||||||
// }
|
return new Environment(
|
||||||
|
this.scopes.with(
|
||||||
|
this.scopes.length-1,
|
||||||
|
new Map([
|
||||||
|
...this.scopes[this.scopes.length-1],
|
||||||
|
[key, value],
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// update variable in the innermost scope where it exists, or create it in the current scope if it doesn't exist yet
|
||||||
|
set(key: string, value: any): Environment {
|
||||||
|
for (let i=this.scopes.length-1; i>=0; i--) {
|
||||||
|
const map = this.scopes[i];
|
||||||
|
if (map.has(key)) {
|
||||||
|
return new Environment(this.scopes.with(i, new Map([
|
||||||
|
...map.entries(),
|
||||||
|
[key, value],
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(this.scopes);
|
||||||
|
return new Environment(this.scopes.with(-1, new Map([
|
||||||
|
...this.scopes[this.scopes.length-1].entries(),
|
||||||
|
[key, value],
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup variable, starting in the currrent (= innermost) scope, then looking into surrounding scopes until found.
|
||||||
|
get(key: string): any {
|
||||||
|
for (let i=this.scopes.length-1; i>=0; i--) {
|
||||||
|
const map = this.scopes[i];
|
||||||
|
const found = map.get(key);
|
||||||
|
if (found !== undefined) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transform<T>(key: string, upd: (old:T) => T, defaultVal: T): Environment {
|
||||||
|
const old = this.get(key) || defaultVal;
|
||||||
|
return this.set(key, upd(old));
|
||||||
|
}
|
||||||
|
|
||||||
|
*entries() {
|
||||||
|
const visited = new Set();
|
||||||
|
for (let i=this.scopes.length-1; i>=0; i--) {
|
||||||
|
const map = this.scopes[i];
|
||||||
|
for (const [key, value] of map.entries()) {
|
||||||
|
if (!visited.has(key)) {
|
||||||
|
yield [key, value];
|
||||||
|
visited.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('env...');
|
||||||
|
// let env = new Environment();
|
||||||
|
// env = env.set("a", 1);
|
||||||
|
// env = env.set("b", 2);
|
||||||
|
// env = env.pushScope();
|
||||||
|
// console.log(env.get("a")); // 1
|
||||||
|
// env = env.newVar("a", 99);
|
||||||
|
// console.log(env.get("a")); // 99
|
||||||
|
// env = env.popScope();
|
||||||
|
// console.log(env.get("a")); // 1
|
||||||
|
// console.log('end env...');
|
||||||
|
|
||||||
export type RT_Statechart = {
|
export type RT_Statechart = {
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
|
|
|
||||||
|
|
@ -93,13 +93,21 @@ product = atom:atom rest:((_ ("*" / "/") _) product)? {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
atom = nested / literal / ref
|
atom = fnCall / nested / literal / ref
|
||||||
|
|
||||||
nested = "(" _ expr:expr _ ")" {
|
nested = "(" _ expr:expr _ ")" {
|
||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
literal = value:(number / boolean) {
|
fnCall = fn:ref param:nested {
|
||||||
|
return {
|
||||||
|
kind: "call",
|
||||||
|
fn,
|
||||||
|
param,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
literal = value:(number / boolean / string) {
|
||||||
return {kind: "literal", value}
|
return {kind: "literal", value}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +119,10 @@ boolean = ("true" / "false") {
|
||||||
return text() === "true";
|
return text() === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string = '"' str:([^"]*) '"' {
|
||||||
|
return str.join('');
|
||||||
|
}
|
||||||
|
|
||||||
raise = "^" _ event:identifier param:("(" expr ")")? {
|
raise = "^" _ event:identifier param:("(" expr ")")? {
|
||||||
return {kind: "raise", event, param: param ? param[1] : undefined};
|
return {kind: "raise", event, param: param ? param[1] : undefined};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue