OILS / asdl / format.py View on Github | oilshell.org

518 lines, 295 significant
1"""
2format.py -- Pretty print an ASDL data structure.
3
4TODO: 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
9Where we try wrap to a single line:
10 - arrays
11 - objects with name fields
12 - abbreviated, unnamed fields
13"""
14from typing import Tuple, List
15
16from _devbuild.gen.hnode_asdl import (hnode, hnode_e, hnode_t, color_e,
17 color_t)
18from core import ansi
19from data_lang import j8_lite
20from pylib import cgi
21from mycpp import mylib
22
23from typing import cast, Any, Optional
24
25if 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
37def 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
46class 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
102class 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
122class 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
193class 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
224INDENT = 2
225
226
227class _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
420def _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
450def _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
515def PrintTree(node, f):
516 # type: (hnode_t, ColorOutput) -> None
517 pp = _PrettyPrinter(100) # max_col
518 pp.PrintNode(node, f, 0) # indent