/** | |
* @fileoverview A class to operate forking. | |
* | |
* This is state of forking. | |
* This has a fork list and manages it. | |
* | |
* @author Toru Nagashima | |
*/ | |
; | |
//------------------------------------------------------------------------------ | |
// Requirements | |
//------------------------------------------------------------------------------ | |
const assert = require("assert"), | |
CodePathSegment = require("./code-path-segment"); | |
//------------------------------------------------------------------------------ | |
// Helpers | |
//------------------------------------------------------------------------------ | |
/** | |
* Determines whether or not a given segment is reachable. | |
* @param {CodePathSegment} segment The segment to check. | |
* @returns {boolean} `true` if the segment is reachable. | |
*/ | |
function isReachable(segment) { | |
return segment.reachable; | |
} | |
/** | |
* Creates a new segment for each fork in the given context and appends it | |
* to the end of the specified range of segments. Ultimately, this ends up calling | |
* `new CodePathSegment()` for each of the forks using the `create` argument | |
* as a wrapper around special behavior. | |
* | |
* The `startIndex` and `endIndex` arguments specify a range of segments in | |
* `context` that should become `allPrevSegments` for the newly created | |
* `CodePathSegment` objects. | |
* | |
* When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and | |
* `end` is `-1`, this creates two new segments, `[g, h]`. This `g` is appended to | |
* the end of the path from `a`, `c`, and `e`. This `h` is appended to the end of | |
* `b`, `d`, and `f`. | |
* @param {ForkContext} context An instance from which the previous segments | |
* will be obtained. | |
* @param {number} startIndex The index of the first segment in the context | |
* that should be specified as previous segments for the newly created segments. | |
* @param {number} endIndex The index of the last segment in the context | |
* that should be specified as previous segments for the newly created segments. | |
* @param {Function} create A function that creates new `CodePathSegment` | |
* instances in a particular way. See the `CodePathSegment.new*` methods. | |
* @returns {Array<CodePathSegment>} An array of the newly-created segments. | |
*/ | |
function createSegments(context, startIndex, endIndex, create) { | |
/** @type {Array<Array<CodePathSegment>>} */ | |
const list = context.segmentsList; | |
/* | |
* Both `startIndex` and `endIndex` work the same way: if the number is zero | |
* or more, then the number is used as-is. If the number is negative, | |
* then that number is added to the length of the segments list to | |
* determine the index to use. That means -1 for either argument | |
* is the last element, -2 is the second to last, and so on. | |
* | |
* So if `startIndex` is 0, `endIndex` is -1, and `list.length` is 3, the | |
* effective `startIndex` is 0 and the effective `endIndex` is 2, so this function | |
* will include items at indices 0, 1, and 2. | |
* | |
* Therefore, if `startIndex` is -1 and `endIndex` is -1, that means we'll only | |
* be using the last segment in `list`. | |
*/ | |
const normalizedBegin = startIndex >= 0 ? startIndex : list.length + startIndex; | |
const normalizedEnd = endIndex >= 0 ? endIndex : list.length + endIndex; | |
/** @type {Array<CodePathSegment>} */ | |
const segments = []; | |
for (let i = 0; i < context.count; ++i) { | |
// this is passed into `new CodePathSegment` to add to code path. | |
const allPrevSegments = []; | |
for (let j = normalizedBegin; j <= normalizedEnd; ++j) { | |
allPrevSegments.push(list[j][i]); | |
} | |
// note: `create` is just a wrapper that augments `new CodePathSegment`. | |
segments.push(create(context.idGenerator.next(), allPrevSegments)); | |
} | |
return segments; | |
} | |
/** | |
* Inside of a `finally` block we end up with two parallel paths. If the code path | |
* exits by a control statement (such as `break` or `continue`) from the `finally` | |
* block, then we need to merge the remaining parallel paths back into one. | |
* @param {ForkContext} context The fork context to work on. | |
* @param {Array<CodePathSegment>} segments Segments to merge. | |
* @returns {Array<CodePathSegment>} The merged segments. | |
*/ | |
function mergeExtraSegments(context, segments) { | |
let currentSegments = segments; | |
/* | |
* We need to ensure that the array returned from this function contains no more | |
* than the number of segments that the context allows. `context.count` indicates | |
* how many items should be in the returned array to ensure that the new segment | |
* entries will line up with the already existing segment entries. | |
*/ | |
while (currentSegments.length > context.count) { | |
const merged = []; | |
/* | |
* Because `context.count` is a factor of 2 inside of a `finally` block, | |
* we can divide the segment count by 2 to merge the paths together. | |
* This loops through each segment in the list and creates a new `CodePathSegment` | |
* that has the segment and the segment two slots away as previous segments. | |
* | |
* If `currentSegments` is [a,b,c,d], this will create new segments e and f, such | |
* that: | |
* | |
* When `i` is 0: | |
* a->e | |
* c->e | |
* | |
* When `i` is 1: | |
* b->f | |
* d->f | |
*/ | |
for (let i = 0, length = Math.floor(currentSegments.length / 2); i < length; ++i) { | |
merged.push(CodePathSegment.newNext( | |
context.idGenerator.next(), | |
[currentSegments[i], currentSegments[i + length]] | |
)); | |
} | |
/* | |
* Go through the loop condition one more time to see if we have the | |
* number of segments for the context. If not, we'll keep merging paths | |
* of the merged segments until we get there. | |
*/ | |
currentSegments = merged; | |
} | |
return currentSegments; | |
} | |
//------------------------------------------------------------------------------ | |
// Public Interface | |
//------------------------------------------------------------------------------ | |
/** | |
* Manages the forking of code paths. | |
*/ | |
class ForkContext { | |
/** | |
* Creates a new instance. | |
* @param {IdGenerator} idGenerator An identifier generator for segments. | |
* @param {ForkContext|null} upper The preceding fork context. | |
* @param {number} count The number of parallel segments in each element | |
* of `segmentsList`. | |
*/ | |
constructor(idGenerator, upper, count) { | |
/** | |
* The ID generator that will generate segment IDs for any new | |
* segments that are created. | |
* @type {IdGenerator} | |
*/ | |
this.idGenerator = idGenerator; | |
/** | |
* The preceding fork context. | |
* @type {ForkContext|null} | |
*/ | |
this.upper = upper; | |
/** | |
* The number of elements in each element of `segmentsList`. In most | |
* cases, this is 1 but can be 2 when there is a `finally` present, | |
* which forks the code path outside of normal flow. In the case of nested | |
* `finally` blocks, this can be a multiple of 2. | |
* @type {number} | |
*/ | |
this.count = count; | |
/** | |
* The segments within this context. Each element in this array has | |
* `count` elements that represent one step in each fork. For example, | |
* when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path | |
* a->c->e and one path b->d->f, and `count` is 2 because each element | |
* is an array with two elements. | |
* @type {Array<Array<CodePathSegment>>} | |
*/ | |
this.segmentsList = []; | |
} | |
/** | |
* The segments that begin this fork context. | |
* @type {Array<CodePathSegment>} | |
*/ | |
get head() { | |
const list = this.segmentsList; | |
return list.length === 0 ? [] : list[list.length - 1]; | |
} | |
/** | |
* Indicates if the context contains no segments. | |
* @type {boolean} | |
*/ | |
get empty() { | |
return this.segmentsList.length === 0; | |
} | |
/** | |
* Indicates if there are any segments that are reachable. | |
* @type {boolean} | |
*/ | |
get reachable() { | |
const segments = this.head; | |
return segments.length > 0 && segments.some(isReachable); | |
} | |
/** | |
* Creates new segments in this context and appends them to the end of the | |
* already existing `CodePathSegment`s specified by `startIndex` and | |
* `endIndex`. | |
* @param {number} startIndex The index of the first segment in the context | |
* that should be specified as previous segments for the newly created segments. | |
* @param {number} endIndex The index of the last segment in the context | |
* that should be specified as previous segments for the newly created segments. | |
* @returns {Array<CodePathSegment>} An array of the newly created segments. | |
*/ | |
makeNext(startIndex, endIndex) { | |
return createSegments(this, startIndex, endIndex, CodePathSegment.newNext); | |
} | |
/** | |
* Creates new unreachable segments in this context and appends them to the end of the | |
* already existing `CodePathSegment`s specified by `startIndex` and | |
* `endIndex`. | |
* @param {number} startIndex The index of the first segment in the context | |
* that should be specified as previous segments for the newly created segments. | |
* @param {number} endIndex The index of the last segment in the context | |
* that should be specified as previous segments for the newly created segments. | |
* @returns {Array<CodePathSegment>} An array of the newly created segments. | |
*/ | |
makeUnreachable(startIndex, endIndex) { | |
return createSegments(this, startIndex, endIndex, CodePathSegment.newUnreachable); | |
} | |
/** | |
* Creates new segments in this context and does not append them to the end | |
* of the already existing `CodePathSegment`s specified by `startIndex` and | |
* `endIndex`. The `startIndex` and `endIndex` are only used to determine if | |
* the new segments should be reachable. If any of the segments in this range | |
* are reachable then the new segments are also reachable; otherwise, the new | |
* segments are unreachable. | |
* @param {number} startIndex The index of the first segment in the context | |
* that should be considered for reachability. | |
* @param {number} endIndex The index of the last segment in the context | |
* that should be considered for reachability. | |
* @returns {Array<CodePathSegment>} An array of the newly created segments. | |
*/ | |
makeDisconnected(startIndex, endIndex) { | |
return createSegments(this, startIndex, endIndex, CodePathSegment.newDisconnected); | |
} | |
/** | |
* Adds segments to the head of this context. | |
* @param {Array<CodePathSegment>} segments The segments to add. | |
* @returns {void} | |
*/ | |
add(segments) { | |
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`); | |
this.segmentsList.push(mergeExtraSegments(this, segments)); | |
} | |
/** | |
* Replaces the head segments with the given segments. | |
* The current head segments are removed. | |
* @param {Array<CodePathSegment>} replacementHeadSegments The new head segments. | |
* @returns {void} | |
*/ | |
replaceHead(replacementHeadSegments) { | |
assert( | |
replacementHeadSegments.length >= this.count, | |
`${replacementHeadSegments.length} >= ${this.count}` | |
); | |
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, replacementHeadSegments)); | |
} | |
/** | |
* Adds all segments of a given fork context into this context. | |
* @param {ForkContext} otherForkContext The fork context to add from. | |
* @returns {void} | |
*/ | |
addAll(otherForkContext) { | |
assert(otherForkContext.count === this.count); | |
this.segmentsList.push(...otherForkContext.segmentsList); | |
} | |
/** | |
* Clears all segments in this context. | |
* @returns {void} | |
*/ | |
clear() { | |
this.segmentsList = []; | |
} | |
/** | |
* Creates a new root context, meaning that there are no parent | |
* fork contexts. | |
* @param {IdGenerator} idGenerator An identifier generator for segments. | |
* @returns {ForkContext} New fork context. | |
*/ | |
static newRoot(idGenerator) { | |
const context = new ForkContext(idGenerator, null, 1); | |
context.add([CodePathSegment.newRoot(idGenerator.next())]); | |
return context; | |
} | |
/** | |
* Creates an empty fork context preceded by a given context. | |
* @param {ForkContext} parentContext The parent fork context. | |
* @param {boolean} shouldForkLeavingPath Indicates that we are inside of | |
* a `finally` block and should therefore fork the path that leaves | |
* `finally`. | |
* @returns {ForkContext} New fork context. | |
*/ | |
static newEmpty(parentContext, shouldForkLeavingPath) { | |
return new ForkContext( | |
parentContext.idGenerator, | |
parentContext, | |
(shouldForkLeavingPath ? 2 : 1) * parentContext.count | |
); | |
} | |
} | |
module.exports = ForkContext; | |