1 | """
|
2 | format.py -- Pretty print an ASDL data structure.
|
3 |
|
4 | TODO: replace ad hoc line wrapper, e.g. _TrySingleLine
|
5 |
|
6 | - auto-abbreviation of single field things (minus location)
|
7 | - option to omit spaces for SQ, SQ, W? It's all one thing.
|
8 |
|
9 | Where we try wrap to a single line:
|
10 | - arrays
|
11 | - objects with name fields
|
12 | - abbreviated, unnamed fields
|
13 | """
|
14 | from typing import Tuple, List
|
15 |
|
16 | from _devbuild.gen.hnode_asdl import (hnode, hnode_e, hnode_t, color_e,
|
17 | color_t)
|
18 | from core import ansi
|
19 | from data_lang import j8_lite
|
20 | from pylib import cgi
|
21 | from mycpp import mylib
|
22 |
|
23 | from typing import cast, Any, Optional
|
24 |
|
25 | if mylib.PYTHON:
|
26 |
|
27 | def PrettyPrint(obj, f=None):
|
28 | # type: (Any, Optional[mylib.Writer]) -> None
|
29 | """Print abbreviated tree in color. For unit tests."""
|
30 | f = f if f else mylib.Stdout()
|
31 |
|
32 | ast_f = DetectConsoleOutput(f)
|
33 | tree = obj.AbbreviatedTree()
|
34 | PrintTree(tree, ast_f)
|
35 |
|
36 |
|
37 | def DetectConsoleOutput(f):
|
38 | # type: (mylib.Writer) -> ColorOutput
|
39 | """Wrapped to auto-detect."""
|
40 | if f.isatty():
|
41 | return AnsiOutput(f)
|
42 | else:
|
43 | return TextOutput(f)
|
44 |
|
45 |
|
46 | class ColorOutput(object):
|
47 | """Abstract base class for plain text, ANSI color, and HTML color."""
|
48 |
|
49 | def __init__(self, f):
|
50 | # type: (mylib.Writer) -> None
|
51 | self.f = f
|
52 | self.num_chars = 0
|
53 |
|
54 | def NewTempBuffer(self):
|
55 | # type: () -> ColorOutput
|
56 | """Return a temporary buffer for the line wrapping calculation."""
|
57 | raise NotImplementedError()
|
58 |
|
59 | def FileHeader(self):
|
60 | # type: () -> None
|
61 | """Hook for printing a full file."""
|
62 | pass
|
63 |
|
64 | def FileFooter(self):
|
65 | # type: () -> None
|
66 | """Hook for printing a full file."""
|
67 | pass
|
68 |
|
69 | def PushColor(self, e_color):
|
70 | # type: (color_t) -> None
|
71 | raise NotImplementedError()
|
72 |
|
73 | def PopColor(self):
|
74 | # type: () -> None
|
75 | raise NotImplementedError()
|
76 |
|
77 | def write(self, s):
|
78 | # type: (str) -> None
|
79 | self.f.write(s)
|
80 | self.num_chars += len(s) # Only count visible characters!
|
81 |
|
82 | def WriteRaw(self, raw):
|
83 | # type: (Tuple[str, int]) -> None
|
84 | """Write raw data without escaping, and without counting control codes
|
85 | in the length."""
|
86 | s, num_chars = raw
|
87 | self.f.write(s)
|
88 | self.num_chars += num_chars
|
89 |
|
90 | def NumChars(self):
|
91 | # type: () -> int
|
92 | return self.num_chars
|
93 |
|
94 | def GetRaw(self):
|
95 | # type: () -> Tuple[str, int]
|
96 |
|
97 | # NOTE: Ensured by NewTempBuffer()
|
98 | f = cast(mylib.BufWriter, self.f)
|
99 | return f.getvalue(), self.num_chars
|
100 |
|
101 |
|
102 | class TextOutput(ColorOutput):
|
103 | """TextOutput put obeys the color interface, but outputs nothing."""
|
104 |
|
105 | def __init__(self, f):
|
106 | # type: (mylib.Writer) -> None
|
107 | ColorOutput.__init__(self, f)
|
108 |
|
109 | def NewTempBuffer(self):
|
110 | # type: () -> TextOutput
|
111 | return TextOutput(mylib.BufWriter())
|
112 |
|
113 | def PushColor(self, e_color):
|
114 | # type: (color_t) -> None
|
115 | pass # ignore color
|
116 |
|
117 | def PopColor(self):
|
118 | # type: () -> None
|
119 | pass # ignore color
|
120 |
|
121 |
|
122 | class HtmlOutput(ColorOutput):
|
123 | """HTML one can have wider columns. Maybe not even fixed-width font. Hm
|
124 | yeah indentation should be logical then?
|
125 |
|
126 | Color: HTML spans
|
127 | """
|
128 |
|
129 | def __init__(self, f):
|
130 | # type: (mylib.Writer) -> None
|
131 | ColorOutput.__init__(self, f)
|
132 |
|
133 | def NewTempBuffer(self):
|
134 | # type: () -> HtmlOutput
|
135 | return HtmlOutput(mylib.BufWriter())
|
136 |
|
137 | def FileHeader(self):
|
138 | # type: () -> None
|
139 | # TODO: Use a different CSS file to make the colors match. I like string
|
140 | # literals as yellow, etc.
|
141 | #<link rel="stylesheet" type="text/css" href="/css/code.css" />
|
142 | self.f.write("""
|
143 | <html>
|
144 | <head>
|
145 | <title>oil AST</title>
|
146 | <style>
|
147 | .n { color: brown }
|
148 | .s { font-weight: bold }
|
149 | .o { color: darkgreen }
|
150 | </style>
|
151 | </head>
|
152 | <body>
|
153 | <pre>
|
154 | """)
|
155 |
|
156 | def FileFooter(self):
|
157 | # type: () -> None
|
158 | self.f.write("""
|
159 | </pre>
|
160 | </body>
|
161 | </html>
|
162 | """)
|
163 |
|
164 | def PushColor(self, e_color):
|
165 | # type: (color_t) -> None
|
166 | # To save bandwidth, use single character CSS names.
|
167 | if e_color == color_e.TypeName:
|
168 | css_class = 'n'
|
169 | elif e_color == color_e.StringConst:
|
170 | css_class = 's'
|
171 | elif e_color == color_e.OtherConst:
|
172 | css_class = 'o'
|
173 | elif e_color == color_e.External:
|
174 | css_class = 'o'
|
175 | elif e_color == color_e.UserType:
|
176 | css_class = 'o'
|
177 | else:
|
178 | raise AssertionError(e_color)
|
179 | self.f.write('<span class="%s">' % css_class)
|
180 |
|
181 | def PopColor(self):
|
182 | # type: () -> None
|
183 | self.f.write('</span>')
|
184 |
|
185 | def write(self, s):
|
186 | # type: (str) -> None
|
187 |
|
188 | # PROBLEM: Double escaping!
|
189 | self.f.write(cgi.escape(s))
|
190 | self.num_chars += len(s) # Only count visible characters!
|
191 |
|
192 |
|
193 | class AnsiOutput(ColorOutput):
|
194 | """For the console."""
|
195 |
|
196 | def __init__(self, f):
|
197 | # type: (mylib.Writer) -> None
|
198 | ColorOutput.__init__(self, f)
|
199 |
|
200 | def NewTempBuffer(self):
|
201 | # type: () -> AnsiOutput
|
202 | return AnsiOutput(mylib.BufWriter())
|
203 |
|
204 | def PushColor(self, e_color):
|
205 | # type: (color_t) -> None
|
206 | if e_color == color_e.TypeName:
|
207 | self.f.write(ansi.YELLOW)
|
208 | elif e_color == color_e.StringConst:
|
209 | self.f.write(ansi.BOLD)
|
210 | elif e_color == color_e.OtherConst:
|
211 | self.f.write(ansi.GREEN)
|
212 | elif e_color == color_e.External:
|
213 | self.f.write(ansi.BOLD + ansi.BLUE)
|
214 | elif e_color == color_e.UserType:
|
215 | self.f.write(ansi.GREEN) # Same color as other literals for now
|
216 | else:
|
217 | raise AssertionError(e_color)
|
218 |
|
219 | def PopColor(self):
|
220 | # type: () -> None
|
221 | self.f.write(ansi.RESET)
|
222 |
|
223 |
|
224 | INDENT = 2
|
225 |
|
226 |
|
227 | class _PrettyPrinter(object):
|
228 |
|
229 | def __init__(self, max_col):
|
230 | # type: (int) -> None
|
231 | self.max_col = max_col
|
232 |
|
233 | def _PrintWrappedArray(self, array, prefix_len, f, indent):
|
234 | # type: (List[hnode_t], int, ColorOutput, int) -> bool
|
235 | """Print an array of objects with line wrapping.
|
236 |
|
237 | Returns whether they all fit on a single line, so you can print
|
238 | the closing brace properly.
|
239 | """
|
240 | all_fit = True
|
241 | chars_so_far = prefix_len
|
242 |
|
243 | for i, val in enumerate(array):
|
244 | if i != 0:
|
245 | f.write(' ')
|
246 |
|
247 | single_f = f.NewTempBuffer()
|
248 | if _TrySingleLine(val, single_f, self.max_col - chars_so_far):
|
249 | s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
|
250 | f.WriteRaw((s, num_chars))
|
251 | chars_so_far += single_f.NumChars()
|
252 | else: # WRAP THE LINE
|
253 | f.write('\n')
|
254 | self.PrintNode(val, f, indent + INDENT)
|
255 |
|
256 | chars_so_far = 0 # allow more
|
257 | all_fit = False
|
258 | return all_fit
|
259 |
|
260 | def _PrintWholeArray(self, array, prefix_len, f, indent):
|
261 | # type: (List[hnode_t], int, ColorOutput, int) -> bool
|
262 |
|
263 | # This is UNLIKE the abbreviated case above, where we do WRAPPING.
|
264 | # Here, ALL children must fit on a single line, or else we separate
|
265 | # each one onto a separate line. This is to avoid the following:
|
266 | #
|
267 | # children: [(C ...)
|
268 | # (C ...)
|
269 | # ]
|
270 | # The first child is out of line. The abbreviated objects have a
|
271 | # small header like C or DQ so it doesn't matter as much.
|
272 | all_fit = True
|
273 | pieces = [] # type: List[Tuple[str, int]]
|
274 | chars_so_far = prefix_len
|
275 | for item in array:
|
276 | single_f = f.NewTempBuffer()
|
277 | if _TrySingleLine(item, single_f, self.max_col - chars_so_far):
|
278 | s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
|
279 | pieces.append((s, num_chars))
|
280 | chars_so_far += single_f.NumChars()
|
281 | else:
|
282 | all_fit = False
|
283 | break
|
284 |
|
285 | if all_fit:
|
286 | for i, p in enumerate(pieces):
|
287 | if i != 0:
|
288 | f.write(' ')
|
289 | f.WriteRaw(p)
|
290 | f.write(']')
|
291 | return all_fit
|
292 |
|
293 | def _PrintRecord(self, node, f, indent):
|
294 | # type: (hnode.Record, ColorOutput, int) -> None
|
295 | """Print a CompoundObj in abbreviated or normal form."""
|
296 | ind = ' ' * indent
|
297 |
|
298 | if node.abbrev: # abbreviated
|
299 | prefix = ind + node.left
|
300 | f.write(prefix)
|
301 | if len(node.node_type):
|
302 | f.PushColor(color_e.TypeName)
|
303 | f.write(node.node_type)
|
304 | f.PopColor()
|
305 | f.write(' ')
|
306 |
|
307 | prefix_len = len(prefix) + len(node.node_type) + 1
|
308 | all_fit = self._PrintWrappedArray(node.unnamed_fields, prefix_len,
|
309 | f, indent)
|
310 |
|
311 | if not all_fit:
|
312 | f.write('\n')
|
313 | f.write(ind)
|
314 | f.write(node.right)
|
315 |
|
316 | else: # full form like (SimpleCommand ...)
|
317 | f.write(ind + node.left)
|
318 |
|
319 | f.PushColor(color_e.TypeName)
|
320 | f.write(node.node_type)
|
321 | f.PopColor()
|
322 |
|
323 | f.write('\n')
|
324 | for field in node.fields:
|
325 | name = field.name
|
326 | val = field.val
|
327 |
|
328 | ind1 = ' ' * (indent + INDENT)
|
329 | UP_val = val # for mycpp
|
330 | tag = val.tag()
|
331 | if tag == hnode_e.Array:
|
332 | val = cast(hnode.Array, UP_val)
|
333 |
|
334 | name_str = '%s%s: [' % (ind1, name)
|
335 | f.write(name_str)
|
336 | prefix_len = len(name_str)
|
337 |
|
338 | if not self._PrintWholeArray(val.children, prefix_len, f,
|
339 | indent):
|
340 | f.write('\n')
|
341 | for child in val.children:
|
342 | self.PrintNode(child, f, indent + INDENT + INDENT)
|
343 | f.write('\n')
|
344 | f.write('%s]' % ind1)
|
345 |
|
346 | else: # primitive field
|
347 | name_str = '%s%s: ' % (ind1, name)
|
348 | f.write(name_str)
|
349 | prefix_len = len(name_str)
|
350 |
|
351 | # Try to print it on the same line as the field name; otherwise print
|
352 | # it on a separate line.
|
353 | single_f = f.NewTempBuffer()
|
354 | if _TrySingleLine(val, single_f,
|
355 | self.max_col - prefix_len):
|
356 | s, num_chars = single_f.GetRaw(
|
357 | ) # extra unpacking for mycpp
|
358 | f.WriteRaw((s, num_chars))
|
359 | else:
|
360 | f.write('\n')
|
361 | self.PrintNode(val, f, indent + INDENT + INDENT)
|
362 |
|
363 | f.write('\n') # separate fields
|
364 |
|
365 | f.write(ind + node.right)
|
366 |
|
367 | def PrintNode(self, node, f, indent):
|
368 | # type: (hnode_t, ColorOutput, int) -> None
|
369 | """Second step of printing: turn homogeneous tree into a colored
|
370 | string.
|
371 |
|
372 | Args:
|
373 | node: homogeneous tree node
|
374 | f: ColorOutput instance.
|
375 | max_col: don't print past this column number on ANY line
|
376 | NOTE: See asdl/run.sh line-length-hist for a test of this. It's
|
377 | approximate.
|
378 | TODO: Use the terminal width.
|
379 | """
|
380 | ind = ' ' * indent
|
381 |
|
382 | # Try printing on a single line
|
383 | single_f = f.NewTempBuffer()
|
384 | single_f.write(ind)
|
385 | if _TrySingleLine(node, single_f, self.max_col - indent):
|
386 | s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
|
387 | f.WriteRaw((s, num_chars))
|
388 | return
|
389 |
|
390 | UP_node = node # for mycpp
|
391 | tag = node.tag()
|
392 | if tag == hnode_e.Leaf:
|
393 | node = cast(hnode.Leaf, UP_node)
|
394 | f.PushColor(node.color)
|
395 | f.write(j8_lite.EncodeString(node.s, unquoted_ok=True))
|
396 | f.PopColor()
|
397 |
|
398 | elif tag == hnode_e.External:
|
399 | node = cast(hnode.External, UP_node)
|
400 | f.PushColor(color_e.External)
|
401 | if mylib.PYTHON:
|
402 | f.write(repr(node.obj))
|
403 | else:
|
404 | f.write('UNTYPED any')
|
405 | f.PopColor()
|
406 |
|
407 | elif tag == hnode_e.Record:
|
408 | node = cast(hnode.Record, UP_node)
|
409 | self._PrintRecord(node, f, indent)
|
410 |
|
411 | elif tag == hnode_e.AlreadySeen:
|
412 | node = cast(hnode.AlreadySeen, UP_node)
|
413 | # ... means omitting second reference, while --- means a cycle
|
414 | f.write('...0x%s' % mylib.hex_lower(node.heap_id))
|
415 |
|
416 | else:
|
417 | raise AssertionError(node)
|
418 |
|
419 |
|
420 | def _TrySingleLineObj(node, f, max_chars):
|
421 | # type: (hnode.Record, ColorOutput, int) -> bool
|
422 | """Print an object on a single line."""
|
423 | f.write(node.left)
|
424 | if node.abbrev:
|
425 | if len(node.node_type):
|
426 | f.PushColor(color_e.TypeName)
|
427 | f.write(node.node_type)
|
428 | f.PopColor()
|
429 | f.write(' ')
|
430 |
|
431 | for i, val in enumerate(node.unnamed_fields):
|
432 | if i != 0:
|
433 | f.write(' ')
|
434 | if not _TrySingleLine(val, f, max_chars):
|
435 | return False
|
436 | else:
|
437 | f.PushColor(color_e.TypeName)
|
438 | f.write(node.node_type)
|
439 | f.PopColor()
|
440 |
|
441 | for field in node.fields:
|
442 | f.write(' %s:' % field.name)
|
443 | if not _TrySingleLine(field.val, f, max_chars):
|
444 | return False
|
445 |
|
446 | f.write(node.right)
|
447 | return True
|
448 |
|
449 |
|
450 | def _TrySingleLine(node, f, max_chars):
|
451 | # type: (hnode_t, ColorOutput, int) -> bool
|
452 | """Try printing on a single line.
|
453 |
|
454 | Args:
|
455 | node: homogeneous tree node
|
456 | f: ColorOutput instance
|
457 | max_chars: maximum number of characters to print on THIS line
|
458 | indent: current indent level
|
459 |
|
460 | Returns:
|
461 | ok: whether it fit on the line of the given size.
|
462 | If False, you can't use the value of f.
|
463 | """
|
464 | UP_node = node # for mycpp
|
465 | tag = node.tag()
|
466 | if tag == hnode_e.Leaf:
|
467 | node = cast(hnode.Leaf, UP_node)
|
468 | f.PushColor(node.color)
|
469 | f.write(j8_lite.EncodeString(node.s, unquoted_ok=True))
|
470 | f.PopColor()
|
471 |
|
472 | elif tag == hnode_e.External:
|
473 | node = cast(hnode.External, UP_node)
|
474 |
|
475 | f.PushColor(color_e.External)
|
476 | if mylib.PYTHON:
|
477 | f.write(repr(node.obj))
|
478 | else:
|
479 | f.write('UNTYPED any')
|
480 | f.PopColor()
|
481 |
|
482 | elif tag == hnode_e.Array:
|
483 | node = cast(hnode.Array, UP_node)
|
484 |
|
485 | # Can we fit the WHOLE array on the line?
|
486 | f.write('[')
|
487 | for i, item in enumerate(node.children):
|
488 | if i != 0:
|
489 | f.write(' ')
|
490 | if not _TrySingleLine(item, f, max_chars):
|
491 | return False
|
492 | f.write(']')
|
493 |
|
494 | elif tag == hnode_e.Record:
|
495 | node = cast(hnode.Record, UP_node)
|
496 |
|
497 | return _TrySingleLineObj(node, f, max_chars)
|
498 |
|
499 | elif tag == hnode_e.AlreadySeen:
|
500 | node = cast(hnode.AlreadySeen, UP_node)
|
501 | # ... means omitting second reference, while --- means a cycle
|
502 | f.write('...0x%s' % mylib.hex_lower(node.heap_id))
|
503 |
|
504 | else:
|
505 | raise AssertionError(node)
|
506 |
|
507 | # Take into account the last char.
|
508 | num_chars_so_far = f.NumChars()
|
509 | if num_chars_so_far > max_chars:
|
510 | return False
|
511 |
|
512 | return True
|
513 |
|
514 |
|
515 | def PrintTree(node, f):
|
516 | # type: (hnode_t, ColorOutput) -> None
|
517 | pp = _PrettyPrinter(100) # max_col
|
518 | pp.PrintNode(node, f, 0) # indent
|