re-organize project structure a bit + add icons

This commit is contained in:
Joeri Exelmans 2025-10-13 17:14:21 +02:00
parent 3cb3ef91d2
commit 5e7b944978
24 changed files with 514 additions and 249 deletions

View file

@ -0,0 +1,287 @@
import { evalExpr } from "./actionlang_interpreter";
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
import { Action } from "./label_ast";
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, TimerElapseEvent, Timers } from "./runtime_types";
export function initialize(ast: Statechart): BigStepOutput {
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
environment: new Map(),
...initialRaised,
});
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
}
type ActionScope = {
environment: Environment,
} & RaisedEvents;
type EnteredScope = { enteredStates: Mode } & ActionScope;
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
for (const action of state.entryActions) {
(actionScope = execAction(action, actionScope));
}
// schedule timers
// we store timers in the environment (dirty!)
const environment = new Map(actionScope.environment);
const timers: Timers = [...(environment.get("_timers") || [])];
for (const timeOffset of state.timers) {
const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled
timers.push([futureSimTime, {state: state.uid, timeDurMs: timeOffset}]);
}
timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first
environment.set("_timers", timers);
return {...actionScope, environment};
}
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
for (const action of state.exitActions) {
(actionScope = execAction(action, actionScope));
}
// cancel timers
const environment = new Map(actionScope.environment);
const timers: Timers = environment.get("_timers") || [];
const filtered = timers.filter(([_, {state: s}]) => s !== state.uid);
environment.set("_timers", filtered);
return {...actionScope, environment};
}
export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope {
let actionScope = rt;
// execute entry actions
actionScope = entryActions(simtime, state, 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, ...actionScope} = enterDefault(simtime, child, actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
}
else if (state.kind === "or") {
// same as AND-state, but we only enter the initial state(s)
if (state.initial.length > 0) {
if (state.initial.length > 1) {
console.warn(state.uid + ': multiple initial states, only entering one of them');
}
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, state.initial[0][1], actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
console.warn(state.uid + ': no initial state');
}
return {enteredStates, ...actionScope};
}
export function enterPath(simtime: number, path: ConcreteState[], rt: ActionScope): EnteredScope {
let actionScope = rt;
const [state, ...rest] = path;
// execute entry actions
actionScope = entryActions(simtime, state, actionScope);
// enter children...
let enteredStates = new Set([state.uid]);
if (state.kind === "and") {
// enter every child
for (const child of state.children) {
let enteredChildren;
if (rest.length > 0 && child.uid === rest[0].uid) {
({enteredStates: enteredChildren, ...actionScope} = enterPath(simtime, rest, actionScope));
}
else {
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
}
enteredStates = enteredStates.union(enteredChildren);
}
}
else if (state.kind === "or") {
if (rest.length > 0) {
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterPath(simtime, rest, actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
else {
// same as AND-state, but we only enter the initial state(s)
for (const [_, child] of state.initial) {
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
enteredStates = enteredStates.union(enteredChildren);
}
}
}
return { enteredStates, ...actionScope };
}
// exit the given state and all its active descendants
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
let {enteredStates, ...actionScope} = rt;
// exit all active children...
for (const child of state.children) {
if (enteredStates.has(child.uid)) {
actionScope = exitCurrent(simtime, child, {enteredStates, ...actionScope});
}
}
// execute exit actions
actionScope = exitActions(simtime, state, actionScope);
return actionScope;
}
export function exitPath(simtime: number, path: ConcreteState[], rt: EnteredScope): ActionScope {
let {enteredStates, ...actionScope} = rt;
const toExit = enteredStates.difference(new Set(path));
const [state, ...rest] = path;
// exit state and all its children, *except* states along the rest of the path
actionScope = exitCurrent(simtime, state, {enteredStates: toExit, ...actionScope});
if (rest.length > 0) {
actionScope = exitPath(simtime, rest, {enteredStates, ...actionScope});
}
// execute exit actions
actionScope = exitActions(simtime, state, actionScope);
return actionScope;
}
export function execAction(action: Action, rt: ActionScope): ActionScope {
if (action.kind === "assignment") {
const rhs = evalExpr(action.rhs, rt.environment);
const newEnvironment = new Map(rt.environment);
newEnvironment.set(action.lhs, rhs);
return {
...rt,
environment: newEnvironment,
};
}
else if (action.kind === "raise") {
if (action.event.startsWith('_')) {
// append to internal events
return {
...rt,
internalEvents: [...rt.internalEvents, action.event],
};
}
else {
// append to output events
return {
...rt,
outputEvents: [...rt.outputEvents, action.event],
}
}
}
throw new Error("should never reach here");
}
export function handleEvent(simtime: number, event: string | TimerElapseEvent, statechart: Statechart, activeParent: ConcreteState, {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) || [];
let triggered;
if (typeof event === 'string') {
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "event") {
return trigger.event === event;
}
return false;
});
}
else {
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "after") {
return trigger.durationMs === event.timeDurMs;
}
return false;
});
}
const enabled = triggered.filter(transition =>
evalExpr(transition.label[0].guard, environment)
);
if (enabled.length > 0) {
if (enabled.length > 1) {
console.warn('nondeterminism!!!!');
}
const t = enabled[0];
console.log('enabled:', transitionDescription(t));
const {arena, srcPath, tgtPath} = computeArena(t);
let overlapping = false;
for (const alreadyFired of arenasFired) {
if (isOverlapping(arena, alreadyFired)) {
overlapping = true;
}
}
if (!overlapping) {
console.log('^ firing');
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
arenasFired.add(arena);
}
else {
console.log('skip (overlapping arenas)');
}
}
else {
// no enabled outgoing transitions, try the children:
({environment, mode, ...raised} = handleEvent(simtime, event, statechart, state, {environment, mode, ...raised}));
}
}
}
return {environment, mode, ...raised};
}
export function handleInputEvent(simtime: number, event: string, statechart: Statechart, {mode, environment}: {mode: Mode, environment: Environment}): BigStepOutput {
let raised = initialRaised;
({mode, environment, ...raised} = handleEvent(simtime, event, statechart, statechart.root, {mode, environment, ...raised}));
return handleInternalEvents(simtime, statechart, {mode, environment, ...raised});
}
export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput {
while (raised.internalEvents.length > 0) {
const [internalEvent, ...rest] = raised.internalEvents;
({mode, environment, ...raised} = handleEvent(simtime, internalEvent, statechart, statechart.root, {mode, environment, internalEvents: rest, outputEvents: raised.outputEvents}));
}
return {mode, environment, outputEvents: raised.outputEvents};
}
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 {
// console.log('fire ', transitionDescription(t), {arena, srcPath, tgtPath});
// exit src
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...raised}));
const toExit = getDescendants(arena);
toExit.delete(arena.uid); // do not exit the arena itself
const exitedMode = mode.difference(toExit);
// exec transition actions
for (const action of t.label[0].actions) {
({environment, ...raised} = execAction(action, {environment, ...raised}));
}
// enter tgt
let enteredStates;
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
const enteredMode = exitedMode.union(enteredStates);
return {mode: enteredMode, environment, ...raised};
}