diff --git a/__tests__/CausalityGraph.test.ts b/__tests__/CausalityGraph.test.ts new file mode 100644 index 000000000..e7b47cfd6 --- /dev/null +++ b/__tests__/CausalityGraph.test.ts @@ -0,0 +1,90 @@ +import { Reactor, App, Triggers, Args, Timer, OutPort, InPort, TimeUnit, TimeValue, Origin, Log, LogLevel, Action } from '../src/core/internal'; + +/* Set a port in startup to get thing going */ +class Starter extends Reactor { + public in = new InPort(this); + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.in), + new Args(this.in, this.writable(this.out)), + function(this, __in, __out) { + __out.set(4); + + } + ); + } +} + +class R1 extends Reactor { + public in = new InPort(this); + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.in), + new Args(this.in, this.writable(this.out)), + function(this, __in, __out) { + let tmp = __in.get() + let out:number = 0 + if (tmp) { + out = tmp - 1; + } + if (out) { + __out.set(out) + } + } + ) + } +} + +class R2 extends Reactor { + public in = new InPort(this); + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.in), + new Args(this.in, this.writable(this.out)), + function(this, __in, __out) { + let tmp = __in.get() + let out:number = 0; + if(tmp && tmp == 0) { + this.util.requestStop + } else { + if (tmp) { + __out.set(tmp - 1) + } + } + } + ) + } +} + +class testApp extends App { + start: Starter + reactor1: R1; + reactor2: R2; + + constructor () { + super(); + this.start = new Starter(this); + this.reactor1 = new R1(this); + this.reactor2 = new R2(this); + this._connect(this.start.out, this.reactor1.in) + this._connect(this.reactor1.out, this.reactor2.in) + // this tests the accuracy of the CausalityGraph used in the connect function + test('test if adding cyclic dependency is caught', () => { + expect(() => { + this._connect(this.reactor2.out, this.start.in) + }).toThrowError("New connection introduces cycle.") + }) + } +} + +var app = new testApp() +app._start() diff --git a/__tests__/InvalidMutations.ts b/__tests__/InvalidMutations.ts new file mode 100644 index 000000000..ef3c81986 --- /dev/null +++ b/__tests__/InvalidMutations.ts @@ -0,0 +1,80 @@ +import { Reactor, State, Bank, App, Triggers, Args, Timer, OutPort, InPort, TimeUnit, TimeValue, Origin, Log, LogLevel, Action } from '../src/core/internal'; + +class Starter extends Reactor { + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.startup), + new Args(this.writable(this.out)), + function(this, __out) { + __out.set(4); + + } + ); + } + +} + +class R1 extends Reactor { + public in1 = new InPort(this); + public in2 = new InPort(this); + public out1 = new OutPort(this); + public out2 = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.in1), + new Args(this.in1, this.writable(this.out1)), + function(this, __in, __out) { + __out.set(4) + } + ) + + this.addMutation( + new Triggers(this.in1), + new Args(this.in1, this.in2, this.out1, this.out2), + function(this, __in1, __in2, __out1, __out2) { + test('expect error on creating creating direct feed through', () => { + expect(() => { + this.connect(__in2, __out2) + }).toThrowError("New connection introduces direct feed through.") + }) + test('expect error when creating connection outside container', () => { + expect(() => { + this.connect(__out2, __in2) + }).toThrowError("New connection is outside of container.") + }) + let R2 = new R1(this.getReactor()) + test('expect error on mutation creating race condition on an output port', () => { + expect(() => { + this.connect(R2.out1, __out1) + }).toThrowError("Destination port is already occupied.") + }) + test('expect error on spawning and creating loop within a reactor', () => { + expect(() => { + this.connect(R2.out1, R2.in1) + }).toThrowError("New connection introduces cycle.") + }) + } + ) + } + +} + +class testApp extends App { + start: Starter + reactor1: R1; + + constructor () { + super(); + this.start = new Starter(this); + this.reactor1 = new R1(this); + this._connect(this.start.out, this.reactor1.in1) + } +} + +var app = new testApp() +app._start() diff --git a/__tests__/SimpleMutation.test.ts b/__tests__/SimpleMutation.test.ts new file mode 100644 index 000000000..4906151c9 --- /dev/null +++ b/__tests__/SimpleMutation.test.ts @@ -0,0 +1,78 @@ +import { Reactor, App, Triggers, Args, Timer, OutPort, InPort, TimeUnit, TimeValue, Origin, Log, LogLevel, Action } from '../src/core/internal'; + +/* Set a port in startup to get thing going */ +class Starter extends Reactor { + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.startup), + new Args(this.writable(this.out)), + function(this, __out) { + __out.set(4); + + } + ); + } + +} + +class R1 extends Reactor { + public in = new InPort(this); + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.in), + new Args(this.in, this.writable(this.out)), + function(this, __in, __out) { + __out.set(4) + } + ) + } + + + +} + +class R2 extends Reactor { + public in = new InPort(this); + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addMutation( + new Triggers(this.in), + new Args(this.in, this.out), + function(this, __in, __out) { + test('expect error to be thrown', () => { + expect(() => { + this.connect(__out, __in) + }).toThrowError("New connection is outside of container.") + }) + } + ) + } + +} + + +class testApp extends App { + start: Starter + reactor1: R1; + reactor2: R2; + + constructor () { + super(); + this.start = new Starter(this); + this.reactor1 = new R1(this); + this.reactor2 = new R2(this); + this._connect(this.start.out, this.reactor1.in) + this._connect(this.reactor1.out, this.reactor2.in) + } +} + +var app = new testApp() +app._start() diff --git a/__tests__/SingleEvent.test.ts b/__tests__/SingleEvent.test.ts index a9e4fdbf7..ea2f8f44d 100644 --- a/__tests__/SingleEvent.test.ts +++ b/__tests__/SingleEvent.test.ts @@ -34,7 +34,8 @@ describe('SingleEvent', function () { expect(expect(seTest.logger).toBeInstanceOf(Logger)); - expect(seTest.canConnect(seTest.singleEvent.o, seTest.logger.i)).toBe(false); + expect(function(){seTest.canConnect(seTest.singleEvent.o, seTest.logger.i)}) + .toThrow(new Error("Destination port is already occupied.")) expect(seTest.canConnect(seTest.logger.i, seTest.singleEvent.o)).toBe(false); seTest._start(); diff --git a/__tests__/disconnect.test.ts b/__tests__/disconnect.test.ts new file mode 100644 index 000000000..d6e5ed362 --- /dev/null +++ b/__tests__/disconnect.test.ts @@ -0,0 +1,69 @@ +import { Reactor, State, Bank, App, Triggers, Args, Timer, OutPort, InPort, TimeUnit, TimeValue, Origin, Log, LogLevel, Action } from '../src/core/internal'; + +class Starter extends Reactor { + public out = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.startup), + new Args(this.writable(this.out)), + function(this, __out) { + __out.set(4); + + } + ); + } + +} + +class R1 extends Reactor { + public in1 = new InPort(this); + public in2 = new InPort(this); + public out1 = new OutPort(this); + public out2 = new OutPort(this); + + constructor(parent: Reactor|null) { + super(parent); + this.addReaction( + new Triggers(this.in1), + new Args(this.in1, this.writable(this.out1)), + function(this, __in, __out) { + __out.set(4) + } + ) + + this.addMutation( + new Triggers(this.in1), + new Args(this.in1, this.in2, this.out2), + function(this, __in1, __in2, __out2) { + let R2 = new R1(this.getReactor()) + test('expect that disconnecting an existing connection will not result in an error being thrown', () => { + expect(() => { + this.connect(R2.out2, R2.in2) + this.disconnect(R2.out2, R2.in2) + this.connect(R2.out2, R2.in2) + this.disconnect(R2.out2) + this.connect(R2.out2, R2.in2) + }).not.toThrow(); + }) + } + ) + } + +} + +class testApp extends App { + start: Starter + reactor1: R1; + + constructor () { + super(); + this.start = new Starter(this); + this.reactor1 = new R1(this); + this._connect(this.start.out, this.reactor1.in1) + } +} + +var app = new testApp() +app._start() diff --git a/__tests__/graph.test.ts b/__tests__/graph.test.ts new file mode 100644 index 000000000..03907ad73 --- /dev/null +++ b/__tests__/graph.test.ts @@ -0,0 +1,216 @@ +import {DependencyGraph, PrioritySet, PrioritySetElement, SortableDependencyGraph, Sortable} from "../src/core/graph"; +/** + * The tests below test the functionality of the hasCycle() utility function on various + * dependency graphs, in combination with various graph manipulation utilities + * + * @author Matt Chorlian (mattchorlian@berkeley.edu) + */ + +let node1:number = 1 +let node2:number = 2 +let node3:number = 3 +let node4:number = 4 +let node5:number = 5 + +let d0 = new DependencyGraph() +d0.addNode(node1) +d0.addEdge(node1, node1) + +test('test if one node cycle is caught', () => { + expect(d0.hasCycle()).toEqual(true) +}) + +let d1 = new DependencyGraph() +d1.addNode(node1) +d1.addNode(node2) + +test('test hasCycle utility function on no cycle', () => { + expect(d1.hasCycle()).toEqual(false) +}) + +let d2 = new DependencyGraph() +d2.addNode(node1) +d2.addNode(node2) +d2.addEdge(node1, node2) + +test('test leafNodes() helper function', () => { + expect(d2.leafNodes()).toEqual(new Set([node1])) +}) + +test('test rootNodes() helper function', () => { + expect(d2.rootNodes()).toEqual(new Set([node2])) +}) + +let d3 = new DependencyGraph() +d3.addNode(node1) +d3.addNode(node2) +d3.addEdge(node1, node2) +d3.addEdge(node2, node1) + +test('test hasCycle utility function on a cycle', () => { + expect(d3.hasCycle()).toEqual(true) +}) + +test('test number of edges', () => { + expect(d3.size()[1]).toBe(2) +}) + +let d4 = new DependencyGraph() +d4.addNode(node1) +d4.addNode(node2) +d4.addNode(node3) +d4.addNode(node4) +d4.addEdge(node2, node1) +d4.addEdge(node3, node2) +d4.addEdge(node4, node3) +d4.addEdge(node1, node4) + +test('test hasCycle utility function on a larger cycle', () => { + expect(d4.hasCycle()).toEqual(true) +}) + +let d5 = new DependencyGraph() +d5.addNode(node1) +d5.addNode(node2) +d5.addNode(node3) +d5.addEdge(node2, node1) +d5.addEdge(node3, node2) +d5.addEdge(node1, node3) + +test('test hasCycle along on mutated graph', () => { + expect(d5.hasCycle()).toEqual(true) +}) + +let d6 = new DependencyGraph() +d6.addNode(node1) +d6.addNode(node2) +d6.addNode(node3) +d6.addEdge(node2, node1) +d6.addEdge(node3, node2) +d6.addEdge(node3, node1) + +test('test hasCycle along on mutated graph with no cycles', () => { + expect(d6.hasCycle()).toEqual(false) +}) +class SimpleElement implements PrioritySetElement { + next: PrioritySetElement | undefined; + constructor(private priority: number) {} + getPriority(): number { + return this.priority + } + hasPriorityOver (node: PrioritySetElement): boolean { + return this.priority < node.getPriority() + } + updateIfDuplicateOf (node: PrioritySetElement | undefined): boolean { + if(node) { + return this.priority == node.getPriority() + } + return false + } +} +let ps0 = new PrioritySet() +test('test priority set', () => { + ps0.push(new SimpleElement(3)) + ps0.push(new SimpleElement(5)) + ps0.push(new SimpleElement(5)) + ps0.push(new SimpleElement(7)) + ps0.push(new SimpleElement(1)) + ps0.push(new SimpleElement(4)) + ps0.push(new SimpleElement(1)) + expect(ps0.size()).toBe(5) + expect(ps0.peek()?.getPriority()).toBe(1) + expect(ps0.pop()?.getPriority()).toBe(1) + ps0.empty() + expect(ps0.size()).toBe(0) + +}) +let d7 = new DependencyGraph() +let d8 = new DependencyGraph() +let d9 = new DependencyGraph() +test('test dependency graph', () => { + expect(d7.getEdges(node1).size).toBe(0) + d7.merge(d5) + expect(d7.size()).toStrictEqual(d5.size()) + + d8.addNode(node1) + d9.addEdge(node1, node2) + d8.merge(d9) + expect(d8.size()).toStrictEqual(d9.size()) + expect(d9.getBackEdges(node2).size).toBe(1) + d8.removeNode(node2) + expect(d8.size()).toStrictEqual([1,0]) +}) + + +let d10 = new DependencyGraph() +test('test add/remove Edges', () => { + d10.addEdge(node1, node2) // {(node1 -> node2)} + expect(d10.size()).toStrictEqual([2, 1]) + + d10.addBackEdges(node2, new Set().add(node1).add(node3)) // {(node1 -> node2), (node3 -> node2)} + expect(d10.size()).toStrictEqual([3, 2]) + + d10.addEdges(node1, new Set().add(node2).add(node3).add(node4)) // {(node1 -> node2), (node1 -> node3), (node1 -> node4), (node3 -> node2)} + expect(d10.size()).toStrictEqual([4, 4]) + + d10.addEdges(node5, new Set().add(node1)) // {(node1 -> node2), (node1 -> node3), (node1 -> node4), (node3 -> node2), {node5 -> node1}} + expect(d10.size()).toStrictEqual([5, 5]) + + d10.removeEdge(node1, node2) // {(node1 -> node3), (node1 -> node4), (node3 -> node2), {node5 -> node1}} + expect(d10.size()).toStrictEqual([5, 4]) + +}) + +let d11 = new DependencyGraph() +let d12 = new DependencyGraph() +test('test the DOT representation of the dependency graph', () => { + expect(d11.toString()).toBe('digraph G {'+'\n}') + + d11.addNode(node1) // { node1 } + expect(d11.toString()).toBe('digraph G {\n"1";\n}') + + d11.addEdge(node1, node2) // { (node1 -> node2) } + d11.addEdge(node2, node3) // { (node1 -> node2 -> node3) } + expect(d11.toString()).toBe('digraph G {\n"1"->"2"->"3";\n}') + + let obj = {0:1} + d12.addNode(obj) + expect(d12.toString()).toBe('digraph G {\n"[object Object]";\n}') + + d11.addEdge(node2, node1) + expect(d11.toString()).toBe('digraph G {\n"2"->"1"->"2"->"3";\n}') + d11.addEdge(node1, node3) + expect(d11.toString()).toBe('digraph G {\n"1"->"2"->"1"->"3";\n"2"->"3";\n}') +}) + +let d13 = new DependencyGraph() +test('test for reachableOrigins function of the dependency graph', () => { + d13.addEdge(node1, node2) + d13.addEdge(node1, node3) + d13.addEdge(node2, node4) // { (node1 -> node2 -> node4), (node1 -> node3) } + expect(d13.reachableOrigins(node1, new Set(d13.nodes())).size).toBe(3) + expect(d13.reachableOrigins(node2, new Set(d13.nodes())).size).toBe(1) + expect(d13.reachableOrigins(node3, new Set(d13.nodes())).size).toBe(0) + expect(d13.reachableOrigins(node4, new Set(d13.nodes())).size).toBe(0) +}) + +let sd0 = new SortableDependencyGraph>() +let sd1 = new SortableDependencyGraph>() + +class SortVariable implements Sortable { + next: PrioritySetElement | undefined; + constructor(private priority: number) {} + setPriority(priority: number): void { + priority = this.priority + } +} + +let s0 = new SortVariable(0) +let s1 = new SortVariable(1) +test('test sortable dependency graph', () => { + sd0.addEdge(s0, s1) + expect(sd0.updatePriorities(false,100)).toBe(true) + sd0.addEdge(s1, s0) + expect(sd0.updatePriorities(true, 0)).toBe(false) + expect(sd1.updatePriorities(true, 0)).toBe(true) +}) diff --git a/__tests__/simple.ts b/__tests__/simple.ts index cccc6d16e..a3fd90ca6 100644 --- a/__tests__/simple.ts +++ b/__tests__/simple.ts @@ -98,7 +98,18 @@ describe('Test names for contained reactors', () => { "myApp.y[M0]"->"myApp[M0]"; }`); }); - + + it('graph after disconnect', () => { + this._disconnect(this.y.b, this.x.a) + expect(this._getPrecedenceGraph().toString()).toBe( + StringUtil.dontIndent + `digraph G { + "myApp.x.a"; + "myApp.y.b"; + "myApp.x[M0]"->"myApp[M0]"; + "myApp.y[M0]"->"myApp[M0]"; + }`); + }); // it('graph after disconnect', () => { diff --git a/src/core/graph.ts b/src/core/graph.ts new file mode 100644 index 000000000..355045905 --- /dev/null +++ b/src/core/graph.ts @@ -0,0 +1,459 @@ +import { Log } from "./util"; + +/** + * Utilities for the reactor runtime. + * + * @author Marten Lohstroh (marten@berkeley.edu) + */ + + export interface PrioritySetElement

{ + + /** + * Pointer to the next node in the priority set. + */ + next: PrioritySetElement

| undefined; + + /** + * Return the priority of this node. + */ + getPriority(): P; + + /** + * Determine whether this node has priority over the given node or not. + * @param node A node to compare the priority of this node to. + */ + hasPriorityOver: (node: PrioritySetElement

) => boolean; + + /** + * If the given node is considered a duplicate of this node, then + * update this node if needed, and return true. Return false otherwise. + * @param node A node that may or may not be a duplicate of this node. + */ + updateIfDuplicateOf: (node: PrioritySetElement

| undefined) => boolean; +} + +export interface Sortable

{ + setPriority(priority: P): void; + + // getSTPUntil(): TimeInstant + // setSTPUntil(): TimeInstant +} + +/** + * A priority queue that overwrites duplicate entries. + */ +export class PrioritySet

{ + + private head: PrioritySetElement

| undefined; + private count: number = 0; + + push(element: PrioritySetElement

) { + // update linked list + if (this.head == undefined) { + // create head + element.next = undefined; + this.head = element; + this.count++; + return; + } else if (element.updateIfDuplicateOf(this.head)) { + // updateIfDuplicateOf returned true, i.e., + // it has updated the value of this.head to + // equal that of element. + return; + } else { + // prepend + if (element.hasPriorityOver(this.head)) { + element.next = this.head; + this.head = element; + this.count++; + return; + } + // seek + var curr: PrioritySetElement

| undefined = this.head; + while (curr) { + let next: PrioritySetElement

| undefined = curr.next; + if (next) { + if (element.updateIfDuplicateOf(next)) { + // updateIfDuplicateOf returned true, i.e., + // it has updated the value of this.head to + // equal that of element. + return; + } else if (element.hasPriorityOver(next)) { + break; + } else { + curr = next; + } + } else { + break; + } + } + if (curr) { + // insert + element.next = curr.next; // undefined if last + curr.next = element; + this.count++; + return; + } + } + } + + pop(): PrioritySetElement

| undefined { + if (this.head) { + let node = this.head; + this.head = this.head.next; + node.next = undefined; // unhook from linked list + this.count--; + return node; + } + } + + peek(): PrioritySetElement

| undefined { + if (this.head) { + return this.head; + } + } + + size(): number { + return this.count; + } + + empty(): void { + this.head = undefined; + this.count = 0; + } +} + +export class DependencyGraph { + /** + * Map nodes to the set of nodes that they depend on. + **/ + protected adjacencyMap: Map> = new Map(); + + protected numberOfEdges = 0; + + merge(apg: this) { + for (const [k, v] of apg.adjacencyMap) { + let nodes = this.adjacencyMap.get(k); + if (nodes) { + for (let n of v) { + if (!nodes.has(n)) { + nodes.add(n); + this.numberOfEdges++; + } + } + } else { + this.adjacencyMap.set(k, v); + this.numberOfEdges += v.size; + } + } + } + + addNode(node: T) { + if (!this.adjacencyMap.has(node)) { + this.adjacencyMap.set(node, new Set()); + } + } + + getEdges(node: T): Set { // FIXME: use different terminology: origins/effects + let nodes = this.adjacencyMap.get(node) + if (nodes !== undefined) { + return nodes + } else { + return new Set() + } + } + + getBackEdges(node: T): Set { + let backEdges = new Set() + this.adjacencyMap.forEach((edges, dep) => edges.forEach((edge) => {if (edge === node) { backEdges.add(dep)}})) + return backEdges + } + + /** + * Return the subset of origins that are reachable from the given effect. + * @param effect A node in the graph that to search upstream of. + * @param origins A set of nodes to be found anywhere upstream of effect. + */ + reachableOrigins(effect: T, origins : Set): Set { + let visited = new Set() + let reachable = new Set() + let self = this + + /** + * Recursively traverse the graph to collect reachable origins. + * @param current The current node being visited. + */ + function search(current: T) { + visited.add(current) + if (origins.has(current)) reachable.add(current) + for (let next of self.getEdges(current)) { + if (!visited.has(next)) search(next) + } + } + search(effect) + reachable.delete(effect) + + return reachable + } + + hasCycle(): boolean { + let toVisit = new Set(this.nodes()); + let inPath = new Set() + let self = this + + function cycleFound(current: T): boolean { + if (toVisit.has(current)) { + toVisit.delete(current) + inPath.add(current) + for (let node of self.getEdges(current)) { + if (toVisit.has(node) && cycleFound(node)) { + return true + } else if (inPath.has(node)) { + return true + } + } + } + inPath.delete(current) + return false + } + + while (toVisit.size > 0) { + const [node] = toVisit; + if (cycleFound(node)) { + return true + } + } + return false + } + + removeNode(node: T) { + let deps: Set | undefined; + if (deps = this.adjacencyMap.get(node)) { + this.numberOfEdges -= deps.size; + this.adjacencyMap.delete(node); + for (const [v, e] of this.adjacencyMap) { + if (e.has(node)) { + e.delete(node); + this.numberOfEdges--; + } + } + } + } + + // node -> deps + addEdge(node: T, dependsOn: T) { + let deps = this.adjacencyMap.get(node); + if (!deps) { + this.adjacencyMap.set(node, new Set([dependsOn])); + this.numberOfEdges++; + } else { + if (!deps.has(dependsOn)) { + deps.add(dependsOn); + 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(dependsOn)) { + this.adjacencyMap.set(dependsOn, new Set()); + } + } + + addBackEdges(node: T, dependentNodes: Set) { + for (let a of dependentNodes) { + this.addEdge(a, node); + } + } + + addEdges(node: T, dependsOn: Set) { + let deps = this.adjacencyMap.get(node); + if (!deps) { + this.adjacencyMap.set(node, new Set(dependsOn)); + this.numberOfEdges += dependsOn.size; + } else { + for (let dependency of dependsOn) { + if (!deps.has(dependency)) { + deps.add(dependency); + this.numberOfEdges++; + } + if (!this.adjacencyMap.has(dependency)) { + this.adjacencyMap.set(dependency, new Set()); + } + } + } + } + + removeEdge(node: T, dependsOn: T) { + let deps = this.adjacencyMap.get(node); + if (deps && deps.has(dependsOn)) { + deps.delete(dependsOn); + this.numberOfEdges--; + } + } + + size() { + return [this.adjacencyMap.size, this.numberOfEdges]; + } + + nodes() { + return this.adjacencyMap.keys(); + } + + /** + * Return a DOT representation of the graph. + */ + toString() { + var dot = ""; + var graph = this.adjacencyMap; + var visited: Set = 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: T, chain: Array) { + dot += "\n"; + dot += '"' + node + '"' + if ((node as Object).toString() == "[object Object]") { + console.error("Encountered node with no toString() implementation: " + (node as Object).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: T, chain: Array) { + let match = false; + for (let [v, e] of graph) { + if (e.has(node)) { + // Found next link in the chain. + let deps = graph.get(node); + if (match || !deps || deps.size == 0) { + // Start a new line when this is not the first match, + // or when the current node is a start node. + chain = new Array(); + Log.global.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)) { + Log.global.debug("Cycle detected."); + printChain(v, chain); + } else if (visited.has(v)) { + Log.global.debug("Overlapping chain detected."); + printChain(v, chain); + } else { + Log.global.debug("Adding link to the chain."); + buildChain(v, chain); + } + // Indicate that a match has been found. + match = true; + } + } + if (!match) { + Log.global.debug("End of chain."); + printChain(node, chain); + } + } + + let start: Array = new Array(); + // Build a start set of node without dependencies. + for (const [v, e] of this.adjacencyMap) { + if (!e || e.size == 0) { + start.push(v); + } + } + + // Build the chains. + for (let s of start) { + buildChain(s, new Array()); + } + + return "digraph G {" + dot + "\n}"; + } + + public rootNodes(): Set { + var roots: Set = new Set(); + /* Populate start set */ + for (const [v, e] of this.adjacencyMap) { + if (!e || e.size == 0) { + roots.add(v); // leaf nodes have no dependencies + //clone.delete(v); // FIXME add a removeNodes function to factor out the duplicate code below + } + } + return roots + } + + // Leaf nodes are nodes that do not depend on any other nodes. + // + // In the context of cyclic graphs it is therefore possible to + // have a graph without any leaf nodes. + // As a result, starting a graph search only from leaf nodes in a + // cyclic graph, will not necessarily traverse the entire graph. + public leafNodes(): Set { + var leafs: Set = new Set(this.nodes()); + for (let node of this.nodes()) { + for (let dep of this.getEdges(node)) { + leafs.delete(dep) + } + } + return leafs + } +} + +export class SortableDependencyGraph> extends DependencyGraph { + updatePriorities(destructive: boolean, spacing: number = 100) { + var start: Array = new Array(); + var graph: Map> + var 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 || e.size == 0) { + start.push(v); // start nodes have no dependencies + graph.delete(v); + } + } + /* Sort reactions */ + for (var n: T | undefined; (n = start.shift()); 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; // ERROR: cycle detected + } else { + return true; + } + } +} diff --git a/src/core/internal.ts b/src/core/internal.ts index 9e8fb5bbc..74d6be3e8 100644 --- a/src/core/internal.ts +++ b/src/core/internal.ts @@ -2,6 +2,7 @@ export * from "./types" export * from "./strings" export * from "./time" export * from "./util" +export * from "./graph" export * from "./reaction" export * from "./component" export * from "./trigger" diff --git a/src/core/reactor.ts b/src/core/reactor.ts index 33f32b755..b9be3d06b 100644 --- a/src/core/reactor.ts +++ b/src/core/reactor.ts @@ -398,6 +398,14 @@ export abstract class Reactor extends Component { } } + public disconnect(src: IOPort, dst?: IOPort): void { + if (src instanceof IOPort && (dst === undefined || dst instanceof IOPort)) { + return 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. */ @@ -949,7 +957,15 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { (src: IOPort, dst: IOPort) { // Immediate rule out trivial self loops. if (src === dst) { - return false + throw Error("Source port and destination port are the same.") + } + + // Check the race condition + // - between reactors and reactions (NOTE: check also needs to happen + // in addReaction) + var deps = this._dependencyGraph.getEdges(dst) // FIXME this will change with multiplex ports + if (deps != undefined && deps.size > 0) { + throw Error("Destination port is already occupied.") } if (this._runtime.isRunning() == false) { @@ -957,20 +973,13 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { // Validate connections between callers and callees. // Additional checks for regular ports. - console.log("IOPort") + // console.log("IOPort") // Rule out write conflicts. // - (between reactors) if (this._dependencyGraph.getBackEdges(dst).size > 0) { return false; } - // - between reactors and reactions (NOTE: check also needs to happen - // in addReaction) - var deps = this._dependencyGraph.getEdges(dst) // FIXME this will change with multiplex ports - if (deps != undefined && deps.size > 0) { - return false; - } - return this._isInScope(src, dst) } else { @@ -978,6 +987,13 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { // 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)) { + throw Error("New connection is outside of container.") + } + // Take the local graph and merge in all the causality interfaces // of contained reactors. Then: let graph: DependencyGraph | Reaction> = new DependencyGraph() @@ -991,27 +1007,28 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { graph.addEdge(dst, src) // 1) check for loops - if (graph.hasCycle()) { - return false - } + let hasCycle = graph.hasCycle() // 2) check for direct feed through. - let inputs = this._findOwnInputs() - for (let output of this._findOwnOutputs()) { - let newReachable = graph.reachableOrigins(output, inputs) - let oldReachable = this._causalityGraph.reachableOrigins(output, inputs) - - for (let origin of newReachable) { - if (origin instanceof Port && !oldReachable.has(origin)) { - return false - } - } + // FIXME: This doesn't handle while direct feed thorugh cases. + let hasDirectFeedThrough = false; + if (src instanceof InPort && dst instanceof OutPort) { + hasDirectFeedThrough = dst.getContainer() == src.getContainer(); + } + // Throw error cases + if (hasDirectFeedThrough && hasCycle) { + throw Error("New connection introduces direct feed through and cycle.") + } else if (hasCycle) { + throw Error("New connection introduces cycle.") + } else if (hasDirectFeedThrough) { + throw Error("New connection introduces direct feed through.") } + return true } } - private _isInScope(src: IOPort, dst: IOPort): boolean { + private _isInScope(src: IOPort, dst?: IOPort): boolean { // Assure that the general scoping and connection rules are adhered to. if (src instanceof OutPort) { if (dst instanceof InPort) { @@ -1023,7 +1040,7 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { } } else { // OUT to OUT - if (src._isContainedByContainerOf(this) && dst._isContainedBy(this)) { + if (src._isContainedByContainerOf(this) && (dst === undefined || dst._isContainedBy(this))) { return true; } else { return false; @@ -1256,7 +1273,7 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { private _findOwnOutputs() { let outputs = new Set>() for (let component of this._keyChain.keys()) { - if (component instanceof InPort) { + if (component instanceof OutPort) { outputs.add(component) } } @@ -1275,22 +1292,38 @@ protected _getFirstReactionOrMutation(): Reaction | undefined { /** - * - * @param src - * @param dst + * 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. */ - private _disconnect(src: Port, dst: Port) { - Log.debug(this, () => "disconnecting " + src + " and " + dst); - //src.getManager(this.getKey(src)).delReceiver(dst); - - - // FIXME + protected _disconnect(src: IOPort, dst?:IOPort) { + if ((!this._runtime.isRunning() && this._isInScope(src, dst)) + || (this._runtime.isRunning())) { + this._uncheckedDisconnect(src, dst); + } else { + throw new Error("ERROR disconnecting " + src + " to " + dst); + } + } - // let dests = this._destinationPorts.get(src); - // if (dests != null) { - // dests.delete(dst); - // } - // this._sourcePort.delete(src); + private _uncheckedDisconnect(src: IOPort, dst?: IOPort) { + Log.debug(this, () => "disconnecting " + src + " and " + dst); + if (dst instanceof IOPort) { + let writer = dst.asWritable(this._getKey(dst)); + src.getManager(this._getKey(src)).delReceiver + (writer as WritablePort); + this._dependencyGraph.removeEdge(dst, src); + } else { + let nodes = this._dependencyGraph.getBackEdges(src); + for (let node of nodes) { + if (node instanceof IOPort) { + let writer = node.asWritable(this._getKey(node)); + src.getManager(this._getKey(src)).delReceiver + (writer as WritablePort); + this._dependencyGraph.removeEdge(node, src); + } + } + } } // /** @@ -1573,7 +1606,7 @@ export interface MutationSandbox extends ReactionSandbox { connect (src: CallerPort | IOPort, dst: CalleePort | IOPort):void; - //disconnect(src: Port, dst?: Port): void; + disconnect(src: IOPort, dst?: IOPort): void; delete(reactor: Reactor): void; diff --git a/src/core/util.ts b/src/core/util.ts index 12deeb457..af34883fb 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -6,461 +6,6 @@ import ULog from 'ulog'; * @author Marten Lohstroh (marten@berkeley.edu) */ -export interface PrioritySetElement

{ - - /** - * Pointer to the next node in the priority set. - */ - next: PrioritySetElement

| undefined; - - /** - * Return the priority of this node. - */ - getPriority(): P; - - /** - * Determine whether this node has priority over the given node or not. - * @param node A node to compare the priority of this node to. - */ - hasPriorityOver: (node: PrioritySetElement

| undefined) => boolean; - - /** - * If the given node is considered a duplicate of this node, then - * update this node if needed, and return true. Return false otherwise. - * @param node A node that may or may not be a duplicate of this node. - */ - updateIfDuplicateOf: (node: PrioritySetElement

| undefined) => boolean; -} - -export interface Sortable

{ - setPriority(priority: P): void; - - // getSTPUntil(): TimeInstant - // setSTPUntil(): TimeInstant -} - -/** - * A priority queue that overwrites duplicate entries. - */ -export class PrioritySet

{ - - private head: PrioritySetElement

| undefined; - private count: number = 0; - - push(element: PrioritySetElement

) { - // update linked list - if (this.head == undefined) { - // create head - element.next = undefined; - this.head = element; - this.count++; - return; - } else if (element.updateIfDuplicateOf(this.head)) { - // updateIfDuplicateOf returned true, i.e., - // it has updated the value of this.head to - // equal that of element. - return; - } else { - // prepend - if (element.hasPriorityOver(this.head)) { - element.next = this.head; - this.head = element; - this.count++; - return; - } - // seek - var curr: PrioritySetElement

| undefined = this.head; - while (curr) { - let next: PrioritySetElement

| undefined = curr.next; - if (next) { - if (element.updateIfDuplicateOf(next)) { - // updateIfDuplicateOf returned true, i.e., - // it has updated the value of this.head to - // equal that of element. - return; - } else if (element.hasPriorityOver(next)) { - break; - } else { - curr = next; - } - } else { - break; - } - } - if (curr) { - // insert - element.next = curr.next; // undefined if last - curr.next = element; - this.count++; - return; - } - } - } - - pop(): PrioritySetElement

| undefined { - if (this.head) { - let node = this.head; - this.head = this.head.next; - node.next = undefined; // unhook from linked list - this.count--; - return node; - } - } - - peek(): PrioritySetElement

| undefined { - if (this.head) { - return this.head; - } - } - - size(): number { - return this.count; - } - - empty(): void { - this.head = undefined; - this.count = 0; - } -} - -export class DependencyGraph { - /** - * Map nodes to the set of nodes that they depend on. - **/ - protected adjacencyMap: Map> = new Map(); - - protected numberOfEdges = 0; - - merge(apg: this) { - for (const [k, v] of apg.adjacencyMap) { - let nodes = this.adjacencyMap.get(k); - if (nodes) { - for (let n of v) { - if (!nodes.has(n)) { - nodes.add(n); - this.numberOfEdges++; - } - } - } else { - this.adjacencyMap.set(k, v); - this.numberOfEdges += v.size; - } - } - } - - addNode(node: T) { - if (!this.adjacencyMap.has(node)) { - this.adjacencyMap.set(node, new Set()); - } - } - - getEdges(node: T): Set { // FIXME: use different terminology: origins/effects - let nodes = this.adjacencyMap.get(node) - if (nodes !== undefined) { - return nodes - } else { - return new Set() - } - } - - getBackEdges(node: T): Set { - let backEdges = new Set() - this.adjacencyMap.forEach((edges, dep) => edges.forEach((edge) => {if (edge === node) { backEdges.add(dep)}})) - return backEdges - } - - /** - * Return the subset of origins that are reachable from the given effect. - * @param effect A node in the graph that to search upstream of. - * @param origins A set of nodes to be found anywhere upstream of effect. - */ - reachableOrigins(effect: T, origins : Set): Set { - let visited = new Set() - let reachable = new Set() - let self = this - - /** - * Recursively traverse the graph to collect reachable origins. - * @param current The current node being visited. - */ - function search(current: T) { - for (let next of self.getEdges(current)) { - if (!visited.has(current)) { - // Do not visit a node twice. - if (origins.has(current)) { - // If the current node is among the origins searched - // for, add it to the reachable set. - reachable.add(current) - } - // Continue search, depth first. - if (reachable.size == origins.size) { - search(next) - } else { - // Preempt search of all origins are reachable. - return - } - } - } - } - - search(effect) - - return reachable - } - - - hasCycle(): boolean { - let visited = new Set() - let inPath = new Set() - let self = this - - function cycleFound(current: T): boolean { - if (!visited.has(current)) { - for (let node of self.getEdges(current)) { - if (!visited.has(node) && cycleFound(node)) { - return true - } else if (inPath.has(node)) { - return true - } - } - } - inPath.delete(current) - return false - } - - for (let node of this.leafNodes()) { - if (cycleFound(node)) { - return true - } - } - return false - } - - removeNode(node: T) { - let deps: Set | undefined; - if (deps = this.adjacencyMap.get(node)) { - this.numberOfEdges -= deps.size; - this.adjacencyMap.delete(node); - for (const [v, e] of this.adjacencyMap) { - if (e.has(node)) { - e.delete(node); - this.numberOfEdges--; - } - } - } - } - - // node -> deps - addEdge(node: T, dependsOn: T) { - let deps = this.adjacencyMap.get(node); - if (!deps) { - this.adjacencyMap.set(node, new Set([dependsOn])); - this.numberOfEdges++; - } else { - if (!deps.has(dependsOn)) { - deps.add(dependsOn); - 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(dependsOn)) { - this.adjacencyMap.set(dependsOn, new Set()); - } - } - - addBackEdges(node: T, dependentNodes: Set) { - for (let a of dependentNodes) { - this.addEdge(a, node); - } - } - - addEdges(node: T, dependsOn: Set) { - let deps = this.adjacencyMap.get(node); - if (!deps) { - this.adjacencyMap.set(node, new Set(dependsOn)); - this.numberOfEdges += dependsOn.size; - } else { - for (let dependency of dependsOn) { - if (!deps.has(dependency)) { - deps.add(dependency); - this.numberOfEdges++; - } - if (!this.adjacencyMap.has(dependency)) { - this.adjacencyMap.set(dependency, new Set()); - } - } - } - } - - removeEdge(node: T, dependsOn: T) { - let deps = this.adjacencyMap.get(node); - if (deps && deps.has(dependsOn)) { - deps.delete(dependsOn); - this.numberOfEdges--; - } - } - - size() { - return [this.adjacencyMap.size, this.numberOfEdges]; - } - - nodes() { - return this.adjacencyMap.keys(); - } - - /** - * Return a DOT representation of the graph. - */ - toString() { - var dot = ""; - var graph = this.adjacencyMap; - var visited: Set = 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: T, chain: Array) { - dot += "\n"; - dot += '"' + node + '"' - if ((node as Object).toString() == "[object Object]") { - console.error("Encountered node with no toString() implementation: " + (node as Object).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: T, chain: Array) { - let match = false; - for (let [v, e] of graph) { - if (e.has(node)) { - // Found next link in the chain. - let deps = graph.get(node); - if (match || !deps || deps.size == 0) { - // Start a new line when this is not the first match, - // or when the current node is a start node. - chain = new Array(); - Log.global.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)) { - Log.global.debug("Cycle detected."); - printChain(v, chain); - } else if (visited.has(v)) { - Log.global.debug("Overlapping chain detected."); - printChain(v, chain); - } else { - Log.global.debug("Adding link to the chain."); - buildChain(v, chain); - } - // Indicate that a match has been found. - match = true; - } - } - if (!match) { - Log.global.debug("End of chain."); - printChain(node, chain); - } - } - - let start: Array = new Array(); - // Build a start set of node without dependencies. - for (const [v, e] of this.adjacencyMap) { - if (!e || e.size == 0) { - start.push(v); - } - } - - // Build the chains. - for (let s of start) { - buildChain(s, new Array()); - } - - return "digraph G {" + dot + "\n}"; - } - - public rootNodes(): Set { - var roots: Set = new Set(); - /* Populate start set */ - for (const [v, e] of this.adjacencyMap) { - if (!e || e.size == 0) { - roots.add(v); // leaf nodes have no dependencies - //clone.delete(v); // FIXME add a removeNodes function to factor out the duplicate code below - } - } - return roots - } - - public leafNodes(): Set { - var leafs: Set = new Set(this.nodes()); - for (let node of this.nodes()) { - for (let dep of this.getEdges(node)) { - leafs.delete(dep) - } - } - return leafs - } -} - -export class SortableDependencyGraph> extends DependencyGraph { - updatePriorities(destructive: boolean, spacing: number = 100) { - var start: Array = new Array(); - var graph: Map> - var 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 || e.size == 0) { - start.push(v); // start nodes have no dependencies - graph.delete(v); - } - } - /* Sort reactions */ - for (var n: T | undefined; (n = start.shift()); 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; // ERROR: cycle detected - } else { - return true; - } - } -} /** * Log levels for `Log`. * @see Log @@ -549,7 +94,8 @@ export class Log { } } else { if (Log.global.level >= LogLevel.ERROR) { - Log.global.error(message.call(obj)); + //Log.global.error(message.call(obj)); + console.error(message.call(obj)) } } }