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

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