diff --git a/src/App/App.css b/src/App/App.css index 133cb7a..0a4805b 100644 --- a/src/App/App.css +++ b/src/App/App.css @@ -30,9 +30,7 @@ details:has(+ details) { cursor: pointer; } .runtimeState.active { - /* background-color: rgba(255, 140, 0, 0.2); */ background-color: rgba(0,0,255,0.2); - /* border: solid black 3px; */ border: solid blue 1px; } @@ -40,6 +38,11 @@ details:has(+ details) { background-color: lightpink; color: darkred; } + +.runtimeState.runtimeError.active { + border-color: darkred; +} + /* details:not(:has(details)) > summary::marker { color: white; } */ diff --git a/src/App/App.tsx b/src/App/App.tsx index 75fbca9..00874c5 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -151,6 +151,13 @@ export function App() { const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]); const ast = parsed && parsed[0]; const syntaxErrors = parsed && parsed[1]; + const allErrors = syntaxErrors && [ + ...syntaxErrors, + ...(trace && trace.trace[trace.idx].kind === "error") ? [{ + message: trace.trace[trace.idx].error.message, + shapeUid: trace.trace[trace.idx].error.highlight[0], + }] : [], + ] // append editor state to undo history const makeCheckPoint = useCallback(() => { @@ -395,7 +402,8 @@ export function App() { {/* Editor */}
- {editorState && conns && syntaxErrors && } + {editorState && conns && syntaxErrors && + }
diff --git a/src/App/RTHistory.tsx b/src/App/RTHistory.tsx index f89b592..7b80560 100644 --- a/src/App/RTHistory.tsx +++ b/src/App/RTHistory.tsx @@ -50,7 +50,10 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt ; } else { - return
+ // error item + return
onMouseDown(idx, item.simtime), [idx, item.simtime])}>
{formatTime(item.simtime)}   @@ -68,7 +71,8 @@ function ShowEnvironment(props: {environment: Environment}) { return
{ [...props.environment.entries()] .filter(([variable]) => !variable.startsWith('_')) - .map(([variable,value]) => `${variable}: ${value}`).join(', ') + // we strip the first 5 characters from 'variable' (remove "root.") + .map(([variable,value]) => `${variable.slice(5)}: ${value}`).join(', ') }
; } diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index e6cd92a..bd693e3 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -259,5 +259,11 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
+
+ {location.host === "localhost:3000" ? + production + : development + } +
; }); diff --git a/src/App/useAudioContext.ts b/src/App/useAudioContext.ts index 709d3f2..4878ed8 100644 --- a/src/App/useAudioContext.ts +++ b/src/App/useAudioContext.ts @@ -27,7 +27,6 @@ export function useAudioContext(speed: number) { }), [ctx]); function play(url: string, loop: boolean) { - console.log('play', url); const srcPromise = url2AudioBuf(url) .then(audioBuf => { const src = ctx.createBufferSource(); diff --git a/src/statecharts/actionlang_interpreter.ts b/src/statecharts/actionlang_interpreter.ts index 5ac4b6e..1142a5b 100644 --- a/src/statecharts/actionlang_interpreter.ts +++ b/src/statecharts/actionlang_interpreter.ts @@ -1,5 +1,6 @@ // Just a simple recursive interpreter for the action language +import { RuntimeError } from "./interpreter"; import { Expression } from "./label_ast"; import { Environment } from "./runtime_types"; @@ -22,7 +23,8 @@ const BINARY_OPERATOR_MAP: Map any> = new Map([ ["%", (a, b) => a % b], ]); -export function evalExpr(expr: Expression, environment: Environment): any { +// parameter uids: list of UIDs to append to any raised errors +export function evalExpr(expr: Expression, environment: Environment, uids: string[] = []): any { if (expr.kind === "literal") { return expr.value; } @@ -30,22 +32,22 @@ export function evalExpr(expr: Expression, environment: Environment): any { const found = environment.get(expr.variable); if (found === undefined) { console.log({environment}); - throw new Error(`variable '${expr.variable}' does not exist in environment`); + throw new RuntimeError(`variable '${expr.variable}' does not exist in environment`, uids); } return found; } else if (expr.kind === "unaryExpr") { - const arg = evalExpr(expr.expr, environment); + const arg = evalExpr(expr.expr, environment, uids); return UNARY_OPERATOR_MAP.get(expr.operator)!(arg); } else if (expr.kind === "binaryExpr") { - const lhs = evalExpr(expr.lhs, environment); - const rhs = evalExpr(expr.rhs, environment); + const lhs = evalExpr(expr.lhs, environment, uids); + const rhs = evalExpr(expr.rhs, environment, uids); return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs); } else if (expr.kind === "call") { - const fn = evalExpr(expr.fn, environment); - const param = evalExpr(expr.param, environment); + const fn = evalExpr(expr.fn, environment, uids); + const param = evalExpr(expr.param, environment, uids); return fn(param); } throw new Error("should never reach here"); diff --git a/src/statecharts/environment.ts b/src/statecharts/environment.ts index bc1e35f..cead7e5 100644 --- a/src/statecharts/environment.ts +++ b/src/statecharts/environment.ts @@ -97,6 +97,6 @@ export class ScopedEnvironment { } *entries(): Iterator<[string, any]> { - return iterST(this.scopeTree); + yield* iterST(this.scopeTree); } } diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index 11df97f..16839f3 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -39,9 +39,9 @@ export class RuntimeError extends Error { export class NonDeterminismError extends RuntimeError {} -export function execAction(action: Action, rt: ActionScope): ActionScope { +export function execAction(action: Action, rt: ActionScope, uids: string[]): ActionScope { if (action.kind === "assignment") { - const rhs = evalExpr(action.rhs, rt.environment); + const rhs = evalExpr(action.rhs, rt.environment, uids); const environment = rt.environment.set(action.lhs, rhs); return { ...rt, @@ -76,11 +76,10 @@ export function entryActions(simtime: number, state: TransitionSrcTgt, actionSco let {environment, ...rest} = actionScope; - environment = environment.enterScope(state.uid); - for (const action of state.entryActions) { - ({environment, ...rest} = execAction(action, {environment, ...rest})); + ({environment, ...rest} = execAction(action, {environment, ...rest}, [state.uid])); } + // schedule timers if (state.kind !== "pseudo") { // we store timers in the environment (dirty!) @@ -101,10 +100,12 @@ export function entryActions(simtime: number, state: TransitionSrcTgt, actionSco export function exitActions(simtime: number, state: TransitionSrcTgt, {enteredStates, ...actionScope}: EnteredScope): ActionScope { // console.log('exit', stateDescription(state), '...'); - for (const action of state.exitActions) { - (actionScope = execAction(action, actionScope)); - } let environment = actionScope.environment; + + for (const action of state.exitActions) { + (actionScope = execAction(action, actionScope, [state.uid])); + } + // cancel timers if (state.kind !== "pseudo") { const timers: Timers = environment.get("_timers") || []; @@ -112,25 +113,26 @@ export function exitActions(simtime: number, state: TransitionSrcTgt, {enteredSt environment = environment.set("_timers", newTimers); } - environment = environment.exitScope(); - return {...actionScope, environment}; } // recursively enter the given state's default state export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope { - let {firedTransitions, ...actionScope} = rt; + let {firedTransitions, environment, ...actionScope} = rt; + + environment = environment.enterScope(state.uid); + + let enteredStates = new Set([state.uid]); // execute entry actions - ({firedTransitions, ...actionScope} = entryActions(simtime, state, {firedTransitions, ...actionScope})); + ({firedTransitions, environment, ...actionScope} = entryActions(simtime, state, {firedTransitions, environment, ...actionScope})); // enter children... - let enteredStates = new Set([state.uid]); if (state.kind === "and") { // enter every child for (const child of state.children) { let enteredChildren; - ({enteredStates: enteredChildren, firedTransitions, ...actionScope} = enterDefault(simtime, child, {firedTransitions, ...actionScope})); + ({enteredStates: enteredChildren, firedTransitions, environment, ...actionScope} = enterDefault(simtime, child, {firedTransitions, environment, ...actionScope})); enteredStates = enteredStates.union(enteredChildren); } } @@ -143,22 +145,26 @@ export function enterDefault(simtime: number, state: ConcreteState, rt: ActionSc const [arrowUid, toEnter] = state.initial[0]; firedTransitions = [...firedTransitions, arrowUid]; let enteredChildren; - ({enteredStates: enteredChildren, firedTransitions, ...actionScope} = enterDefault(simtime, toEnter, {firedTransitions, ...actionScope})); + ({enteredStates: enteredChildren, firedTransitions, environment, ...actionScope} = enterDefault(simtime, toEnter, {firedTransitions, environment, ...actionScope})); enteredStates = enteredStates.union(enteredChildren); } else { - console.warn(state.uid + ': no initial state'); + throw new RuntimeError(state.uid + ': no initial state', [state.uid]); } } - return {enteredStates, firedTransitions, ...actionScope}; + environment = environment.exitScope(); + + return {enteredStates, firedTransitions, environment, ...actionScope}; } // recursively enter the given state and, if children need to be entered, preferrably those occurring in 'toEnter' will be entered. If no child occurs in 'toEnter', the default child will be entered. -export function enterStates(simtime: number, state: ConcreteState, toEnter: Set, actionScope: ActionScope): EnteredScope { +export function enterStates(simtime: number, state: ConcreteState, toEnter: Set, {environment, ...actionScope}: ActionScope): EnteredScope { + + environment = environment.enterScope(state.uid); // execute entry actions - actionScope = entryActions(simtime, state, actionScope); + actionScope = entryActions(simtime, state, {environment, ...actionScope}); // enter children... let enteredStates = new Set([state.uid]); @@ -167,7 +173,7 @@ export function enterStates(simtime: number, state: ConcreteState, toEnter: Set< // every child must be entered for (const child of state.children) { let enteredChildren; - ({enteredStates: enteredChildren, ...actionScope} = enterStates(simtime, child, toEnter, actionScope)); + ({enteredStates: enteredChildren, environment, ...actionScope} = enterStates(simtime, child, toEnter, {environment, ...actionScope})); enteredStates = enteredStates.union(enteredChildren); } } @@ -177,36 +183,40 @@ export function enterStates(simtime: number, state: ConcreteState, toEnter: Set< if (childToEnter.length === 1) { // good let enteredChildren; - ({enteredStates: enteredChildren, ...actionScope} = enterStates(simtime, childToEnter[0], toEnter, actionScope)); + ({enteredStates: enteredChildren, environment, ...actionScope} = enterStates(simtime, childToEnter[0], toEnter, {environment, ...actionScope})); enteredStates = enteredStates.union(enteredChildren); } else if (childToEnter.length === 0) { // also good, enter default child - return enterDefault(simtime, state, {...actionScope}); + return enterDefault(simtime, state, {environment, ...actionScope}); } else { throw new Error("can only enter one child of an OR-state, stupid!"); } } - return { enteredStates, ...actionScope }; + environment = environment.exitScope(); + + return { enteredStates, environment, ...actionScope }; } // exit the given state and all its active descendants export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope { // console.log('exitCurrent', stateDescription(state)); - let {enteredStates, history, ...actionScope} = rt; + let {enteredStates, history, environment, ...actionScope} = rt; + + environment = environment.enterScope(state.uid); if (enteredStates.has(state.uid)) { // exit all active children... if (state.children) { for (const child of state.children) { - ({history, ...actionScope} = exitCurrent(simtime, child, {enteredStates, history, ...actionScope})); + ({history, environment, ...actionScope} = exitCurrent(simtime, child, {enteredStates, history, environment, ...actionScope})); } } // execute exit actions - ({history, ...actionScope} = exitActions(simtime, state, {enteredStates, history, ...actionScope})); + ({history, environment, ...actionScope} = exitActions(simtime, state, {enteredStates, history, environment, ...actionScope})); // record history if (state.history) { @@ -229,7 +239,9 @@ export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredSc } } - return {history, ...actionScope}; + environment = environment.exitScope(); + + return {history, environment, ...actionScope}; } function allowedToFire(arena: OrState, alreadyFiredArenas: OrState[]) { @@ -272,7 +284,7 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_ } }; const guardEnvironment = environment.set("inState", inState); - const enabled = triggered.filter(([t,l]) => evalExpr(l.guard, guardEnvironment)); + const enabled = triggered.filter(([t,l]) => evalExpr(l.guard, guardEnvironment, [t.uid])); if (enabled.length > 0) { if (enabled.length > 1) { throw new NonDeterminismError(`Non-determinism: state '${stateDescription(sourceState)}' has multiple (${enabled.length}) enabled outgoing transitions: ${enabled.map(([t]) => transitionDescription(t)).join(', ')}`, [...enabled.map(([t]) => t.uid), sourceState.uid]); @@ -376,7 +388,7 @@ export function fire(simtime: number, t: Transition, ts: Map