import { computeArena2, computePath, ConcreteState, getDescendants, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition } from "./abstract_syntax"; import { evalExpr } from "./actionlang_interpreter"; 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([new Map([["_timers", []]])]), ...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 { // console.log('enter', stateDescription(state), '...'); let {environment, ...rest} = actionScope; // environment = environment.pushScope(); for (const action of state.entryActions) { ({environment, ...rest} = execAction(action, {environment, ...rest})); } // schedule timers // we store timers in the environment (dirty!) environment = environment.transform("_timers", oldTimers => { const newTimers = [ ...oldTimers, ...state.timers.map(timeOffset => { const futureSimTime = simtime + timeOffset; return [futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}] as [number, TimerElapseEvent]; }), ]; newTimers.sort((a,b) => a[0] - b[0]); return newTimers; }, []); // new nested scope 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; // cancel timers environment = environment.transform("_timers", oldTimers => { // remove all timers of 'state': return oldTimers.filter(([_, {state: s}]) => s !== state.uid); }, []); // environment = environment.popScope(); 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; if (enteredStates.has(state.uid)) { // exit all active children... for (const child of state.children) { 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.map(s=>s.uid))); 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 environment = rt.environment.set(action.lhs, rhs); return { ...rt, environment, }; } else if (action.kind === "raise") { const raisedEvent = { name: action.event, param: action.param && evalExpr(action.param, rt.environment), }; if (action.event.startsWith('_')) { // append to internal events return { ...rt, internalEvents: [...rt.internalEvents, raisedEvent], }; } else { // append to output events return { ...rt, outputEvents: [...rt.outputEvents, raisedEvent], } } } throw new Error("should never reach here"); } 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(); 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 = labels.filter(([_t,l]) => l.trigger.kind === "event" && l.trigger.event === event.name); } else { // get transitions triggered by timeout triggered = labels.filter(([_t,l]) => l.trigger.kind === "after" && l.trigger.durationMs === event.timeDurMs); } // eval guard 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,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)) { overlapping = true; } } if (!overlapping) { 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( (l.trigger as EventTrigger).paramName as string, 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(); } 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: RT_Event, 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, {kind: "input", ...internalEvent}, // internal event becomes input event 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 fireTransition2(simtime: number, t: Transition, ts: Map, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { console.log('fire', transitionDescription(t)); const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse(); // 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); // 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, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { // exec transition actions for (const action of label.actions) { ({environment, ...raised} = execAction(action, {environment, ...raised})); } 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); // console.log({enteredMode}); return {mode: enteredMode, environment, ...raised}; } }