UNPKG

@lf-lang/reactor-ts

Version:

A reactor-oriented programming framework in TypeScript

1,287 lines (1,286 loc) 84.5 kB
"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() {