OILS / asdl / gen_python.py View on Github | oils.pub

652 lines, 421 significant
1#!/usr/bin/env python2
2"""gen_python.py: Generate Python code from an ASDL schema."""
3from __future__ import print_function
4
5from collections import defaultdict
6
7from asdl import ast
8from asdl import visitor
9from asdl.util import log
10
11from typing import Tuple, cast
12
13_ = log # shut up lint
14
15_PRIMITIVES = {
16 'string': 'str',
17 'int': 'int',
18 'uint16': 'int',
19 'BigInt': 'mops.BigInt',
20 'float': 'float',
21 'bool': 'bool',
22 'any': 'Any',
23 # TODO: frontend/syntax.asdl should properly import id enum instead of
24 # hard-coding it here.
25 'id': 'Id_t',
26}
27
28
29def _MyPyType(typ):
30 """ASDL type to MyPy Type."""
31 if isinstance(typ, ast.ParameterizedType):
32
33 if typ.type_name == 'Dict':
34 k_type = _MyPyType(typ.children[0])
35 v_type = _MyPyType(typ.children[1])
36 return 'Dict[%s, %s]' % (k_type, v_type)
37
38 if typ.type_name == 'List':
39 return 'List[%s]' % _MyPyType(typ.children[0])
40
41 if typ.type_name == 'Optional':
42 return 'Optional[%s]' % _MyPyType(typ.children[0])
43
44 elif isinstance(typ, ast.NamedType):
45 if typ.resolved:
46 if isinstance(typ.resolved, ast.Sum): # includes SimpleSum
47 return '%s_t' % typ.name
48 if isinstance(typ.resolved, ast.Product):
49 return typ.name
50 if isinstance(typ.resolved, ast.Use):
51 return ast.TypeNameHeuristic(typ.name)
52 if isinstance(typ.resolved, ast.Extern):
53 r = typ.resolved
54 type_name = r.names[-1]
55 py_module = r.names[-2]
56 return '%s.%s' % (py_module, type_name)
57
58 # 'id' falls through here
59 return _PRIMITIVES[typ.name]
60
61 else:
62 raise AssertionError()
63
64
65def _CastedNull(mypy_type):
66 # type: (str) -> str
67 return "cast('%s', None)" % mypy_type
68
69
70def _DefaultValue(typ, mypy_type):
71 """Values that the static CreateNull() constructor passes.
72
73 mypy_type is used to cast None, to maintain mypy --strict for ASDL.
74
75 We circumvent the type system on CreateNull(). Then the user is
76 responsible for filling in all the fields. If they do so, we can
77 rely on it when reading fields at runtime.
78 """
79 if isinstance(typ, ast.ParameterizedType):
80 type_name = typ.type_name
81
82 if type_name == 'Optional':
83 return _CastedNull(mypy_type)
84
85 if type_name == 'List':
86 return "[] if alloc_lists else cast('%s', None)" % mypy_type
87
88 if type_name == 'Dict': # TODO: can respect alloc_dicts=True
89 return _CastedNull(mypy_type)
90
91 raise AssertionError(type_name)
92
93 if isinstance(typ, ast.NamedType):
94 type_name = typ.name
95
96 if type_name == 'id': # hard-coded HACK
97 return '-1'
98
99 if type_name == 'int':
100 return '-1'
101
102 if type_name == 'BigInt':
103 return 'mops.BigInt(-1)'
104
105 if type_name == 'bool':
106 return 'False'
107
108 if type_name == 'float':
109 return '0.0' # or should it be NaN?
110
111 if type_name == 'string':
112 return "''"
113
114 if isinstance(typ.resolved, ast.SimpleSum):
115 sum_type = typ.resolved
116 # Just make it the first variant. We could define "Undef" for
117 # each enum, but it doesn't seem worth it.
118 return '%s_e.%s' % (type_name, sum_type.types[0].name)
119
120 # CompoundSum or Product type
121 return _CastedNull(mypy_type)
122
123 else:
124 raise AssertionError()
125
126
127def _HNodeExpr(typ, var_name):
128 # type: (ast.type_ref_t, str) -> Tuple[str, bool]
129 none_guard = False
130
131 if typ.IsOptional():
132 narrow = cast(ast.ParameterizedType, typ)
133 typ = narrow.children[0] # descend one level
134
135 if isinstance(typ, ast.ParameterizedType):
136 code_str = '%s.PrettyTree()' % var_name
137 none_guard = True
138
139 elif isinstance(typ, ast.NamedType):
140 type_name = typ.name
141
142 if type_name == 'bool':
143 code_str = "hnode.Leaf('T' if %s else 'F', color_e.OtherConst)" % var_name
144
145 elif type_name in ('int', 'uint16'):
146 code_str = 'hnode.Leaf(str(%s), color_e.OtherConst)' % var_name
147
148 elif type_name == 'BigInt':
149 code_str = 'hnode.Leaf(mops.ToStr(%s), color_e.OtherConst)' % var_name
150
151 elif type_name == 'float':
152 code_str = 'hnode.Leaf(str(%s), color_e.OtherConst)' % var_name
153
154 elif type_name == 'string':
155 code_str = 'NewLeaf(%s, color_e.StringConst)' % var_name
156
157 elif type_name == 'any':
158 code_str = 'NewLeaf(str(%s), color_e.External)' % var_name
159
160 elif type_name == 'id': # was meta.UserType
161 # This assumes it's Id, which is a simple SumType. TODO: Remove this.
162 code_str = 'hnode.Leaf(Id_str(%s, dot=False), color_e.UserType)' % var_name
163
164 elif typ.resolved and isinstance(typ.resolved, ast.SimpleSum):
165 code_str = 'hnode.Leaf(%s_str(%s), color_e.TypeName)' % (type_name,
166 var_name)
167
168 else:
169 code_str = '%s.PrettyTree(do_abbrev, trav=trav)' % var_name
170 none_guard = True
171
172 else:
173 raise AssertionError()
174
175 return code_str, none_guard
176
177
178class GenMyPyVisitor(visitor.AsdlVisitor):
179 """Generate Python code with MyPy type annotations."""
180
181 def __init__(self,
182 f,
183 abbrev_mod_entries=None,
184 pretty_print_methods=True,
185 py_init_n=False,
186 simple_int_sums=None):
187
188 visitor.AsdlVisitor.__init__(self, f)
189 self.abbrev_mod_entries = abbrev_mod_entries or []
190 self.pretty_print_methods = pretty_print_methods
191 self.py_init_n = py_init_n
192
193 # For Id to use different code gen. It's used like an integer, not just
194 # like an enum.
195 self.simple_int_sums = simple_int_sums or []
196
197 self._shared_type_tags = {}
198 self._product_counter = 64 # matches asdl/gen_cpp.py
199
200 self._products = []
201 self._base_classes = defaultdict(list)
202
203 self._subtypes = []
204
205 def _EmitDict(self, name, d, depth):
206 self.Emit('_%s_str = {' % name, depth)
207 for k in sorted(d):
208 self.Emit('%d: %r,' % (k, d[k]), depth + 1)
209 self.Emit('}', depth)
210 self.Emit('', depth)
211
212 def VisitSimpleSum(self, sum, sum_name, depth):
213 int_to_str = {}
214 variants = []
215 for i, variant in enumerate(sum.types):
216 tag_num = i + 1
217 int_to_str[tag_num] = variant.name
218 variants.append((variant, tag_num))
219
220 add_suffix = not ('no_namespace_suffix' in sum.generate)
221 gen_integers = 'integers' in sum.generate or 'uint16' in sum.generate
222
223 if gen_integers:
224 self.Emit('%s_t = int # type alias for integer' % sum_name)
225 self.Emit('')
226
227 i_name = ('%s_i' % sum_name) if add_suffix else sum_name
228
229 self.Emit('class %s(object):' % i_name, depth)
230
231 for variant, tag_num in variants:
232 line = ' %s = %d' % (variant.name, tag_num)
233 self.Emit(line, depth)
234
235 # Help in sizing array. Note that we're 1-based.
236 line = ' %s = %d' % ('ARRAY_SIZE', len(variants) + 1)
237 self.Emit(line, depth)
238
239 else:
240 # First emit a type
241 self.Emit('class %s_t(pybase.SimpleObj):' % sum_name, depth)
242 self.Emit(' pass', depth)
243 self.Emit('', depth)
244
245 # Now emit a namespace
246 e_name = ('%s_e' % sum_name) if add_suffix else sum_name
247 self.Emit('class %s(object):' % e_name, depth)
248
249 for variant, tag_num in variants:
250 line = ' %s = %s_t(%d)' % (variant.name, sum_name, tag_num)
251 self.Emit(line, depth)
252
253 self.Emit('', depth)
254
255 self._EmitDict(sum_name, int_to_str, depth)
256
257 self.Emit('def %s_str(val, dot=True):' % sum_name, depth)
258 self.Emit(' # type: (%s_t, bool) -> str' % sum_name, depth)
259 self.Emit(' v = _%s_str[val]' % sum_name, depth)
260 self.Emit(' if dot:', depth)
261 self.Emit(' return "%s.%%s" %% v' % sum_name, depth)
262 self.Emit(' else:', depth)
263 self.Emit(' return v', depth)
264 self.Emit('', depth)
265
266 def _EmitCodeForField(self, field, counter):
267 """Generate code that returns an hnode for a field."""
268 out_val_name = 'x%d' % counter
269
270 if field.typ.IsList():
271 iter_name = 'i%d' % counter
272
273 typ = field.typ
274 if typ.type_name == 'Optional': # descend one level
275 typ = typ.children[0]
276 item_type = typ.children[0]
277
278 self.Emit(' if self.%s is not None: # List' % field.name)
279 self.Emit(' %s = hnode.Array([])' % out_val_name)
280 self.Emit(' for %s in self.%s:' % (iter_name, field.name))
281 child_code_str, none_guard = _HNodeExpr(item_type, iter_name)
282
283 if none_guard: # e.g. for List[Optional[value_t]]
284 # TODO: could consolidate with asdl/runtime.py NewLeaf(), which
285 # also uses _ to mean None/nullptr
286 self.Emit(
287 ' h = (hnode.Leaf("_", color_e.OtherConst) if %s is None else %s)'
288 % (iter_name, child_code_str))
289 self.Emit(' %s.children.append(h)' % out_val_name)
290 else:
291 self.Emit(' %s.children.append(%s)' %
292 (out_val_name, child_code_str))
293
294 self.Emit(' L.append(Field(%r, %s))' %
295 (field.name, out_val_name))
296
297 elif field.typ.IsDict():
298 k = 'k%d' % counter
299 v = 'v%d' % counter
300
301 typ = field.typ
302 if typ.type_name == 'Optional': # descend one level
303 typ = typ.children[0]
304
305 k_typ = typ.children[0]
306 v_typ = typ.children[1]
307
308 k_code_str, _ = _HNodeExpr(k_typ, k)
309 v_code_str, _ = _HNodeExpr(v_typ, v)
310
311 unnamed = 'unnamed%d' % counter
312 self.Emit(' if self.%s is not None: # Dict' % field.name)
313 self.Emit(' %s = [] # type: List[hnode_t]' % unnamed)
314 self.Emit(' %s = hnode.Record("", "{", "}", [], %s)' %
315 (out_val_name, unnamed))
316 self.Emit(' for %s, %s in self.%s.iteritems():' %
317 (k, v, field.name))
318 self.Emit(' %s.append(%s)' % (unnamed, k_code_str))
319 self.Emit(' %s.append(%s)' % (unnamed, v_code_str))
320 self.Emit(' L.append(Field(%r, %s))' %
321 (field.name, out_val_name))
322
323 elif field.typ.IsOptional():
324 typ = field.typ.children[0]
325
326 self.Emit(' if self.%s is not None: # Optional' % field.name)
327 child_code_str, _ = _HNodeExpr(typ, 'self.%s' % field.name)
328 self.Emit(' %s = %s' % (out_val_name, child_code_str))
329 self.Emit(' L.append(Field(%r, %s))' %
330 (field.name, out_val_name))
331
332 else:
333 var_name = 'self.%s' % field.name
334 code_str, obj_none_guard = _HNodeExpr(field.typ, var_name)
335 depth = self.current_depth
336 if obj_none_guard: # to satisfy MyPy type system
337 self.Emit(' assert self.%s is not None' % field.name)
338 self.Emit(' %s = %s' % (out_val_name, code_str), depth)
339
340 self.Emit(' L.append(Field(%r, %s))' % (field.name, out_val_name),
341 depth)
342
343 def _GenClassBegin(self, class_name, base_classes, tag_num):
344 self.Emit('class %s(%s):' % (class_name, ', '.join(base_classes)))
345 self.Emit(' _type_tag = %d' % tag_num)
346
347 def _GenListSubclass(self, class_name, base_classes, tag_num, class_ns=''):
348 self._GenClassBegin(class_name, base_classes, tag_num)
349
350 # TODO: Do something nicer
351 base_class_str = [b for b in base_classes if b.startswith('List[')][0]
352
353 # Needed for c = CompoundWord() to work
354 # TODO: make it
355 # c = CompoundWord.New()
356 if 0:
357 self.Emit(' def __init__(self, other=None):')
358 self.Emit(' # type: (Optional[%s]) -> None' % base_class_str,
359 reflow=False)
360 self.Emit(' if other is not None:')
361 self.Emit(' self.extend(other)')
362 self.Emit('')
363
364 # Use our own constructor
365 self.Emit(' @staticmethod')
366 self.Emit(' def New():')
367 self.Emit(' # type: () -> %s' % class_name)
368 self.Emit(' return %s()' % class_name)
369 self.Emit('')
370
371 self.Emit(' @staticmethod')
372 self.Emit(' def Take(plain_list):')
373 self.Emit(' # type: (%s) -> %s' % (base_class_str, class_name))
374 self.Emit(' result = %s(plain_list)' % class_name)
375 self.Emit(' del plain_list[:]')
376 self.Emit(' return result')
377 self.Emit('')
378
379 if self.pretty_print_methods:
380 self._EmitPrettyPrintMethodsForList(class_name)
381
382 def _GenClass(self,
383 fields,
384 class_name,
385 base_classes,
386 tag_num,
387 class_ns=''):
388 """Generate a typed Python class.
389
390 Used for both Sum variants ("constructors") and Product types.
391
392 Args:
393 class_ns: for variants like value.Str
394 """
395 self._GenClassBegin(class_name, base_classes, tag_num)
396
397 field_names = [f.name for f in fields]
398
399 quoted_fields = repr(tuple(field_names))
400 self.Emit(' __slots__ = %s' % quoted_fields)
401 self.Emit('')
402
403 #
404 # __init__
405 #
406
407 args = [f.name for f in fields]
408 arg_types = []
409 default_vals = []
410 for f in fields:
411 mypy_type = _MyPyType(f.typ)
412 arg_types.append(mypy_type)
413
414 d_str = _DefaultValue(f.typ, mypy_type)
415 default_vals.append(d_str)
416
417 self.Emit(' def __init__(self, %s):' % ', '.join(args))
418 self.Emit(' # type: (%s) -> None' % ', '.join(arg_types),
419 reflow=False)
420
421 if not fields:
422 self.Emit(' pass') # for types like NoOp
423
424 for f in fields:
425 # don't wrap the type comment
426 self.Emit(' self.%s = %s' % (f.name, f.name), reflow=False)
427
428 self.Emit('')
429
430 # CreateNull() - another way of initializing
431 if len(fields) and not self.py_init_n:
432 self.Emit(' @staticmethod')
433 self.Emit(' def CreateNull(alloc_lists=False):')
434 self.Emit(' # type: () -> %s%s' % (class_ns, class_name))
435 self.Emit(' return %s%s(%s)' %
436 (class_ns, class_name, ', '.join(default_vals)),
437 reflow=False)
438 self.Emit('')
439
440 # PrettyTree()
441 if self.pretty_print_methods:
442 self._EmitPrettyPrintMethods(class_name, class_ns, fields)
443
444 def _EmitPrettyBegin(self):
445 self.Emit(' def PrettyTree(self, do_abbrev, trav=None):')
446 self.Emit(' # type: (bool, Optional[TraversalState]) -> hnode_t')
447 self.Emit(' trav = trav or TraversalState()')
448 self.Emit(' heap_id = id(self)')
449 self.Emit(' if heap_id in trav.seen:')
450 # cut off recursion
451 self.Emit(' return hnode.AlreadySeen(heap_id)')
452 self.Emit(' trav.seen[heap_id] = True')
453
454 def _EmitPrettyPrintMethodsForList(self, class_name):
455 self._EmitPrettyBegin()
456 self.Emit(' h = runtime.NewRecord(%r)' % class_name)
457 self.Emit(
458 ' h.unnamed_fields = [c.PrettyTree(do_abbrev) for c in self]')
459 self.Emit(' return h')
460 self.Emit('')
461
462 def _EmitPrettyPrintMethods(self, class_name, class_ns, fields):
463 if len(fields) == 0:
464 # value__Stdin -> value.Stdin (defined at top level)
465 pretty_cls_name = class_name.replace('__', '.')
466 else:
467 # value.Str (defined inside the 'class value') namespace
468 pretty_cls_name = '%s%s' % (class_ns, class_name)
469
470 # def PrettyTree(...):
471
472 self._EmitPrettyBegin()
473 self.Emit('')
474
475 if class_ns:
476 # e.g. _command__Simple
477 assert class_ns.endswith('.')
478 abbrev_name = '_%s__%s' % (class_ns[:-1], class_name)
479 else:
480 # e.g. _Token
481 abbrev_name = '_%s' % class_name
482
483 if abbrev_name in self.abbrev_mod_entries:
484 self.Emit(' if do_abbrev:')
485 self.Emit(' p = %s(self)' % abbrev_name)
486 self.Emit(' if p:')
487 self.Emit(' return p')
488 self.Emit('')
489
490 self.Emit(' out_node = NewRecord(%r)' % pretty_cls_name)
491 self.Emit(' L = out_node.fields')
492 self.Emit('')
493
494 # Use the runtime type to be more like asdl/format.py
495 for local_id, field in enumerate(fields):
496 #log('%s :: %s', field_name, field_desc)
497 self.Indent()
498 self._EmitCodeForField(field, local_id)
499 self.Dedent()
500 self.Emit('')
501 self.Emit(' return out_node')
502 self.Emit('')
503
504 def VisitCompoundSum(self, sum, sum_name, depth):
505 """Note that the following is_simple:
506
507 cflow = Break | Continue
508
509 But this is compound:
510
511 cflow = Break | Continue | Return(int val)
512
513 The generated code changes depending on which one it is.
514 """
515 #log('%d variants in %s', len(sum.types), sum_name)
516
517 # We emit THREE Python types for each meta.CompoundType:
518 #
519 # 1. enum for tag (cflow_e)
520 # 2. base class for inheritance (cflow_t)
521 # 3. namespace for classes (cflow) -- TODO: Get rid of this one.
522 #
523 # Should code use cflow_e.tag or isinstance()?
524 # isinstance() is better for MyPy I think. But tag is better for C++.
525 # int tag = static_cast<cflow>(node).tag;
526
527 int_to_str = {}
528
529 # enum for the tag
530 self.Emit('class %s_e(object):' % sum_name, depth)
531
532 for i, variant in enumerate(sum.types):
533 if variant.shared_type:
534 tag_num = self._shared_type_tags[variant.shared_type]
535 # e.g. DoubleQuoted may have base types expr_t, word_part_t
536 base_class = sum_name + '_t'
537 bases = self._base_classes[variant.shared_type]
538 if base_class in bases:
539 raise RuntimeError(
540 "Two tags in sum %r refer to product type %r" %
541 (sum_name, variant.shared_type))
542
543 else:
544 bases.append(base_class)
545 else:
546 tag_num = i + 1
547 self.Emit(' %s = %d' % (variant.name, tag_num), depth)
548 int_to_str[tag_num] = variant.name
549 self.Emit('', depth)
550
551 self._EmitDict(sum_name, int_to_str, depth)
552
553 self.Emit('def %s_str(tag, dot=True):' % sum_name, depth)
554 self.Emit(' # type: (int, bool) -> str', depth)
555 self.Emit(' v = _%s_str[tag]' % sum_name, depth)
556 self.Emit(' if dot:', depth)
557 self.Emit(' return "%s.%%s" %% v' % sum_name, depth)
558 self.Emit(' else:', depth)
559 self.Emit(' return v', depth)
560 self.Emit('', depth)
561
562 # the base class, e.g. 'command_t'
563 self.Emit('class %s_t(pybase.CompoundObj):' % sum_name, depth)
564 self.Indent()
565 depth = self.current_depth
566
567 # To imitate C++ API
568 self.Emit('def tag(self):')
569 self.Emit(' # type: () -> int')
570 self.Emit(' return self._type_tag')
571
572 self.Dedent()
573 depth = self.current_depth
574
575 self.Emit('')
576
577 # Declare any zero argument singleton classes outside of the main
578 # "namespace" class.
579 for i, variant in enumerate(sum.types):
580 if variant.shared_type:
581 continue # Don't generate a class for shared types.
582 if len(variant.fields) == 0:
583 # We must use the old-style naming here, ie. command__NoOp, in order
584 # to support zero field variants as constants.
585 class_name = '%s__%s' % (sum_name, variant.name)
586 self._GenClass(variant.fields, class_name, (sum_name + '_t', ),
587 i + 1)
588
589 # Class that's just a NAMESPACE, e.g. for value.Str
590 self.Emit('class %s(object):' % sum_name, depth)
591
592 self.Indent()
593
594 for i, variant in enumerate(sum.types):
595 if variant.shared_type:
596 continue
597
598 if len(variant.fields) == 0:
599 self.Emit('%s = %s__%s()' %
600 (variant.name, sum_name, variant.name))
601 self.Emit('')
602 else:
603 # Use fully-qualified name, so we can have osh_cmd.Simple and
604 # oil_cmd.Simple.
605 fq_name = variant.name
606 self._GenClass(variant.fields,
607 fq_name, (sum_name + '_t', ),
608 i + 1,
609 class_ns=sum_name + '.')
610 self.Emit(' pass', depth) # in case every variant is first class
611
612 self.Dedent()
613 self.Emit('')
614
615 def VisitSubType(self, subtype):
616 self._shared_type_tags[subtype.name] = self._product_counter
617
618 # Also create these last. They may inherit from sum types that have yet
619 # to be defined.
620 self._subtypes.append((subtype, self._product_counter))
621 self._product_counter += 1
622
623 def VisitProduct(self, product, name, depth):
624 self._shared_type_tags[name] = self._product_counter
625 # Create a tuple of _GenClass args to create LAST. They may inherit from
626 # sum types that have yet to be defined.
627 self._products.append((product, name, depth, self._product_counter))
628 self._product_counter += 1
629
630 def EmitFooter(self):
631 # Now generate all the product types we deferred.
632 for args in self._products:
633 ast_node, name, depth, tag_num = args
634 # Figure out base classes AFTERWARD.
635 bases = self._base_classes[name]
636 if not bases:
637 bases = ['pybase.CompoundObj']
638 self._GenClass(ast_node.fields, name, bases, tag_num)
639
640 for args in self._subtypes:
641 subtype, tag_num = args
642 # Figure out base classes AFTERWARD.
643 bases = self._base_classes[subtype.name]
644 if not bases:
645 bases = ['pybase.CompoundObj']
646
647 bases.append(_MyPyType(subtype.base_class))
648
649 if subtype.base_class.IsList():
650 self._GenListSubclass(subtype.name, bases, tag_num)
651 else:
652 self._GenClass([], subtype.name, bases, tag_num)