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

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