OILS / display / pp_value.py View on Github | oils.pub

353 lines, 243 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3"""
4Render Oils value_t -> doc_t, so it can be pretty printed
5"""
6
7import math
8
9from _devbuild.gen.pretty_asdl import (doc, Measure, MeasuredDoc)
10from _devbuild.gen.runtime_asdl import error_code_e
11from _devbuild.gen.value_asdl import Obj, value, value_e, value_t, value_str
12from core import bash_impl
13from data_lang import j8
14from data_lang import j8_lite
15from display import ansi
16from display import pp_hnode
17from display.pretty import _Break, _Concat, AsciiText
18from frontend import match
19from mycpp import mops
20from mycpp.mylib import log, tagswitch, iteritems
21from typing import cast, List, Dict
22
23import libc
24
25_ = log
26
27
28def ValType(val):
29 # type: (value_t) -> str
30 """Returns a user-facing string like Int, Eggex, BashArray, etc."""
31 return value_str(val.tag(), dot=False)
32
33
34def FloatString(fl):
35 # type: (float) -> str
36
37 # Print in YSH syntax, similar to data_lang/j8.py
38 if math.isinf(fl):
39 s = 'INFINITY'
40 if fl < 0:
41 s = '-' + s
42 elif math.isnan(fl):
43 s = 'NAN'
44 else:
45 s = str(fl)
46 return s
47
48
49#
50# Unicode Helpers
51#
52
53
54def TryUnicodeWidth(s):
55 # type: (str) -> int
56 try:
57 width = libc.wcswidth(s)
58 except UnicodeError:
59 # e.g. en_US.UTF-8 locale missing, just return the number of bytes
60 width = len(s)
61
62 if width == -1: # non-printable wide char
63 return len(s)
64
65 return width
66
67
68def UText(string):
69 # type: (str) -> MeasuredDoc
70 """Print `string` (which must not contain a newline)."""
71 return MeasuredDoc(doc.Text(string), Measure(TryUnicodeWidth(string), -1))
72
73
74class ValueEncoder(pp_hnode.BaseEncoder):
75 """Converts Oils values into `doc`s, which can then be pretty printed."""
76
77 def __init__(self):
78 # type: () -> None
79 pp_hnode.BaseEncoder.__init__(self)
80 self.ysh_style = True
81
82 # These can be configurable later
83 self.int_style = ansi.YELLOW
84 self.float_style = ansi.BLUE
85 self.null_style = ansi.RED
86 self.bool_style = ansi.CYAN
87 self.string_style = ansi.GREEN
88 self.cycle_style = ansi.BOLD + ansi.BLUE
89 self.type_style = ansi.MAGENTA
90
91 def TypePrefix(self, type_str):
92 # type: (str) -> List[MeasuredDoc]
93 """Return docs for type string '(List)', which may break afterward."""
94 type_name = self._Styled(self.type_style, AsciiText(type_str))
95
96 n = len(type_str)
97 # Our maximum string is 'Float'
98 assert n <= 5, type_str
99
100 # Start printing in column 8. Adjust to 6 because () takes 2 spaces.
101 spaces = ' ' * (6 - n)
102
103 mdocs = [AsciiText('('), type_name, AsciiText(')'), _Break(spaces)]
104 return mdocs
105
106 def Value(self, val):
107 # type: (value_t) -> MeasuredDoc
108 """Convert an Oils value into a `doc`, which can then be pretty printed."""
109 self.visiting.clear()
110 return self._Value(val)
111
112 def _DictKey(self, s):
113 # type: (str) -> MeasuredDoc
114 if match.IsValidVarName(s):
115 encoded = s
116 else:
117 if self.ysh_style:
118 encoded = j8_lite.YshEncodeString(s)
119 else:
120 # TODO: remove this dead branch after fixing tests
121 encoded = j8_lite.EncodeString(s)
122 return UText(encoded)
123
124 def _StringLiteral(self, s):
125 # type: (str) -> MeasuredDoc
126 if self.ysh_style:
127 # YSH r'' or b'' style
128 encoded = j8_lite.YshEncodeString(s)
129 else:
130 # TODO: remove this dead branch after fixing tests
131 encoded = j8_lite.EncodeString(s)
132 return self._Styled(self.string_style, UText(encoded))
133
134 def _BashStringLiteral(self, s):
135 # type: (str) -> MeasuredDoc
136
137 # '' or $'' style
138 #
139 # We mimic bash syntax by using $'\\' instead of b'\\'
140 #
141 # $ declare -a array=($'\\')
142 # $ = array
143 # (BashArray) (BashArray $'\\')
144 #
145 # $ declare -A assoc=([k]=$'\\')
146 # $ = assoc
147 # (BashAssoc) (BashAssoc ['k']=$'\\')
148
149 encoded = j8_lite.ShellEncode(s)
150 return self._Styled(self.string_style, UText(encoded))
151
152 def _YshList(self, vlist):
153 # type: (value.List) -> MeasuredDoc
154 """Print a string literal."""
155 if len(vlist.items) == 0:
156 return AsciiText('[]')
157 mdocs = [self._Value(item) for item in vlist.items]
158 return self._Surrounded('[', self._Tabular(mdocs, ','), ']')
159
160 def _DictMdocs(self, d):
161 # type: (Dict[str, value_t]) -> List[MeasuredDoc]
162 mdocs = [] # type: List[MeasuredDoc]
163 for k, v in iteritems(d):
164 mdocs.append(
165 _Concat([self._DictKey(k),
166 AsciiText(': '),
167 self._Value(v)]))
168 return mdocs
169
170 def _YshDict(self, vdict):
171 # type: (value.Dict) -> MeasuredDoc
172 if len(vdict.d) == 0:
173 return AsciiText('{}')
174 mdocs = self._DictMdocs(vdict.d)
175 return self._Surrounded('{', self._Join(mdocs, ',', ' '), '}')
176
177 def _BashArray(self, varray):
178 # type: (value.BashArray) -> MeasuredDoc
179 type_name = self._Styled(self.type_style, AsciiText('BashArray'))
180 if bash_impl.BashArray_Count(varray) == 0:
181 return _Concat([AsciiText('('), type_name, AsciiText(')')])
182 mdocs = [] # type: List[MeasuredDoc]
183 for s in bash_impl.BashArray_GetValues(varray):
184 if s is None:
185 mdocs.append(AsciiText('null'))
186 else:
187 mdocs.append(self._BashStringLiteral(s))
188 return self._SurroundedAndPrefixed('(', type_name, ' ',
189 self._Tabular(mdocs, ''), ')')
190
191 def _BashAssoc(self, vassoc):
192 # type: (value.BashAssoc) -> MeasuredDoc
193 type_name = self._Styled(self.type_style, AsciiText('BashAssoc'))
194 if bash_impl.BashAssoc_Count(vassoc) == 0:
195 return _Concat([AsciiText('('), type_name, AsciiText(')')])
196 mdocs = [] # type: List[MeasuredDoc]
197 for k2, v2 in iteritems(bash_impl.BashAssoc_GetDict(vassoc)):
198 mdocs.append(
199 _Concat([
200 AsciiText('['),
201 self._BashStringLiteral(k2),
202 AsciiText(']='),
203 self._BashStringLiteral(v2)
204 ]))
205 return self._SurroundedAndPrefixed('(', type_name, ' ',
206 self._Join(mdocs, '', ' '), ')')
207
208 def _SparseArray(self, val):
209 # type: (value.SparseArray) -> MeasuredDoc
210 type_name = self._Styled(self.type_style, AsciiText('SparseArray'))
211 if bash_impl.SparseArray_Count(val) == 0:
212 return _Concat([AsciiText('('), type_name, AsciiText(')')])
213 mdocs = [] # type: List[MeasuredDoc]
214 for k2 in bash_impl.SparseArray_GetKeys(val):
215 v2, error_code = bash_impl.SparseArray_GetElement(val, k2)
216 assert error_code == error_code_e.OK, error_code
217 mdocs.append(
218 _Concat([
219 AsciiText('['),
220 self._Styled(self.int_style, AsciiText(mops.ToStr(k2))),
221 AsciiText(']='),
222 self._BashStringLiteral(v2)
223 ]))
224 return self._SurroundedAndPrefixed('(', type_name, ' ',
225 self._Join(mdocs, '', ' '), ')')
226
227 def _Obj(self, obj):
228 # type: (Obj) -> MeasuredDoc
229 chain = [] # type: List[MeasuredDoc]
230 cur = obj
231 while cur is not None:
232 mdocs = self._DictMdocs(cur.d)
233 chain.append(
234 self._Surrounded('(', self._Join(mdocs, ',', ' '), ')'))
235 cur = cur.prototype
236 if cur is not None:
237 chain.append(AsciiText(' --> '))
238
239 return _Concat(chain)
240
241 def _Value(self, val):
242 # type: (value_t) -> MeasuredDoc
243
244 with tagswitch(val) as case:
245 if case(value_e.Null):
246 return self._Styled(self.null_style, AsciiText('null'))
247
248 elif case(value_e.Bool):
249 b = cast(value.Bool, val).b
250 return self._Styled(self.bool_style,
251 AsciiText('true' if b else 'false'))
252
253 elif case(value_e.Int):
254 i = cast(value.Int, val).i
255 return self._Styled(self.int_style, AsciiText(mops.ToStr(i)))
256
257 elif case(value_e.Float):
258 f = cast(value.Float, val).f
259 return self._Styled(self.float_style,
260 AsciiText(FloatString(f)))
261
262 elif case(value_e.Str):
263 s = cast(value.Str, val).s
264 return self._StringLiteral(s)
265
266 elif case(value_e.Range):
267 r = cast(value.Range, val)
268 type_name = self._Styled(self.type_style,
269 AsciiText(ValType(r)))
270 mdocs = [
271 AsciiText(str(r.lower)),
272 AsciiText('..<'),
273 AsciiText(str(r.upper))
274 ]
275 return self._SurroundedAndPrefixed('(', type_name, ' ',
276 self._Join(mdocs, '', ' '),
277 ')')
278
279 elif case(value_e.List):
280 vlist = cast(value.List, val)
281 heap_id = j8.HeapValueId(vlist)
282 if self.visiting.get(heap_id, False):
283 return _Concat([
284 AsciiText('['),
285 self._Styled(self.cycle_style, AsciiText('...')),
286 AsciiText(']')
287 ])
288 else:
289 self.visiting[heap_id] = True
290 result = self._YshList(vlist)
291 self.visiting[heap_id] = False
292 return result
293
294 elif case(value_e.Dict):
295 vdict = cast(value.Dict, val)
296 heap_id = j8.HeapValueId(vdict)
297 if self.visiting.get(heap_id, False):
298 return _Concat([
299 AsciiText('{'),
300 self._Styled(self.cycle_style, AsciiText('...')),
301 AsciiText('}')
302 ])
303 else:
304 self.visiting[heap_id] = True
305 result = self._YshDict(vdict)
306 self.visiting[heap_id] = False
307 return result
308
309 elif case(value_e.SparseArray):
310 sparse = cast(value.SparseArray, val)
311 return self._SparseArray(sparse)
312
313 elif case(value_e.BashArray):
314 varray = cast(value.BashArray, val)
315 return self._BashArray(varray)
316
317 elif case(value_e.BashAssoc):
318 vassoc = cast(value.BashAssoc, val)
319 return self._BashAssoc(vassoc)
320
321 elif case(value_e.Obj):
322 vaobj = cast(Obj, val)
323 heap_id = j8.HeapValueId(vaobj)
324 if self.visiting.get(heap_id, False):
325 return _Concat([
326 AsciiText('('),
327 self._Styled(self.cycle_style, AsciiText('...')),
328 AsciiText(')')
329 ])
330 else:
331 self.visiting[heap_id] = True
332 result = self._Obj(vaobj)
333 self.visiting[heap_id] = False
334 return result
335
336 # Bug fix: these types are GLOBAL singletons in C++. This means
337 # they have no object ID, so j8.ValueIdString() will CRASH on them.
338
339 elif case(value_e.Stdin, value_e.Interrupted):
340 type_name = self._Styled(self.type_style,
341 AsciiText(ValType(val)))
342 return _Concat([AsciiText('<'), type_name, AsciiText('>')])
343
344 else:
345 type_name = self._Styled(self.type_style,
346 AsciiText(ValType(val)))
347 id_str = j8.ValueIdString(val)
348 return _Concat(
349 [AsciiText('<'), type_name,
350 AsciiText(id_str + '>')])
351
352
353# vim: sw=4