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

source-academy / js-slang / 17011975380

16 Aug 2025 07:14PM UTC coverage: 78.56%. Remained the same
17011975380

Pull #1799

github

web-flow
Merge ab7273df7 into 20b0a35b6
Pull Request #1799: Migrate to TypeScript 5.9

2979 of 4115 branches covered (72.39%)

Branch coverage included in aggregate %.

33 of 38 new or added lines in 15 files covered. (86.84%)

1 existing line in 1 file now uncovered.

8981 of 11109 relevant lines covered (80.84%)

155600.21 hits per line

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

79.87
/src/utils/stringify.ts
1
import { MAX_LIST_DISPLAY_LENGTH } from '../constants'
76✔
2
import Closure from '../cse-machine/closure'
76✔
3
import { 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 = (
76✔
20
  value: Value,
21
  indent: number | string = 2,
218✔
22
  splitlineThreshold = 80
199✔
23
): string => {
24
  if (typeof indent === 'string') {
223!
25
    throw 'stringify with arbitrary indent string not supported'
×
26
  }
27
  let indentN: number = indent
223✔
28
  if (indent > 10) {
223✔
29
    indentN = 10
1✔
30
  }
31
  return lineTreeToString(stringDagToLineTree(valueToStringDag(value), indentN, splitlineThreshold))
223✔
32
}
33

34
export function typeToString(type: Type): string {
76✔
35
  return niceTypeToString(type)
×
36
}
37

38
function niceTypeToString(type: Type, nameMap = { _next: 0 }): string {
×
39
  function curriedTypeToString(t: Type) {
40
    return niceTypeToString(t, nameMap)
×
41
  }
42

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

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

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

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

274
interface TerminalStringDag {
275
  type: 'terminal'
276
  str: string
277
  length: number
278
}
279

280
interface MultilineStringDag {
281
  type: 'multiline'
282
  lines: string[]
283
  length: number
284
}
285

286
interface PairStringDag {
287
  type: 'pair'
288
  head: StringDag
289
  tail: StringDag
290
  length: number
291
}
292

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

301
interface KvPairStringDag {
302
  type: 'kvpair'
303
  key: string
304
  value: StringDag
305
  length: number
306
}
307

308
type StringDag =
309
  | TerminalStringDag
310
  | MultilineStringDag
311
  | PairStringDag
312
  | ArrayLikeStringDag
313
  | KvPairStringDag
314

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

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

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

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

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

454
  return convert(value)[0]
224✔
455
}
456

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

466
interface LineLineTree {
467
  type: 'line'
468
  line: StringDag
469
}
470

471
type LineTree = BlockLineTree | LineLineTree
472

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

572
  return format(dag)
228✔
573
}
574

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

605
  return print(dag, []).join('')
1,218✔
606
}
607

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

649
  print(tree, '\n')
228✔
650

651
  return total
228✔
652
}
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

© 2025 Coveralls, Inc