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