Skip to content

Commit

Permalink
chore: Document and refactor ForkContext (#17566)
Browse files Browse the repository at this point in the history
* chore: Document and refactor ForkContext"

* Update lib/linter/code-path-analysis/fork-context.js

Co-authored-by: Francesco Trotta <github@fasttime.org>

* Update lib/linter/code-path-analysis/fork-context.js

Co-authored-by: Francesco Trotta <github@fasttime.org>

* Update lib/linter/code-path-analysis/fork-context.js

Co-authored-by: Francesco Trotta <github@fasttime.org>

* Clean up comments

---------

Co-authored-by: Francesco Trotta <github@fasttime.org>
  • Loading branch information
nzakas and fasttime committed Sep 26, 2023
1 parent 24e1f14 commit bc77c9a
Showing 1 changed file with 173 additions and 72 deletions.
245 changes: 173 additions & 72 deletions lib/linter/code-path-analysis/fork-context.js
Expand Up @@ -21,70 +21,131 @@ const assert = require("assert"),
//------------------------------------------------------------------------------

/**
* Gets whether or not a given segment is reachable.
* @param {CodePathSegment} segment A segment to get.
* 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 new segments from the specific range of `context.segmentsList`.
* 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 `[g, h]`. This `g` is from `a`, `c`, and `e`.
* This `h` is from `b`, `d`, and `f`.
* @param {ForkContext} context An instance.
* @param {number} begin The first index of the previous segments.
* @param {number} end The last index of the previous segments.
* @param {Function} create A factory function of new segments.
* @returns {CodePathSegment[]} New segments.
* `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 makeSegments(context, begin, end, create) {
function createSegments(context, startIndex, endIndex, create) {

/** @type {Array<Array<CodePathSegment>>} */
const list = context.segmentsList;

const normalizedBegin = begin >= 0 ? begin : list.length + begin;
const normalizedEnd = end >= 0 ? end : list.length + end;
/*
* 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;
}

/**
* `segments` becomes doubly in a `finally` block. Then if a code path exits by a
* control statement (such as `break`, `continue`) from the `finally` block, the
* destination's segments may be half of the source segments. In that case, this
* merges segments.
* @param {ForkContext} context An instance.
* @param {CodePathSegment[]} segments Segments to merge.
* @returns {CodePathSegment[]} The merged 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 = [];

for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) {
/*
* 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;
}

Expand All @@ -93,25 +154,55 @@ function mergeExtraSegments(context, segments) {
//------------------------------------------------------------------------------

/**
* A class to manage forking.
* Manages the forking of code paths.
*/
class ForkContext {

/**
* Creates a new instance.
* @param {IdGenerator} idGenerator An identifier generator for segments.
* @param {ForkContext|null} upper An upper fork context.
* @param {number} count A number of parallel 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 head segments.
* @type {CodePathSegment[]}
* The segments that begin this fork context.
* @type {Array<CodePathSegment>}
*/
get head() {
const list = this.segmentsList;
Expand All @@ -120,15 +211,15 @@ class ForkContext {
}

/**
* A flag which shows empty.
* Indicates if the context contains no segments.
* @type {boolean}
*/
get empty() {
return this.segmentsList.length === 0;
}

/**
* A flag which shows reachable.
* Indicates if there are any segments that are reachable.
* @type {boolean}
*/
get reachable() {
Expand All @@ -138,75 +229,82 @@ class ForkContext {
}

/**
* Creates new segments from this context.
* @param {number} begin The first index of previous segments.
* @param {number} end The last index of previous segments.
* @returns {CodePathSegment[]} New segments.
* 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(begin, end) {
return makeSegments(this, begin, end, CodePathSegment.newNext);
makeNext(startIndex, endIndex) {
return createSegments(this, startIndex, endIndex, CodePathSegment.newNext);
}

/**
* Creates new segments from this context.
* The new segments is always unreachable.
* @param {number} begin The first index of previous segments.
* @param {number} end The last index of previous segments.
* @returns {CodePathSegment[]} New segments.
* 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(begin, end) {
return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
makeUnreachable(startIndex, endIndex) {
return createSegments(this, startIndex, endIndex, CodePathSegment.newUnreachable);
}

/**
* Creates new segments from this context.
* The new segments don't have connections for previous segments.
* But these inherit the reachable flag from this context.
* @param {number} begin The first index of previous segments.
* @param {number} end The last index of previous segments.
* @returns {CodePathSegment[]} New segments.
* 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(begin, end) {
return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
makeDisconnected(startIndex, endIndex) {
return createSegments(this, startIndex, endIndex, CodePathSegment.newDisconnected);
}

/**
* Adds segments into this context.
* The added segments become the head.
* @param {CodePathSegment[]} segments Segments to add.
* 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 given segments.
* Replaces the head segments with the given segments.
* The current head segments are removed.
* @param {CodePathSegment[]} segments Segments to add.
* @param {Array<CodePathSegment>} replacementHeadSegments The new head segments.
* @returns {void}
*/
replaceHead(segments) {
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);

this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
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} context A fork context to add.
* @param {ForkContext} otherForkContext The fork context to add from.
* @returns {void}
*/
addAll(context) {
assert(context.count === this.count);

const source = context.segmentsList;

for (let i = 0; i < source.length; ++i) {
this.segmentsList.push(source[i]);
}
addAll(otherForkContext) {
assert(otherForkContext.count === this.count);
this.segmentsList.push(...otherForkContext.segmentsList);
}

/**
Expand All @@ -218,7 +316,8 @@ class ForkContext {
}

/**
* Creates the root fork context.
* 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.
*/
Expand All @@ -233,14 +332,16 @@ class ForkContext {
/**
* Creates an empty fork context preceded by a given context.
* @param {ForkContext} parentContext The parent fork context.
* @param {boolean} forkLeavingPath A flag which shows inside of `finally` block.
* @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, forkLeavingPath) {
static newEmpty(parentContext, shouldForkLeavingPath) {
return new ForkContext(
parentContext.idGenerator,
parentContext,
(forkLeavingPath ? 2 : 1) * parentContext.count
(shouldForkLeavingPath ? 2 : 1) * parentContext.count
);
}
}
Expand Down

0 comments on commit bc77c9a

Please sign in to comment.