pseudo-states appear to be working + variables only exist within the scope where they are created

This commit is contained in:
Joeri Exelmans 2025-10-16 17:10:37 +02:00
parent db1479bfc4
commit d4930eb13d
22 changed files with 742 additions and 569 deletions

View file

@ -1,11 +1,11 @@
import { computeArena2, computePath, ConcreteState, getDescendants, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition } from "./abstract_syntax";
import { evalExpr } from "./actionlang_interpreter";
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
import { Action, AfterTrigger, EventTrigger } from "./label_ast";
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event, TimerElapseEvent } from "./runtime_types";
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
import { BigStepOutput, Environment, initialRaised, Mode, RaisedEvents, RT_Event, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
export function initialize(ast: Statechart): BigStepOutput {
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
environment: new Environment(),
environment: new Environment([new Map([["_timers", []]])]),
...initialRaised,
});
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
@ -18,12 +18,15 @@ type ActionScope = {
type EnteredScope = { enteredStates: Mode } & ActionScope;
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
// console.log('enter', stateDescription(state), '...');
let {environment, ...rest} = actionScope;
environment = environment.pushScope();
for (const action of state.entryActions) {
(actionScope = execAction(action, actionScope));
({environment, ...rest} = execAction(action, {environment, ...rest}));
}
// schedule timers
// we store timers in the environment (dirty!)
let environment = actionScope.environment.transform<Timers>("_timers", oldTimers => {
environment = environment.transform<Timers>("_timers", oldTimers => {
const newTimers = [
...oldTimers,
...state.timers.map(timeOffset => {
@ -35,20 +38,21 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
return newTimers;
}, []);
// new nested scope
environment = environment.pushScope();
return {...actionScope, environment};
return {environment, ...rest};
}
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
// console.log('exit', stateDescription(state), '...');
for (const action of state.exitActions) {
(actionScope = execAction(action, actionScope));
}
let environment = actionScope.environment.popScope();
let environment = actionScope.environment;
// cancel timers
environment = environment.transform<Timers>("_timers", oldTimers => {
// remove all timers of 'state':
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
}, []);
environment = environment.popScope();
return {...actionScope, environment};
}
@ -193,42 +197,42 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
throw new Error("should never reach here");
}
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
const arenasFired = new Set<OrState>();
for (const state of activeParent.children) {
if (mode.has(state.uid)) {
const outgoing = statechart.transitions.get(state.uid) || [];
const labels = outgoing.flatMap(t =>
t.label
.filter(l => l.kind === "transitionLabel")
.map(l => [t,l] as [Transition, TransitionLabel]));
let triggered;
if (event.kind === "input") {
// get transitions triggered by event
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "event") {
return trigger.event === event.name;
}
return false;
});
triggered = labels.filter(([_t,l]) =>
l.trigger.kind === "event" && l.trigger.event === event.name);
}
else {
// get transitions triggered by timeout
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "after") {
return trigger.durationMs === event.timeDurMs;
}
return false;
});
triggered = labels.filter(([_t,l]) =>
l.trigger.kind === "after" && l.trigger.durationMs === event.timeDurMs);
}
// eval guard
const enabled = triggered.filter(transition =>
evalExpr(transition.label[0].guard, environment)
);
const guardEnvironment = environment.set("inState", (stateLabel: string) => {
for (const [uid, state] of statechart.uid2State.entries()) {
if (stateDescription(state) === stateLabel) {
return (mode.has(uid));
}
}
});
const enabled = triggered.filter(([t,l]) =>
evalExpr(l.guard, guardEnvironment));
if (enabled.length > 0) {
if (enabled.length > 1) {
console.warn('nondeterminism!!!!');
}
const t = enabled[0];
const {arena, srcPath, tgtPath} = computeArena(t);
const [t,l] = enabled[0]; // just pick one transition
const arena = computeArena2(t, statechart.transitions);
let overlapping = false;
for (const alreadyFired of arenasFired) {
if (isOverlapping(arena, alreadyFired)) {
@ -236,20 +240,18 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
}
}
if (!overlapping) {
let oldValue;
if (event.kind === "input" && event.param !== undefined) {
// input events may have a parameter
// add event parameter to environment in new scope
environment = environment.pushScope();
environment = environment.newVar(
(t.label[0].trigger as EventTrigger).paramName as string,
(l.trigger as EventTrigger).paramName as string,
event.param,
);
}
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
if (event.kind === "input" && event.param) {
({mode, environment, ...raised} = fireTransition2(simtime, t, statechart.transitions, l, arena, {mode, environment, ...raised}));
if (event.kind === "input" && event.param !== undefined) {
environment = environment.popScope();
// console.log('restored environment:', environment);
}
arenasFired.add(arena);
}
@ -288,27 +290,54 @@ function transitionDescription(t: Transition) {
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
}
export function fireTransition(simtime: number, t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
export function fireTransition2(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
console.log('fire', transitionDescription(t));
console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse();
// exit src
console.log('exit src...');
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
// exit src and other states up to arena
({environment, ...raised} = exitPath(simtime, srcPath, {environment, enteredStates: mode, ...raised}));
const toExit = getDescendants(arena);
toExit.delete(arena.uid); // do not exit the arena itself
const exitedMode = mode.difference(toExit);
const exitedMode = mode.difference(toExit); // active states after exiting the states we need to exit
// console.log({exitedMode});
return fireSecondHalfOfTransition(simtime, t, ts, label, arena, {mode: exitedMode, environment, ...raised});
}
// assuming we've already exited the source state of the transition, now enter the target state
// IF however, the target is a pseudo-state, DON'T enter it (pseudo-states are NOT states), instead fire the first pseudo-outgoing transition.
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
// exec transition actions
for (const action of t.label[0].actions) {
for (const action of label.actions) {
({environment, ...raised} = execAction(action, {environment, ...raised}));
}
// enter tgt
console.log('enter tgt...');
let enteredStates;
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
const enteredMode = exitedMode.union(enteredStates);
if (t.tgt.kind === "pseudo") {
const outgoing = ts.get(t.tgt.uid) || [];
for (const nextT of outgoing) {
for (const nextLabel of nextT.label) {
if (nextLabel.kind === "transitionLabel") {
if (evalExpr(nextLabel.guard, environment)) {
console.log('fire', transitionDescription(nextT));
// found ourselves an enabled transition
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, ...raised});
}
}
}
}
throw new Error("stuck in pseudo-state!!")
}
else {
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
// enter tgt
let enteredStates;
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath, {environment, ...raised}));
const enteredMode = mode.union(enteredStates);
return {mode: enteredMode, environment, ...raised};
// console.log({enteredMode});
return {mode: enteredMode, environment, ...raised};
}
}