250 lines
7.5 KiB
JavaScript
250 lines
7.5 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
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Gets whether or not a given segment is reachable.
|
|
* @param {CodePathSegment} segment A segment to get.
|
|
* @returns {boolean} `true` if the segment is reachable.
|
|
*/
|
|
function isReachable(segment) {
|
|
return segment.reachable;
|
|
}
|
|
|
|
/**
|
|
* Creates new segments from the specific range of `context.segmentsList`.
|
|
*
|
|
* 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.
|
|
*/
|
|
function makeSegments(context, begin, end, create) {
|
|
const list = context.segmentsList;
|
|
|
|
const normalizedBegin = begin >= 0 ? begin : list.length + begin;
|
|
const normalizedEnd = end >= 0 ? end : list.length + end;
|
|
|
|
const segments = [];
|
|
|
|
for (let i = 0; i < context.count; ++i) {
|
|
const allPrevSegments = [];
|
|
|
|
for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
|
|
allPrevSegments.push(list[j][i]);
|
|
}
|
|
|
|
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.
|
|
*/
|
|
function mergeExtraSegments(context, segments) {
|
|
let currentSegments = segments;
|
|
|
|
while (currentSegments.length > context.count) {
|
|
const merged = [];
|
|
|
|
for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) {
|
|
merged.push(CodePathSegment.newNext(
|
|
context.idGenerator.next(),
|
|
[currentSegments[i], currentSegments[i + length]]
|
|
));
|
|
}
|
|
currentSegments = merged;
|
|
}
|
|
return currentSegments;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Public Interface
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* A class to manage forking.
|
|
*/
|
|
class ForkContext {
|
|
|
|
// eslint-disable-next-line jsdoc/require-description
|
|
/**
|
|
* @param {IdGenerator} idGenerator An identifier generator for segments.
|
|
* @param {ForkContext|null} upper An upper fork context.
|
|
* @param {number} count A number of parallel segments.
|
|
*/
|
|
constructor(idGenerator, upper, count) {
|
|
this.idGenerator = idGenerator;
|
|
this.upper = upper;
|
|
this.count = count;
|
|
this.segmentsList = [];
|
|
}
|
|
|
|
/**
|
|
* The head segments.
|
|
* @type {CodePathSegment[]}
|
|
*/
|
|
get head() {
|
|
const list = this.segmentsList;
|
|
|
|
return list.length === 0 ? [] : list[list.length - 1];
|
|
}
|
|
|
|
/**
|
|
* A flag which shows empty.
|
|
* @type {boolean}
|
|
*/
|
|
get empty() {
|
|
return this.segmentsList.length === 0;
|
|
}
|
|
|
|
/**
|
|
* A flag which shows reachable.
|
|
* @type {boolean}
|
|
*/
|
|
get reachable() {
|
|
const segments = this.head;
|
|
|
|
return segments.length > 0 && segments.some(isReachable);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
makeNext(begin, end) {
|
|
return makeSegments(this, begin, end, 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.
|
|
*/
|
|
makeUnreachable(begin, end) {
|
|
return makeSegments(this, begin, end, 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.
|
|
*/
|
|
makeDisconnected(begin, end) {
|
|
return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
|
|
}
|
|
|
|
/**
|
|
* Adds segments into this context.
|
|
* The added segments become the head.
|
|
* @param {CodePathSegment[]} segments 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.
|
|
* The current head segments are removed.
|
|
* @param {CodePathSegment[]} segments Segments to add.
|
|
* @returns {void}
|
|
*/
|
|
replaceHead(segments) {
|
|
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
|
|
|
|
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
|
|
}
|
|
|
|
/**
|
|
* Adds all segments of a given fork context into this context.
|
|
* @param {ForkContext} context A fork context to add.
|
|
* @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]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears all segments in this context.
|
|
* @returns {void}
|
|
*/
|
|
clear() {
|
|
this.segmentsList = [];
|
|
}
|
|
|
|
/**
|
|
* Creates the root fork context.
|
|
* @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} forkLeavingPath A flag which shows inside of `finally` block.
|
|
* @returns {ForkContext} New fork context.
|
|
*/
|
|
static newEmpty(parentContext, forkLeavingPath) {
|
|
return new ForkContext(
|
|
parentContext.idGenerator,
|
|
parentContext,
|
|
(forkLeavingPath ? 2 : 1) * parentContext.count
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = ForkContext;
|