/** * @fileoverview A class to operate forking. * * This is state of forking. * This has a fork list and manages it. * * @author Toru Nagashima */ "use strict"; //------------------------------------------------------------------------------ // 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} An array of the newly-created segments. */ function createSegments(context, startIndex, endIndex, create) { /** @type {Array>} */ 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} */ 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} segments Segments to merge. * @returns {Array} 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>} */ this.segmentsList = []; } /** * The segments that begin this fork context. * @type {Array} */ 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} 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} 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} 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} 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} 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;