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

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