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

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