fix nested environment scopes + highlight runtime errors in editor
This commit is contained in:
parent
56467e5ea5
commit
a81fe1e884
9 changed files with 77 additions and 43 deletions
|
|
@ -30,9 +30,7 @@ details:has(+ details) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.runtimeState.active {
|
.runtimeState.active {
|
||||||
/* background-color: rgba(255, 140, 0, 0.2); */
|
|
||||||
background-color: rgba(0,0,255,0.2);
|
background-color: rgba(0,0,255,0.2);
|
||||||
/* border: solid black 3px; */
|
|
||||||
border: solid blue 1px;
|
border: solid blue 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,6 +38,11 @@ details:has(+ details) {
|
||||||
background-color: lightpink;
|
background-color: lightpink;
|
||||||
color: darkred;
|
color: darkred;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.runtimeState.runtimeError.active {
|
||||||
|
border-color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
/* details:not(:has(details)) > summary::marker {
|
/* details:not(:has(details)) > summary::marker {
|
||||||
color: white;
|
color: white;
|
||||||
} */
|
} */
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,13 @@ export function App() {
|
||||||
const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]);
|
const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]);
|
||||||
const ast = parsed && parsed[0];
|
const ast = parsed && parsed[0];
|
||||||
const syntaxErrors = parsed && parsed[1];
|
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
|
// append editor state to undo history
|
||||||
const makeCheckPoint = useCallback(() => {
|
const makeCheckPoint = useCallback(() => {
|
||||||
|
|
@ -395,7 +402,8 @@ export function App() {
|
||||||
</div>
|
</div>
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||||
{editorState && conns && syntaxErrors && <VisualEditor {...{state: editorState, setState: setEditorState, conns, trace, setTrace, syntaxErrors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>}
|
{editorState && conns && syntaxErrors &&
|
||||||
|
<VisualEditor {...{state: editorState, setState: setEditorState, conns, trace, setTrace, syntaxErrors: allErrors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,10 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return <div className="runtimeState runtimeError">
|
// error item
|
||||||
|
return <div
|
||||||
|
className={"runtimeState runtimeError" + (active ? " active" : "")}
|
||||||
|
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
|
||||||
<div>
|
<div>
|
||||||
{formatTime(item.simtime)}
|
{formatTime(item.simtime)}
|
||||||
 
|
 
|
||||||
|
|
@ -68,7 +71,8 @@ function ShowEnvironment(props: {environment: Environment}) {
|
||||||
return <div>{
|
return <div>{
|
||||||
[...props.environment.entries()]
|
[...props.environment.entries()]
|
||||||
.filter(([variable]) => !variable.startsWith('_'))
|
.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(', ')
|
||||||
}</div>;
|
}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,5 +259,11 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="toolbarGroup">
|
||||||
|
{location.host === "localhost:3000" ?
|
||||||
|
<a href={`https://deemz.org/public/statebuddy/${location.hash}`}>production</a>
|
||||||
|
: <a href={`http://localhost:3000/${location.hash}`}>development</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ export function useAudioContext(speed: number) {
|
||||||
}), [ctx]);
|
}), [ctx]);
|
||||||
|
|
||||||
function play(url: string, loop: boolean) {
|
function play(url: string, loop: boolean) {
|
||||||
console.log('play', url);
|
|
||||||
const srcPromise = url2AudioBuf(url)
|
const srcPromise = url2AudioBuf(url)
|
||||||
.then(audioBuf => {
|
.then(audioBuf => {
|
||||||
const src = ctx.createBufferSource();
|
const src = ctx.createBufferSource();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// Just a simple recursive interpreter for the action language
|
// Just a simple recursive interpreter for the action language
|
||||||
|
|
||||||
|
import { RuntimeError } from "./interpreter";
|
||||||
import { Expression } from "./label_ast";
|
import { Expression } from "./label_ast";
|
||||||
import { Environment } from "./runtime_types";
|
import { Environment } from "./runtime_types";
|
||||||
|
|
||||||
|
|
@ -22,7 +23,8 @@ const BINARY_OPERATOR_MAP: Map<string, (a: any, b: any) => any> = new Map([
|
||||||
["%", (a, b) => a % b],
|
["%", (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") {
|
if (expr.kind === "literal") {
|
||||||
return expr.value;
|
return expr.value;
|
||||||
}
|
}
|
||||||
|
|
@ -30,22 +32,22 @@ export function evalExpr(expr: Expression, environment: Environment): any {
|
||||||
const found = environment.get(expr.variable);
|
const found = environment.get(expr.variable);
|
||||||
if (found === undefined) {
|
if (found === undefined) {
|
||||||
console.log({environment});
|
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;
|
return found;
|
||||||
}
|
}
|
||||||
else if (expr.kind === "unaryExpr") {
|
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);
|
return UNARY_OPERATOR_MAP.get(expr.operator)!(arg);
|
||||||
}
|
}
|
||||||
else if (expr.kind === "binaryExpr") {
|
else if (expr.kind === "binaryExpr") {
|
||||||
const lhs = evalExpr(expr.lhs, environment);
|
const lhs = evalExpr(expr.lhs, environment, uids);
|
||||||
const rhs = evalExpr(expr.rhs, environment);
|
const rhs = evalExpr(expr.rhs, environment, uids);
|
||||||
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
|
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
|
||||||
}
|
}
|
||||||
else if (expr.kind === "call") {
|
else if (expr.kind === "call") {
|
||||||
const fn = evalExpr(expr.fn, environment);
|
const fn = evalExpr(expr.fn, environment, uids);
|
||||||
const param = evalExpr(expr.param, environment);
|
const param = evalExpr(expr.param, environment, uids);
|
||||||
return fn(param);
|
return fn(param);
|
||||||
}
|
}
|
||||||
throw new Error("should never reach here");
|
throw new Error("should never reach here");
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,6 @@ export class ScopedEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
*entries(): Iterator<[string, any]> {
|
*entries(): Iterator<[string, any]> {
|
||||||
return iterST(this.scopeTree);
|
yield* iterST(this.scopeTree);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@ export class RuntimeError extends Error {
|
||||||
|
|
||||||
export class NonDeterminismError extends RuntimeError {}
|
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") {
|
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);
|
const environment = rt.environment.set(action.lhs, rhs);
|
||||||
return {
|
return {
|
||||||
...rt,
|
...rt,
|
||||||
|
|
@ -76,11 +76,10 @@ export function entryActions(simtime: number, state: TransitionSrcTgt, actionSco
|
||||||
|
|
||||||
let {environment, ...rest} = actionScope;
|
let {environment, ...rest} = actionScope;
|
||||||
|
|
||||||
environment = environment.enterScope(state.uid);
|
|
||||||
|
|
||||||
for (const action of state.entryActions) {
|
for (const action of state.entryActions) {
|
||||||
({environment, ...rest} = execAction(action, {environment, ...rest}));
|
({environment, ...rest} = execAction(action, {environment, ...rest}, [state.uid]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// schedule timers
|
// schedule timers
|
||||||
if (state.kind !== "pseudo") {
|
if (state.kind !== "pseudo") {
|
||||||
// we store timers in the environment (dirty!)
|
// 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 {
|
export function exitActions(simtime: number, state: TransitionSrcTgt, {enteredStates, ...actionScope}: EnteredScope): ActionScope {
|
||||||
// console.log('exit', stateDescription(state), '...');
|
// console.log('exit', stateDescription(state), '...');
|
||||||
|
|
||||||
for (const action of state.exitActions) {
|
|
||||||
(actionScope = execAction(action, actionScope));
|
|
||||||
}
|
|
||||||
let environment = actionScope.environment;
|
let environment = actionScope.environment;
|
||||||
|
|
||||||
|
for (const action of state.exitActions) {
|
||||||
|
(actionScope = execAction(action, actionScope, [state.uid]));
|
||||||
|
}
|
||||||
|
|
||||||
// cancel timers
|
// cancel timers
|
||||||
if (state.kind !== "pseudo") {
|
if (state.kind !== "pseudo") {
|
||||||
const timers: Timers = environment.get("_timers") || [];
|
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.set("_timers", newTimers);
|
||||||
}
|
}
|
||||||
|
|
||||||
environment = environment.exitScope();
|
|
||||||
|
|
||||||
return {...actionScope, environment};
|
return {...actionScope, environment};
|
||||||
}
|
}
|
||||||
|
|
||||||
// recursively enter the given state's default state
|
// recursively enter the given state's default state
|
||||||
export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope {
|
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
|
// execute entry actions
|
||||||
({firedTransitions, ...actionScope} = entryActions(simtime, state, {firedTransitions, ...actionScope}));
|
({firedTransitions, environment, ...actionScope} = entryActions(simtime, state, {firedTransitions, environment, ...actionScope}));
|
||||||
|
|
||||||
// enter children...
|
// enter children...
|
||||||
let enteredStates = new Set([state.uid]);
|
|
||||||
if (state.kind === "and") {
|
if (state.kind === "and") {
|
||||||
// enter every child
|
// enter every child
|
||||||
for (const child of state.children) {
|
for (const child of state.children) {
|
||||||
let enteredChildren;
|
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);
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,22 +145,26 @@ export function enterDefault(simtime: number, state: ConcreteState, rt: ActionSc
|
||||||
const [arrowUid, toEnter] = state.initial[0];
|
const [arrowUid, toEnter] = state.initial[0];
|
||||||
firedTransitions = [...firedTransitions, arrowUid];
|
firedTransitions = [...firedTransitions, arrowUid];
|
||||||
let enteredChildren;
|
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);
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
else {
|
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.
|
// 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<string>, actionScope: ActionScope): EnteredScope {
|
export function enterStates(simtime: number, state: ConcreteState, toEnter: Set<string>, {environment, ...actionScope}: ActionScope): EnteredScope {
|
||||||
|
|
||||||
|
environment = environment.enterScope(state.uid);
|
||||||
|
|
||||||
// execute entry actions
|
// execute entry actions
|
||||||
actionScope = entryActions(simtime, state, actionScope);
|
actionScope = entryActions(simtime, state, {environment, ...actionScope});
|
||||||
|
|
||||||
// enter children...
|
// enter children...
|
||||||
let enteredStates = new Set([state.uid]);
|
let enteredStates = new Set([state.uid]);
|
||||||
|
|
@ -167,7 +173,7 @@ export function enterStates(simtime: number, state: ConcreteState, toEnter: Set<
|
||||||
// every child must be entered
|
// every child must be entered
|
||||||
for (const child of state.children) {
|
for (const child of state.children) {
|
||||||
let enteredChildren;
|
let enteredChildren;
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterStates(simtime, child, toEnter, actionScope));
|
({enteredStates: enteredChildren, environment, ...actionScope} = enterStates(simtime, child, toEnter, {environment, ...actionScope}));
|
||||||
enteredStates = enteredStates.union(enteredChildren);
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,36 +183,40 @@ export function enterStates(simtime: number, state: ConcreteState, toEnter: Set<
|
||||||
if (childToEnter.length === 1) {
|
if (childToEnter.length === 1) {
|
||||||
// good
|
// good
|
||||||
let enteredChildren;
|
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);
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
}
|
}
|
||||||
else if (childToEnter.length === 0) {
|
else if (childToEnter.length === 0) {
|
||||||
// also good, enter default child
|
// also good, enter default child
|
||||||
return enterDefault(simtime, state, {...actionScope});
|
return enterDefault(simtime, state, {environment, ...actionScope});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error("can only enter one child of an OR-state, stupid!");
|
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
|
// exit the given state and all its active descendants
|
||||||
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
||||||
// console.log('exitCurrent', stateDescription(state));
|
// 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)) {
|
if (enteredStates.has(state.uid)) {
|
||||||
// exit all active children...
|
// exit all active children...
|
||||||
if (state.children) {
|
if (state.children) {
|
||||||
for (const child of 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
|
// execute exit actions
|
||||||
({history, ...actionScope} = exitActions(simtime, state, {enteredStates, history, ...actionScope}));
|
({history, environment, ...actionScope} = exitActions(simtime, state, {enteredStates, history, environment, ...actionScope}));
|
||||||
|
|
||||||
// record history
|
// record history
|
||||||
if (state.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[]) {
|
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 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 > 0) {
|
||||||
if (enabled.length > 1) {
|
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]);
|
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<string, Transition[
|
||||||
|
|
||||||
// transition actions
|
// transition actions
|
||||||
for (const action of label.actions) {
|
for (const action of label.actions) {
|
||||||
({environment, history, ...rest} = execAction(action, {environment, history, ...rest}));
|
({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
||||||
|
|
|
||||||
0
src/statecharts/timed_reactive.ts
Normal file
0
src/statecharts/timed_reactive.ts
Normal file
Loading…
Add table
Add a link
Reference in a new issue