@lf-lang/reactor-ts
Version:
A reactor-oriented programming framework in TypeScript
439 lines • 15.7 kB
JavaScript
"use strict";
/**
* @file A collection of classes for handling graphs.
* @author Marten Lohstroh <marten@berkeley.edu>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReactionGraph = exports.SortablePrecedenceGraph = exports.PrecedenceGraph = void 0;
const reaction_1 = require("./reaction");
const util_1 = require("./util");
/**
* A generic precedence graph.
*/
class PrecedenceGraph {
/**
* A map from nodes to the set of their upstream neighbors.
*/
adjacencyMap = new Map();
/**
* The total number of edges in the graph.
*/
numberOfEdges = 0;
/**
* Add all the nodes and edges from the given precedence graph to this one.
* @param pg A precedence graph
*/
addAll(pg) {
for (const [k, v] of pg.adjacencyMap) {
const nodes = this.adjacencyMap.get(k);
if (nodes != null) {
for (const n of v) {
if (!nodes.has(n)) {
nodes.add(n);
this.numberOfEdges++;
}
}
}
else {
this.adjacencyMap.set(k, new Set(v));
this.numberOfEdges += v.size;
}
}
}
/**
* Add the given node to this graph.
* @param node
*/
addNode(node) {
if (!this.adjacencyMap.has(node)) {
this.adjacencyMap.set(node, new Set());
}
}
/**
* Return the set of all downstream neighbors of the given node.
* @param node The node to retrieve the outgoing nodes of.
*/
getDownstreamNeighbors(node) {
const backEdges = new Set();
this.adjacencyMap.forEach((upstreamNeighbors, downstream) => {
upstreamNeighbors.forEach((upstream) => {
if (upstream === node) {
backEdges.add(downstream);
}
});
});
return backEdges;
}
/**
* Return the set of all upstream neighbors of the given node.
* @param node The node to retrieve the incoming nodes of.
*/
getUpstreamNeighbors(node) {
return this.adjacencyMap.get(node) ?? new Set();
}
/**
* Return true if the graph has a cycle in it.
*/
hasCycle() {
const stack = new Array();
const visited = new Set();
const currentDirectAncestors = new Set();
// This uses DFS with iteration to check for back edges in the graph.
// Iteration is used because TS does not have tail recursion.
// Refer to https://stackoverflow.com/a/56317289
for (const v of this.getNodes()) {
if (visited.has(v)) {
continue;
}
stack.push(v);
while (stack.length !== 0) {
const top = stack[stack.length - 1];
if (visited.has(top)) {
currentDirectAncestors.delete(top);
stack.pop();
}
else {
visited.add(top);
currentDirectAncestors.add(top);
}
for (const child of this.getUpstreamNeighbors(top)) {
if (currentDirectAncestors.has(child))
return true;
if (!visited.has(child))
stack.push(child);
}
}
}
return false;
}
/**
* Remove the given node from the graph.
* @param node The node to remove.
*/
removeNode(node) {
let deps;
if ((deps = this.adjacencyMap.get(node)) != null) {
this.numberOfEdges -= deps.size;
this.adjacencyMap.delete(node);
for (const [, e] of this.adjacencyMap) {
if (e.has(node)) {
e.delete(node);
this.numberOfEdges--;
}
}
}
}
/**
* Add an edge from an upstream node to a downstream one.
* @param upstream The node at which the directed edge starts.
* @param downstream The node at which the directed edge ends.
*/
addEdge(upstream, downstream) {
const deps = this.adjacencyMap.get(downstream);
if (deps == null) {
this.adjacencyMap.set(downstream, new Set([upstream]));
this.numberOfEdges++;
}
else {
if (!deps.has(upstream)) {
deps.add(upstream);
this.numberOfEdges++;
}
}
// Create an entry for `dependsOn` if it doesn't exist.
// This is so that the keys of the map contain all the
// nodes in the graph.
if (!this.adjacencyMap.has(upstream)) {
this.adjacencyMap.set(upstream, new Set());
}
}
/**
* Remove a directed edge from an upstream node to a downstream one.
* @param upstream The node at which the directed edge starts.
* @param downstream The node at which the directed edge ends.
*/
removeEdge(upstream, downstream) {
const deps = this.adjacencyMap.get(downstream);
if (deps?.has(upstream) ?? false) {
deps?.delete(upstream);
this.numberOfEdges--;
}
}
/**
* Return the size of the graph in terms of number of nodes and edges.
*/
size() {
return [this.adjacencyMap.size, this.numberOfEdges];
}
/**
* Return an iterator over the nodes in the graph.
*/
getNodes() {
return this.adjacencyMap.keys();
}
toString = () => this.toMermaidString();
/**
* Return a representation that conforms with the syntax of mermaid.js
* @param edgesWithIssue An array containing arrays with [origin, effect].
* Denotes edges in the graph that causes issues to the execution, will be visualized as `--x` in mermaid.
*/
toMermaidString(edgesWithIssue) {
if (edgesWithIssue == null)
edgesWithIssue = [];
let result = "graph";
const nodeToNumber = new Map();
const getNodeString = (node, def) => {
if (node == null || node?.toString === Object.prototype.toString) {
console.error(`Encountered node with no toString() implementation: ${String(node?.constructor)}`);
return def;
}
return node.toString();
};
// Build a block here since we only need `counter` temporarily here
// We use numbers instead of names of reactors directly as node names
// in mermaid.js because mermaid has strict restrictions regarding
// what could be used as names of the node.
{
let counter = 0;
for (const v of this.getNodes()) {
result += `\n${counter}["${getNodeString(v, String(counter))}"]`;
nodeToNumber.set(v, counter++);
}
}
// This is the effect
for (const s of this.getNodes()) {
// This is the origin
for (const t of this.getUpstreamNeighbors(s)) {
result += `\n${nodeToNumber.get(t)}`;
result += edgesWithIssue.some((v) => v[0] === t && v[1] === s)
? " --x "
: " --> ";
result += `${nodeToNumber.get(s)}`;
}
}
return result;
}
/**
* Return a DOT representation of the graph.
*/
toDotString() {
let dot = "";
const graph = this.adjacencyMap;
const visited = new Set();
/**
* Store the DOT representation of the given chain, which is really
* just a stack of nodes. The top node of the stack (i.e., the first)
* element in the chain is given separately.
* @param node The node that is currently being visited.
* @param chain The current chain that is being built.
*/
function printChain(node, chain) {
dot += "\n";
dot += `"${node}"`;
// TODO (axmmisaka): check if this is equivalent;
// https://stackoverflow.com/a/47903498
if (node?.toString === Object.prototype.toString) {
console.error(`Encountered node with no toString() implementation: ${String(node?.constructor)}`);
}
while (chain.length > 0) {
dot += `->"${chain.pop()}"`;
}
dot += ";";
}
/**
* Recursively build the chains that emanate from the given node.
* @param node The node that is currently being visited.
* @param chain The current chain that is being built.
*/
function buildChain(node, chain) {
let match = false;
for (const [v, e] of graph) {
if (e.has(node)) {
// Found next link in the chain.
const deps = graph.get(node);
if (match || deps == null || deps.size === 0) {
// Start a new line when this is not the first match,
// or when the current node is a start node.
chain = [];
util_1.Log.globalLogger.debug("Starting new chain.");
}
// Mark current node as visited.
visited.add(node);
// Add this node to the chain.
chain.push(node);
if (chain.includes(v)) {
util_1.Log.globalLogger.debug("Cycle detected.");
printChain(v, chain);
}
else if (visited.has(v)) {
util_1.Log.globalLogger.debug("Overlapping chain detected.");
printChain(v, chain);
}
else {
util_1.Log.globalLogger.debug("Adding link to the chain.");
buildChain(v, chain);
}
// Indicate that a match has been found.
match = true;
}
}
if (!match) {
util_1.Log.globalLogger.debug("End of chain.");
printChain(node, chain);
}
}
const start = new Array();
// Build a start set of node without dependencies.
for (const [v, e] of this.adjacencyMap) {
if (e == null || e.size === 0) {
start.push(v);
}
}
// Build the chains.
for (const s of start) {
buildChain(s, []);
}
return "digraph G {" + dot + "\n}";
}
/**
* Return the nodes in the graph that have no upstream neighbors.
*/
getSourceNodes() {
const roots = new Set();
/* Populate start set */
for (const [v, e] of this.adjacencyMap) {
if (e == null || e.size === 0) {
roots.add(v);
}
}
return roots;
}
/**
* Return the nodes in the graph that have no downstream neighbors.
*/
getSinkNodes() {
const leafs = new Set(this.getNodes());
for (const node of this.getNodes()) {
for (const dep of this.getUpstreamNeighbors(node)) {
leafs.delete(dep);
}
}
return leafs;
}
}
exports.PrecedenceGraph = PrecedenceGraph;
/**
* A precedence graph with nodes that are sortable by assigning a numeric priority.
*/
class SortablePrecedenceGraph extends PrecedenceGraph {
/**
* Create a sortable precedence graph. If a type and precedence graph are given,
* then remove all nodes that are not of the given type in a way the preserves
* the original lineage.
* @param type A type that extends T.
* @param pg A precedence graph.
*/
constructor(type, pg) {
super();
if (pg == null || type == null)
return;
const visited = new Set();
const startNodes = pg.getSourceNodes();
const search = (upstreamNode, downstreamNodes) => {
for (const downstreamNode of downstreamNodes) {
if (downstreamNode instanceof type) {
this.addEdge(upstreamNode, downstreamNode);
if (!visited.has(downstreamNode)) {
visited.add(downstreamNode);
search(downstreamNode, pg.getDownstreamNeighbors(downstreamNode));
}
}
else {
// Look further downstream for neighbors that match the type.
search(upstreamNode, pg.getDownstreamNeighbors(downstreamNode));
}
}
};
for (const node of startNodes) {
if (node instanceof type) {
this.addNode(node);
search(node, pg.getDownstreamNeighbors(node));
}
else {
// Look further upstream for start nodes that match the type.
for (const newStartNode of pg.getDownstreamNeighbors(node)) {
if (!visited.has(newStartNode)) {
startNodes.add(newStartNode);
}
}
}
}
}
/**
* Assign priorities to the nodes of the graph such that any two nodes of
* which one has precedence over the other, the priority of the one node is
* lower than the other.
*
* @param destructive Destroy the graph structure if true, leave it in tact by
* working on a copy if false (the default).
* @param spacing The minimum spacing between the priorities of two nodes that
* are in a precedence relationship. The default is 100.
* @returns True if priorities were assigned successfully, false if the graph
* has one or more cycles.
*/
updatePriorities(destructive = false, spacing = 100) {
// This implements Kahn's algorithm
const start = new Array();
let graph;
let count = 0;
if (!destructive) {
graph = new Map();
/* Duplicate the map */
for (const [v, e] of this.adjacencyMap) {
graph.set(v, new Set(e));
}
}
else {
graph = this.adjacencyMap;
}
/* Populate start set */
for (const [v, e] of this.adjacencyMap) {
if (e == null || e.size === 0) {
start.push(v); // start nodes have no dependencies
graph.delete(v);
}
}
/* Sort reactions */
for (let n; (n = start.shift()) != null; count += spacing) {
n.setPriority(count);
// for each node v with an edge e from n to v do
for (const [v, e] of graph) {
if (e.has(n)) {
// v depends on n
e.delete(n);
}
if (e.size === 0) {
start.push(v);
graph.delete(v);
}
}
}
if (graph.size !== 0) {
return false; // cycle detected
}
else {
return true;
}
}
}
exports.SortablePrecedenceGraph = SortablePrecedenceGraph;
/**
* A sortable precedence graph for reactions.
*/
class ReactionGraph extends SortablePrecedenceGraph {
constructor(pg) {
super((reaction_1.Reaction), pg);
}
}
exports.ReactionGraph = ReactionGraph;
//# sourceMappingURL=graph.js.map