• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / js-slang / 23995741899

05 Apr 2026 06:14AM UTC coverage: 77.093% (+0.002%) from 77.091%
23995741899

push

github

web-flow
Upgrade to TypeScript 6 and Prettier improvements (#1936)

* Upgrade TypeScript to v6

* Fix import source

* Fix tsconfig

* Fix preexisting type errors

* Remove scm-slang

* Bump node types

* Fix tsconfig

* Fix node types specifier

* Enable trailing commas

* Enable semicolons

* Check and commit files with changed line numbers

* Update Yarn to 4.13.0

* Remove unneeded sicp package deps

3112 of 4282 branches covered (72.68%)

Branch coverage included in aggregate %.

3761 of 5218 new or added lines in 152 files covered. (72.08%)

26 existing lines in 9 files now uncovered.

7136 of 9011 relevant lines covered (79.19%)

175254.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.92
/src/utils/stringify.ts
1
import { MAX_LIST_DISPLAY_LENGTH } from '../constants';
2
import Closure from '../cse-machine/closure';
3
import type { Type, Value } from '../types';
4

5
export interface ArrayLike {
6
  replPrefix: string;
7
  replSuffix: string;
8
  replArrayContents: () => Value[];
9
}
10

11
function isArrayLike(v: Value) {
12
  return (
96✔
13
    typeof v.replPrefix === 'string' &&
264✔
14
    typeof v.replSuffix === 'string' &&
15
    typeof v.replArrayContents === 'function'
16
  );
17
}
18

19
export const stringify = (
65✔
20
  value: Value,
21
  indent: number | string = 2,
231✔
22
  splitlineThreshold = 80,
231✔
23
): string => {
24
  if (typeof indent === 'string') {
231!
NEW
25
    throw 'stringify with arbitrary indent string not supported';
×
26
  }
27
  let indentN: number = indent;
231✔
28
  if (indent > 10) {
231✔
29
    indentN = 10;
1✔
30
  }
31
  return lineTreeToString(
231✔
32
    stringDagToLineTree(valueToStringDag(value), indentN, splitlineThreshold),
33
  );
34
};
35

36
export function typeToString(type: Type): string {
NEW
37
  return niceTypeToString(type);
×
38
}
39

40
function niceTypeToString(type: Type, nameMap = { _next: 0 }): string {
×
41
  function curriedTypeToString(t: Type) {
NEW
42
    return niceTypeToString(t, nameMap);
×
43
  }
44

45
  switch (type.kind) {
×
46
    case 'primitive':
NEW
47
      return type.name;
×
48
    case 'variable':
49
      if (type.constraint && type.constraint !== 'none') {
×
NEW
50
        return type.constraint;
×
51
      }
52
      if (!(type.name in nameMap)) {
×
53
        // type name is not in map, so add it
NEW
54
        (nameMap as any)[type.name] = 'T' + nameMap._next++;
×
55
      }
NEW
56
      return (nameMap as any)[type.name];
×
57
    case 'list':
NEW
58
      return `List<${curriedTypeToString(type.elementType)}>`;
×
59
    case 'array':
NEW
60
      return `Array<${curriedTypeToString(type.elementType)}>`;
×
61
    case 'pair':
NEW
62
      const headType = curriedTypeToString(type.headType);
×
63
      // convert [T1 , List<T1>] back to List<T1>
64
      if (
×
65
        type.tailType.kind === 'list' &&
×
66
        headType === curriedTypeToString(type.tailType.elementType)
67
      )
NEW
68
        return `List<${headType}>`;
×
NEW
69
      return `[${curriedTypeToString(type.headType)}, ${curriedTypeToString(type.tailType)}]`;
×
70
    case 'function':
NEW
71
      let parametersString = type.parameterTypes.map(curriedTypeToString).join(', ');
×
72
      if (type.parameterTypes.length !== 1 || type.parameterTypes[0].kind === 'function') {
×
NEW
73
        parametersString = `(${parametersString})`;
×
74
      }
NEW
75
      return `${parametersString} -> ${curriedTypeToString(type.returnType)}`;
×
76
    default:
NEW
77
      return 'Unable to infer type';
×
78
  }
79
}
80

81
/**
82
 *
83
 * stringify problem overview
84
 *
85
 * We need a fast stringify function so that display calls are fast.
86
 * However, we also want features like nice formatting so that it's easy to read the output.
87
 *
88
 * Here's a sample of the kind of output we want:
89
 *
90
 *     > build_list(10, x => build_list(10, x=>x));
91
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
92
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
93
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
94
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
95
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
96
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
97
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
98
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
99
 *     [ [0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]],
100
 *     [[0, [1, [2, [3, [4, [5, [6, [7, [8, [9, null]]]]]]]]]], null]]]]]]]]]]
101
 *
102
 * Notice that relatively short lists that can fit on a single line
103
 * are simply placed on the same line.
104
 * Pairs have the first element indented, but the second element on the same level.
105
 * This allows lists to be displayed vertically.
106
 *
107
 *     > x => { return x; };
108
 *     x => {
109
 *       return x;
110
 *     }
111
 *
112
 * Functions simply have .toString() called on them.
113
 * However, notice that this sometimes creates a multiline string.
114
 * This means that when we have a pair that contains a multiline function,
115
 * we should put the two elements on different lines, even if the total number of characters is small:
116
 *
117
 *     > pair(x => { return x; }, 0);
118
 *     [ x => {
119
 *         return x;
120
 *       },
121
 *     0]
122
 *
123
 * Notice that the multiline string needs two spaces added to the start of every line, not just the first line.
124
 * Also notice that the opening bracket '[' takes up residence inside the indentation area,
125
 * so that the element itself is fully on the same indentation level.
126
 *
127
 * Furthermore, deeper indentation levels should just work:
128
 *
129
 *     > pair(pair(x => { return x; }, 0), 0);
130
 *     [ [ x => {
131
 *           return x;
132
 *         },
133
 *       0],
134
 *     0]
135
 *
136
 * Importantly, note we have to be able to do this indentation quickly, with a linear time algorithm.
137
 * Thus, simply doing the indentation every time we need to would be too slow,
138
 * as it may take O(n) time per indentation, and so O(n^2) time overall.
139
 *
140
 * Arrays are not the same as pairs, and they indent every element to the same level:
141
 *     > [1, 2, x => { return x; }];
142
 *     [ 1,
143
 *       2,
144
 *       x => {
145
 *         return x;
146
 *       }]
147
 *
148
 * Some data structures are "array-like",
149
 * so we can re-use the same logic for arrays, objects, and lists.
150
 *
151
 *     > display_list(list(1, list(2, 3), x => { return x; }));
152
 *     list(1,
153
 *          list(2, 3),
154
 *          x => {
155
 *            return x;
156
 *          })
157
 *
158
 *     > { a: 1, b: true, c: () => 1, d: { e: 5, f: 6 }, g: 0, h: 0, i: 0, j: 0, k: 0, l: 0, m: 0, n: 0};
159
 *     { "a": 1,
160
 *       "b": true,
161
 *       "c": () => 1,
162
 *       "d": {"e": 5, "f": 6},
163
 *       "g": 0,
164
 *       "h": 0,
165
 *       "i": 0,
166
 *       "j": 0,
167
 *       "k": 0,
168
 *       "l": 0,
169
 *       "m": 0,
170
 *       "n": 0}
171
 *
172
 * Notice the way that just like pairs,
173
 * short lists/objects are placed on the same line,
174
 * while longer lists/objects, or ones that are necessarily multiline,
175
 * are split into multiple lines, with one element per line.
176
 *
177
 * It is also possible to create data structures with large amounts of sharing
178
 * as well as cycles. Here is an example of a cyclic data structure with sharing:
179
 *
180
 *     > const x = pair(1, 'y');
181
 *     > const y = pair(2, 'z');
182
 *     > const z = pair(3, 'x');
183
 *     > set_tail(x, y);
184
 *     > set_tail(y, z);
185
 *     > set_tail(z, x);
186
 *     > display_list(list(x, y, z));
187
 *      list([1, [2, [3, ...<circular>]]],
188
 *           [2, [3, [1, ...<circular>]]],
189
 *           [3, [1, [2, ...<circular>]]])
190
 *
191
 * It might be difficult to maximise sharing in the face of cycles,
192
 * because when we cut a cycle, we have to replace a node somewhere with "...<circular>"
193
 * However, doing this means that a second pointer into this cyclic data structure
194
 * might have the "...<circular>" placed too early,
195
 * so we need to re-generate a different representation of the cyclic data structure for every possible root.
196
 * Otherwise, a naive memoization approach might generate the following output:
197
 *
198
 *      list([1, [2, [3, ...<circular>]]],
199
 *           [2, [3, ...<circular>]],
200
 *           [3, ...<circular>])
201
 *
202
 * which might be confusing if we interpret this to mean that the cycles have different lengths,
203
 * while in fact they each have length 3.
204
 *
205
 * It would be good if we can maximise sharing so that as much of the workload
206
 * scales with respect to the size of the input as opposed to the output.
207
 *
208
 * In summary, here are the list of challenges:
209
 *
210
 *  1) Avoid repeated string concatenation.
211
 *  2) Also avoid repeated multiline string indenting.
212
 *  3) Intelligently format values as either single line or multiline,
213
 *     depending on whether it contains any multiline elements,
214
 *     or whether it's too long and should be broken up.
215
 *  4) Indentation columns have to expand to fit array-like prefix strings,
216
 *     when they are longer than the indentation size (see display_list examples).
217
 *  5) Correctly handle cyclic data structures.
218
 *  6) Ideally, maximise re-use of shared data structures.
219
 *
220
 */
221

222
/**
223
 *
224
 * stringify notes on other implementations
225
 *
226
 * Python's pretty printer (pprint) has a strategy of stringifying each value at most twice.
227
 * The first time, it will assume that the value fits on a single line and simply calls repr.
228
 * If the repr is too long, then it'll format the value with pretty print rules
229
 * in multiple lines and with nice indentation.
230
 *
231
 * Theoretically, the repr can be memoized and so repr is called at most once on every value.
232
 * (In practice, I don't know if they actually do this)
233
 *
234
 * This gives us a nice bound of every value being repr'd at most once,
235
 * and every position in the output is pretty printed at most once.
236
 * With the string builder pattern, each can individually be done in O(n) time,
237
 * and so the algorithm as a whole runs in O(n) time.
238
 *
239
 */
240

241
/**
242
 *
243
 * stringify high level algorithm overview
244
 *
245
 * The algorithm we'll use is not quite the same as Python's algorithm but should work better or just as well.
246
 *
247
 * First we solve the problem of memoizing cyclic data structures by not solving the problem.
248
 * We keep a flag that indicates whether a particular value is cyclic, and only memoize values that are acyclic.
249
 * It is possible to use a strongly connected components style algorithm to speed this up,
250
 * but I haven't implemented it yet to keep the algorithm simple,
251
 * and it's still fast enough even in the cyclic case.
252
 *
253
 * This first step converts the value graph into a printable DAG.
254
 * To assemble this DAG into a single string, we have a second memo table
255
 * mapping each node of the printable DAG to its corresponding string representation,
256
 * but where lines are split and indentation is represented with a wrapper object that reifies
257
 * the action of incrementing the indentation level.
258
 * By handling the actual calculation of indentation levels outside this representation,
259
 * we can re-use string representations that are shared among different parts of the graph,
260
 * which may be on different indentation levels.
261
 *
262
 * With this data structure, we can easily build the partial string representations bottom up,
263
 * deferring the final calculation and printing of indentation levels to a straightforward final pass.
264
 *
265
 * In summary, here are the passes we have to make:
266
 *
267
 * - value graph -> string dag (resolves cycles, stringifies terminal nodes, leaves nonterminals abstract, computes lengths)
268
 * - string dag -> line based representation (pretty prints "main" content to lines, while leaving indentation / prefixes in general abstract)
269
 * - line based representation -> final string (basically assemble all the indentation strings together with the content strings)
270
 *
271
 * Memoization is added at every level so printing of extremely shared data structures
272
 * approaches the speed of string concatenation/substring in the limit.
273
 *
274
 */
275

276
interface TerminalStringDag {
277
  type: 'terminal';
278
  str: string;
279
  length: number;
280
}
281

282
interface MultilineStringDag {
283
  type: 'multiline';
284
  lines: string[];
285
  length: number;
286
}
287

288
interface PairStringDag {
289
  type: 'pair';
290
  head: StringDag;
291
  tail: StringDag;
292
  length: number;
293
}
294

295
interface ArrayLikeStringDag {
296
  type: 'arraylike';
297
  prefix: string;
298
  elems: StringDag[];
299
  suffix: string;
300
  length: number;
301
}
302

303
interface KvPairStringDag {
304
  type: 'kvpair';
305
  key: string;
306
  value: StringDag;
307
  length: number;
308
}
309

310
type StringDag =
311
  | TerminalStringDag
312
  | MultilineStringDag
313
  | PairStringDag
314
  | ArrayLikeStringDag
315
  | KvPairStringDag;
316

317
export function valueToStringDag(value: Value): StringDag {
318
  const ancestors: Map<Value, number> = new Map();
232✔
319
  const memo: Map<Value, StringDag> = new Map();
232✔
320
  function convertPair(value: Value): [StringDag, boolean] {
321
    const memoResult = memo.get(value);
919✔
322
    if (memoResult !== undefined) {
919!
NEW
323
      return [memoResult, false];
×
324
    }
325
    ancestors.set(value, ancestors.size);
919✔
326
    const elems: Value[] = value;
919✔
327
    const [headDag, headIsCircular] = convert(elems[0]);
919✔
328
    const [tailDag, tailIsCircular] = convert(elems[1]);
919✔
329
    const isCircular = headIsCircular || tailIsCircular;
919✔
330
    ancestors.delete(value);
919✔
331
    const result: StringDag = {
919✔
332
      type: 'pair',
333
      head: headDag,
334
      tail: tailDag,
335
      length: headDag.length + tailDag.length + 4,
336
    };
337
    if (!isCircular) {
919✔
338
      memo.set(value, result);
862✔
339
    }
340
    return [result, isCircular];
919✔
341
  }
342

343
  function convertArrayLike(
344
    value: Value,
345
    elems: Value[],
346
    prefix: string,
347
    suffix: string,
348
  ): [StringDag, boolean] {
349
    const memoResult = memo.get(value);
118✔
350
    if (memoResult !== undefined) {
118✔
351
      return [memoResult, false];
1✔
352
    }
353
    ancestors.set(value, ancestors.size);
117✔
354
    const converted = elems.map(convert);
117✔
355
    let length = prefix.length + suffix.length + Math.max(0, converted.length - 1) * 2;
117✔
356
    let isCircular = false;
117✔
357
    for (let i = 0; i < converted.length; i++) {
117✔
358
      if (converted[i] == null) {
590✔
359
        // the `elems.map` above preserves the sparseness of the array
360
        converted[i] = convert(undefined);
5✔
361
      }
362
      length += converted[i][0].length;
590✔
363
      isCircular ||= converted[i][1];
590✔
364
    }
365
    ancestors.delete(value);
117✔
366
    const result: StringDag = {
117✔
367
      type: 'arraylike',
368
      elems: converted.map(c => c[0]),
590✔
369
      prefix,
370
      suffix,
371
      length,
372
    };
373
    if (!isCircular) {
117✔
374
      memo.set(value, result);
112✔
375
    }
376
    return [result, isCircular];
117✔
377
  }
378

379
  function convertObject(value: Value): [StringDag, boolean] {
380
    const memoResult = memo.get(value);
9✔
381
    if (memoResult !== undefined) {
9!
NEW
382
      return [memoResult, false];
×
383
    }
384
    ancestors.set(value, ancestors.size);
9✔
385
    const entries = Object.entries(value);
9✔
386
    const converted = entries.map(kv => convert(kv[1]));
29✔
387
    let length = 2 + Math.max(0, entries.length - 1) * 2 + entries.length * 2;
9✔
388
    let isCircular = false;
9✔
389
    const kvpairs: StringDag[] = [];
9✔
390
    for (let i = 0; i < converted.length; i++) {
9✔
391
      length += entries[i][0].length;
29✔
392
      length += converted[i].length;
29✔
393
      isCircular ||= converted[i][1];
29✔
394
      kvpairs.push({
29✔
395
        type: 'kvpair',
396
        key: entries[i][0],
397
        value: converted[i][0],
398
        length: converted[i][0].length + entries[i][0].length,
399
      });
400
    }
401
    ancestors.delete(value);
9✔
402
    const result: StringDag = {
9✔
403
      type: 'arraylike',
404
      elems: kvpairs,
405
      prefix: '{',
406
      suffix: '}',
407
      length,
408
    };
409
    if (!isCircular) {
9✔
410
      memo.set(value, result);
8✔
411
    }
412
    return [result, isCircular];
9✔
413
  }
414

415
  function convertRepr(repr: string): [StringDag, boolean] {
416
    const lines: string[] = repr.split('\n');
773✔
417
    return lines.length === 1
773✔
418
      ? [{ type: 'terminal', str: lines[0], length: lines[0].length }, false]
419
      : [{ type: 'multiline', lines, length: Infinity }, false];
420
  }
421

422
  function convert(v: Value): [StringDag, boolean] {
423
    if (v === null) {
2,689✔
424
      return [{ type: 'terminal', str: 'null', length: 4 }, false];
330✔
425
    } else if (v === undefined) {
2,359✔
426
      return [{ type: 'terminal', str: 'undefined', length: 9 }, false];
31✔
427
    } else if (ancestors.has(v)) {
2,328✔
428
      return [{ type: 'terminal', str: '...<circular>', length: 13 }, true];
22✔
429
    } else if (v instanceof Closure) {
2,306!
NEW
430
      return convertRepr(v.toString());
×
431
    } else if (typeof v === 'string') {
2,306✔
432
      const str = JSON.stringify(v);
487✔
433
      return [{ type: 'terminal', str, length: str.length }, false];
487✔
434
    } else if (typeof v !== 'object') {
1,819✔
435
      return convertRepr(v.toString());
769✔
436
    } else if (ancestors.size > MAX_LIST_DISPLAY_LENGTH) {
1,050!
NEW
437
      return [{ type: 'terminal', str: '...<truncated>', length: 14 }, false];
×
438
    } else if (typeof v.toReplString === 'function') {
1,050✔
439
      return convertRepr(v.toReplString());
1✔
440
    } else if (Array.isArray(v)) {
1,049✔
441
      if (v.length === 2) {
953✔
442
        return convertPair(v);
919✔
443
      } else {
444
        return convertArrayLike(v, v, '[', ']');
34✔
445
      }
446
    } else if (isArrayLike(v)) {
96✔
447
      return convertArrayLike(v, v.replArrayContents(), v.replPrefix, v.replSuffix);
84✔
448
    } else {
449
      // use prototype chain to check if it is literal object
450
      return Object.getPrototypeOf(v) === Object.prototype
12✔
451
        ? convertObject(v)
452
        : convertRepr(v.toString());
453
    }
454
  }
455

456
  return convert(value)[0];
232✔
457
}
458

459
interface BlockLineTree {
460
  type: 'block';
461
  prefixFirst: string;
462
  prefixRest: string;
463
  block: LineTree[];
464
  suffixRest: string;
465
  suffixLast: string;
466
}
467

468
interface LineLineTree {
469
  type: 'line';
470
  line: StringDag;
471
}
472

473
type LineTree = BlockLineTree | LineLineTree;
474

475
export function stringDagToLineTree(
476
  dag: StringDag,
477
  indent: number,
478
  splitlineThreshold: number,
479
): LineTree {
480
  // precompute some useful strings
481
  const indentSpacesMinusOne = ' '.repeat(Math.max(0, indent - 1));
236✔
482
  const bracketAndIndentSpacesMinusOne = '[' + indentSpacesMinusOne;
236✔
483
  const memo: Map<StringDag, LineTree> = new Map();
236✔
484
  function format(dag: StringDag): LineTree {
485
    const memoResult = memo.get(dag);
2,734✔
486
    if (memoResult !== undefined) {
2,734✔
487
      return memoResult;
1✔
488
    }
489
    let result: LineTree;
490
    if (dag.type === 'terminal') {
2,733✔
491
      result = { type: 'line', line: dag };
1,647✔
492
    } else if (dag.type === 'multiline') {
1,086✔
493
      result = {
6✔
494
        type: 'block',
495
        prefixFirst: '',
496
        prefixRest: '',
497
        block: dag.lines.map(s => ({
20✔
498
          type: 'line',
499
          line: { type: 'terminal', str: s, length: s.length },
500
        })),
501
        suffixRest: '',
502
        suffixLast: '',
503
      };
504
    } else if (dag.type === 'pair') {
1,080✔
505
      const headTree = format(dag.head);
925✔
506
      const tailTree = format(dag.tail);
925✔
507
      // - 2 is there for backward compatibility
508
      if (
925✔
509
        dag.length - 2 > splitlineThreshold ||
1,235✔
510
        headTree.type !== 'line' ||
511
        tailTree.type !== 'line'
512
      ) {
513
        result = {
770✔
514
          type: 'block',
515
          prefixFirst: bracketAndIndentSpacesMinusOne,
516
          prefixRest: '',
517
          block: [headTree, tailTree],
518
          suffixRest: ',',
519
          suffixLast: ']',
520
        };
521
      } else {
522
        result = {
155✔
523
          type: 'line',
524
          line: dag,
525
        };
526
      }
527
    } else if (dag.type === 'arraylike') {
155✔
528
      const elemTrees = dag.elems.map(format);
126✔
529
      if (
126✔
530
        dag.length - dag.prefix.length - dag.suffix.length > splitlineThreshold ||
234✔
531
        elemTrees.some(t => t.type !== 'line')
400✔
532
      ) {
533
        result = {
18✔
534
          type: 'block',
535
          prefixFirst: dag.prefix + ' '.repeat(Math.max(0, indent - dag.prefix.length)),
536
          prefixRest: ' '.repeat(Math.max(dag.prefix.length, indent)),
537
          block: elemTrees,
538
          suffixRest: ',',
539
          suffixLast: dag.suffix,
540
        };
541
      } else {
542
        result = {
108✔
543
          type: 'line',
544
          line: dag,
545
        };
546
      }
547
    } else if (dag.type === 'kvpair') {
29!
548
      const valueTree = format(dag.value);
29✔
549
      if (dag.length > splitlineThreshold || valueTree.type !== 'line') {
29!
550
        result = {
×
551
          type: 'block',
552
          prefixFirst: '',
553
          prefixRest: '',
554
          block: [
555
            { type: 'line', line: { type: 'terminal', str: JSON.stringify(dag.key), length: 0 } },
556
            valueTree,
557
          ],
558
          suffixRest: ':',
559
          suffixLast: '',
560
        };
561
      } else {
562
        result = {
29✔
563
          type: 'line',
564
          line: dag,
565
        };
566
      }
567
    } else {
NEW
568
      throw 'up';
×
569
    }
570
    memo.set(dag, result);
2,733✔
571
    return result;
2,733✔
572
  }
573

574
  return format(dag);
236✔
575
}
576

577
export function stringDagToSingleLine(dag: StringDag): string {
578
  function print(dag: StringDag, total: string[]): string[] {
579
    if (dag.type === 'multiline') {
1,960!
NEW
580
      throw 'Tried to format multiline string as single line string';
×
581
    } else if (dag.type === 'terminal') {
1,960✔
582
      total.push(dag.str);
1,669✔
583
    } else if (dag.type === 'pair') {
291✔
584
      total.push('[');
153✔
585
      print(dag.head, total);
153✔
586
      total.push(', ');
153✔
587
      print(dag.tail, total);
153✔
588
      total.push(']');
153✔
589
    } else if (dag.type === 'kvpair') {
138✔
590
      total.push(JSON.stringify(dag.key));
29✔
591
      total.push(': ');
29✔
592
      print(dag.value, total);
29✔
593
    } else if (dag.type === 'arraylike') {
109!
594
      total.push(dag.prefix);
109✔
595
      if (dag.elems.length > 0) {
109✔
596
        print(dag.elems[0], total);
104✔
597
      }
598
      for (let i = 1; i < dag.elems.length; i++) {
109✔
599
        total.push(', ');
297✔
600
        print(dag.elems[i], total);
297✔
601
      }
602
      total.push(dag.suffix);
109✔
603
    }
604
    return total;
1,960✔
605
  }
606

607
  return print(dag, []).join('');
1,224✔
608
}
609

610
export function lineTreeToString(tree: LineTree): string {
611
  let total = '';
236✔
612
  const stringDagToLineMemo: Map<StringDag, string> = new Map();
236✔
613
  const stringDagToMultilineMemo: Map<LineTree, Map<number, [number, number]>> = new Map();
236✔
614
  function print(tree: LineTree, lineSep: string) {
615
    const multilineMemoResult = stringDagToMultilineMemo.get(tree);
2,020✔
616
    if (multilineMemoResult !== undefined) {
2,020!
NEW
617
      const startEnd = multilineMemoResult.get(lineSep.length);
×
618
      if (startEnd !== undefined) {
×
NEW
619
        total += total.substring(startEnd[0], startEnd[1]);
×
NEW
620
        return;
×
621
      }
622
    }
623
    const start = total.length;
2,020✔
624
    if (tree.type === 'line') {
2,020✔
625
      if (!stringDagToLineMemo.has(tree.line)) {
1,224!
626
        stringDagToLineMemo.set(tree.line, stringDagToSingleLine(tree.line));
1,224✔
627
      }
628
      total += stringDagToLineMemo.get(tree.line)!;
1,224✔
629
    } else if (tree.type === 'block') {
796!
630
      total += tree.prefixFirst;
796✔
631
      const indentedLineSepFirst = lineSep + ' '.repeat(tree.prefixFirst.length);
796✔
632
      const indentedLineSepRest = lineSep + tree.prefixRest;
796✔
633
      print(tree.block[0], indentedLineSepFirst);
796✔
634
      for (let i = 1; i < tree.block.length; i++) {
796✔
635
        total += tree.suffixRest;
988✔
636
        total += indentedLineSepRest;
988✔
637
        print(tree.block[i], indentedLineSepRest);
988✔
638
      }
639
      total += tree.suffixLast;
796✔
640
    }
641
    const end = total.length;
2,020✔
642
    if (multilineMemoResult === undefined) {
2,020!
643
      const newmap = new Map();
2,020✔
644
      newmap.set(lineSep.length, [start, end]);
2,020✔
645
      stringDagToMultilineMemo.set(tree, newmap);
2,020✔
646
    } else {
NEW
647
      multilineMemoResult.set(lineSep.length, [start, end]);
×
648
    }
649
  }
650

651
  print(tree, '\n');
236✔
652

653
  return total;
236✔
654
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc