@lf-lang/reactor-ts
Version:
A reactor-oriented programming framework in TypeScript
1,580 lines (1,441 loc) • 86 kB
text/typescript
/**
* Core of the reactor runtime.
*
* @author Marten Lohstroh (marten@berkeley.edu),
* @author Matt Weber (matt.weber@berkeley.edu),
* @author Hokeun Kim (hokeunkim@berkeley.edu)
*/
import {
type Priority,
type Absent,
type ArgList,
type Read,
type Sched,
type Variable,
type Write,
type TriggerManager,
ReactionGraph,
TimeValue,
Tag,
Origin,
getCurrentPhysicalTime,
Alarm,
PrioritySet,
Log,
PrecedenceGraph,
Reaction,
Mutation,
Procedure,
SchedulableAction,
TaggedEvent,
Component,
ScheduledTrigger,
Trigger,
Action,
InPort,
IOPort,
MultiPort,
OutPort,
Port,
WritablePort,
Startup,
Shutdown,
WritableMultiPort,
Dummy,
ConnectablePort
} from "./internal";
import {v4 as uuidv4} from "uuid";
import {Bank} from "./bank";
// Set the default log level.
Log.setLevel(Log.LogLevel.ERROR);
// --------------------------------------------------------------------------//
// Interfaces //
// --------------------------------------------------------------------------//
/**
* Interface for the invocation of remote procedures.
*/
export interface Call<A, R> extends Write<A>, Read<R> {
invoke: (args: A) => R | undefined;
}
export enum CanConnectResult {
SUCCESS = 0,
SELF_LOOP = "Source port and destination port are the same.",
DESTINATION_OCCUPIED = "Destination port is already occupied.",
DOWNSTREAM_WRITE_CONFLICT = "Write conflict: port is already occupied.",
NOT_IN_SCOPE = "Source and destination ports are not in scope.",
RT_CONNECTION_OUTSIDE_CONTAINER = "New connection is outside of container.",
RT_DIRECT_FEED_THROUGH = "New connection introduces direct feed through.",
RT_CYCLE = "New connection introduces cycle.",
MUTATION_CAUSALITY_LOOP = "New connection will change the causal effect of the mutation that triggered this connection."
}
/**
* Abstract class for a schedulable action. It is intended as a wrapper for a
* regular action. In addition to a get method, it also has a schedule method
* that allows for the action to be scheduled.
*/
// --------------------------------------------------------------------------//
// Core Reactor Classes //
// --------------------------------------------------------------------------//
export class Parameter<T> implements Read<T> {
constructor(private readonly value: T) {}
get(): T {
return this.value;
}
}
/**
* A timer is an attribute of a reactor which periodically (or just once)
* creates a timer event. A timer has an offset and a period. Upon initialization
* the timer will schedule an event at the given offset from starting wall clock time.
* Whenever this timer's event comes off the event queue, it will
* reschedule the event at the current logical time + period in the future. A 0
* period indicates the timer's event is a one-off and should not be rescheduled.
*/
export class Timer extends ScheduledTrigger<Tag> implements Read<Tag> {
period: TimeValue;
offset: TimeValue;
/**
* Timer constructor.
* @param __container__ The reactor this timer is attached to.
* @param offset The interval between the start of execution and the first
* timer event. Cannot be negative.
* @param period The interval between rescheduled timer events. If 0, will
* not reschedule. Cannot be negative.
*/
constructor(
__container__: Reactor,
offset: TimeValue | 0,
period: TimeValue | 0
) {
super(__container__);
if (!(offset instanceof TimeValue)) {
this.offset = TimeValue.secs(0);
} else {
this.offset = offset;
}
if (!(period instanceof TimeValue)) {
this.period = TimeValue.secs(0);
} else {
this.period = period;
}
Log.debug(this, () => "Creating timer: " + this._getFullyQualifiedName());
// Initialize this timer.
this.runtime.initialize(this);
}
public toString(): string {
return `Timer from ${this._getContainer()._getFullyQualifiedName()}
with period: ${this.period}
offset: ${this.offset}`;
}
public get(): Tag | Absent {
if (this.isPresent()) {
return this.tag;
} else {
return undefined;
}
}
}
/**
* A reactor is a software component that reacts to input events, timer events,
* and action events. It has private state variables that are not visible to any
* other reactor. Its reactions can consist of altering its own state, sending
* messages to other reactors, or affecting the environment through some kind of
* actuation or side effect.
*/
export abstract class Reactor extends Component {
/**
* Data structure to keep track of registered components.
* Note: declare this class member before any other ones as they may
* attempt to access it.
*/
private readonly _keyChain = new Map<Component, symbol>();
/**
* This graph has in it all the dependencies implied by this container's
* ports, reactions, and connections.
*/
protected _dependencyGraph = new PrecedenceGraph<
Port<unknown> | Reaction<Variable[]>
>();
/**
* The runtime object, which has a collection of privileged functions that are passed down from the
* container.
*/
private _runtime!: Runtime;
/**
* Index that specifies the location of the reactor instance in a bank,
* if it is a member of one.
*/
private readonly _bankIndex: number;
/**
* Return the location of the reactor instance in a bank,
* if it is a member of one; return -1 otherwise.
*/
public getBankIndex(): number {
if (this._bankIndex === undefined) {
return -1;
}
return this._bankIndex;
}
/**
* This graph has some overlap with the reactors dependency graph, but is
* different in two respects:
* - transitive dependencies between ports have been collapsed; and
* - it incorporates the causality interfaces of all contained reactors.
* It thereby carries enough information to find out whether adding a new
* connection at runtime could result in a cyclic dependency, _without_
* having to consult other reactors.
*/
private readonly _causalityGraph = new PrecedenceGraph<Port<unknown>>();
/**
* Indicates whether this reactor is active (meaning it has reacted to a
* startup action), or not (in which case it either never started up or has
* reacted to a shutdown action).
*/
protected _active = false;
/**
* This reactor's shutdown action.
*/
readonly shutdown: Shutdown;
/**
* This reactor's startup action.
*/
readonly startup: Startup;
/**
* This reactor's dummy action.
*/
readonly __dummy: Dummy;
/**
* The list of reactions this reactor has.
*/
private readonly _reactions: Array<Reaction<Variable[]>> = [];
/**
* Sandbox for the execution of reactions.
*/
private _reactionScope: ReactionSandbox;
/**
* The list of mutations this reactor has.
*/
private readonly _mutations: Array<Mutation<Variable[]>> = [];
/**
* Sandbox for the execution of mutations.
*/
private _mutationScope: MutationSandbox;
/**
* Receive the runtime object from the container of this reactor.
* Invoking this method in any user-written code will result in a
* runtime error.
* @param runtime The runtime object handed down from the container.
*/
public _receiveRuntimeObject(runtime: Runtime): void {
if (this._runtime == null && runtime != null) {
this._runtime = runtime;
// In addition to setting the runtime object, also make its
// utility functions available as a protected member.
this.util = runtime.util;
} else {
throw new Error("Can only establish link to runtime once.");
}
}
/**
* Add a component to this container.
*
* A component that is created as part of this reactor invokes this method
* upon creation.
* @param component The component to register.
* @param key The component's key.
*/
public _register(component: Component, key: symbol): void {
if (component === undefined || component === null) {
throw new Error("Unable to register undefined or null component");
}
if (component._isRegistered()) {
throw new Error(
"Unable to register " +
component._getFullyQualifiedName() +
" as it already has a container."
);
}
// Only add key if the component isn't a self-reference
// and isn't already registered.
if (component !== this && !this._keyChain.has(component)) {
this._keyChain.set(component, key);
}
}
public _requestRuntimeObject(component: Component): void {
if (component._isContainedBy(this)) {
component._receiveRuntimeObject(this._runtime);
}
}
/**
* Remove all the connections associated with a given reactor.
* @param reactor
*/
private _deleteConnections(reactor: Reactor): void {
for (const port of reactor._findOwnPorts()) {
this._dependencyGraph.removeNode(port);
}
}
/**
* Remove this reactor from its container and sever any connections it may
* still have. This reactor will become defunct and is ready for garbage
* collection.
*/
protected _unplug(): void {
this._getContainer()._deregister(this, this._key);
}
/**
* Remove the given reactor and its connections from this container if
* the key matches.
* @param reactor
* @param key
*/
public _deregister(reactor: Reactor, key: symbol): void {
let found;
for (const v of this._keyChain.values()) {
if (v === key) {
found = true;
break;
}
}
if (found ?? false) {
this._keyChain.delete(reactor);
this._deleteConnections(reactor);
} else {
console.log(
"Unable to deregister reactor: " + reactor._getFullyQualifiedName()
);
}
}
private _getLast(
reactions: Set<Reaction<Variable[]>>
): Reaction<Variable[]> | undefined {
let index = -1;
const all = this._getReactionsAndMutations();
for (const reaction of reactions) {
const found = all.findIndex((r) => r === reaction);
if (found >= 0) {
index = Math.max(found, index);
}
}
if (index >= 0) {
return all[index];
}
}
private _getFirst(
reactions: Set<Reaction<Variable[]>>
): Reaction<Variable[]> | undefined {
let index = -1;
const all = this._getReactionsAndMutations();
for (const reaction of reactions) {
const found = all.findIndex((r) => r === reaction);
if (found >= 0) {
index = Math.min(found, index);
}
}
if (index >= 0) {
return all[index];
}
}
/**
* If the given component is owned by this reactor, look up its key and
* return it. Otherwise, if a key has been provided, and it matches the
* key of this reactor, also look up the component's key and return it.
* Otherwise, if the component is owned by a reactor that is owned by
* this reactor, request the component's key from that reactor and return
* it. If the component is an action, this request is not honored because
* actions are never supposed to be accessed across levels of hierarchy.
* @param component The component to look up the key for.
* @param key The key that verifies the containment relation between this
* reactor and the component, with at most one level of indirection.
*/
public _getKey(component: Trigger, key?: symbol): symbol | undefined {
if (component._isContainedBy(this) || this._key === key) {
return this._keyChain.get(component);
} else if (
!(component instanceof Action) &&
component._isContainedByContainerOf(this)
) {
const owner = component.getContainer();
if (owner !== null) {
return owner._getKey(component, this._keyChain.get(owner));
}
}
}
/**
* Collection of utility functions for this reactor and its subclasses.
*/
protected util!: UtilityFunctions;
/**
* Mark this reactor for deletion, trigger all of its shutdown reactions
* and mutations, and also delete all of the reactors that this reactor
* contains.
*/
private _delete(): void {
// console.log("Marking for deletion: " + this._getFullyQualifiedName())
this._runtime.delete(this);
this.shutdown.update(
new TaggedEvent(this.shutdown, this.util.getCurrentTag(), null)
);
// this._findOwnReactors().forEach(r => r._delete())
}
/**
* Inner class intended to provide access to methods that should be
* accessible to mutations, not to reactions.
*/
private readonly _mutationSandbox = class implements MutationSandbox {
public util: UtilityFunctions;
constructor(private readonly reactor: Reactor) {
this.reactor = reactor;
this.util = reactor.util;
this.getBankIndex = () => reactor.getBankIndex();
}
getBankIndex: () => number;
/**
*
* @param src
* @param dst
*/
public connect<R, S extends R>(
src: ConnectablePort<S>,
dst: ConnectablePort<R>
): void;
public connect<A extends T, R, T, S extends R>(
src: CallerPort<A, R>,
dst: CalleePort<T, S>
): void;
public connect<A extends T, R, T, S extends R>(
...[src, dst]:
| [ConnectablePort<S>, ConnectablePort<R>]
| [CallerPort<A, R>, CalleePort<T, S>]
): void {
if (src instanceof CallerPort && dst instanceof CalleePort) {
this.reactor._connectCall(src, dst);
} else if (
src instanceof ConnectablePort &&
dst instanceof ConnectablePort
) {
this.reactor._connect(src.getPort(), dst.getPort());
} else {
throw Error(
"Logically unreachable code: src and dst type mismatch, Caller(ee) port cannot be connected to IOPort."
);
}
}
public disconnect<R, S extends R>(src: IOPort<S>, dst?: IOPort<R>): void {
if (
src instanceof IOPort &&
(dst === undefined || dst instanceof IOPort)
) {
this.reactor._disconnect(src, dst);
} else {
// FIXME: Add an error reporting mechanism such as an exception.
}
}
/**
* Return the reactor containing the mutation using this sandbox.
*/
public getReactor(): Reactor {
return this.reactor;
}
/**
* Mark the given reactor for deletion.
*
* @param reactor
*/
public delete(reactor: Reactor): void {
reactor._delete();
}
};
/**
* Inner class that furnishes an execution environment for reactions.
*/
private readonly _reactionSandbox = class implements ReactionSandbox {
public util: UtilityFunctions;
public getBankIndex: () => number;
constructor(public reactor: Reactor) {
this.util = reactor.util;
this.getBankIndex = () => reactor.getBankIndex();
}
};
/**
* Create a new reactor.
* @param container The container of this reactor.
*/
constructor(container: Reactor | null) {
super(container);
this._bankIndex = -1;
if (container !== null) {
const index = Bank.initializationMap.get(container);
if (index !== undefined) {
this._bankIndex = index;
}
}
this._linkToRuntimeObject();
this.shutdown = new Shutdown(this);
this.startup = new Startup(this);
this.__dummy = new Dummy(this);
// Utils get passed down the hierarchy. If this is an App,
// the container refers to this object, making the following
// assignment idemponent.
// this.util = this._getContainer().util
// Create sandboxes for the reactions and mutations to execute in.
this._reactionScope = new this._reactionSandbox(this);
this._mutationScope = new this._mutationSandbox(this);
// Pass in a reference to the reactor because the runtime object
// is inaccessible for the top-level reactor (it is created after this constructor returns).
const self = this as Reactor;
this.addMutation([this.shutdown], [], function (this) {
self._findOwnReactors().forEach((r) => {
r._delete();
});
});
// If this reactor was created at runtime, simply set the priority of
// the default to the priority of from the last mutation of its
// container plus one. Subsequent reactions and mutations that are added
// will get a priority relative to this one.
// FIXME: If any of the assigned priorities is larger than any downstream
// reaction, then the priorities of those downstream reactions must be
// increased.
if (!(this instanceof App) && this._runtime.isRunning()) {
const toDependOn = this._getContainer()._getLastMutation();
if (toDependOn != null)
this._mutations[0].setPriority(toDependOn.getPriority() + 1);
}
}
protected _initializeReactionScope(): void {
this._reactionScope = new this._reactionSandbox(this);
}
protected _initializeMutationScope(): void {
this._mutationScope = new this._mutationSandbox(this);
}
// protected _isActive(): boolean {
// return this._active
// }
//
public allWritable<T>(port: MultiPort<T>): WritableMultiPort<T> {
return port.asWritable(this._getKey(port));
}
public writable<T>(port: IOPort<T>): WritablePort<T> {
return port.asWritable(this._getKey(port));
}
/**
* Return the index of the reaction given as an argument.
* @param reaction The reaction to return the index of.
*/
public _getReactionIndex(reaction: Reaction<Variable[]>): number {
let index: number | undefined;
if (reaction instanceof Mutation) {
index = this._mutations.indexOf(reaction as Mutation<Variable[]>);
} else {
index = this._reactions.indexOf(reaction);
}
if (index !== undefined) return index;
throw new Error("Reaction is not listed.");
}
protected schedulable<T>(action: Action<T>): Sched<T> {
return action.asSchedulable(this._getKey(action));
}
private _recordDeps<T extends Variable[]>(reaction: Reaction<T>): void {
// Add a dependency on the previous reaction or mutation, if it exists.
const prev = this._getLastReactionOrMutation();
if (prev != null) {
this._dependencyGraph.addEdge(
prev,
reaction as unknown as Reaction<Variable[]>
);
}
// FIXME: Add a dependency on the last mutation that the owner of this reactor
// has. How do we know that it is the last? We have a "lastCaller" problem here.
// Probably better to solve this at the level of the dependency graph with a function
// that allows for a link to be updated.
// Set up the triggers.
for (const t of reaction.trigs) {
// Link the trigger to the reaction.
if (t instanceof Trigger) {
t.getManager(this._getKey(t)).addReaction(
reaction as unknown as Reaction<Variable[]>
);
} else if (t instanceof Array) {
t.forEach((trigger) => {
if (trigger instanceof Trigger) {
trigger
.getManager(this._getKey(trigger))
.addReaction(reaction as unknown as Reaction<Variable[]>);
} else {
throw new Error("Non-Trigger included in Triggers list.");
}
});
}
// Also record this trigger as a dependency.
if (t instanceof IOPort) {
this._dependencyGraph.addEdge(
t,
reaction as unknown as Reaction<Variable[]>
);
} else if (t instanceof MultiPort) {
t.channels().forEach((channel) => {
this._dependencyGraph.addEdge(
channel,
reaction as unknown as Reaction<Variable[]>
);
});
} else if (t instanceof Array) {
t.forEach((trigger) => {
if (trigger instanceof IOPort) {
this._dependencyGraph.addEdge(
trigger,
reaction as unknown as Reaction<Variable[]>
);
} else if (trigger instanceof MultiPort) {
trigger.channels().forEach((channel) => {
this._dependencyGraph.addEdge(
channel,
reaction as unknown as Reaction<Variable[]>
);
});
} else {
throw new Error("Non-Port included in Triggers list.");
}
});
} else {
Log.debug(
this,
() => `
>>>>> not a dependency: ${t}`
);
}
}
const sources = new Set<Port<unknown>>();
const effects = new Set<Port<unknown>>();
for (const a of reaction.args) {
if (a instanceof IOPort) {
this._dependencyGraph.addEdge(
a,
reaction as unknown as Reaction<Variable[]>
);
sources.add(a);
} else if (a instanceof MultiPort) {
a.channels().forEach((channel) => {
this._dependencyGraph.addEdge(
channel,
reaction as unknown as Reaction<Variable[]>
);
sources.add(channel);
});
} else if (a instanceof CalleePort) {
this._dependencyGraph.addEdge(
reaction as unknown as Reaction<Variable[]>,
a
);
} else if (a instanceof CallerPort) {
this._dependencyGraph.addEdge(
a,
reaction as unknown as Reaction<Variable[]>
);
}
// Only necessary if we want to add actions to the dependency graph.
else if (a instanceof Action) {
// dep
} else if (a instanceof SchedulableAction) {
// antidep
} else if (a instanceof WritablePort) {
this._dependencyGraph.addEdge(
reaction as unknown as Reaction<Variable[]>,
a.getPort()
);
effects.add(a.getPort());
} else if (a instanceof WritableMultiPort) {
a.getPorts().forEach((channel) => {
this._dependencyGraph.addEdge(
reaction as unknown as Reaction<Variable[]>,
channel
);
effects.add(channel);
});
}
}
// Make effects dependent on sources.
for (const effect of effects) {
for (const source of sources) {
this._causalityGraph.addEdge(source, effect);
}
}
}
/**
* Given a reaction, return the reaction within this reactor that directly
* precedes it, or `undefined` if there is none.
* @param reaction A reaction to find the predecessor of.
*/
protected prevReaction(
reaction: Reaction<Variable[]>
): Reaction<Variable[]> | undefined {
let index: number | undefined;
if (reaction instanceof Mutation) {
index = this._mutations.indexOf(reaction as Mutation<Variable[]>);
if (index !== undefined && index > 0) {
return this._mutations[index - 1];
}
} else {
index = this._reactions.indexOf(reaction);
if (index !== undefined && index > 0) {
return this._reactions[index - 1];
} else {
const len = this._mutations.length;
if (len > 0) {
return this._mutations[len - 1];
}
}
}
}
/**
* Given a reaction, return the reaction within this reactior that directly
* succeeds it, or `undefined` if there is none.
* @param reaction A reaction to find the successor of.
*/
protected nextReaction(
reaction: Reaction<Variable[]>
): Reaction<Variable[]> | undefined {
let index: number | undefined;
if (reaction instanceof Mutation) {
index = this._mutations.indexOf(reaction as Mutation<Variable[]>);
if (index !== undefined && index < this._mutations.length - 1) {
return this._mutations[index + 1];
} else if (this._reactions.length > 0) {
return this._reactions[0];
}
} else {
index = this._reactions.indexOf(reaction);
if (index !== undefined && index < this._reactions.length - 1) {
return this._reactions[index + 1];
}
}
}
/**
* Add a reaction to this reactor. Each newly added reaction will acquire a
* dependency either on the previously added reaction, or on the last added
* mutation (in case no reactions had been added prior to this one). A
* reaction is specified by a list of triggers, a list of arguments, a react
* function, an optional deadline, and an optional late function (which
* represents the reaction body of the deadline). All triggers a reaction
* needs access must be included in the arguments.
*
* @param trigs
* @param args
* @param react
* @param deadline
* @param late
*/
protected addReaction<T extends Variable[]>(
trigs: Variable[],
args: [...ArgList<T>],
react: (this: ReactionSandbox, ...args: ArgList<T>) => void,
deadline?: TimeValue,
late: (this: ReactionSandbox, ...args: ArgList<T>) => void = () => {
Log.globalLogger.warn("Deadline violation occurred!");
}
): void {
const calleePorts = trigs.filter((trig) => trig instanceof CalleePort);
if (calleePorts.length > 0) {
// This is a procedure.
const port = calleePorts[0] as CalleePort<unknown, unknown>;
const procedure = new Procedure(
this,
this._reactionScope,
trigs,
args,
react,
deadline,
late
);
if (trigs.length > 1) {
// A procedure can only have a single trigger.
throw new Error(`Procedure "${procedure}" has multiple triggers.`);
}
procedure.active = true;
this._recordDeps(procedure);
// Let the last caller point to the reaction that precedes this one.
// This lets the first caller depend on it.
port
.getManager(this._getKey(port))
.setLastCaller(this._getLastReactionOrMutation());
this._reactions.push(procedure as unknown as Procedure<Variable[]>);
// FIXME: set priority manually if this happens at runtime.
} else {
// This is an ordinary reaction.
const reaction = new Reaction(
this,
this._reactionScope,
trigs,
args,
react,
deadline,
late
);
// Stage it directly if it to be triggered immediately.
if (reaction.isTriggeredImmediately()) {
this._runtime.stage(reaction as unknown as Reaction<Variable[]>);
// FIXME: if we're already running, then we need to set the priority as well.
}
reaction.active = true;
this._recordDeps(reaction);
this._reactions.push(reaction as unknown as Reaction<Variable[]>);
// FIXME: set priority manually if this happens at runtime.
}
}
protected addMutation<T extends Variable[]>(
trigs: Variable[],
args: [...ArgList<T>],
react: (this: MutationSandbox, ...args: ArgList<T>) => void,
deadline?: TimeValue,
late: (this: MutationSandbox, ...args: ArgList<T>) => void = () => {
Log.globalLogger.warn("Deadline violation occurred!");
}
): void {
const mutation = new Mutation(
this,
this._mutationScope,
trigs,
args,
react,
deadline,
late
);
// Stage it directly if it to be triggered immediately.
if (mutation.isTriggeredImmediately()) {
this._runtime.stage(mutation as unknown as Mutation<Variable[]>);
}
mutation.active = true;
this._recordDeps(mutation);
this._mutations.push(mutation as unknown as Mutation<Variable[]>);
}
private _addHierarchicalDependencies(): void {
const dependent = this._getFirstReactionOrMutation();
const toDependOn = this._getContainer()._getLastMutation();
if (
dependent != null &&
toDependOn != null &&
this._getContainer() !== this
) {
this._dependencyGraph.addEdge(toDependOn, dependent); // FIXME: this assumes there is always at least one mutation.
}
}
private _addRPCDependencies(): void {
// FIXME: Potentially do this in connect instead upon connecting to a
// callee port. So far, it is unclear how RPCs would work when
// established at runtime by a mutation.
//
// Check if there are any callee ports owned by this reactor.
// If there are, add a dependency from its last caller to the antidependencies
// of the procedure (excluding the callee port itself).
const calleePorts = this._findOwnCalleePorts();
for (const p of calleePorts) {
const procedure = p.getManager(this._getKey(p)).getProcedure();
const lastCaller = p.getManager(this._getKey(p)).getLastCaller();
if (procedure != null && lastCaller != null) {
const effects = this._dependencyGraph.getDownstreamNeighbors(procedure);
for (const e of effects) {
if (!(e instanceof CalleePort)) {
// Also add edge to the local graph.
this._dependencyGraph.addEdge(lastCaller, e);
}
}
} else {
Error("No procedure");
}
}
}
/**
* Recursively collect the local dependency graph of each contained reactor
* and merge them all in one graph.
*
* The recursion depth can be limited via the depth parameter. A depth of 0
* will only return the local dependency graph of this reactor, a depth
* of 1 will merge the local graph only with this reactor's immediate
* children, etc. The default dept is -1, which will let this method
* recurse until it has reached a reactor with no children.
*
* Some additional constraits are added to guarantee the following:
* - The first reaction or mutation has a dependency on the last mutation
* of this reactor's container; and
* - RPCs occur in a deterministic order.
* @param depth The depth of recursion.
*/
protected _getPrecedenceGraph(
depth = -1
): PrecedenceGraph<Port<unknown> | Reaction<Variable[]>> {
const graph = new PrecedenceGraph<Port<unknown> | Reaction<Variable[]>>();
this._addHierarchicalDependencies();
this._addRPCDependencies();
graph.addAll(this._dependencyGraph);
if (depth > 0 || depth < 0) {
if (depth > 0) {
depth--;
}
for (const r of this._getOwnReactors()) {
graph.addAll(r._getPrecedenceGraph(depth));
}
}
return graph;
}
/**
* Return the reactors that this reactor owns.
*/
private _getOwnReactors(): Reactor[] {
// eslint-disable-next-line
return Array.from(this._keyChain.keys()).filter(
(it) => it instanceof Reactor
) as Reactor[];
}
/**
* Return a list of reactions owned by this reactor.
*/
protected _getReactions(): Array<Reaction<Variable[]>> {
const arr = new Array<Reaction<Variable[]>>();
this._reactions.forEach((it) => arr.push(it));
return arr;
}
/**
* Return a list of reactions and mutations owned by this reactor.
*/
protected _getReactionsAndMutations(): Array<Reaction<Variable[]>> {
const arr = new Array<Reaction<Variable[]>>();
this._mutations.forEach((it) => arr.push(it));
this._reactions.forEach((it) => arr.push(it));
return arr;
}
/**
* Return the last mutation of this reactor. All contained reactors
* must have their reactions depend on this.
*/
protected _getLastMutation(): Mutation<Variable[]> | undefined {
const len = this._mutations.length;
if (len > 0) {
return this._mutations[len - 1];
}
}
protected _getFirstReactionOrMutation(): Reaction<Variable[]> | undefined {
if (this._mutations.length > 0) {
return this._mutations[0];
}
if (this._reactions.length > 0) {
return this._reactions[0];
}
}
/**
* Return the last reaction or mutation of this reactor.
*/
protected _getLastReactionOrMutation(): Reaction<Variable[]> | undefined {
let len = this._reactions.length;
if (len > 0) {
return this._reactions[len - 1];
}
len = this._mutations.length;
if (len > 0) {
return this._mutations[len - 1];
}
}
/**
* Return a list of reactions owned by this reactor.
*
* The returned list is a copy of the list kept inside of the reactor,
* so changing it will not affect this reactor.
*/
protected _getMutations(): Array<Reaction<Variable[]>> {
const arr = new Array<Reaction<Variable[]>>();
this._mutations.forEach((it) => arr.push(it));
return arr;
}
/**
* Report whether the given port is downstream of this reactor. If so, the
* given port can be connected to with an output port of this reactor.
* @param port
*/
public _isDownstream(port: Port<unknown>): boolean {
if (port instanceof InPort) {
if (port._isContainedByContainerOf(this)) {
return true;
}
}
if (port instanceof OutPort) {
if (port._isContainedBy(this)) {
return true;
}
}
return false;
}
/**
* Report whether the given port is upstream of this reactor. If so, the
* given port can be connected to an input port of this reactor.
* @param port
*/
public _isUpstream(port: Port<unknown>): boolean {
if (port instanceof OutPort) {
if (port._isContainedByContainerOf(this)) {
return true;
}
}
if (port instanceof InPort) {
if (port._isContainedBy(this)) {
return true;
}
}
return false;
}
public canConnectCall<A extends T, R, T, S extends R>(
src: CallerPort<A, R>,
dst: CalleePort<T, S>
): boolean {
// FIXME: can we change the inheritance relationship so that we can overload?
if (!this._runtime.isRunning()) {
// console.log("Connecting before running")
// Validate connections between callers and callees.
if (
src._isContainedByContainerOf(this) &&
dst._isContainedByContainerOf(this)
) {
return true;
}
return false;
} else {
// FIXME
}
return false;
}
/**
* Returns true if a given source port can be connected to the
* given destination port, false otherwise. Valid connections
* must:
* (1) adhere to the scoping rules and connectivity constraints
* of reactors; and
* (2) not introduce cycles.
*
* The scoping rules of reactors can be summarized as follows:
* - A port cannot connect to itself;
* - Unless the connection is between a caller and callee, the
* destination can only be connected to one source;
* - ...
* @param src The start point of a new connection.
* @param dst The end point of a new connection.
*/
public canConnect<R, S extends R>(
src: IOPort<S>,
dst: IOPort<R>
): CanConnectResult {
// Immediate rule out trivial self loops.
if (src === dst) {
return CanConnectResult.SELF_LOOP;
}
// Check the race condition
// - between reactors and reactions (NOTE: check also needs to happen
// in addReaction)
const deps = this._dependencyGraph.getUpstreamNeighbors(dst); // FIXME this will change with multiplex ports
if (deps !== undefined && deps.size > 0) {
return CanConnectResult.DESTINATION_OCCUPIED;
}
if (!this._runtime.isRunning()) {
// console.log("Connecting before running")
// Validate connections between callers and callees.
// Additional checks for regular ports.
// console.log("IOPort")
// Rule out write conflicts.
// - (between reactors)
if (this._dependencyGraph.getDownstreamNeighbors(dst).size > 0) {
return CanConnectResult.DOWNSTREAM_WRITE_CONFLICT;
}
if (!this._isInScope(src, dst)) {
return CanConnectResult.NOT_IN_SCOPE;
}
return CanConnectResult.SUCCESS;
} else {
// Attempt to make a connection while executing.
// Check the local dependency graph to figure out whether this change
// introduces zero-delay feedback.
// console.log("Runtime connect.")
// check if the connection is outside of container
if (
src instanceof OutPort &&
dst instanceof InPort &&
src._isContainedBy(this) &&
dst._isContainedBy(this)
) {
return CanConnectResult.RT_CONNECTION_OUTSIDE_CONTAINER;
}
/**
* TODO (axmmisaka): The following code is commented for multiple reasons:
* The causality interface check is not fully implemented so new checks are failing
* Second, direct feedthrough itself would not cause any problem *per se*.
* To ensure there is no cycle, the safest way is to check against the global dependency graph.
*/
let app = this as Reactor;
while (app._getContainer() !== app) {
app = app._getContainer();
}
const graph = app._getPrecedenceGraph();
graph.addEdge(src, dst);
if (graph.hasCycle()) {
return CanConnectResult.RT_CYCLE;
}
return CanConnectResult.SUCCESS;
}
}
private _isInScope(src: IOPort<unknown>, dst?: IOPort<unknown>): boolean {
// Assure that the general scoping and connection rules are adhered to.
if (src instanceof OutPort) {
if (dst instanceof InPort) {
// OUT to IN
if (
src._isContainedByContainerOf(this) &&
dst._isContainedByContainerOf(this)
) {
return true;
} else {
return false;
}
} else {
// OUT to OUT
if (
src._isContainedByContainerOf(this) &&
(dst === undefined || dst._isContainedBy(this))
) {
return true;
} else {
return false;
}
}
} else {
if (dst instanceof InPort) {
// IN to IN
if (src._isContainedBy(this) && dst._isContainedByContainerOf(this)) {
return true;
} else {
return false;
}
} else {
// IN to OUT
if (
src._isContainedBy(this) &&
dst !== undefined &&
dst._isContainedBy(this)
) {
return true;
} else {
return false;
}
}
}
}
/**
* Connect a source port to a downstream destination port without canConnect() check.
* This must be used with caution after checking canConnect for the given ports.
* @param src The source port to connect.
* @param dst The destination port to connect.
*/
private _uncheckedConnect<R, S extends R>(
src: IOPort<S>,
dst: IOPort<R>
): void {
Log.debug(this, () => `connecting ${src} and ${dst}`);
// Add dependency implied by connection to local graph.
this._dependencyGraph.addEdge(src, dst);
// Register receiver for value propagation.
const writer = dst.asWritable(this._getKey(dst));
src
.getManager(this._getKey(src))
.addReceiver(writer as unknown as WritablePort<S>);
const val = src.get();
if (this._runtime.isRunning() && val !== undefined) {
writer.set(val);
}
}
/**
* Connect a source port to a downstream destination port. If a source is a
* regular port, then the type variable of the source has to be a subtype of
* the type variable of the destination. If the source is a caller port,
* then the destination has to be a callee port that is effectively a
* subtype of the caller port. Because functions have a contra-variant
* subtype relation, the arguments of the caller side must be a subtype of
* the callee's, and the return value of the callee's must be a subtype of
* the caller's.
* @param src The source port to connect.
* @param dst The destination port to connect.
*/
protected _connect<R, S extends R>(src: IOPort<S>, dst: IOPort<R>): void {
if (src === undefined || src === null) {
throw new Error("Cannot connect unspecified source");
}
if (dst === undefined || dst === null) {
throw new Error("Cannot connect unspecified destination");
}
const canConnectResult = this.canConnect(src, dst);
// I know, this looks a bit weird. But
if (canConnectResult !== CanConnectResult.SUCCESS) {
throw new Error(
`ERROR connecting ${src} to ${dst}. Reason is ${canConnectResult.valueOf()}`
);
}
this._uncheckedConnect(src, dst);
}
protected _connectMulti<R, S extends R>(
src: Array<MultiPort<S> | IOPort<S>>,
dest: Array<MultiPort<R> | IOPort<R>>,
repeatLeft: boolean
): void {
const leftPorts = new Array<IOPort<S>>(0);
const rightPorts = new Array<IOPort<R>>(0);
// TODO(hokeun): Check if the multiport's container is Bank when Bank is implemented.
src.forEach((port) => {
if (port instanceof MultiPort) {
port.channels().forEach((singlePort) => {
leftPorts.push(singlePort);
});
} else if (port instanceof IOPort) {
leftPorts.push(port);
}
});
dest.forEach((port) => {
if (port instanceof MultiPort) {
port.channels().forEach((singlePort) => {
rightPorts.push(singlePort);
});
} else if (port instanceof IOPort) {
rightPorts.push(port);
}
});
if (repeatLeft) {
const leftPortsSize = leftPorts.length;
for (let i = 0; leftPorts.length < rightPorts.length; i++) {
leftPorts.push(leftPorts[i % leftPortsSize]);
}
}
if (leftPorts.length < rightPorts.length) {
Log.warn(
null,
() => "There are more right ports than left ports. ",
"Not all ports will be connected!"
);
} else if (leftPorts.length > rightPorts.length) {
Log.warn(
null,
() => "There are more left ports than right ports. ",
"Not all ports will be connected!"
);
}
for (let i = 0; i < leftPorts.length && i < rightPorts.length; i++) {
const canConnectResult = this.canConnect(leftPorts[i], rightPorts[i]);
if (canConnectResult !== CanConnectResult.SUCCESS) {
throw new Error(
`ERROR connecting ${leftPorts[i]}
to ${rightPorts[i]}
in multiple connections from ${src}
to ${dest}`
);
}
}
for (let i = 0; i < leftPorts.length && i < rightPorts.length; i++) {
this._uncheckedConnect(leftPorts[i], rightPorts[i]);
}
}
protected _connectCall<A extends T, R, T, S extends R>(
src: CallerPort<A, R>,
dst: CalleePort<T, S>
): void {
if (this.canConnectCall(src, dst)) {
Log.debug(this, () => `connecting ${src} and ${dst}`);
// Treat connections between callers and callees separately.
// Note that because A extends T and S extends R, we can safely
// cast CalleePort<T,S> to CalleePort<A,R>.
src.remotePort = dst as unknown as CalleePort<A, R>;
// Register the caller in the callee reactor so that it can
// establish dependencies on the callers.
const calleeManager = dst.getManager(this._getKey(dst));
const callerManager = src.getManager(this._getKey(src));
const container = callerManager.getContainer();
const callers = new Set<Reaction<Variable[]>>();
container._dependencyGraph.getDownstreamNeighbors(src).forEach((dep) => {
if (dep instanceof Reaction) {
callers.add(dep);
}
});
const first = container._getFirst(callers);
const last = container._getLast(callers);
const lastCaller = calleeManager.getLastCaller();
if (lastCaller !== undefined) {
// This means the callee port is bound to a reaction and
// there may be zero or more callers. We now continue
// building a chain of callers.
if (first != null) {
this._dependencyGraph.addEdge(lastCaller, first);
} else {
this._dependencyGraph.addEdge(dst, src);
}
if (last != null) calleeManager.setLastCaller(last);
} else {
throw new Error("No procedure linked to callee port");
}
} else {
throw new Error(`ERROR connecting ${src} to ${dst}`);
}
}
/**
* Return a dependency graph consisting of only this reactor's own ports
* and the dependencies between them.
*/
protected _getCausalityInterface(): PrecedenceGraph<Port<unknown>> {
const ifGraph = this._causalityGraph;
// Find all the input and output ports that this reactor owns.
const inputs = this._findOwnInputs();
const outputs = this._findOwnOutputs();
const visited = new Set();
const search = (
output: OutPort<unknown>,
nodes: Set<Port<unknown> | Reaction<Variable[]>>
): void => {
for (const node of nodes) {
if (!visited.has(node)) {
visited.add(node);
if (node instanceof InPort && inputs.has(node as InPort<unknown>)) {
ifGraph.addEdge(node, output);
} else {
search(output, this._dependencyGraph.getUpstreamNeighbors(output));
}
}
}
};
// For each output, walk the graph and add dependencies to
// the inputs that are reachable.
for (const output of outputs) {
search(output, this._dependencyGraph.getUpstreamNeighbors(output));
visited.clear();
}
return ifGraph;
}
private _findOwnCalleePorts(): Set<CalleePort<unknown, unknown>> {
const ports = new Set<CalleePort<unknown, unknown>>();
for (const component of this._keyChain.keys()) {
if (component instanceof CalleePort) {
ports.add(component as CalleePort<unknown, unknown>);
}
}
return ports;
}
private _findOwnPorts(): Set<Port<unknown>> {
const ports = new Set<Port<unknown>>();
for (const component of this._keyChain.keys()) {
if (component instanceof Port) {
ports.add(component as Port<unknown>);
}
}
return ports;
}
private _findOwnInputs(): Set<InPort<unknown>> {
const inputs = new Set<InPort<unknown>>();
for (const component of this._keyChain.keys()) {
if (component instanceof InPort) {
inputs.add(component as InPort<unknown>);
}
}
return inputs;
}
private _findOwnOutputs(): Set<OutPort<unknown>> {
const outputs = new Set<OutPort<unknown>>();
for (const component of this._keyChain.keys()) {
if (component instanceof OutPort) {
outputs.add(component as OutPort<unknown>);
}
}
return outputs;
}
private _findOwnReactors(): Set<Reactor> {
const reactors = new Set<Reactor>();
for (const component of this._keyChain.keys()) {
if (component instanceof Reactor) {
reactors.add(component);
}
}
return reactors;
}
/**
* Delete the connection between the source and destination nodes.
* If the destination node is not specified, all connections from the source node to any other node are deleted.
* @param src Source port of connection to be disconnected.
* @param dst Destination port of connection to be disconnected. If undefined, disconnect all connections from the source port.
*/
protected _disconnect<R, S extends R>(src: IOPort<S>, dst?: IOPort<R>): void {
if (
(!this._runtime.isRunning() && this._isInScope(src, dst)) ||
this._runtime.isRunning()
) {
this._uncheckedDisconnect(src, dst);
} else {
throw new Error(`ERROR disconnecting ${src} to ${dst}`);
}
}
private _uncheckedDisconnect(
src: IOPort<unknown>,
dst?: IOPort<unknown>
): void {
Log.debug(this, () => `disconnecting ${src} and ${dst}`);
if (dst instanceof IOPort) {
const writer = dst.asWritable(this._getKey(dst));
src.getManager(this._getKey(src)).delReceiver(writer);
this._dependencyGraph.removeEdge(src, dst);
} else {
const nodes = this._dependencyGraph.getDownstreamNeighbors(src);
for (const node of nodes) {
if (node instanceof IOPort) {
const writer = node.asWritable(this._getKey(node));
src.getManager(this._getKey(src)).delReceiver(writer);
this._dependencyGraph.removeEdge(src, node);
}
}
}
}
// /**
// * Set all the timers of this reactor.
// */
// protected _setTimers(): void {
// Log.debug(this, () => "Setting timers for: " + this);
// let timers = new Set<Timer>();
// for (const [k, v] of Object.entries(this)) {
// if (v instanceof Timer) {
// this._setTimer(v);
// }
// }
// }
// protected _setTimer(timer: Timer): void {
// Log.debug(this, () => ">>>>>>>>>>>>>>>>>>>>>>>>Setting timer: " + timer);
// let startTime;
// if (timer.offset.isZero()) {
// // getLaterTime always returns a microstep of zero, so handle the
// // zero offset case explicitly.
// startTime = this.util.getCurrentTag().getMicroStepsLater();
// } else {
// startTime = this.util.getCurrentTag().getLaterTag(timer.offset);
// }// FIXME: startup and a timer with offset zero should be simultaneous and not retrigger events
// this._schedule(new TaggedEvent(timer, this.util.getCurrentTag().getLaterTag(timer.offset), null));
// }
/**
* Report a timer to the app so that it gets unscheduled.
* @param timer The timer to report to the app.
*/
protected _unsetTimer(timer: Timer): void {
// FIXME: we could either set the timer to 'inactive' to tell the
// scheduler to ignore future event and prevent it from rescheduling any.
// The problem with this approach is that if, for some reason, a timer would get
// reactivated, it could start seeing events that were scheduled prior to its
// becoming inactive. Alternatively, we could remove the event from the queue,
// but we'd have to add functionality for this.
}
/**
* Unset all the timers of this reactor.
*/
protected _unsetTimers(): void {
// Log.global.debug("Getting timers for: " + this)
for (const [, v] of Object.entries(this)) {
if (v instanceof Timer) {
this._unsetTimer(v);
}
}
}
/**
* Return the fully qualified name of this reactor.
*