| 1 | #!/usr/bin/env python2
|
| 2 | from __future__ import print_function
|
| 3 | """pp_hnode.py - Base class for pretty printing, and HNodeEncoder"""
|
| 4 |
|
| 5 | from _devbuild.gen.hnode_asdl import hnode, hnode_e, hnode_t, Field, color_e
|
| 6 | from _devbuild.gen.pretty_asdl import (doc, MeasuredDoc, Measure)
|
| 7 | from data_lang import j8_lite
|
| 8 | from display import ansi
|
| 9 | from display import pretty
|
| 10 | from display.pretty import (_Break, _Concat, _Flat, _Group, _IfFlat, _Indent,
|
| 11 | _EmptyMeasure, AsciiText)
|
| 12 | from mycpp import mylib
|
| 13 | from mycpp.mylib import log, tagswitch, switch
|
| 14 | from typing import cast, List, Dict, Optional
|
| 15 |
|
| 16 | _ = log
|
| 17 |
|
| 18 |
|
| 19 | class BaseEncoder(object):
|
| 20 |
|
| 21 | def __init__(self):
|
| 22 | # type: () -> None
|
| 23 |
|
| 24 | # Default values
|
| 25 | self.indent = 4
|
| 26 | self.use_styles = True
|
| 27 | # Tuned for 'data_lang/pretty-benchmark.sh float-demo'
|
| 28 | # TODO: might want options for float width
|
| 29 | self.max_tabular_width = 22
|
| 30 |
|
| 31 | self.visiting = {} # type: Dict[int, bool]
|
| 32 |
|
| 33 | def SetIndent(self, indent):
|
| 34 | # type: (int) -> None
|
| 35 | """Set the number of spaces per indent."""
|
| 36 | self.indent = indent
|
| 37 |
|
| 38 | def SetUseStyles(self, use_styles):
|
| 39 | # type: (bool) -> None
|
| 40 | """Print with ansi colors and styles, rather than plain text."""
|
| 41 | self.use_styles = use_styles
|
| 42 |
|
| 43 | def SetMaxTabularWidth(self, max_tabular_width):
|
| 44 | # type: (int) -> None
|
| 45 | """Set the maximum width that list elements can be, for them to be
|
| 46 | vertically aligned."""
|
| 47 | self.max_tabular_width = max_tabular_width
|
| 48 |
|
| 49 | def _Styled(self, style, mdoc):
|
| 50 | # type: (str, MeasuredDoc) -> MeasuredDoc
|
| 51 | """Apply the ANSI style string to the given node, if use_styles is set."""
|
| 52 | if self.use_styles:
|
| 53 | return _Concat([
|
| 54 | MeasuredDoc(doc.Text(style), _EmptyMeasure()), mdoc,
|
| 55 | MeasuredDoc(doc.Text(ansi.RESET), _EmptyMeasure())
|
| 56 | ])
|
| 57 | else:
|
| 58 | return mdoc
|
| 59 |
|
| 60 | def _StyledAscii(self, style, s):
|
| 61 | # type: (str, str) -> MeasuredDoc
|
| 62 | """Apply the ANSI style string to the given node, if use_styles is set."""
|
| 63 | measure = Measure(len(s), -1) # like AsciiText()
|
| 64 | if self.use_styles:
|
| 65 | s = '%s%s%s' % (style, s, ansi.RESET)
|
| 66 | return MeasuredDoc(doc.Text(s), measure)
|
| 67 |
|
| 68 | def _Surrounded(self, left, mdoc, right):
|
| 69 | # type: (str, MeasuredDoc, str) -> MeasuredDoc
|
| 70 | """Print one of two options (using '[', ']' for left, right):
|
| 71 |
|
| 72 | [mdoc]
|
| 73 | ------
|
| 74 | [
|
| 75 | mdoc
|
| 76 | ]
|
| 77 | """
|
| 78 | # TODO:
|
| 79 | # - left and right AsciiText often CONSTANT mdocs
|
| 80 | # - _Break too
|
| 81 | return _Group(
|
| 82 | _Concat([
|
| 83 | AsciiText(left),
|
| 84 | _Indent(self.indent, _Concat([_Break(''), mdoc])),
|
| 85 | _Break(''),
|
| 86 | AsciiText(right)
|
| 87 | ]))
|
| 88 |
|
| 89 | def _SurroundedAndPrefixed(self, left, prefix, sep, mdoc, right):
|
| 90 | # type: (str, MeasuredDoc, str, MeasuredDoc, str) -> MeasuredDoc
|
| 91 | """Print one of two options
|
| 92 | (using '[', 'prefix', ':', 'mdoc', ']' for left, prefix, sep, mdoc, right):
|
| 93 |
|
| 94 | [prefix:mdoc]
|
| 95 | ------
|
| 96 | [prefix
|
| 97 | mdoc
|
| 98 | ]
|
| 99 | """
|
| 100 | return _Group(
|
| 101 | _Concat([
|
| 102 | AsciiText(left), prefix,
|
| 103 | _Indent(self.indent, _Concat([_Break(sep), mdoc])),
|
| 104 | _Break(''),
|
| 105 | AsciiText(right)
|
| 106 | ]))
|
| 107 |
|
| 108 | def _Join(self, items, sep, space):
|
| 109 | # type: (List[MeasuredDoc], str, str) -> MeasuredDoc
|
| 110 | """Join `items`, using either 'sep+space' or 'sep+newline' between them.
|
| 111 |
|
| 112 | e.g., if sep and space are ',' and '_', print one of these two cases:
|
| 113 |
|
| 114 | first,_second,_third
|
| 115 | ------
|
| 116 | first,
|
| 117 | second,
|
| 118 | third
|
| 119 | """
|
| 120 | seq = [] # type: List[MeasuredDoc]
|
| 121 | for i, item in enumerate(items):
|
| 122 | if i != 0:
|
| 123 | seq.append(AsciiText(sep))
|
| 124 | seq.append(_Break(space))
|
| 125 | seq.append(item)
|
| 126 | return _Concat(seq)
|
| 127 |
|
| 128 | def _Tabular(self, items, sep):
|
| 129 | # type: (List[MeasuredDoc], str) -> MeasuredDoc
|
| 130 | """Join `items` together, using one of three styles:
|
| 131 |
|
| 132 | (showing spaces as underscores for clarity)
|
| 133 |
|
| 134 | first,_second,_third,_fourth,_fifth,_sixth,_seventh,_eighth
|
| 135 | ------
|
| 136 | first,___second,__third,
|
| 137 | fourth,__fifth,___sixth,
|
| 138 | seventh,_eighth
|
| 139 | ------
|
| 140 | first,
|
| 141 | second,
|
| 142 | third,
|
| 143 | fourth,
|
| 144 | fifth,
|
| 145 | sixth,
|
| 146 | seventh,
|
| 147 | eighth
|
| 148 |
|
| 149 | The first "single line" style is used if the items fit on one line. The
|
| 150 | second "tabular" style is used if the flat width of all items is no
|
| 151 | greater than self.max_tabular_width. The third "multi line" style is
|
| 152 | used otherwise.
|
| 153 | """
|
| 154 | # Why not "just" use tabular alignment so long as two items fit on every
|
| 155 | # line? Because it isn't possible to check for that in the pretty
|
| 156 | # printing language. There are two sorts of conditionals we can do:
|
| 157 | #
|
| 158 | # A. Inside the pretty printing language, which supports exactly one
|
| 159 | # conditional: "does it fit on one line?".
|
| 160 | # B. Outside the pretty printing language we can run arbitrary Python
|
| 161 | # code, but we don't know how much space is available on the line
|
| 162 | # because it depends on the context in which we're printed, which may
|
| 163 | # vary.
|
| 164 | #
|
| 165 | # We're picking between the three styles, by using (A) to check if the
|
| 166 | # first style fits on one line, then using (B) with "are all the items
|
| 167 | # smaller than self.max_tabular_width?" to pick between style 2 and
|
| 168 | # style 3.
|
| 169 |
|
| 170 | if len(items) == 0:
|
| 171 | # TODO: this should be turned into "append nothing"
|
| 172 | return AsciiText('')
|
| 173 |
|
| 174 | max_flat_len = 0
|
| 175 | seq = [] # type: List[MeasuredDoc]
|
| 176 | for i, item in enumerate(items):
|
| 177 | if i != 0:
|
| 178 | seq.append(AsciiText(sep))
|
| 179 | seq.append(_Break(' '))
|
| 180 | # It would be nice if we could extend other _Concat() nodes here
|
| 181 | seq.append(item)
|
| 182 |
|
| 183 | max_flat_len = max(max_flat_len, item.measure.flat)
|
| 184 |
|
| 185 | non_tabular = _Concat(seq)
|
| 186 |
|
| 187 | #log('MAX FLAT %d', max_flat_len)
|
| 188 |
|
| 189 | sep_width = len(sep)
|
| 190 | if max_flat_len + sep_width + 1 <= self.max_tabular_width:
|
| 191 | tabular_seq = [] # type: List[MeasuredDoc]
|
| 192 | for i, item in enumerate(items):
|
| 193 | tabular_seq.append(_Flat(item))
|
| 194 | if i != len(items) - 1:
|
| 195 | padding = max_flat_len - item.measure.flat + 1
|
| 196 | tabular_seq.append(AsciiText(sep))
|
| 197 | tabular_seq.append(_Group(_Break(' ' * padding)))
|
| 198 | tabular = _Concat(tabular_seq)
|
| 199 | return _Group(_IfFlat(non_tabular, tabular))
|
| 200 | else:
|
| 201 | return non_tabular
|
| 202 |
|
| 203 |
|
| 204 | class HNodeEncoder(BaseEncoder):
|
| 205 |
|
| 206 | def __init__(self):
|
| 207 | # type: () -> None
|
| 208 | BaseEncoder.__init__(self)
|
| 209 |
|
| 210 | self.type_color = ansi.YELLOW
|
| 211 | self.field_color = ansi.MAGENTA
|
| 212 |
|
| 213 | def HNode(self, h):
|
| 214 | # type: (hnode_t) -> MeasuredDoc
|
| 215 | self.visiting.clear()
|
| 216 | return self._HNode(h)
|
| 217 |
|
| 218 | def _Field(self, field):
|
| 219 | # type: (Field) -> MeasuredDoc
|
| 220 | name = AsciiText(field.name + ':')
|
| 221 |
|
| 222 | # TODO: the _HNode is often a _Concat node, and we could optimize them
|
| 223 | # together. That means we also have to concatenate their measures.
|
| 224 | return _Concat([name, self._HNode(field.val)])
|
| 225 |
|
| 226 | def _HNode(self, h):
|
| 227 | # type: (hnode_t) -> MeasuredDoc
|
| 228 |
|
| 229 | UP_h = h
|
| 230 | with tagswitch(h) as case:
|
| 231 | if case(hnode_e.AlreadySeen):
|
| 232 | h = cast(hnode.AlreadySeen, UP_h)
|
| 233 | return pretty.AsciiText('...0x%s' % mylib.hex_lower(h.heap_id))
|
| 234 |
|
| 235 | elif case(hnode_e.Leaf):
|
| 236 | h = cast(hnode.Leaf, UP_h)
|
| 237 |
|
| 238 | with switch(h.color) as case2:
|
| 239 | if case2(color_e.TypeName):
|
| 240 | color = ansi.YELLOW
|
| 241 | elif case2(color_e.StringConst):
|
| 242 | color = ansi.BOLD
|
| 243 | elif case2(color_e.OtherConst):
|
| 244 | color = ansi.GREEN
|
| 245 | elif case2(color_e.External):
|
| 246 | color = ansi.BOLD + ansi.BLUE
|
| 247 | elif case2(color_e.UserType):
|
| 248 | color = ansi.GREEN # Same color as other literals for now
|
| 249 | else:
|
| 250 | raise AssertionError()
|
| 251 |
|
| 252 | # TODO: what do we do with node.color
|
| 253 | s = j8_lite.EncodeString(h.s, unquoted_ok=True)
|
| 254 |
|
| 255 | # Could be Unicode, but we don't want that dependency right now
|
| 256 | return self._StyledAscii(color, s)
|
| 257 | #return self._Styled(color, AsciiText(s))
|
| 258 |
|
| 259 | elif case(hnode_e.Array):
|
| 260 | h = cast(hnode.Array, UP_h)
|
| 261 |
|
| 262 | # Reduces Max RSS! Because we build up the trees all at once,
|
| 263 | # and there's a ton fo garbage.
|
| 264 | mylib.MaybeCollect()
|
| 265 |
|
| 266 | if len(h.children) == 0:
|
| 267 | return AsciiText('[]')
|
| 268 | children = [self._HNode(item) for item in h.children]
|
| 269 | return self._Surrounded('[', self._Tabular(children, ''), ']')
|
| 270 |
|
| 271 | elif case(hnode_e.Record):
|
| 272 | h = cast(hnode.Record, UP_h)
|
| 273 |
|
| 274 | type_name = None # type: Optional[MeasuredDoc]
|
| 275 | if len(h.node_type):
|
| 276 | type_name = self._StyledAscii(self.type_color, h.node_type)
|
| 277 | #type_name = self._Styled(self.type_color, AsciiText(h.node_type))
|
| 278 |
|
| 279 | mdocs = None # type: Optional[List[MeasuredDoc]]
|
| 280 | if h.unnamed_fields is not None and len(h.unnamed_fields):
|
| 281 | mdocs = [self._HNode(item) for item in h.unnamed_fields]
|
| 282 | elif len(h.fields) != 0:
|
| 283 | mdocs = [self._Field(field) for field in h.fields]
|
| 284 |
|
| 285 | if mdocs is None:
|
| 286 | m = [AsciiText(h.left)]
|
| 287 | if type_name is not None: # {}
|
| 288 | m.append(type_name)
|
| 289 | m.append(AsciiText(h.right))
|
| 290 |
|
| 291 | # e.g. (value.Stdin) with no fields
|
| 292 | return _Concat(m)
|
| 293 |
|
| 294 | # Named or unnamed
|
| 295 | child = self._Join(mdocs, '', ' ')
|
| 296 |
|
| 297 | if type_name is not None:
|
| 298 | # e.g. (Token id:LitChars col:5)
|
| 299 | return self._SurroundedAndPrefixed(h.left, type_name, ' ',
|
| 300 | child, h.right)
|
| 301 | else:
|
| 302 | # e.g. <Id.Lit_Chars foo>
|
| 303 | return self._Surrounded(h.left, child, h.right)
|
| 304 |
|
| 305 | else:
|
| 306 | raise AssertionError()
|
| 307 |
|
| 308 |
|
| 309 | # vim: sw=4
|