350 lines
13 KiB
JavaScript
350 lines
13 KiB
JavaScript
/**
|
|
* @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<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;
|