Graph: move invariant checker to bottom of class (#1026)

This moves the invariant checking code from the top of the Graph class
to the bottom. Most readers of this file will probably be more
interested in seeing the API, and reading the invariant checker first
is likely to be confusing and off-putting.

Test plan: `yarn test` suffices. No semantic change.
This commit is contained in:
Dandelion Mané 2019-01-03 14:15:40 -08:00 committed by GitHub
parent bbe773bb67
commit 7c7fa2d83d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 170 additions and 170 deletions

View File

@ -74,176 +74,6 @@ export class Graph {
_inEdges: Map<NodeAddressT, Edge[]>;
_outEdges: Map<NodeAddressT, Edge[]>;
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<EdgeAddressT> = new Set();
const outEdgesSeen: Set<EdgeAddressT> = 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<EdgeAddressT> = new Set();
const outEdgesSeen: Set<EdgeAddressT> = 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 {