fix nested environment scopes + highlight runtime errors in editor

This commit is contained in:
Joeri Exelmans 2025-10-27 10:37:42 +01:00
parent 56467e5ea5
commit a81fe1e884
9 changed files with 77 additions and 43 deletions

View file

@ -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<string, (a: any, b: any) => 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");

View file

@ -97,6 +97,6 @@ export class ScopedEnvironment {
}
*entries(): Iterator<[string, any]> {
return iterST(this.scopeTree);
yield* iterST(this.scopeTree);
}
}

View file

@ -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<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
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<string, Transition[
// transition 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});

View file