@lf-lang/reactor-ts
Version:
A reactor-oriented programming framework in TypeScript
1,287 lines (1,286 loc) • 84.5 kB
JavaScript
"use strict";
/**
* Core of the reactor runtime.
*
* @author Marten Lohstroh (marten@berkeley.edu),
* @author Matt Weber (matt.weber@berkeley.edu),
* @author Hokeun Kim (hokeunkim@berkeley.edu)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.App = exports.CalleePort = exports.CallerPort = exports.Reactor = exports.Timer = exports.Parameter = exports.CanConnectResult = void 0;
const internal_1 = require("./internal");
const uuid_1 = require("uuid");
const bank_1 = require("./bank");
// Set the default log level.
internal_1.Log.setLevel(internal_1.Log.LogLevel.ERROR);
var CanConnectResult;
(function (CanConnectResult) {
CanConnectResult[CanConnectResult["SUCCESS"] = 0] = "SUCCESS";
CanConnectResult["SELF_LOOP"] = "Source port and destination port are the same.";
CanConnectResult["DESTINATION_OCCUPIED"] = "Destination port is already occupied.";
CanConnectResult["DOWNSTREAM_WRITE_CONFLICT"] = "Write conflict: port is already occupied.";
CanConnectResult["NOT_IN_SCOPE"] = "Source and destination ports are not in scope.";
CanConnectResult["RT_CONNECTION_OUTSIDE_CONTAINER"] = "New connection is outside of container.";
CanConnectResult["RT_DIRECT_FEED_THROUGH"] = "New connection introduces direct feed through.";
CanConnectResult["RT_CYCLE"] = "New connection introduces cycle.";
CanConnectResult["MUTATION_CAUSALITY_LOOP"] = "New connection will change the causal effect of the mutation that triggered this connection.";
})(CanConnectResult || (exports.CanConnectResult = CanConnectResult = {}));
/**
* 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 //
// --------------------------------------------------------------------------//
class Parameter {
value;
constructor(value) {
this.value = value;
}
get() {
return this.value;
}
}
exports.Parameter = Parameter;
/**
* 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.
*/
class Timer extends internal_1.ScheduledTrigger {
period;
offset;
/**
* 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__, offset, period) {
super(__container__);
if (!(offset instanceof internal_1.TimeValue)) {
this.offset = internal_1.TimeValue.secs(0);
}
else {
this.offset = offset;
}
if (!(period instanceof internal_1.TimeValue)) {
this.period = internal_1.TimeValue.secs(0);
}
else {
this.period = period;
}
internal_1.Log.debug(this, () => "Creating timer: " + this._getFullyQualifiedName());
// Initialize this timer.
this.runtime.initialize(this);
}
toString() {
return `Timer from ${this._getContainer()._getFullyQualifiedName()}
with period: ${this.period}
offset: ${this.offset}`;
}
get() {
if (this.isPresent()) {
return this.tag;
}
else {
return undefined;
}
}
}
exports.Timer = Timer;
/**
* 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.
*/
class Reactor extends internal_1.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.
*/
_keyChain = new Map();
/**
* This graph has in it all the dependencies implied by this container's
* ports, reactions, and connections.
*/
_dependencyGraph = new internal_1.PrecedenceGraph();
/**
* The runtime object, which has a collection of privileged functions that are passed down from the
* container.
*/
_runtime;
/**
* Index that specifies the location of the reactor instance in a bank,
* if it is a member of one.
*/
_bankIndex;
/**
* Return the location of the reactor instance in a bank,
* if it is a member of one; return -1 otherwise.
*/
getBankIndex() {
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.
*/
_causalityGraph = new internal_1.PrecedenceGraph();
/**
* 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).
*/
_active = false;
/**
* This reactor's shutdown action.
*/
shutdown;
/**
* This reactor's startup action.
*/
startup;
/**
* This reactor's dummy action.
*/
__dummy;
/**
* The list of reactions this reactor has.
*/
_reactions = [];
/**
* Sandbox for the execution of reactions.
*/
_reactionScope;
/**
* The list of mutations this reactor has.
*/
_mutations = [];
/**
* Sandbox for the execution of mutations.
*/
_mutationScope;
/**
* 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.
*/
_receiveRuntimeObject(runtime) {
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.
*/
_register(component, key) {
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);
}
}
_requestRuntimeObject(component) {
if (component._isContainedBy(this)) {
component._receiveRuntimeObject(this._runtime);
}
}
/**
* Remove all the connections associated with a given reactor.
* @param reactor
*/
_deleteConnections(reactor) {
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.
*/
_unplug() {
this._getContainer()._deregister(this, this._key);
}
/**
* Remove the given reactor and its connections from this container if
* the key matches.
* @param reactor
* @param key
*/
_deregister(reactor, key) {
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());
}
}
_getLast(reactions) {
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];
}
}
_getFirst(reactions) {
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.
*/
_getKey(component, key) {
if (component._isContainedBy(this) || this._key === key) {
return this._keyChain.get(component);
}
else if (!(component instanceof internal_1.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.
*/
util;
/**
* Mark this reactor for deletion, trigger all of its shutdown reactions
* and mutations, and also delete all of the reactors that this reactor
* contains.
*/
_delete() {
// console.log("Marking for deletion: " + this._getFullyQualifiedName())
this._runtime.delete(this);
this.shutdown.update(new internal_1.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.
*/
_mutationSandbox = class {
reactor;
util;
constructor(reactor) {
this.reactor = reactor;
this.reactor = reactor;
this.util = reactor.util;
this.getBankIndex = () => reactor.getBankIndex();
}
getBankIndex;
connect(...[src, dst]) {
if (src instanceof CallerPort && dst instanceof CalleePort) {
this.reactor._connectCall(src, dst);
}
else if (src instanceof internal_1.ConnectablePort &&
dst instanceof internal_1.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.");
}
}
disconnect(src, dst) {
if (src instanceof internal_1.IOPort &&
(dst === undefined || dst instanceof internal_1.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.
*/
getReactor() {
return this.reactor;
}
/**
* Mark the given reactor for deletion.
*
* @param reactor
*/
delete(reactor) {
reactor._delete();
}
};
/**
* Inner class that furnishes an execution environment for reactions.
*/
_reactionSandbox = class {
reactor;
util;
getBankIndex;
constructor(reactor) {
this.reactor = reactor;
this.util = reactor.util;
this.getBankIndex = () => reactor.getBankIndex();
}
};
/**
* Create a new reactor.
* @param container The container of this reactor.
*/
constructor(container) {
super(container);
this._bankIndex = -1;
if (container !== null) {
const index = bank_1.Bank.initializationMap.get(container);
if (index !== undefined) {
this._bankIndex = index;
}
}
this._linkToRuntimeObject();
this.shutdown = new internal_1.Shutdown(this);
this.startup = new internal_1.Startup(this);
this.__dummy = new internal_1.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;
this.addMutation([this.shutdown], [], function () {
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);
}
}
_initializeReactionScope() {
this._reactionScope = new this._reactionSandbox(this);
}
_initializeMutationScope() {
this._mutationScope = new this._mutationSandbox(this);
}
// protected _isActive(): boolean {
// return this._active
// }
//
allWritable(port) {
return port.asWritable(this._getKey(port));
}
writable(port) {
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.
*/
_getReactionIndex(reaction) {
let index;
if (reaction instanceof internal_1.Mutation) {
index = this._mutations.indexOf(reaction);
}
else {
index = this._reactions.indexOf(reaction);
}
if (index !== undefined)
return index;
throw new Error("Reaction is not listed.");
}
schedulable(action) {
return action.asSchedulable(this._getKey(action));
}
_recordDeps(reaction) {
// Add a dependency on the previous reaction or mutation, if it exists.
const prev = this._getLastReactionOrMutation();
if (prev != null) {
this._dependencyGraph.addEdge(prev, reaction);
}
// 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 internal_1.Trigger) {
t.getManager(this._getKey(t)).addReaction(reaction);
}
else if (t instanceof Array) {
t.forEach((trigger) => {
if (trigger instanceof internal_1.Trigger) {
trigger
.getManager(this._getKey(trigger))
.addReaction(reaction);
}
else {
throw new Error("Non-Trigger included in Triggers list.");
}
});
}
// Also record this trigger as a dependency.
if (t instanceof internal_1.IOPort) {
this._dependencyGraph.addEdge(t, reaction);
}
else if (t instanceof internal_1.MultiPort) {
t.channels().forEach((channel) => {
this._dependencyGraph.addEdge(channel, reaction);
});
}
else if (t instanceof Array) {
t.forEach((trigger) => {
if (trigger instanceof internal_1.IOPort) {
this._dependencyGraph.addEdge(trigger, reaction);
}
else if (trigger instanceof internal_1.MultiPort) {
trigger.channels().forEach((channel) => {
this._dependencyGraph.addEdge(channel, reaction);
});
}
else {
throw new Error("Non-Port included in Triggers list.");
}
});
}
else {
internal_1.Log.debug(this, () => `
>>>>> not a dependency: ${t}`);
}
}
const sources = new Set();
const effects = new Set();
for (const a of reaction.args) {
if (a instanceof internal_1.IOPort) {
this._dependencyGraph.addEdge(a, reaction);
sources.add(a);
}
else if (a instanceof internal_1.MultiPort) {
a.channels().forEach((channel) => {
this._dependencyGraph.addEdge(channel, reaction);
sources.add(channel);
});
}
else if (a instanceof CalleePort) {
this._dependencyGraph.addEdge(reaction, a);
}
else if (a instanceof CallerPort) {
this._dependencyGraph.addEdge(a, reaction);
}
// Only necessary if we want to add actions to the dependency graph.
else if (a instanceof internal_1.Action) {
// dep
}
else if (a instanceof internal_1.SchedulableAction) {
// antidep
}
else if (a instanceof internal_1.WritablePort) {
this._dependencyGraph.addEdge(reaction, a.getPort());
effects.add(a.getPort());
}
else if (a instanceof internal_1.WritableMultiPort) {
a.getPorts().forEach((channel) => {
this._dependencyGraph.addEdge(reaction, 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.
*/
prevReaction(reaction) {
let index;
if (reaction instanceof internal_1.Mutation) {
index = this._mutations.indexOf(reaction);
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.
*/
nextReaction(reaction) {
let index;
if (reaction instanceof internal_1.Mutation) {
index = this._mutations.indexOf(reaction);
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
*/
addReaction(trigs, args, react, deadline, late = () => {
internal_1.Log.globalLogger.warn("Deadline violation occurred!");
}) {
const calleePorts = trigs.filter((trig) => trig instanceof CalleePort);
if (calleePorts.length > 0) {
// This is a procedure.
const port = calleePorts[0];
const procedure = new internal_1.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);
// FIXME: set priority manually if this happens at runtime.
}
else {
// This is an ordinary reaction.
const reaction = new internal_1.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);
// 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);
// FIXME: set priority manually if this happens at runtime.
}
}
addMutation(trigs, args, react, deadline, late = () => {
internal_1.Log.globalLogger.warn("Deadline violation occurred!");
}) {
const mutation = new internal_1.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);
}
mutation.active = true;
this._recordDeps(mutation);
this._mutations.push(mutation);
}
_addHierarchicalDependencies() {
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.
}
}
_addRPCDependencies() {
// 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.
*/
_getPrecedenceGraph(depth = -1) {
const graph = new internal_1.PrecedenceGraph();
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.
*/
_getOwnReactors() {
// eslint-disable-next-line
return Array.from(this._keyChain.keys()).filter((it) => it instanceof Reactor);
}
/**
* Return a list of reactions owned by this reactor.
*/
_getReactions() {
const arr = new Array();
this._reactions.forEach((it) => arr.push(it));
return arr;
}
/**
* Return a list of reactions and mutations owned by this reactor.
*/
_getReactionsAndMutations() {
const arr = new Array();
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.
*/
_getLastMutation() {
const len = this._mutations.length;
if (len > 0) {
return this._mutations[len - 1];
}
}
_getFirstReactionOrMutation() {
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.
*/
_getLastReactionOrMutation() {
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.
*/
_getMutations() {
const arr = new Array();
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
*/
_isDownstream(port) {
if (port instanceof internal_1.InPort) {
if (port._isContainedByContainerOf(this)) {
return true;
}
}
if (port instanceof internal_1.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
*/
_isUpstream(port) {
if (port instanceof internal_1.OutPort) {
if (port._isContainedByContainerOf(this)) {
return true;
}
}
if (port instanceof internal_1.InPort) {
if (port._isContainedBy(this)) {
return true;
}
}
return false;
}
canConnectCall(src, dst) {
// 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.
*/
canConnect(src, dst) {
// 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 internal_1.OutPort &&
dst instanceof internal_1.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;
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;
}
}
_isInScope(src, dst) {
// Assure that the general scoping and connection rules are adhered to.
if (src instanceof internal_1.OutPort) {
if (dst instanceof internal_1.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 internal_1.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.
*/
_uncheckedConnect(src, dst) {
internal_1.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);
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.
*/
_connect(src, dst) {
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);
}
_connectMulti(src, dest, repeatLeft) {
const leftPorts = new Array(0);
const rightPorts = new Array(0);
// TODO(hokeun): Check if the multiport's container is Bank when Bank is implemented.
src.forEach((port) => {
if (port instanceof internal_1.MultiPort) {
port.channels().forEach((singlePort) => {
leftPorts.push(singlePort);
});
}
else if (port instanceof internal_1.IOPort) {
leftPorts.push(port);
}
});
dest.forEach((port) => {
if (port instanceof internal_1.MultiPort) {
port.channels().forEach((singlePort) => {
rightPorts.push(singlePort);
});
}
else if (port instanceof internal_1.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) {
internal_1.Log.warn(null, () => "There are more right ports than left ports. ", "Not all ports will be connected!");
}
else if (leftPorts.length > rightPorts.length) {
internal_1.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]);
}
}
_connectCall(src, dst) {
if (this.canConnectCall(src, dst)) {
internal_1.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;
// 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();
container._dependencyGraph.getDownstreamNeighbors(src).forEach((dep) => {
if (dep instanceof internal_1.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.
*/
_getCausalityInterface() {
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, nodes) => {
for (const node of nodes) {
if (!visited.has(node)) {
visited.add(node);
if (node instanceof internal_1.InPort && inputs.has(node)) {
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;
}
_findOwnCalleePorts() {
const ports = new Set();
for (const component of this._keyChain.keys()) {
if (component instanceof CalleePort) {
ports.add(component);
}
}
return ports;
}
_findOwnPorts() {
const ports = new Set();
for (const component of this._keyChain.keys()) {
if (component instanceof internal_1.Port) {
ports.add(component);
}
}
return ports;
}
_findOwnInputs() {
const inputs = new Set();
for (const component of this._keyChain.keys()) {
if (component instanceof internal_1.InPort) {
inputs.add(component);
}
}
return inputs;
}
_findOwnOutputs() {
const outputs = new Set();
for (const component of this._keyChain.keys()) {
if (component instanceof internal_1.OutPort) {
outputs.add(component);
}
}
return outputs;
}
_findOwnReactors() {
const reactors = new Set();
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.
*/
_disconnect(src, dst) {
if ((!this._runtime.isRunning() && this._isInScope(src, dst)) ||
this._runtime.isRunning()) {
this._uncheckedDisconnect(src, dst);
}
else {
throw new Error(`ERROR disconnecting ${src} to ${dst}`);
}
}
_uncheckedDisconnect(src, dst) {
internal_1.Log.debug(this, () => `disconnecting ${src} and ${dst}`);
if (dst instanceof internal_1.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 internal_1.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.
*/
_unsetTimer(timer) {
// 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.
*/
_unsetTimers() {
// 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.
*/
toString() {