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

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