implement copy paste

This commit is contained in:
Joeri Exelmans 2025-10-15 13:53:49 +02:00
parent 39a229bf21
commit ec49c47b39
14 changed files with 580 additions and 234 deletions

View file

@ -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

View file

@ -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>{
[...props.environment.entries()]
.filter(([variable]) => !variable.startsWith('_')) .filter(([variable]) => !variable.startsWith('_'))
.map(([variable,value]) => .map(([variable,value]) => `${variable}: ${value}`).join(', ')
`${variable}: ${value}` }</div>;
).join(', ')}</div>;
} }
function ShowMode(props: {mode: Mode, statechart: Statechart}) { function ShowMode(props: {mode: Mode, statechart: Statechart}) {

View file

@ -116,7 +116,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
</div> </div>
&emsp; &emsp;
<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>
&emsp; &emsp;

View 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");
}
}
}

View file

@ -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 {

View file

@ -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,10 +232,11 @@ 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 // start dragging
setDragging({ setDragging({
lastMousePos: currentPointer, lastMousePos: currentPointer,
@ -241,7 +244,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
return; return;
} }
} }
// otherwise, just start making a selection // otherwise, just start making a selection
setDragging(null); setDragging(null);
setSelectingState({ setSelectingState({
@ -249,6 +251,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
size: {x: 0, y: 0}, size: {x: 0, y: 0},
}); });
setSelection([]); 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,6 +323,25 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
} }
if (selectingState) { if (selectingState) {
// we were making a selection // we were making a selection
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) {
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;
}
}
}
}
else {
const normalizedSS = normalizeRect(selectingState); const normalizedSS = normalizeRect(selectingState);
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[]; const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
const shapesInSelection = shapes.filter(el => { const shapesInSelection = shapes.filter(el => {
@ -339,19 +361,15 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
} }
} }
setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({ setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({
kind: "rountangle",
uid, uid,
parts: [...parts], parts: [...parts],
}))); })));
setSelectingState(null); // no longer making a selection
} }
}
setSelectingState(null); // no longer making a selection
}; };
const onKeyDown = (e: KeyboardEvent) => { function deleteShapes(selection: Selection) {
if (e.key === "Delete") {
// delete selection
if (selection.length > 0) {
checkPoint();
setState(state => ({ setState(state => ({
...state, ...state,
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)), rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
@ -360,6 +378,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
})); }));
setSelection([]); setSelection([]);
} }
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Delete") {
// delete selection
if (selection.length > 0) {
checkPoint();
deleteShapes(selection);
}
} }
if (e.key === "o") { if (e.key === "o") {
// selected states become OR-states // selected states become OR-states
@ -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>;
} }

View file

@ -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 = {

View file

@ -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],

View file

@ -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;
if (enteredStates.has(state.uid)) {
// exit all active children... // exit all active children...
for (const child of state.children) { for (const child of state.children) {
if (enteredStates.has(child.uid)) {
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);

View file

@ -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 = {
@ -74,3 +79,9 @@ export type Literal = {
kind: "literal"; kind: "literal";
value: any; value: any;
} }
export type FunctionCall = {
kind: "call",
fn: VarRef,
param: Expression,
}

View file

@ -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,6 +1166,8 @@ function peg$parse(input, options) {
function peg$parseatom() { function peg$parseatom() {
let s0; let s0;
s0 = peg$parsefnCall();
if (s0 === peg$FAILED) {
s0 = peg$parsenested(); s0 = peg$parsenested();
if (s0 === peg$FAILED) { if (s0 === peg$FAILED) {
s0 = peg$parseliteral(); s0 = peg$parseliteral();
@ -1159,6 +1175,7 @@ function peg$parse(input, options) {
s0 = peg$parseref(); s0 = peg$parseref();
} }
} }
}
return s0; return s0;
} }
@ -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;

View file

@ -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;
if (parsed.kind === "transitionLabel") {
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows); const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
if (belongsToArrow) { if (belongsToArrow) {
const belongsToTransition = uid2Transition.get(belongsToArrow.uid); const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
if (belongsToTransition) { if (belongsToTransition) {
// parse as transition label
belongsToTransition.label.push(parsed); belongsToTransition.label.push(parsed);
if (parsed.kind === "transitionLabel") {
// 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,15 +229,14 @@ 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;
} }
} }
else {
// text does not belong to transition... // text does not belong to transition...
// so it belongs to a rountangle (a state) // so it belongs to a rountangle (a state)
const rountangle = findRountangle(text.topLeft, state.rountangles); const rountangle = findRountangle(text.topLeft, state.rountangles);
@ -260,6 +264,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
belongsToState.comments.push([text.uid, parsed.text]); belongsToState.comments.push([text.uid, parsed.text]);
} }
} }
}
for (const transition of uid2Transition.values()) { for (const transition of uid2Transition.values()) {
if (transition.label.length === 0) { if (transition.label.length === 0) {

View file

@ -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;

View file

@ -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};
} }