OILS / data_lang / pretty.py View on Github | oilshell.org

744 lines, 357 significant
1#!/usr/bin/env python2
2"""
3Pretty print Oils values (and later other data/languages as well).
4
5Pretty printing means intelligently choosing whitespace including indentation
6and newline placement, to attempt to display data nicely while staying within a
7maximum line width.
8"""
9
10# ~~~ Architecture ~~~
11#
12# Based on a version of the algorithm from Wadler's "A Prettier Printer".
13#
14# Pretty printing proceeds in two phases:
15#
16# 1. Convert the thing you want to print into a `doc`.
17# 2. Print the `doc` using a standard algorithm.
18#
19# This separation keeps the details of the data you want to print separate from
20# the printing algorithm.
21
22# ~~~ Pretty Printing Overview ~~~
23#
24# If you're just using this file, you don't need to know how pretty printing
25# works. Just call `PrettyPrinter().PrintValue()`. However if you want to change
26# or extend how values are printed, you'll need to know, so here's an overview.
27#
28# You may want to first read Walder's "A Prettier Printer", which this is based
29# off of:
30# https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
31#
32# Some additional reading, though only tangentially related:
33#
34# - https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
35# - https://lindig.github.io/papers/strictly-pretty-2000.pdf
36# - https://justinpombrio.net/2024/02/23/a-twist-on-Wadlers-printer.html
37# - https://lobste.rs/s/1r0aak/twist_on_wadler_s_printer
38# - https://lobste.rs/s/aevptj/why_is_prettier_rock_solid
39#
40# ~ Constructors ~
41#
42# There are just a few constructors for `doc`, from which everything else is
43# built from.
44#
45# Text(string) prints like:
46# |string
47#
48# Break(string) prints like:
49# |string
50# or like a newline:
51# |
52# |
53# (It does the latter if printed in "flat" mode, and the former otherwise. See
54# Group for details.)
55#
56# Concat(a, b) prints like:
57# |AAAAA
58# |AAABBB
59# |BBBBB
60#
61# Indent(3, a) prints like:
62# |AAAAA
63# | AAAAA
64# | AAAAA
65#
66# Group(a) makes a decision. It either:
67# - Prints `a` "flat", meaning that (i) every Break inside of it is printed as a
68# string instead of as a newline, and (ii) every Group nested inside of it is
69# printed flat.
70# - Prints `a` normally, meaning that (i) the Breaks inside of it are printed as
71# newlines, and (ii) the Groups inside of it make their own decision about
72# whether to be flat.
73# It makes this decision greedily. If the current line would not overflow if the
74# group printed flat, then it will print flat. This takes into account not only
75# the group itself, but the content before and after it on the same line.
76#
77# IfFlat(a, b) prints a if in flat mode or b otherwise.
78#
79# Flat(a) prints a in flat mode. You should generally not need to use it.
80#
81# ~ Measures ~
82#
83# The algorithm used here is close to the one originally described by Wadler,
84# but it precomputes a "measure" for each node in the `doc`. This "measure"
85# allows each Groups to decide whether to print flat or not without needing to
86# look ahead per Wadler's algorithm. A measure has two pieces of information:
87#
88# - Measure.flat is the width of the doc if it's printed flat.
89# - Measure.nonflat is the width of the doc until the _earliest possible_
90# newline, or -1 if it doesn't contain a Break.
91#
92# Measures are used in two steps. First, they're computed bottom-up on the
93# `doc`, measuring the size of each node. Later, PrintDoc() stores a measure in
94# each DocFragment. These Measures measure something different: the width from
95# the doc _to the end of the entire doc tree_. This second set of Measures (the
96# ones in the DocFragments) are computed top-down, and they're used to decide
97# for each Group whether to use flat mode or not, without needing to scan ahead.
98
99from __future__ import print_function
100
101import math
102
103from _devbuild.gen.pretty_asdl import doc, doc_e, DocFragment, Measure, MeasuredDoc
104from _devbuild.gen.value_asdl import value, value_e, value_t, value_str
105from data_lang import j8
106from data_lang import j8_lite
107from core import ansi
108from frontend import match
109from mycpp import mops
110from mycpp.mylib import log, tagswitch, BufWriter, iteritems
111from typing import cast, List, Dict
112import libc
113
114_ = log
115
116
117def ValType(val):
118 # type: (value_t) -> str
119 """Returns a user-facing string like Int, Eggex, BashArray, etc."""
120 return value_str(val.tag(), dot=False)
121
122
123def _FloatString(fl):
124 # type: (float) -> str
125
126 # Print in YSH syntax, similar to data_lang/j8.py
127 if math.isinf(fl):
128 s = 'INFINITY'
129 if fl < 0:
130 s = '-' + s
131 elif math.isnan(fl):
132 s = 'NAN'
133 else:
134 s = str(fl)
135 return s
136
137
138################
139# Measurements #
140################
141
142
143def TryUnicodeWidth(s):
144 # type: (str) -> int
145 try:
146 width = libc.wcswidth(s)
147 except UnicodeError:
148 # e.g. en_US.UTF-8 locale missing, just return the number of bytes
149 width = len(s)
150
151 if width == -1: # non-printable wide char
152 return len(s)
153
154 return width
155
156
157def _EmptyMeasure():
158 # type: () -> Measure
159 """The measure of an empty doc."""
160 return Measure(0, -1)
161
162
163def _FlattenMeasure(measure):
164 # type: (Measure) -> Measure
165 """The measure if its document is rendered flat."""
166 return Measure(measure.flat, -1)
167
168
169def _ConcatMeasure(m1, m2):
170 # type: (Measure, Measure) -> Measure
171 """Compute the measure of concatenated docs.
172
173 If m1 and m2 are the measures of doc1 and doc2,
174 then _ConcatMeasure(m1, m2) is the measure of doc.Concat([doc1, doc2]).
175 This concatenation is associative but not commutative."""
176 if m1.nonflat != -1:
177 return Measure(m1.flat + m2.flat, m1.nonflat)
178 elif m2.nonflat != -1:
179 return Measure(m1.flat + m2.flat, m1.flat + m2.nonflat)
180 else:
181 return Measure(m1.flat + m2.flat, -1)
182
183
184def _SuffixLen(measure):
185 # type: (Measure) -> int
186 """The width until the earliest possible newline, or end of document."""
187 if measure.nonflat != -1:
188 return measure.nonflat
189 else:
190 return measure.flat
191
192
193####################
194# Doc Construction #
195####################
196
197
198def _Text(string):
199 # type: (str) -> MeasuredDoc
200 """Print `string` (which must not contain a newline)."""
201 return MeasuredDoc(doc.Text(string), Measure(TryUnicodeWidth(string), -1))
202
203
204def _Break(string):
205 # type: (str) -> MeasuredDoc
206 """If in `flat` mode, print `string`, otherwise print `\n`."""
207 return MeasuredDoc(doc.Break(string), Measure(TryUnicodeWidth(string), 0))
208
209
210def _Indent(indent, mdoc):
211 # type: (int, MeasuredDoc) -> MeasuredDoc
212 """Add `indent` spaces after every newline in `mdoc`."""
213 return MeasuredDoc(doc.Indent(indent, mdoc), mdoc.measure)
214
215
216def _Concat(mdocs):
217 # type: (List[MeasuredDoc]) -> MeasuredDoc
218 """Print the mdocs in order (with no space in between)."""
219 measure = _EmptyMeasure()
220 for mdoc in mdocs:
221 measure = _ConcatMeasure(measure, mdoc.measure)
222 return MeasuredDoc(doc.Concat(mdocs), measure)
223
224
225def _Group(mdoc):
226 # type: (MeasuredDoc) -> MeasuredDoc
227 """Print `mdoc`. Use flat mode if `mdoc` will fit on the current line."""
228 return MeasuredDoc(doc.Group(mdoc), mdoc.measure)
229
230
231def _IfFlat(flat_mdoc, nonflat_mdoc):
232 # type: (MeasuredDoc, MeasuredDoc) -> MeasuredDoc
233 """If in flat mode, print `flat_mdoc` otherwise print `nonflat_mdoc`."""
234 return MeasuredDoc(
235 doc.IfFlat(flat_mdoc, nonflat_mdoc),
236 Measure(flat_mdoc.measure.flat, nonflat_mdoc.measure.nonflat))
237
238
239def _Flat(mdoc):
240 # type: (MeasuredDoc) -> MeasuredDoc
241 """Prints `mdoc` in flat mode."""
242 return MeasuredDoc(doc.Flat(mdoc), _FlattenMeasure(mdoc.measure))
243
244
245###################
246# Pretty Printing #
247###################
248
249
250class PrettyPrinter(object):
251 """Pretty print an Oils value."""
252
253 def __init__(self, max_width):
254 # type: (int) -> None
255 """Construct a PrettyPrinter with default configuration options.
256
257 Use the Set*() methods for configuration before printing."""
258 self.max_width = max_width
259
260 def _Fits(self, prefix_len, group, suffix_measure):
261 # type: (int, doc.Group, Measure) -> bool
262 """Will `group` fit flat on the current line?"""
263 measure = _ConcatMeasure(_FlattenMeasure(group.mdoc.measure),
264 suffix_measure)
265 return prefix_len + _SuffixLen(measure) <= self.max_width
266
267 def PrintDoc(self, document, buf):
268 # type: (MeasuredDoc, BufWriter) -> None
269 """Pretty print a `pretty.doc` to a BufWriter."""
270
271 # The width of the text we've printed so far on the current line
272 prefix_len = 0
273 # A _stack_ of document fragments to print. Each fragment contains:
274 # - A MeasuredDoc (doc node and its measure, saying how "big" it is)
275 # - The indentation level to print this doc node at.
276 # - Is this doc node being printed in flat mode?
277 # - The measure _from just after the doc node, to the end of the entire document_.
278 # (Call this the suffix_measure)
279 fragments = [DocFragment(_Group(document), 0, False, _EmptyMeasure())]
280
281 while len(fragments) > 0:
282 frag = fragments.pop()
283 with tagswitch(frag.mdoc.doc) as case:
284
285 if case(doc_e.Text):
286 text = cast(doc.Text, frag.mdoc.doc)
287 buf.write(text.string)
288 prefix_len += frag.mdoc.measure.flat
289
290 elif case(doc_e.Break):
291 if frag.is_flat:
292 break_str = cast(doc.Break, frag.mdoc.doc).string
293 buf.write(break_str)
294 prefix_len += frag.mdoc.measure.flat
295 else:
296 buf.write('\n')
297 buf.write_spaces(frag.indent)
298 prefix_len = frag.indent
299
300 elif case(doc_e.Indent):
301 indented = cast(doc.Indent, frag.mdoc.doc)
302 fragments.append(
303 DocFragment(indented.mdoc,
304 frag.indent + indented.indent,
305 frag.is_flat, frag.measure))
306
307 elif case(doc_e.Concat):
308 # If we encounter Concat([A, B, C]) with a suffix measure M,
309 # we need to push A,B,C onto the stack in reverse order:
310 # - C, with suffix_measure = B.measure + A.measure + M
311 # - B, with suffix_measure = A.measure + M
312 # - A, with suffix_measure = M
313 concat = cast(doc.Concat, frag.mdoc.doc)
314 measure = frag.measure
315 for mdoc in reversed(concat.mdocs):
316 fragments.append(
317 DocFragment(mdoc, frag.indent, frag.is_flat,
318 measure))
319 measure = _ConcatMeasure(mdoc.measure, measure)
320
321 elif case(doc_e.Group):
322 # If the group would fit on the current line when printed
323 # flat, do so. Otherwise, print it non-flat.
324 group = cast(doc.Group, frag.mdoc.doc)
325 flat = self._Fits(prefix_len, group, frag.measure)
326 fragments.append(
327 DocFragment(group.mdoc, frag.indent, flat,
328 frag.measure))
329
330 elif case(doc_e.IfFlat):
331 if_flat = cast(doc.IfFlat, frag.mdoc.doc)
332 if frag.is_flat:
333 subdoc = if_flat.flat_mdoc
334 else:
335 subdoc = if_flat.nonflat_mdoc
336 fragments.append(
337 DocFragment(subdoc, frag.indent, frag.is_flat,
338 frag.measure))
339
340 elif case(doc_e.Flat):
341 flat_doc = cast(doc.Flat, frag.mdoc.doc)
342 fragments.append(
343 DocFragment(flat_doc.mdoc, frag.indent, True,
344 frag.measure))
345
346
347################
348# Value -> Doc #
349################
350
351_DEFAULT_INDENTATION = 4
352_DEFAULT_USE_STYLES = True
353
354# Tuned for 'data_lang/pretty-benchmark.sh float-demo'
355# TODO: might want options for float width
356_DEFAULT_MAX_TABULAR_WIDTH = 22
357
358
359class ValueEncoder:
360 """Converts Oils values into `doc`s, which can then be pretty printed."""
361
362 def __init__(self):
363 # type: () -> None
364 self.indent = _DEFAULT_INDENTATION
365 self.use_styles = _DEFAULT_USE_STYLES
366 self.max_tabular_width = _DEFAULT_MAX_TABULAR_WIDTH
367 self.ysh_style = False
368
369 self.visiting = {} # type: Dict[int, bool]
370
371 # These can be configurable later
372 self.int_style = ansi.YELLOW
373 self.float_style = ansi.BLUE
374 self.null_style = ansi.RED
375 self.bool_style = ansi.CYAN
376 self.string_style = ansi.GREEN
377 self.cycle_style = ansi.BOLD + ansi.BLUE
378 self.type_style = ansi.MAGENTA
379
380 def SetIndent(self, indent):
381 # type: (int) -> None
382 """Set the number of spaces per indent."""
383 self.indent = indent
384
385 def SetUseStyles(self, use_styles):
386 # type: (bool) -> None
387 """Print with ansi colors and styles, rather than plain text."""
388 self.use_styles = use_styles
389
390 def SetMaxTabularWidth(self, max_tabular_width):
391 # type: (int) -> None
392 """Set the maximum width that list elements can be, for them to be
393 vertically aligned."""
394 self.max_tabular_width = max_tabular_width
395
396 def SetYshStyle(self):
397 # type: () -> None
398 self.ysh_style = True
399
400 def TypePrefix(self, type_str):
401 # type: (str) -> List[MeasuredDoc]
402 type_name = self._Styled(self.type_style, _Text(type_str))
403 mdocs = [_Text("("), type_name, _Text(")"), _Break(" ")]
404 return mdocs
405
406 def Value(self, val):
407 # type: (value_t) -> MeasuredDoc
408 """Convert an Oils value into a `doc`, which can then be pretty printed."""
409 self.visiting.clear()
410 return self._Value(val)
411
412 def _Styled(self, style, mdoc):
413 # type: (str, MeasuredDoc) -> MeasuredDoc
414 """Apply the ANSI style string to the given node, if use_styles is set."""
415 if self.use_styles:
416 return _Concat([
417 MeasuredDoc(doc.Text(style), _EmptyMeasure()), mdoc,
418 MeasuredDoc(doc.Text(ansi.RESET), _EmptyMeasure())
419 ])
420 else:
421 return mdoc
422
423 def _Surrounded(self, open, mdoc, close):
424 # type: (str, MeasuredDoc, str) -> MeasuredDoc
425 """Print one of two options (using '[', ']' for open, close):
426
427 ```
428 [mdoc]
429 ------
430 [
431 mdoc
432 ]
433 ```
434 """
435 return _Group(
436 _Concat([
437 _Text(open),
438 _Indent(self.indent, _Concat([_Break(""), mdoc])),
439 _Break(""),
440 _Text(close)
441 ]))
442
443 def _SurroundedAndPrefixed(self, open, prefix, sep, mdoc, close):
444 # type: (str, MeasuredDoc, str, MeasuredDoc, str) -> MeasuredDoc
445 """Print one of two options
446 (using '[', 'prefix', ':', 'mdoc', ']' for open, prefix, sep, mdoc, close):
447
448 ```
449 [prefix:mdoc]
450 ------
451 [prefix
452 mdoc
453 ]
454 ```
455 """
456 return _Group(
457 _Concat([
458 _Text(open), prefix,
459 _Indent(self.indent, _Concat([_Break(sep), mdoc])),
460 _Break(""),
461 _Text(close)
462 ]))
463
464 def _Join(self, items, sep, space):
465 # type: (List[MeasuredDoc], str, str) -> MeasuredDoc
466 """Join `items`, using either 'sep+space' or 'sep+newline' between them.
467
468 E.g., if sep and space are ',' and '_', print one of these two cases:
469 ```
470 first,_second,_third
471 ------
472 first,
473 second,
474 third
475 ```
476 """
477 seq = [] # type: List[MeasuredDoc]
478 for i, item in enumerate(items):
479 if i != 0:
480 seq.append(_Text(sep))
481 seq.append(_Break(space))
482 seq.append(item)
483 return _Concat(seq)
484
485 def _Tabular(self, items, sep):
486 # type: (List[MeasuredDoc], str) -> MeasuredDoc
487 """Join `items` together, using one of three styles:
488
489 (showing spaces as underscores for clarity)
490 ```
491 first,_second,_third,_fourth,_fifth,_sixth,_seventh,_eighth
492 ------
493 first,___second,__third,
494 fourth,__fifth,___sixth,
495 seventh,_eighth
496 ------
497 first,
498 second,
499 third,
500 fourth,
501 fifth,
502 sixth,
503 seventh,
504 eighth
505 ```
506
507 The first "single line" style is used if the items fit on one line. The
508 second "tabular' style is used if the flat width of all items is no
509 greater than `self.max_tabular_width`. The third "multi line" style is
510 used otherwise.
511 """
512
513 # Why not "just" use tabular alignment so long as two items fit on every
514 # line? Because it isn't possible to check for that in the pretty
515 # printing language. There are two sorts of conditionals we can do:
516 #
517 # A. Inside the pretty printing language, which supports exactly one
518 # conditional: "does it fit on one line?".
519 # B. Outside the pretty printing language we can run arbitrary Python
520 # code, but we don't know how much space is available on the line
521 # because it depends on the context in which we're printed, which may
522 # vary.
523 #
524 # We're picking between the three styles, by using (A) to check if the
525 # first style fits on one line, then using (B) with "are all the items
526 # smaller than `self.max_tabular_width`?" to pick between style 2 and
527 # style 3.
528
529 if len(items) == 0:
530 return _Text("")
531
532 max_flat_len = 0
533 seq = [] # type: List[MeasuredDoc]
534 for i, item in enumerate(items):
535 if i != 0:
536 seq.append(_Text(sep))
537 seq.append(_Break(" "))
538 seq.append(item)
539 max_flat_len = max(max_flat_len, item.measure.flat)
540 non_tabular = _Concat(seq)
541
542 sep_width = TryUnicodeWidth(sep)
543 if max_flat_len + sep_width + 1 <= self.max_tabular_width:
544 tabular_seq = [] # type: List[MeasuredDoc]
545 for i, item in enumerate(items):
546 tabular_seq.append(_Flat(item))
547 if i != len(items) - 1:
548 padding = max_flat_len - item.measure.flat + 1
549 tabular_seq.append(_Text(sep))
550 tabular_seq.append(_Group(_Break(" " * padding)))
551 tabular = _Concat(tabular_seq)
552 return _Group(_IfFlat(non_tabular, tabular))
553 else:
554 return non_tabular
555
556 def _DictKey(self, s):
557 # type: (str) -> MeasuredDoc
558 if match.IsValidVarName(s):
559 encoded = s
560 else:
561 if self.ysh_style:
562 encoded = j8_lite.YshEncodeString(s)
563 else:
564 # TODO: remove this dead branch after fixing tests
565 encoded = j8_lite.EncodeString(s)
566 return _Text(encoded)
567
568 def _StringLiteral(self, s):
569 # type: (str) -> MeasuredDoc
570 if self.ysh_style:
571 # YSH r'' or b'' style
572 encoded = j8_lite.YshEncodeString(s)
573 else:
574 # TODO: remove this dead branch after fixing tests
575 encoded = j8_lite.EncodeString(s)
576 return self._Styled(self.string_style, _Text(encoded))
577
578 def _BashStringLiteral(self, s):
579 # type: (str) -> MeasuredDoc
580
581 # '' or $'' style
582 #
583 # We mimic bash syntax by using $'\\' instead of b'\\'
584 #
585 # $ declare -a array=($'\\')
586 # $ = array
587 # (BashArray) (BashArray $'\\')
588 #
589 # $ declare -A assoc=([k]=$'\\')
590 # $ = assoc
591 # (BashAssoc) (BashAssoc ['k']=$'\\')
592
593 encoded = j8_lite.ShellEncode(s)
594 return self._Styled(self.string_style, _Text(encoded))
595
596 def _YshList(self, vlist):
597 # type: (value.List) -> MeasuredDoc
598 """Print a string literal."""
599 if len(vlist.items) == 0:
600 return _Text("[]")
601 mdocs = [self._Value(item) for item in vlist.items]
602 return self._Surrounded("[", self._Tabular(mdocs, ","), "]")
603
604 def _YshDict(self, vdict):
605 # type: (value.Dict) -> MeasuredDoc
606 if len(vdict.d) == 0:
607 return _Text("{}")
608 mdocs = [] # type: List[MeasuredDoc]
609 for k, v in iteritems(vdict.d):
610 mdocs.append(
611 _Concat([self._DictKey(k),
612 _Text(": "),
613 self._Value(v)]))
614 return self._Surrounded("{", self._Join(mdocs, ",", " "), "}")
615
616 def _BashArray(self, varray):
617 # type: (value.BashArray) -> MeasuredDoc
618 type_name = self._Styled(self.type_style, _Text("BashArray"))
619 if len(varray.strs) == 0:
620 return _Concat([_Text("("), type_name, _Text(")")])
621 mdocs = [] # type: List[MeasuredDoc]
622 for s in varray.strs:
623 if s is None:
624 mdocs.append(_Text("null"))
625 else:
626 mdocs.append(self._BashStringLiteral(s))
627 return self._SurroundedAndPrefixed("(", type_name, " ",
628 self._Tabular(mdocs, ""), ")")
629
630 def _BashAssoc(self, vassoc):
631 # type: (value.BashAssoc) -> MeasuredDoc
632 type_name = self._Styled(self.type_style, _Text("BashAssoc"))
633 if len(vassoc.d) == 0:
634 return _Concat([_Text("("), type_name, _Text(")")])
635 mdocs = [] # type: List[MeasuredDoc]
636 for k2, v2 in iteritems(vassoc.d):
637 mdocs.append(
638 _Concat([
639 _Text("["),
640 self._BashStringLiteral(k2),
641 _Text("]="),
642 self._BashStringLiteral(v2)
643 ]))
644 return self._SurroundedAndPrefixed("(", type_name, " ",
645 self._Join(mdocs, "", " "), ")")
646
647 def _SparseArray(self, val):
648 # type: (value.SparseArray) -> MeasuredDoc
649 type_name = self._Styled(self.type_style, _Text("SparseArray"))
650 if len(val.d) == 0:
651 return _Concat([_Text("("), type_name, _Text(")")])
652 mdocs = [] # type: List[MeasuredDoc]
653 for k2, v2 in iteritems(val.d):
654 mdocs.append(
655 _Concat([
656 _Text("["),
657 self._Styled(self.int_style, _Text(mops.ToStr(k2))),
658 _Text("]="),
659 self._BashStringLiteral(v2)
660 ]))
661 return self._SurroundedAndPrefixed("(", type_name, " ",
662 self._Join(mdocs, "", " "), ")")
663
664 def _Value(self, val):
665 # type: (value_t) -> MeasuredDoc
666
667 with tagswitch(val) as case:
668 if case(value_e.Null):
669 return self._Styled(self.null_style, _Text("null"))
670
671 elif case(value_e.Bool):
672 b = cast(value.Bool, val).b
673 return self._Styled(self.bool_style,
674 _Text("true" if b else "false"))
675
676 elif case(value_e.Int):
677 i = cast(value.Int, val).i
678 return self._Styled(self.int_style, _Text(mops.ToStr(i)))
679
680 elif case(value_e.Float):
681 f = cast(value.Float, val).f
682 return self._Styled(self.float_style, _Text(_FloatString(f)))
683
684 elif case(value_e.Str):
685 s = cast(value.Str, val).s
686 return self._StringLiteral(s)
687
688 elif case(value_e.Range):
689 r = cast(value.Range, val)
690 type_name = self._Styled(self.type_style, _Text(ValType(r)))
691 mdocs = [_Text(str(r.lower)), _Text(".."), _Text(str(r.upper))]
692 return self._SurroundedAndPrefixed("(", type_name, " ",
693 self._Join(mdocs, "", " "),
694 ")")
695
696 elif case(value_e.List):
697 vlist = cast(value.List, val)
698 heap_id = j8.HeapValueId(vlist)
699 if self.visiting.get(heap_id, False):
700 return _Concat([
701 _Text("["),
702 self._Styled(self.cycle_style, _Text("...")),
703 _Text("]")
704 ])
705 else:
706 self.visiting[heap_id] = True
707 result = self._YshList(vlist)
708 self.visiting[heap_id] = False
709 return result
710
711 elif case(value_e.Dict):
712 vdict = cast(value.Dict, val)
713 heap_id = j8.HeapValueId(vdict)
714 if self.visiting.get(heap_id, False):
715 return _Concat([
716 _Text("{"),
717 self._Styled(self.cycle_style, _Text("...")),
718 _Text("}")
719 ])
720 else:
721 self.visiting[heap_id] = True
722 result = self._YshDict(vdict)
723 self.visiting[heap_id] = False
724 return result
725
726 elif case(value_e.SparseArray):
727 sparse = cast(value.SparseArray, val)
728 return self._SparseArray(sparse)
729
730 elif case(value_e.BashArray):
731 varray = cast(value.BashArray, val)
732 return self._BashArray(varray)
733
734 elif case(value_e.BashAssoc):
735 vassoc = cast(value.BashAssoc, val)
736 return self._BashAssoc(vassoc)
737
738 else:
739 type_name = self._Styled(self.type_style, _Text(ValType(val)))
740 id_str = j8.ValueIdString(val)
741 return _Concat([_Text("<"), type_name, _Text(id_str + ">")])
742
743
744# vim: sw=4