[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview A class to operate forking.
|
---|
| 3 | *
|
---|
| 4 | * This is state of forking.
|
---|
| 5 | * This has a fork list and manages it.
|
---|
| 6 | *
|
---|
| 7 | * @author Toru Nagashima
|
---|
| 8 | */
|
---|
| 9 |
|
---|
| 10 | "use strict";
|
---|
| 11 |
|
---|
| 12 | //------------------------------------------------------------------------------
|
---|
| 13 | // Requirements
|
---|
| 14 | //------------------------------------------------------------------------------
|
---|
| 15 |
|
---|
| 16 | const assert = require("assert"),
|
---|
| 17 | CodePathSegment = require("./code-path-segment");
|
---|
| 18 |
|
---|
| 19 | //------------------------------------------------------------------------------
|
---|
| 20 | // Helpers
|
---|
| 21 | //------------------------------------------------------------------------------
|
---|
| 22 |
|
---|
| 23 | /**
|
---|
| 24 | * Determines whether or not a given segment is reachable.
|
---|
| 25 | * @param {CodePathSegment} segment The segment to check.
|
---|
| 26 | * @returns {boolean} `true` if the segment is reachable.
|
---|
| 27 | */
|
---|
| 28 | function isReachable(segment) {
|
---|
| 29 | return segment.reachable;
|
---|
| 30 | }
|
---|
| 31 |
|
---|
| 32 | /**
|
---|
| 33 | * Creates a new segment for each fork in the given context and appends it
|
---|
| 34 | * to the end of the specified range of segments. Ultimately, this ends up calling
|
---|
| 35 | * `new CodePathSegment()` for each of the forks using the `create` argument
|
---|
| 36 | * as a wrapper around special behavior.
|
---|
| 37 | *
|
---|
| 38 | * The `startIndex` and `endIndex` arguments specify a range of segments in
|
---|
| 39 | * `context` that should become `allPrevSegments` for the newly created
|
---|
| 40 | * `CodePathSegment` objects.
|
---|
| 41 | *
|
---|
| 42 | * When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
|
---|
| 43 | * `end` is `-1`, this creates two new segments, `[g, h]`. This `g` is appended to
|
---|
| 44 | * the end of the path from `a`, `c`, and `e`. This `h` is appended to the end of
|
---|
| 45 | * `b`, `d`, and `f`.
|
---|
| 46 | * @param {ForkContext} context An instance from which the previous segments
|
---|
| 47 | * will be obtained.
|
---|
| 48 | * @param {number} startIndex The index of the first segment in the context
|
---|
| 49 | * that should be specified as previous segments for the newly created segments.
|
---|
| 50 | * @param {number} endIndex The index of the last segment in the context
|
---|
| 51 | * that should be specified as previous segments for the newly created segments.
|
---|
| 52 | * @param {Function} create A function that creates new `CodePathSegment`
|
---|
| 53 | * instances in a particular way. See the `CodePathSegment.new*` methods.
|
---|
| 54 | * @returns {Array<CodePathSegment>} An array of the newly-created segments.
|
---|
| 55 | */
|
---|
| 56 | function createSegments(context, startIndex, endIndex, create) {
|
---|
| 57 |
|
---|
| 58 | /** @type {Array<Array<CodePathSegment>>} */
|
---|
| 59 | const list = context.segmentsList;
|
---|
| 60 |
|
---|
| 61 | /*
|
---|
| 62 | * Both `startIndex` and `endIndex` work the same way: if the number is zero
|
---|
| 63 | * or more, then the number is used as-is. If the number is negative,
|
---|
| 64 | * then that number is added to the length of the segments list to
|
---|
| 65 | * determine the index to use. That means -1 for either argument
|
---|
| 66 | * is the last element, -2 is the second to last, and so on.
|
---|
| 67 | *
|
---|
| 68 | * So if `startIndex` is 0, `endIndex` is -1, and `list.length` is 3, the
|
---|
| 69 | * effective `startIndex` is 0 and the effective `endIndex` is 2, so this function
|
---|
| 70 | * will include items at indices 0, 1, and 2.
|
---|
| 71 | *
|
---|
| 72 | * Therefore, if `startIndex` is -1 and `endIndex` is -1, that means we'll only
|
---|
| 73 | * be using the last segment in `list`.
|
---|
| 74 | */
|
---|
| 75 | const normalizedBegin = startIndex >= 0 ? startIndex : list.length + startIndex;
|
---|
| 76 | const normalizedEnd = endIndex >= 0 ? endIndex : list.length + endIndex;
|
---|
| 77 |
|
---|
| 78 | /** @type {Array<CodePathSegment>} */
|
---|
| 79 | const segments = [];
|
---|
| 80 |
|
---|
| 81 | for (let i = 0; i < context.count; ++i) {
|
---|
| 82 |
|
---|
| 83 | // this is passed into `new CodePathSegment` to add to code path.
|
---|
| 84 | const allPrevSegments = [];
|
---|
| 85 |
|
---|
| 86 | for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
|
---|
| 87 | allPrevSegments.push(list[j][i]);
|
---|
| 88 | }
|
---|
| 89 |
|
---|
| 90 | // note: `create` is just a wrapper that augments `new CodePathSegment`.
|
---|
| 91 | segments.push(create(context.idGenerator.next(), allPrevSegments));
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | return segments;
|
---|
| 95 | }
|
---|
| 96 |
|
---|
| 97 | /**
|
---|
| 98 | * Inside of a `finally` block we end up with two parallel paths. If the code path
|
---|
| 99 | * exits by a control statement (such as `break` or `continue`) from the `finally`
|
---|
| 100 | * block, then we need to merge the remaining parallel paths back into one.
|
---|
| 101 | * @param {ForkContext} context The fork context to work on.
|
---|
| 102 | * @param {Array<CodePathSegment>} segments Segments to merge.
|
---|
| 103 | * @returns {Array<CodePathSegment>} The merged segments.
|
---|
| 104 | */
|
---|
| 105 | function mergeExtraSegments(context, segments) {
|
---|
| 106 | let currentSegments = segments;
|
---|
| 107 |
|
---|
| 108 | /*
|
---|
| 109 | * We need to ensure that the array returned from this function contains no more
|
---|
| 110 | * than the number of segments that the context allows. `context.count` indicates
|
---|
| 111 | * how many items should be in the returned array to ensure that the new segment
|
---|
| 112 | * entries will line up with the already existing segment entries.
|
---|
| 113 | */
|
---|
| 114 | while (currentSegments.length > context.count) {
|
---|
| 115 | const merged = [];
|
---|
| 116 |
|
---|
| 117 | /*
|
---|
| 118 | * Because `context.count` is a factor of 2 inside of a `finally` block,
|
---|
| 119 | * we can divide the segment count by 2 to merge the paths together.
|
---|
| 120 | * This loops through each segment in the list and creates a new `CodePathSegment`
|
---|
| 121 | * that has the segment and the segment two slots away as previous segments.
|
---|
| 122 | *
|
---|
| 123 | * If `currentSegments` is [a,b,c,d], this will create new segments e and f, such
|
---|
| 124 | * that:
|
---|
| 125 | *
|
---|
| 126 | * When `i` is 0:
|
---|
| 127 | * a->e
|
---|
| 128 | * c->e
|
---|
| 129 | *
|
---|
| 130 | * When `i` is 1:
|
---|
| 131 | * b->f
|
---|
| 132 | * d->f
|
---|
| 133 | */
|
---|
| 134 | for (let i = 0, length = Math.floor(currentSegments.length / 2); i < length; ++i) {
|
---|
| 135 | merged.push(CodePathSegment.newNext(
|
---|
| 136 | context.idGenerator.next(),
|
---|
| 137 | [currentSegments[i], currentSegments[i + length]]
|
---|
| 138 | ));
|
---|
| 139 | }
|
---|
| 140 |
|
---|
| 141 | /*
|
---|
| 142 | * Go through the loop condition one more time to see if we have the
|
---|
| 143 | * number of segments for the context. If not, we'll keep merging paths
|
---|
| 144 | * of the merged segments until we get there.
|
---|
| 145 | */
|
---|
| 146 | currentSegments = merged;
|
---|
| 147 | }
|
---|
| 148 |
|
---|
| 149 | return currentSegments;
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | //------------------------------------------------------------------------------
|
---|
| 153 | // Public Interface
|
---|
| 154 | //------------------------------------------------------------------------------
|
---|
| 155 |
|
---|
| 156 | /**
|
---|
| 157 | * Manages the forking of code paths.
|
---|
| 158 | */
|
---|
| 159 | class ForkContext {
|
---|
| 160 |
|
---|
| 161 | /**
|
---|
| 162 | * Creates a new instance.
|
---|
| 163 | * @param {IdGenerator} idGenerator An identifier generator for segments.
|
---|
| 164 | * @param {ForkContext|null} upper The preceding fork context.
|
---|
| 165 | * @param {number} count The number of parallel segments in each element
|
---|
| 166 | * of `segmentsList`.
|
---|
| 167 | */
|
---|
| 168 | constructor(idGenerator, upper, count) {
|
---|
| 169 |
|
---|
| 170 | /**
|
---|
| 171 | * The ID generator that will generate segment IDs for any new
|
---|
| 172 | * segments that are created.
|
---|
| 173 | * @type {IdGenerator}
|
---|
| 174 | */
|
---|
| 175 | this.idGenerator = idGenerator;
|
---|
| 176 |
|
---|
| 177 | /**
|
---|
| 178 | * The preceding fork context.
|
---|
| 179 | * @type {ForkContext|null}
|
---|
| 180 | */
|
---|
| 181 | this.upper = upper;
|
---|
| 182 |
|
---|
| 183 | /**
|
---|
| 184 | * The number of elements in each element of `segmentsList`. In most
|
---|
| 185 | * cases, this is 1 but can be 2 when there is a `finally` present,
|
---|
| 186 | * which forks the code path outside of normal flow. In the case of nested
|
---|
| 187 | * `finally` blocks, this can be a multiple of 2.
|
---|
| 188 | * @type {number}
|
---|
| 189 | */
|
---|
| 190 | this.count = count;
|
---|
| 191 |
|
---|
| 192 | /**
|
---|
| 193 | * The segments within this context. Each element in this array has
|
---|
| 194 | * `count` elements that represent one step in each fork. For example,
|
---|
| 195 | * when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path
|
---|
| 196 | * a->c->e and one path b->d->f, and `count` is 2 because each element
|
---|
| 197 | * is an array with two elements.
|
---|
| 198 | * @type {Array<Array<CodePathSegment>>}
|
---|
| 199 | */
|
---|
| 200 | this.segmentsList = [];
|
---|
| 201 | }
|
---|
| 202 |
|
---|
| 203 | /**
|
---|
| 204 | * The segments that begin this fork context.
|
---|
| 205 | * @type {Array<CodePathSegment>}
|
---|
| 206 | */
|
---|
| 207 | get head() {
|
---|
| 208 | const list = this.segmentsList;
|
---|
| 209 |
|
---|
| 210 | return list.length === 0 ? [] : list[list.length - 1];
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | /**
|
---|
| 214 | * Indicates if the context contains no segments.
|
---|
| 215 | * @type {boolean}
|
---|
| 216 | */
|
---|
| 217 | get empty() {
|
---|
| 218 | return this.segmentsList.length === 0;
|
---|
| 219 | }
|
---|
| 220 |
|
---|
| 221 | /**
|
---|
| 222 | * Indicates if there are any segments that are reachable.
|
---|
| 223 | * @type {boolean}
|
---|
| 224 | */
|
---|
| 225 | get reachable() {
|
---|
| 226 | const segments = this.head;
|
---|
| 227 |
|
---|
| 228 | return segments.length > 0 && segments.some(isReachable);
|
---|
| 229 | }
|
---|
| 230 |
|
---|
| 231 | /**
|
---|
| 232 | * Creates new segments in this context and appends them to the end of the
|
---|
| 233 | * already existing `CodePathSegment`s specified by `startIndex` and
|
---|
| 234 | * `endIndex`.
|
---|
| 235 | * @param {number} startIndex The index of the first segment in the context
|
---|
| 236 | * that should be specified as previous segments for the newly created segments.
|
---|
| 237 | * @param {number} endIndex The index of the last segment in the context
|
---|
| 238 | * that should be specified as previous segments for the newly created segments.
|
---|
| 239 | * @returns {Array<CodePathSegment>} An array of the newly created segments.
|
---|
| 240 | */
|
---|
| 241 | makeNext(startIndex, endIndex) {
|
---|
| 242 | return createSegments(this, startIndex, endIndex, CodePathSegment.newNext);
|
---|
| 243 | }
|
---|
| 244 |
|
---|
| 245 | /**
|
---|
| 246 | * Creates new unreachable segments in this context and appends them to the end of the
|
---|
| 247 | * already existing `CodePathSegment`s specified by `startIndex` and
|
---|
| 248 | * `endIndex`.
|
---|
| 249 | * @param {number} startIndex The index of the first segment in the context
|
---|
| 250 | * that should be specified as previous segments for the newly created segments.
|
---|
| 251 | * @param {number} endIndex The index of the last segment in the context
|
---|
| 252 | * that should be specified as previous segments for the newly created segments.
|
---|
| 253 | * @returns {Array<CodePathSegment>} An array of the newly created segments.
|
---|
| 254 | */
|
---|
| 255 | makeUnreachable(startIndex, endIndex) {
|
---|
| 256 | return createSegments(this, startIndex, endIndex, CodePathSegment.newUnreachable);
|
---|
| 257 | }
|
---|
| 258 |
|
---|
| 259 | /**
|
---|
| 260 | * Creates new segments in this context and does not append them to the end
|
---|
| 261 | * of the already existing `CodePathSegment`s specified by `startIndex` and
|
---|
| 262 | * `endIndex`. The `startIndex` and `endIndex` are only used to determine if
|
---|
| 263 | * the new segments should be reachable. If any of the segments in this range
|
---|
| 264 | * are reachable then the new segments are also reachable; otherwise, the new
|
---|
| 265 | * segments are unreachable.
|
---|
| 266 | * @param {number} startIndex The index of the first segment in the context
|
---|
| 267 | * that should be considered for reachability.
|
---|
| 268 | * @param {number} endIndex The index of the last segment in the context
|
---|
| 269 | * that should be considered for reachability.
|
---|
| 270 | * @returns {Array<CodePathSegment>} An array of the newly created segments.
|
---|
| 271 | */
|
---|
| 272 | makeDisconnected(startIndex, endIndex) {
|
---|
| 273 | return createSegments(this, startIndex, endIndex, CodePathSegment.newDisconnected);
|
---|
| 274 | }
|
---|
| 275 |
|
---|
| 276 | /**
|
---|
| 277 | * Adds segments to the head of this context.
|
---|
| 278 | * @param {Array<CodePathSegment>} segments The segments to add.
|
---|
| 279 | * @returns {void}
|
---|
| 280 | */
|
---|
| 281 | add(segments) {
|
---|
| 282 | assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
|
---|
| 283 | this.segmentsList.push(mergeExtraSegments(this, segments));
|
---|
| 284 | }
|
---|
| 285 |
|
---|
| 286 | /**
|
---|
| 287 | * Replaces the head segments with the given segments.
|
---|
| 288 | * The current head segments are removed.
|
---|
| 289 | * @param {Array<CodePathSegment>} replacementHeadSegments The new head segments.
|
---|
| 290 | * @returns {void}
|
---|
| 291 | */
|
---|
| 292 | replaceHead(replacementHeadSegments) {
|
---|
| 293 | assert(
|
---|
| 294 | replacementHeadSegments.length >= this.count,
|
---|
| 295 | `${replacementHeadSegments.length} >= ${this.count}`
|
---|
| 296 | );
|
---|
| 297 | this.segmentsList.splice(-1, 1, mergeExtraSegments(this, replacementHeadSegments));
|
---|
| 298 | }
|
---|
| 299 |
|
---|
| 300 | /**
|
---|
| 301 | * Adds all segments of a given fork context into this context.
|
---|
| 302 | * @param {ForkContext} otherForkContext The fork context to add from.
|
---|
| 303 | * @returns {void}
|
---|
| 304 | */
|
---|
| 305 | addAll(otherForkContext) {
|
---|
| 306 | assert(otherForkContext.count === this.count);
|
---|
| 307 | this.segmentsList.push(...otherForkContext.segmentsList);
|
---|
| 308 | }
|
---|
| 309 |
|
---|
| 310 | /**
|
---|
| 311 | * Clears all segments in this context.
|
---|
| 312 | * @returns {void}
|
---|
| 313 | */
|
---|
| 314 | clear() {
|
---|
| 315 | this.segmentsList = [];
|
---|
| 316 | }
|
---|
| 317 |
|
---|
| 318 | /**
|
---|
| 319 | * Creates a new root context, meaning that there are no parent
|
---|
| 320 | * fork contexts.
|
---|
| 321 | * @param {IdGenerator} idGenerator An identifier generator for segments.
|
---|
| 322 | * @returns {ForkContext} New fork context.
|
---|
| 323 | */
|
---|
| 324 | static newRoot(idGenerator) {
|
---|
| 325 | const context = new ForkContext(idGenerator, null, 1);
|
---|
| 326 |
|
---|
| 327 | context.add([CodePathSegment.newRoot(idGenerator.next())]);
|
---|
| 328 |
|
---|
| 329 | return context;
|
---|
| 330 | }
|
---|
| 331 |
|
---|
| 332 | /**
|
---|
| 333 | * Creates an empty fork context preceded by a given context.
|
---|
| 334 | * @param {ForkContext} parentContext The parent fork context.
|
---|
| 335 | * @param {boolean} shouldForkLeavingPath Indicates that we are inside of
|
---|
| 336 | * a `finally` block and should therefore fork the path that leaves
|
---|
| 337 | * `finally`.
|
---|
| 338 | * @returns {ForkContext} New fork context.
|
---|
| 339 | */
|
---|
| 340 | static newEmpty(parentContext, shouldForkLeavingPath) {
|
---|
| 341 | return new ForkContext(
|
---|
| 342 | parentContext.idGenerator,
|
---|
| 343 | parentContext,
|
---|
| 344 | (shouldForkLeavingPath ? 2 : 1) * parentContext.count
|
---|
| 345 | );
|
---|
| 346 | }
|
---|
| 347 | }
|
---|
| 348 |
|
---|
| 349 | module.exports = ForkContext;
|
---|