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

354 lines, 244 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 # (InternalStringArray) (InternalStringArray $'\\')
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 _InternalStringArray(self, varray):
178 # type: (value.InternalStringArray) -> MeasuredDoc
179 type_name = self._Styled(self.type_style,
180 AsciiText('InternalStringArray'))
181 if bash_impl.InternalStringArray_Count(varray) == 0:
182 return _Concat([AsciiText('('), type_name, AsciiText(')')])
183 mdocs = [] # type: List[MeasuredDoc]
184 for s in bash_impl.InternalStringArray_GetValues(varray):
185 if s is None:
186 mdocs.append(AsciiText('null'))
187 else:
188 mdocs.append(self._BashStringLiteral(s))
189 return self._SurroundedAndPrefixed('(', type_name, ' ',
190 self._Tabular(mdocs, ''), ')')
191
192 def _BashAssoc(self, vassoc):
193 # type: (value.BashAssoc) -> MeasuredDoc
194 type_name = self._Styled(self.type_style, AsciiText('BashAssoc'))
195 if bash_impl.BashAssoc_Count(vassoc) == 0:
196 return _Concat([AsciiText('('), type_name, AsciiText(')')])
197 mdocs = [] # type: List[MeasuredDoc]
198 for k2, v2 in iteritems(bash_impl.BashAssoc_GetDict(vassoc)):
199 mdocs.append(
200 _Concat([
201 AsciiText('['),
202 self._BashStringLiteral(k2),
203 AsciiText(']='),
204 self._BashStringLiteral(v2)
205 ]))
206 return self._SurroundedAndPrefixed('(', type_name, ' ',
207 self._Join(mdocs, '', ' '), ')')
208
209 def _BashArray(self, val):
210 # type: (value.BashArray) -> MeasuredDoc
211 type_name = self._Styled(self.type_style, AsciiText('BashArray'))
212 if bash_impl.BashArray_Count(val) == 0:
213 return _Concat([AsciiText('('), type_name, AsciiText(')')])
214 mdocs = [] # type: List[MeasuredDoc]
215 for k2 in bash_impl.BashArray_GetKeys(val):
216 v2, error_code = bash_impl.BashArray_GetElement(val, k2)
217 assert error_code == error_code_e.OK, error_code
218 mdocs.append(
219 _Concat([
220 AsciiText('['),
221 self._Styled(self.int_style, AsciiText(mops.ToStr(k2))),
222 AsciiText(']='),
223 self._BashStringLiteral(v2)
224 ]))
225 return self._SurroundedAndPrefixed('(', type_name, ' ',
226 self._Join(mdocs, '', ' '), ')')
227
228 def _Obj(self, obj):
229 # type: (Obj) -> MeasuredDoc
230 chain = [] # type: List[MeasuredDoc]
231 cur = obj
232 while cur is not None:
233 mdocs = self._DictMdocs(cur.d)
234 chain.append(
235 self._Surrounded('(', self._Join(mdocs, ',', ' '), ')'))
236 cur = cur.prototype
237 if cur is not None:
238 chain.append(AsciiText(' --> '))
239
240 return _Concat(chain)
241
242 def _Value(self, val):
243 # type: (value_t) -> MeasuredDoc
244
245 with tagswitch(val) as case:
246 if case(value_e.Null):
247 return self._Styled(self.null_style, AsciiText('null'))
248
249 elif case(value_e.Bool):
250 b = cast(value.Bool, val).b
251 return self._Styled(self.bool_style,
252 AsciiText('true' if b else 'false'))
253
254 elif case(value_e.Int):
255 i = cast(value.Int, val).i
256 return self._Styled(self.int_style, AsciiText(mops.ToStr(i)))
257
258 elif case(value_e.Float):
259 f = cast(value.Float, val).f
260 return self._Styled(self.float_style,
261 AsciiText(FloatString(f)))
262
263 elif case(value_e.Str):
264 s = cast(value.Str, val).s
265 return self._StringLiteral(s)
266
267 elif case(value_e.Range):
268 r = cast(value.Range, val)
269 type_name = self._Styled(self.type_style,
270 AsciiText(ValType(r)))
271 mdocs = [
272 AsciiText(str(r.lower)),
273 AsciiText('..<'),
274 AsciiText(str(r.upper))
275 ]
276 return self._SurroundedAndPrefixed('(', type_name, ' ',
277 self._Join(mdocs, '', ' '),
278 ')')
279
280 elif case(value_e.List):
281 vlist = cast(value.List, val)
282 heap_id = j8.HeapValueId(vlist)
283 if self.visiting.get(heap_id, False):
284 return _Concat([
285 AsciiText('['),
286 self._Styled(self.cycle_style, AsciiText('...')),
287 AsciiText(']')
288 ])
289 else:
290 self.visiting[heap_id] = True
291 result = self._YshList(vlist)
292 self.visiting[heap_id] = False
293 return result
294
295 elif case(value_e.Dict):
296 vdict = cast(value.Dict, val)
297 heap_id = j8.HeapValueId(vdict)
298 if self.visiting.get(heap_id, False):
299 return _Concat([
300 AsciiText('{'),
301 self._Styled(self.cycle_style, AsciiText('...')),
302 AsciiText('}')
303 ])
304 else:
305 self.visiting[heap_id] = True
306 result = self._YshDict(vdict)
307 self.visiting[heap_id] = False
308 return result
309
310 elif case(value_e.BashArray):
311 sparse = cast(value.BashArray, val)
312 return self._BashArray(sparse)
313
314 elif case(value_e.InternalStringArray):
315 varray = cast(value.InternalStringArray, val)
316 return self._InternalStringArray(varray)
317
318 elif case(value_e.BashAssoc):
319 vassoc = cast(value.BashAssoc, val)
320 return self._BashAssoc(vassoc)
321
322 elif case(value_e.Obj):
323 vaobj = cast(Obj, val)
324 heap_id = j8.HeapValueId(vaobj)
325 if self.visiting.get(heap_id, False):
326 return _Concat([
327 AsciiText('('),
328 self._Styled(self.cycle_style, AsciiText('...')),
329 AsciiText(')')
330 ])
331 else:
332 self.visiting[heap_id] = True
333 result = self._Obj(vaobj)
334 self.visiting[heap_id] = False
335 return result
336
337 # Bug fix: these types are GLOBAL singletons in C++. This means
338 # they have no object ID, so j8.ValueIdString() will CRASH on them.
339
340 elif case(value_e.Stdin, value_e.Interrupted):
341 type_name = self._Styled(self.type_style,
342 AsciiText(ValType(val)))
343 return _Concat([AsciiText('<'), type_name, AsciiText('>')])
344
345 else:
346 type_name = self._Styled(self.type_style,
347 AsciiText(ValType(val)))
348 id_str = j8.ValueIdString(val)
349 return _Concat(
350 [AsciiText('<'), type_name,
351 AsciiText(id_str + '>')])
352
353
354# vim: sw=4