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

source-academy / js-slang / 24868044425

24 Apr 2026 01:47AM UTC coverage: 78.522% (+0.1%) from 78.391%
24868044425

push

github

web-flow
Error Handling and Stringify Changes (#1893)

* Modify stringify to prioritize toReplString

* Make the extract declarations helper actually work

* Add ability to change loader for source modules

* Add a new option for controlling how Source modules are loaded

* Improve typing for CSE machine

* Add ability to check if modules are loaded with the wrong Source chapter

* Refactor errors to extend from Error class

* Refactor modules errors

* Refactor parser errors

* Refactor cse machine errors

* Mostly fix error handling in the tracer

* Tidy up generator and explainer implementations for tracer

* Remove unnecessary imports and type guards

* Adjust rttc checks to be type guards instead of returning errors

* Adjust miscellanous error changes

* Add eslint rule for useless constructor

* Fix incorrect ordering for checking exceptionerrors

* Minor changes

* Run format

* Run linting

* Override the message property, but also enable noImplicitOverride to prevent accidental overriding

* Add some documentation to some errors

* Add errors to possible imports for modules

* Update getIds helper

* Minor fix to test case

* Run format

* Allow modules to try and load the context of other modules without crashing

* Fix incorrect stdlib name

* Add some more functions to the stdlib list library

* Change to use GeneralRuntimeError for list

* Revert "Change to use GeneralRuntimeError for list"

This reverts commit 642bd99e6.

* Update src/errors/errors.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Add ability to change manifest and docs importers

* Change how external builtins are defined

* Fix typings and list lib overloads

* Miscellanous changes

* Add the Source equality function to stdlib/misc

* Merge from main branch for tracer

* Add handling for the new importers

* Change errors and made redex a local variable

* Improve tracer typing

* Relocate... (continued)

3125 of 4193 branches covered (74.53%)

Branch coverage included in aggregate %.

899 of 1089 new or added lines in 96 files covered. (82.55%)

21 existing lines in 12 files now uncovered.

7031 of 8741 relevant lines covered (80.44%)

185057.72 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 { InternalRuntimeError } from '../errors/base';
4
import type { Type, Value } from '../types';
5
import { callIfFuncAndRightArgs } from './operators';
6

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

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

21
export function stringify(
22
  value: Value,
23
  indent: number | string = 2,
291✔
24
  splitlineThreshold = 80,
291✔
25
): string {
26
  if (typeof indent === 'string') {
291!
NEW
27
    throw new InternalRuntimeError(`${stringify.name} with arbitrary indent string not supported`);
×
28
  }
29
  let indentN: number = indent;
291✔
30
  if (indent > 10) {
291✔
31
    indentN = 10;
1✔
32
  }
33
  return lineTreeToString(
291✔
34
    stringDagToLineTree(valueToStringDag(value), indentN, splitlineThreshold),
35
  );
36
}
37

38
export function typeToString(type: Type): string {
39
  return niceTypeToString(type);
×
40
}
41

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

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

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

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

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

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

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

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

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

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

312
type StringDag =
313
  | TerminalStringDag
314
  | MultilineStringDag
315
  | PairStringDag
316
  | ArrayLikeStringDag
317
  | KvPairStringDag;
318

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

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

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

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

424
  function convert(v: Value): [StringDag, boolean] {
425
    if (v === null) {
2,754✔
426
      return [{ type: 'terminal', str: 'null', length: 4 }, false];
332✔
427
    } else if (v === undefined) {
2,422✔
428
      return [{ type: 'terminal', str: 'undefined', length: 9 }, false];
31✔
429
    } else if (ancestors.has(v)) {
2,391✔
430
      return [{ type: 'terminal', str: '...<circular>', length: 13 }, true];
22✔
431
    } else if (v instanceof Closure) {
2,369!
432
      return convertRepr(v.toString());
×
433
    } else if (typeof v === 'string') {
2,369✔
434
      const str = JSON.stringify(v);
501✔
435
      return [{ type: 'terminal', str, length: str.length }, false];
501✔
436
    } else if (typeof v.toReplString === 'function') {
1,868✔
437
      // callIfFuncAndRight args is necessary because if we implement toReplString as a function
438
      // in Source, it gets wrapped by the transpiler
439
      // this allows object literals to implement toReplString
440
      const reprValue = callIfFuncAndRightArgs(v.toReplString.bind(v), -1, -1, null, undefined);
27✔
441
      return convertRepr(reprValue);
27✔
442
    } else if (typeof v !== 'object') {
1,841✔
443
      return convertRepr(v.toString());
790✔
444
    } else if (ancestors.size > MAX_LIST_DISPLAY_LENGTH) {
1,051!
445
      return [{ type: 'terminal', str: '...<truncated>', length: 14 }, false];
×
446
    } else if (Array.isArray(v)) {
1,051✔
447
      if (v.length === 2) {
955✔
448
        return convertPair(v);
920✔
449
      } else {
450
        return convertArrayLike(v, v, '[', ']');
35✔
451
      }
452
    } else if (isArrayLike(v)) {
96✔
453
      return convertArrayLike(v, v.replArrayContents(), v.replPrefix, v.replSuffix);
84✔
454
    } else {
455
      // use prototype chain to check if it is literal object
456
      return Object.getPrototypeOf(v) === Object.prototype
12✔
457
        ? convertObject(v)
458
        : convertRepr(v.toString());
459
    }
460
  }
461

462
  return convert(value)[0];
292✔
463
}
464

465
interface BlockLineTree {
466
  type: 'block';
467
  prefixFirst: string;
468
  prefixRest: string;
469
  block: LineTree[];
470
  suffixRest: string;
471
  suffixLast: string;
472
}
473

474
interface LineLineTree {
475
  type: 'line';
476
  line: StringDag;
477
}
478

479
type LineTree = BlockLineTree | LineLineTree;
480

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

580
  return format(dag);
296✔
581
}
582

583
export function stringDagToSingleLine(dag: StringDag): string {
584
  function print(dag: StringDag, total: string[]): string[] {
585
    if (dag.type === 'multiline') {
2,028!
586
      throw 'Tried to format multiline string as single line string';
×
587
    } else if (dag.type === 'terminal') {
2,028✔
588
      total.push(dag.str);
1,735✔
589
    } else if (dag.type === 'pair') {
293✔
590
      total.push('[');
154✔
591
      print(dag.head, total);
154✔
592
      total.push(', ');
154✔
593
      print(dag.tail, total);
154✔
594
      total.push(']');
154✔
595
    } else if (dag.type === 'kvpair') {
139✔
596
      total.push(JSON.stringify(dag.key));
29✔
597
      total.push(': ');
29✔
598
      print(dag.value, total);
29✔
599
    } else if (dag.type === 'arraylike') {
110!
600
      total.push(dag.prefix);
110✔
601
      if (dag.elems.length > 0) {
110✔
602
        print(dag.elems[0], total);
105✔
603
      }
604
      for (let i = 1; i < dag.elems.length; i++) {
110✔
605
        total.push(', ');
299✔
606
        print(dag.elems[i], total);
299✔
607
      }
608
      total.push(dag.suffix);
110✔
609
    }
610
    return total;
2,028✔
611
  }
612

613
  return print(dag, []).join('');
1,287✔
614
}
615

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

657
  print(tree, '\n');
296✔
658

659
  return total;
296✔
660
}
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