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

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