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

755 lines, 360 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_DEFAULT_MAX_WIDTH = 80
250
251
252class PrettyPrinter(object):
253 """Pretty print an Oils value."""
254
255 def __init__(self):
256 # type: () -> None
257 """Construct a PrettyPrinter with default configuration options.
258
259 Use the Set*() methods for configuration before printing."""
260 self.max_width = _DEFAULT_MAX_WIDTH
261
262 def SetMaxWidth(self, max_width):
263 # type: (int) -> None
264 """Set the maximum line width.
265
266 Pretty printing will attempt to (but does not guarantee to) fit the doc
267 within this width.
268 """
269 self.max_width = max_width
270
271 def _Fits(self, prefix_len, group, suffix_measure):
272 # type: (int, doc.Group, Measure) -> bool
273 """Will `group` fit flat on the current line?"""
274 measure = _ConcatMeasure(_FlattenMeasure(group.mdoc.measure),
275 suffix_measure)
276 return prefix_len + _SuffixLen(measure) <= self.max_width
277
278 def PrintDoc(self, document, buf):
279 # type: (MeasuredDoc, BufWriter) -> None
280 """Pretty print a `pretty.doc` to a BufWriter."""
281
282 # The width of the text we've printed so far on the current line
283 prefix_len = 0
284 # A _stack_ of document fragments to print. Each fragment contains:
285 # - A MeasuredDoc (doc node and its measure, saying how "big" it is)
286 # - The indentation level to print this doc node at.
287 # - Is this doc node being printed in flat mode?
288 # - The measure _from just after the doc node, to the end of the entire document_.
289 # (Call this the suffix_measure)
290 fragments = [DocFragment(_Group(document), 0, False, _EmptyMeasure())]
291
292 while len(fragments) > 0:
293 frag = fragments.pop()
294 with tagswitch(frag.mdoc.doc) as case:
295
296 if case(doc_e.Text):
297 text = cast(doc.Text, frag.mdoc.doc)
298 buf.write(text.string)
299 prefix_len += frag.mdoc.measure.flat
300
301 elif case(doc_e.Break):
302 if frag.is_flat:
303 break_str = cast(doc.Break, frag.mdoc.doc).string
304 buf.write(break_str)
305 prefix_len += frag.mdoc.measure.flat
306 else:
307 buf.write('\n')
308 buf.write_spaces(frag.indent)
309 prefix_len = frag.indent
310
311 elif case(doc_e.Indent):
312 indented = cast(doc.Indent, frag.mdoc.doc)
313 fragments.append(
314 DocFragment(indented.mdoc,
315 frag.indent + indented.indent,
316 frag.is_flat, frag.measure))
317
318 elif case(doc_e.Concat):
319 # If we encounter Concat([A, B, C]) with a suffix measure M,
320 # we need to push A,B,C onto the stack in reverse order:
321 # - C, with suffix_measure = B.measure + A.measure + M
322 # - B, with suffix_measure = A.measure + M
323 # - A, with suffix_measure = M
324 concat = cast(doc.Concat, frag.mdoc.doc)
325 measure = frag.measure
326 for mdoc in reversed(concat.mdocs):
327 fragments.append(
328 DocFragment(mdoc, frag.indent, frag.is_flat,
329 measure))
330 measure = _ConcatMeasure(mdoc.measure, measure)
331
332 elif case(doc_e.Group):
333 # If the group would fit on the current line when printed
334 # flat, do so. Otherwise, print it non-flat.
335 group = cast(doc.Group, frag.mdoc.doc)
336 flat = self._Fits(prefix_len, group, frag.measure)
337 fragments.append(
338 DocFragment(group.mdoc, frag.indent, flat,
339 frag.measure))
340
341 elif case(doc_e.IfFlat):
342 if_flat = cast(doc.IfFlat, frag.mdoc.doc)
343 if frag.is_flat:
344 subdoc = if_flat.flat_mdoc
345 else:
346 subdoc = if_flat.nonflat_mdoc
347 fragments.append(
348 DocFragment(subdoc, frag.indent, frag.is_flat,
349 frag.measure))
350
351 elif case(doc_e.Flat):
352 flat_doc = cast(doc.Flat, frag.mdoc.doc)
353 fragments.append(
354 DocFragment(flat_doc.mdoc, frag.indent, True,
355 frag.measure))
356
357
358################
359# Value -> Doc #
360################
361
362_DEFAULT_INDENTATION = 4
363_DEFAULT_USE_STYLES = True
364
365# Tuned for 'data_lang/pretty-benchmark.sh float-demo'
366# TODO: might want options for float width
367_DEFAULT_MAX_TABULAR_WIDTH = 22
368
369
370class ValueEncoder:
371 """Converts Oils values into `doc`s, which can then be pretty printed."""
372
373 def __init__(self):
374 # type: () -> None
375 self.indent = _DEFAULT_INDENTATION
376 self.use_styles = _DEFAULT_USE_STYLES
377 self.max_tabular_width = _DEFAULT_MAX_TABULAR_WIDTH
378 self.ysh_style = False
379
380 self.visiting = {} # type: Dict[int, bool]
381
382 # These can be configurable later
383 self.int_style = ansi.YELLOW
384 self.float_style = ansi.BLUE
385 self.null_style = ansi.RED
386 self.bool_style = ansi.CYAN
387 self.string_style = ansi.GREEN
388 self.cycle_style = ansi.BOLD + ansi.BLUE
389 self.type_style = ansi.MAGENTA
390
391 def SetIndent(self, indent):
392 # type: (int) -> None
393 """Set the number of spaces per indent."""
394 self.indent = indent
395
396 def SetUseStyles(self, use_styles):
397 # type: (bool) -> None
398 """Print with ansi colors and styles, rather than plain text."""
399 self.use_styles = use_styles
400
401 def SetMaxTabularWidth(self, max_tabular_width):
402 # type: (int) -> None
403 """Set the maximum width that list elements can be, for them to be
404 vertically aligned."""
405 self.max_tabular_width = max_tabular_width
406
407 def SetYshStyle(self):
408 # type: () -> None
409 self.ysh_style = True
410
411 def TypePrefix(self, type_str):
412 # type: (str) -> List[MeasuredDoc]
413 type_name = self._Styled(self.type_style, _Text(type_str))
414 mdocs = [_Text("("), type_name, _Text(")"), _Break(" ")]
415 return mdocs
416
417 def Value(self, val):
418 # type: (value_t) -> MeasuredDoc
419 """Convert an Oils value into a `doc`, which can then be pretty printed."""
420 self.visiting.clear()
421 return self._Value(val)
422
423 def _Styled(self, style, mdoc):
424 # type: (str, MeasuredDoc) -> MeasuredDoc
425 """Apply the ANSI style string to the given node, if use_styles is set."""
426 if self.use_styles:
427 return _Concat([
428 MeasuredDoc(doc.Text(style), _EmptyMeasure()), mdoc,
429 MeasuredDoc(doc.Text(ansi.RESET), _EmptyMeasure())
430 ])
431 else:
432 return mdoc
433
434 def _Surrounded(self, open, mdoc, close):
435 # type: (str, MeasuredDoc, str) -> MeasuredDoc
436 """Print one of two options (using '[', ']' for open, close):
437
438 ```
439 [mdoc]
440 ------
441 [
442 mdoc
443 ]
444 ```
445 """
446 return _Group(
447 _Concat([
448 _Text(open),
449 _Indent(self.indent, _Concat([_Break(""), mdoc])),
450 _Break(""),
451 _Text(close)
452 ]))
453
454 def _SurroundedAndPrefixed(self, open, prefix, sep, mdoc, close):
455 # type: (str, MeasuredDoc, str, MeasuredDoc, str) -> MeasuredDoc
456 """Print one of two options
457 (using '[', 'prefix', ':', 'mdoc', ']' for open, prefix, sep, mdoc, close):
458
459 ```
460 [prefix:mdoc]
461 ------
462 [prefix
463 mdoc
464 ]
465 ```
466 """
467 return _Group(
468 _Concat([
469 _Text(open), prefix,
470 _Indent(self.indent, _Concat([_Break(sep), mdoc])),
471 _Break(""),
472 _Text(close)
473 ]))
474
475 def _Join(self, items, sep, space):
476 # type: (List[MeasuredDoc], str, str) -> MeasuredDoc
477 """Join `items`, using either 'sep+space' or 'sep+newline' between them.
478
479 E.g., if sep and space are ',' and '_', print one of these two cases:
480 ```
481 first,_second,_third
482 ------
483 first,
484 second,
485 third
486 ```
487 """
488 seq = [] # type: List[MeasuredDoc]
489 for i, item in enumerate(items):
490 if i != 0:
491 seq.append(_Text(sep))
492 seq.append(_Break(space))
493 seq.append(item)
494 return _Concat(seq)
495
496 def _Tabular(self, items, sep):
497 # type: (List[MeasuredDoc], str) -> MeasuredDoc
498 """Join `items` together, using one of three styles:
499
500 (showing spaces as underscores for clarity)
501 ```
502 first,_second,_third,_fourth,_fifth,_sixth,_seventh,_eighth
503 ------
504 first,___second,__third,
505 fourth,__fifth,___sixth,
506 seventh,_eighth
507 ------
508 first,
509 second,
510 third,
511 fourth,
512 fifth,
513 sixth,
514 seventh,
515 eighth
516 ```
517
518 The first "single line" style is used if the items fit on one line. The
519 second "tabular' style is used if the flat width of all items is no
520 greater than `self.max_tabular_width`. The third "multi line" style is
521 used otherwise.
522 """
523
524 # Why not "just" use tabular alignment so long as two items fit on every
525 # line? Because it isn't possible to check for that in the pretty
526 # printing language. There are two sorts of conditionals we can do:
527 #
528 # A. Inside the pretty printing language, which supports exactly one
529 # conditional: "does it fit on one line?".
530 # B. Outside the pretty printing language we can run arbitrary Python
531 # code, but we don't know how much space is available on the line
532 # because it depends on the context in which we're printed, which may
533 # vary.
534 #
535 # We're picking between the three styles, by using (A) to check if the
536 # first style fits on one line, then using (B) with "are all the items
537 # smaller than `self.max_tabular_width`?" to pick between style 2 and
538 # style 3.
539
540 if len(items) == 0:
541 return _Text("")
542
543 max_flat_len = 0
544 seq = [] # type: List[MeasuredDoc]
545 for i, item in enumerate(items):
546 if i != 0:
547 seq.append(_Text(sep))
548 seq.append(_Break(" "))
549 seq.append(item)
550 max_flat_len = max(max_flat_len, item.measure.flat)
551 non_tabular = _Concat(seq)
552
553 sep_width = TryUnicodeWidth(sep)
554 if max_flat_len + sep_width + 1 <= self.max_tabular_width:
555 tabular_seq = [] # type: List[MeasuredDoc]
556 for i, item in enumerate(items):
557 tabular_seq.append(_Flat(item))
558 if i != len(items) - 1:
559 padding = max_flat_len - item.measure.flat + 1
560 tabular_seq.append(_Text(sep))
561 tabular_seq.append(_Group(_Break(" " * padding)))
562 tabular = _Concat(tabular_seq)
563 return _Group(_IfFlat(non_tabular, tabular))
564 else:
565 return non_tabular
566
567 def _DictKey(self, s):
568 # type: (str) -> MeasuredDoc
569 if match.IsValidVarName(s):
570 encoded = s
571 else:
572 if self.ysh_style:
573 encoded = j8_lite.YshEncodeString(s)
574 else:
575 # TODO: remove this dead branch after fixing tests
576 encoded = j8_lite.EncodeString(s)
577 return _Text(encoded)
578
579 def _StringLiteral(self, s):
580 # type: (str) -> MeasuredDoc
581 if self.ysh_style:
582 # YSH r'' or b'' style
583 encoded = j8_lite.YshEncodeString(s)
584 else:
585 # TODO: remove this dead branch after fixing tests
586 encoded = j8_lite.EncodeString(s)
587 return self._Styled(self.string_style, _Text(encoded))
588
589 def _BashStringLiteral(self, s):
590 # type: (str) -> MeasuredDoc
591
592 # '' or $'' style
593 #
594 # We mimic bash syntax by using $'\\' instead of b'\\'
595 #
596 # $ declare -a array=($'\\')
597 # $ = array
598 # (BashArray) (BashArray $'\\')
599 #
600 # $ declare -A assoc=([k]=$'\\')
601 # $ = assoc
602 # (BashAssoc) (BashAssoc ['k']=$'\\')
603
604 encoded = j8_lite.ShellEncode(s)
605 return self._Styled(self.string_style, _Text(encoded))
606
607 def _YshList(self, vlist):
608 # type: (value.List) -> MeasuredDoc
609 """Print a string literal."""
610 if len(vlist.items) == 0:
611 return _Text("[]")
612 mdocs = [self._Value(item) for item in vlist.items]
613 return self._Surrounded("[", self._Tabular(mdocs, ","), "]")
614
615 def _YshDict(self, vdict):
616 # type: (value.Dict) -> MeasuredDoc
617 if len(vdict.d) == 0:
618 return _Text("{}")
619 mdocs = [] # type: List[MeasuredDoc]
620 for k, v in iteritems(vdict.d):
621 mdocs.append(
622 _Concat([self._DictKey(k),
623 _Text(": "),
624 self._Value(v)]))
625 return self._Surrounded("{", self._Join(mdocs, ",", " "), "}")
626
627 def _BashArray(self, varray):
628 # type: (value.BashArray) -> MeasuredDoc
629 type_name = self._Styled(self.type_style, _Text("BashArray"))
630 if len(varray.strs) == 0:
631 return _Concat([_Text("("), type_name, _Text(")")])
632 mdocs = [] # type: List[MeasuredDoc]
633 for s in varray.strs:
634 if s is None:
635 mdocs.append(_Text("null"))
636 else:
637 mdocs.append(self._BashStringLiteral(s))
638 return self._SurroundedAndPrefixed("(", type_name, " ",
639 self._Tabular(mdocs, ""), ")")
640
641 def _BashAssoc(self, vassoc):
642 # type: (value.BashAssoc) -> MeasuredDoc
643 type_name = self._Styled(self.type_style, _Text("BashAssoc"))
644 if len(vassoc.d) == 0:
645 return _Concat([_Text("("), type_name, _Text(")")])
646 mdocs = [] # type: List[MeasuredDoc]
647 for k2, v2 in iteritems(vassoc.d):
648 mdocs.append(
649 _Concat([
650 _Text("["),
651 self._BashStringLiteral(k2),
652 _Text("]="),
653 self._BashStringLiteral(v2)
654 ]))
655 return self._SurroundedAndPrefixed("(", type_name, " ",
656 self._Join(mdocs, "", " "), ")")
657
658 def _SparseArray(self, val):
659 # type: (value.SparseArray) -> MeasuredDoc
660 type_name = self._Styled(self.type_style, _Text("SparseArray"))
661 if len(val.d) == 0:
662 return _Concat([_Text("("), type_name, _Text(")")])
663 mdocs = [] # type: List[MeasuredDoc]
664 for k2, v2 in iteritems(val.d):
665 mdocs.append(
666 _Concat([
667 _Text("["),
668 self._Styled(self.int_style, _Text(mops.ToStr(k2))),
669 _Text("]="),
670 self._BashStringLiteral(v2)
671 ]))
672 return self._SurroundedAndPrefixed("(", type_name, " ",
673 self._Join(mdocs, "", " "), ")")
674
675 def _Value(self, val):
676 # type: (value_t) -> MeasuredDoc
677
678 with tagswitch(val) as case:
679 if case(value_e.Null):
680 return self._Styled(self.null_style, _Text("null"))
681
682 elif case(value_e.Bool):
683 b = cast(value.Bool, val).b
684 return self._Styled(self.bool_style,
685 _Text("true" if b else "false"))
686
687 elif case(value_e.Int):
688 i = cast(value.Int, val).i
689 return self._Styled(self.int_style, _Text(mops.ToStr(i)))
690
691 elif case(value_e.Float):
692 f = cast(value.Float, val).f
693 return self._Styled(self.float_style, _Text(_FloatString(f)))
694
695 elif case(value_e.Str):
696 s = cast(value.Str, val).s
697 return self._StringLiteral(s)
698
699 elif case(value_e.Range):
700 r = cast(value.Range, val)
701 type_name = self._Styled(self.type_style, _Text(ValType(r)))
702 mdocs = [_Text(str(r.lower)), _Text(".."), _Text(str(r.upper))]
703 return self._SurroundedAndPrefixed("(", type_name, " ",
704 self._Join(mdocs, "", " "),
705 ")")
706
707 elif case(value_e.List):
708 vlist = cast(value.List, val)
709 heap_id = j8.HeapValueId(vlist)
710 if self.visiting.get(heap_id, False):
711 return _Concat([
712 _Text("["),
713 self._Styled(self.cycle_style, _Text("...")),
714 _Text("]")
715 ])
716 else:
717 self.visiting[heap_id] = True
718 result = self._YshList(vlist)
719 self.visiting[heap_id] = False
720 return result
721
722 elif case(value_e.Dict):
723 vdict = cast(value.Dict, val)
724 heap_id = j8.HeapValueId(vdict)
725 if self.visiting.get(heap_id, False):
726 return _Concat([
727 _Text("{"),
728 self._Styled(self.cycle_style, _Text("...")),
729 _Text("}")
730 ])
731 else:
732 self.visiting[heap_id] = True
733 result = self._YshDict(vdict)
734 self.visiting[heap_id] = False
735 return result
736
737 elif case(value_e.SparseArray):
738 sparse = cast(value.SparseArray, val)
739 return self._SparseArray(sparse)
740
741 elif case(value_e.BashArray):
742 varray = cast(value.BashArray, val)
743 return self._BashArray(varray)
744
745 elif case(value_e.BashAssoc):
746 vassoc = cast(value.BashAssoc, val)
747 return self._BashAssoc(vassoc)
748
749 else:
750 type_name = self._Styled(self.type_style, _Text(ValType(val)))
751 id_str = j8.ValueIdString(val)
752 return _Concat([_Text("<"), type_name, _Text(id_str + ">")])
753
754
755# vim: sw=4