diff --git a/src/core/graph.js b/src/core/graph.js index 12cb890..8fca52b 100644 --- a/src/core/graph.js +++ b/src/core/graph.js @@ -74,176 +74,6 @@ export class Graph { _inEdges: Map; _outEdges: Map; - checkInvariants() { - if (this._invariantsLastChecked.when !== this._modificationCount) { - let failure: ?string = null; - try { - this._checkInvariants(); - } catch (e) { - failure = e.message; - } finally { - this._invariantsLastChecked = { - when: this._modificationCount, - failure, - }; - } - } - if (this._invariantsLastChecked.failure != null) { - throw new Error(this._invariantsLastChecked.failure); - } - } - - _checkInvariants() { - // Definition. A node `n` is in the graph if `_nodes.has(n)`. - // - // Definition. An edge `e` is in the graph if `e` is deep-equal to - // `_edges.get(e.address)`. - // - // Definition. A *logical value* is an equivalence class of ECMAScript - // values modulo deep equality (or, from context, an element of such a - // class). - - // Invariant 1. For a node `n`, if `n` is in the graph, then - // `_inEdges.has(n)` and `_outEdges.has(n)`. The values of - // `_inEdges.get(n)` and `_outEdges.get(n)` are arrays of `Edge`s. - for (const node of this._nodes) { - if (!this._inEdges.has(node)) { - throw new Error(`missing in-edges for ${NodeAddress.toString(node)}`); - } - if (!this._outEdges.has(node)) { - throw new Error(`missing out-edges for ${NodeAddress.toString(node)}`); - } - } - - // Invariant 2. For an edge address `a`, if `_edges.has(a)` and - // `_edges.get(a) === e`, then: - // 1. `e.address` equals `a`; - // 2. `e.src` is in the graph; - // 3. `e.dst` is in the graph; - // 4. `_inEdges.get(e.dst)` contains `e`; and - // 5. `_outEdges.get(e.src)` contains `e`. - // - // We check 2.1, 2.2, and 2.3 here, and check 2.4 and 2.5 later for - // improved performance. - for (const [address, edge] of this._edges.entries()) { - if (edge.address !== address) { - throw new Error( - `bad edge address: ${edgeToString(edge)} does not match ${address}` - ); - } - if (!this._nodes.has(edge.src)) { - throw new Error(`missing src for edge: ${edgeToString(edge)}`); - } - if (!this._nodes.has(edge.dst)) { - throw new Error(`missing dst for edge: ${edgeToString(edge)}`); - } - } - - // Invariant 3. Suppose that `_inEdges.has(n)` and, let `es` be - // `_inEdges.get(n)`. Then - // 1. `n` is in the graph; - // 2. `es` contains any logical value at most once; - // 3. if `es` contains `e`, then `e` is in the graph; and - // 4. if `es` contains `e`, then `e.dst === n`. - // - // Invariant 4. Suppose that `_outEdges.has(n)` and, let `es` be - // `_outEdges.get(n)`. Then - // 1. `n` is in the graph; - // 2. `es` contains any logical value at most once; - // 3. if `es` contains `e`, then `e` is in the graph; and - // 4. if `es` contains `e`, then `e.src === n`. - // - // Note that Invariant 3.2 is equivalent to the following: - // - // Invariant 3.2*. If `a` is an address, then there is at most - // one index `i` such that `es[i].address` is `a`. - // - // It is immediate that 3.2* implies 3.2. To see that 3.2 implies - // 3.2*, suppose that `i` and `j` are such that `es[i].address` and - // `es[j].address` are both `a`. Then, by Invariant 3.3, each of - // `es[i]` and `es[j]` is in the graph, so each is deep-equal to - // `_edges.get(a)`. Therefore, `es[i]` and `es[j]` are deep-equal to - // each other. By 3.2, `es` contains a logical value at most once, - // so `i` must be equal to `j`. - // - // Therefore, it is valid to verify that 3.2*, which we will do. The - // same logic of course applies to Invariant 4.2. - const inEdgesSeen: Set = new Set(); - const outEdgesSeen: Set = new Set(); - for (const {seen, map, baseNodeAccessor, kind} of [ - { - seen: inEdgesSeen, - map: this._inEdges, - baseNodeAccessor: (e) => e.dst, - kind: "in-edge", - }, - { - seen: outEdgesSeen, - map: this._outEdges, - baseNodeAccessor: (e) => e.src, - kind: "out-edge", - }, - ]) { - for (const [base, edges] of map.entries()) { - if (!this._nodes.has(base)) { - // 3.1/4.1 - throw new Error( - `spurious ${kind}s for ${NodeAddress.toString(base)}` - ); - } - for (const edge of edges) { - // 3.2/4.2 - if (seen.has(edge.address)) { - throw new Error(`duplicate ${kind}: ${edgeToString(edge)}`); - } - seen.add(edge.address); - const expected = this._edges.get(edge.address); - // 3.3/4.3 - if (!deepEqual(edge, expected)) { - if (expected == null) { - throw new Error(`spurious ${kind}: ${edgeToString(edge)}`); - } else { - const vs = `${edgeToString(edge)} vs. ${edgeToString(expected)}`; - throw new Error(`bad ${kind}: ${vs}`); - } - } - // 3.4/4.4 - const expectedBase = baseNodeAccessor(edge); - if (base !== baseNodeAccessor(edge)) { - throw new Error( - `bad ${kind}: ${edgeToString(edge)} should be ` + - `should be anchored at ${NodeAddress.toString(expectedBase)}` - ); - } - } - } - } - - // We now return to check 2.4 and 2.5, with the help of the - // structures that we have built up in checking Invariants 3 and 4. - for (const edge of this._edges.values()) { - // That `_inEdges.get(n)` contains `e` for some `n` is sufficient - // to show that `_inEdges.get(e.dst)` contains `e`: if `n` were - // something other than `e.dst`, then we would have a failure of - // invariant 3.4, which would have been caught earlier. Likewise - // for `_outEdges`. - if (!inEdgesSeen.has(edge.address)) { - throw new Error(`missing in-edge: ${edgeToString(edge)}`); - } - if (!outEdgesSeen.has(edge.address)) { - throw new Error(`missing out-edge: ${edgeToString(edge)}`); - } - } - } - - _maybeCheckInvariants() { - if (process.env.NODE_ENV === "test") { - // TODO(perf): If this method becomes really slow, we can disable - // it on specific tests wherein we construct large graphs. - this.checkInvariants(); - } - } - // Incremented each time that any change is made to the graph. Used to // check for comodification and to avoid needlessly checking // invariants. @@ -587,6 +417,176 @@ export class Graph { } return result; } + + checkInvariants() { + if (this._invariantsLastChecked.when !== this._modificationCount) { + let failure: ?string = null; + try { + this._checkInvariants(); + } catch (e) { + failure = e.message; + } finally { + this._invariantsLastChecked = { + when: this._modificationCount, + failure, + }; + } + } + if (this._invariantsLastChecked.failure != null) { + throw new Error(this._invariantsLastChecked.failure); + } + } + + _checkInvariants() { + // Definition. A node `n` is in the graph if `_nodes.has(n)`. + // + // Definition. An edge `e` is in the graph if `e` is deep-equal to + // `_edges.get(e.address)`. + // + // Definition. A *logical value* is an equivalence class of ECMAScript + // values modulo deep equality (or, from context, an element of such a + // class). + + // Invariant 1. For a node `n`, if `n` is in the graph, then + // `_inEdges.has(n)` and `_outEdges.has(n)`. The values of + // `_inEdges.get(n)` and `_outEdges.get(n)` are arrays of `Edge`s. + for (const node of this._nodes) { + if (!this._inEdges.has(node)) { + throw new Error(`missing in-edges for ${NodeAddress.toString(node)}`); + } + if (!this._outEdges.has(node)) { + throw new Error(`missing out-edges for ${NodeAddress.toString(node)}`); + } + } + + // Invariant 2. For an edge address `a`, if `_edges.has(a)` and + // `_edges.get(a) === e`, then: + // 1. `e.address` equals `a`; + // 2. `e.src` is in the graph; + // 3. `e.dst` is in the graph; + // 4. `_inEdges.get(e.dst)` contains `e`; and + // 5. `_outEdges.get(e.src)` contains `e`. + // + // We check 2.1, 2.2, and 2.3 here, and check 2.4 and 2.5 later for + // improved performance. + for (const [address, edge] of this._edges.entries()) { + if (edge.address !== address) { + throw new Error( + `bad edge address: ${edgeToString(edge)} does not match ${address}` + ); + } + if (!this._nodes.has(edge.src)) { + throw new Error(`missing src for edge: ${edgeToString(edge)}`); + } + if (!this._nodes.has(edge.dst)) { + throw new Error(`missing dst for edge: ${edgeToString(edge)}`); + } + } + + // Invariant 3. Suppose that `_inEdges.has(n)` and, let `es` be + // `_inEdges.get(n)`. Then + // 1. `n` is in the graph; + // 2. `es` contains any logical value at most once; + // 3. if `es` contains `e`, then `e` is in the graph; and + // 4. if `es` contains `e`, then `e.dst === n`. + // + // Invariant 4. Suppose that `_outEdges.has(n)` and, let `es` be + // `_outEdges.get(n)`. Then + // 1. `n` is in the graph; + // 2. `es` contains any logical value at most once; + // 3. if `es` contains `e`, then `e` is in the graph; and + // 4. if `es` contains `e`, then `e.src === n`. + // + // Note that Invariant 3.2 is equivalent to the following: + // + // Invariant 3.2*. If `a` is an address, then there is at most + // one index `i` such that `es[i].address` is `a`. + // + // It is immediate that 3.2* implies 3.2. To see that 3.2 implies + // 3.2*, suppose that `i` and `j` are such that `es[i].address` and + // `es[j].address` are both `a`. Then, by Invariant 3.3, each of + // `es[i]` and `es[j]` is in the graph, so each is deep-equal to + // `_edges.get(a)`. Therefore, `es[i]` and `es[j]` are deep-equal to + // each other. By 3.2, `es` contains a logical value at most once, + // so `i` must be equal to `j`. + // + // Therefore, it is valid to verify that 3.2*, which we will do. The + // same logic of course applies to Invariant 4.2. + const inEdgesSeen: Set = new Set(); + const outEdgesSeen: Set = new Set(); + for (const {seen, map, baseNodeAccessor, kind} of [ + { + seen: inEdgesSeen, + map: this._inEdges, + baseNodeAccessor: (e) => e.dst, + kind: "in-edge", + }, + { + seen: outEdgesSeen, + map: this._outEdges, + baseNodeAccessor: (e) => e.src, + kind: "out-edge", + }, + ]) { + for (const [base, edges] of map.entries()) { + if (!this._nodes.has(base)) { + // 3.1/4.1 + throw new Error( + `spurious ${kind}s for ${NodeAddress.toString(base)}` + ); + } + for (const edge of edges) { + // 3.2/4.2 + if (seen.has(edge.address)) { + throw new Error(`duplicate ${kind}: ${edgeToString(edge)}`); + } + seen.add(edge.address); + const expected = this._edges.get(edge.address); + // 3.3/4.3 + if (!deepEqual(edge, expected)) { + if (expected == null) { + throw new Error(`spurious ${kind}: ${edgeToString(edge)}`); + } else { + const vs = `${edgeToString(edge)} vs. ${edgeToString(expected)}`; + throw new Error(`bad ${kind}: ${vs}`); + } + } + // 3.4/4.4 + const expectedBase = baseNodeAccessor(edge); + if (base !== baseNodeAccessor(edge)) { + throw new Error( + `bad ${kind}: ${edgeToString(edge)} should be ` + + `should be anchored at ${NodeAddress.toString(expectedBase)}` + ); + } + } + } + } + + // We now return to check 2.4 and 2.5, with the help of the + // structures that we have built up in checking Invariants 3 and 4. + for (const edge of this._edges.values()) { + // That `_inEdges.get(n)` contains `e` for some `n` is sufficient + // to show that `_inEdges.get(e.dst)` contains `e`: if `n` were + // something other than `e.dst`, then we would have a failure of + // invariant 3.4, which would have been caught earlier. Likewise + // for `_outEdges`. + if (!inEdgesSeen.has(edge.address)) { + throw new Error(`missing in-edge: ${edgeToString(edge)}`); + } + if (!outEdgesSeen.has(edge.address)) { + throw new Error(`missing out-edge: ${edgeToString(edge)}`); + } + } + } + + _maybeCheckInvariants() { + if (process.env.NODE_ENV === "test") { + // TODO(perf): If this method becomes really slow, we can disable + // it on specific tests wherein we construct large graphs. + this.checkInvariants(); + } + } } export function edgeToString(edge: Edge): string {