OILS / display / pp_hnode.py View on Github | oilshell.org

311 lines, 151 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3"""
4Base class for pretty printing, and HNodeEncoder
5"""
6
7from _devbuild.gen.hnode_asdl import hnode, hnode_e, hnode_t, Field, color_e
8from _devbuild.gen.pretty_asdl import (doc, MeasuredDoc, Measure)
9from data_lang import j8_lite
10from display import ansi
11from display import pretty
12from display.pretty import (_Break, _Concat, _Flat, _Group, _IfFlat, _Indent,
13 _EmptyMeasure, AsciiText)
14from mycpp import mylib
15from mycpp.mylib import log, tagswitch, switch
16from typing import cast, List, Dict, Optional
17
18_ = log
19
20
21class 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
206class 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