File: classdef.py

package info (click to toggle)
mypy 0.812-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 18,596 kB
  • sloc: python: 74,869; cpp: 11,212; ansic: 3,935; makefile: 238; sh: 13
file content (495 lines) | stat: -rw-r--r-- 21,534 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
"""Transform class definitions from the mypy AST form to IR."""

from typing import List, Optional

from mypy.nodes import (
    ClassDef, FuncDef, OverloadedFuncDef, PassStmt, AssignmentStmt, NameExpr, StrExpr,
    ExpressionStmt, TempNode, Decorator, Lvalue, RefExpr, is_class_var
)
from mypyc.ir.ops import (
    Value, Register, Call, LoadErrorValue, LoadStatic, InitStatic, TupleSet, SetAttr, Return,
    BasicBlock, Branch, MethodCall, NAMESPACE_TYPE, LoadAddress
)
from mypyc.ir.rtypes import (
    object_rprimitive, bool_rprimitive, dict_rprimitive, is_optional_type,
    is_object_rprimitive, is_none_rprimitive
)
from mypyc.ir.func_ir import FuncDecl, FuncSignature
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
from mypyc.primitives.generic_ops import py_setattr_op, py_hasattr_op
from mypyc.primitives.misc_ops import (
    dataclass_sleight_of_hand, pytype_from_template_op, py_calc_meta_op, type_object_op,
    not_implemented_op
)
from mypyc.primitives.dict_ops import dict_set_item_op, dict_new_op
from mypyc.irbuild.util import (
    is_dataclass_decorator, get_func_def, is_dataclass, is_constant
)
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.function import transform_method


def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
    """Create IR for a class definition.

    This can generate both extension (native) and non-extension
    classes.  These are generated in very different ways. In the
    latter case we construct a Python type object at runtime by doing
    the equivalent of "type(name, bases, dict)" in IR. Extension
    classes are defined via C structs that are generated later in
    mypyc.codegen.emitclass.

    This is the main entry point to this module.
    """
    ir = builder.mapper.type_to_ir[cdef.info]

    # We do this check here because the base field of parent
    # classes aren't necessarily populated yet at
    # prepare_class_def time.
    if any(ir.base_mro[i].base != ir. base_mro[i + 1] for i in range(len(ir.base_mro) - 1)):
        builder.error("Non-trait MRO must be linear", cdef.line)

    if ir.allow_interpreted_subclasses:
        for parent in ir.mro:
            if not parent.allow_interpreted_subclasses:
                builder.error(
                    'Base class "{}" does not allow interpreted subclasses'.format(
                        parent.fullname), cdef.line)

    # Currently, we only create non-extension classes for classes that are
    # decorated or inherit from Enum. Classes decorated with @trait do not
    # apply here, and are handled in a different way.
    if ir.is_ext_class:
        # If the class is not decorated, generate an extension class for it.
        type_obj = allocate_class(builder, cdef)  # type: Optional[Value]
        non_ext = None  # type: Optional[NonExtClassInfo]
        dataclass_non_ext = dataclass_non_ext_info(builder, cdef)
    else:
        non_ext_bases = populate_non_ext_bases(builder, cdef)
        non_ext_metaclass = find_non_ext_metaclass(builder, cdef, non_ext_bases)
        non_ext_dict = setup_non_ext_dict(builder, cdef, non_ext_metaclass, non_ext_bases)
        # We populate __annotations__ for non-extension classes
        # because dataclasses uses it to determine which attributes to compute on.
        # TODO: Maybe generate more precise types for annotations
        non_ext_anns = builder.call_c(dict_new_op, [], cdef.line)
        non_ext = NonExtClassInfo(non_ext_dict, non_ext_bases, non_ext_anns, non_ext_metaclass)
        dataclass_non_ext = None
        type_obj = None

    attrs_to_cache = []  # type: List[Lvalue]

    for stmt in cdef.defs.body:
        if isinstance(stmt, OverloadedFuncDef) and stmt.is_property:
            if not ir.is_ext_class:
                # properties with both getters and setters in non_extension
                # classes not supported
                builder.error("Property setters not supported in non-extension classes",
                           stmt.line)
            for item in stmt.items:
                with builder.catch_errors(stmt.line):
                    transform_method(builder, cdef, non_ext, get_func_def(item))
        elif isinstance(stmt, (FuncDef, Decorator, OverloadedFuncDef)):
            # Ignore plugin generated methods (since they have no
            # bodies to compile and will need to have the bodies
            # provided by some other mechanism.)
            if cdef.info.names[stmt.name].plugin_generated:
                continue
            with builder.catch_errors(stmt.line):
                transform_method(builder, cdef, non_ext, get_func_def(stmt))
        elif isinstance(stmt, PassStmt):
            continue
        elif isinstance(stmt, AssignmentStmt):
            if len(stmt.lvalues) != 1:
                builder.error("Multiple assignment in class bodies not supported", stmt.line)
                continue
            lvalue = stmt.lvalues[0]
            if not isinstance(lvalue, NameExpr):
                builder.error("Only assignment to variables is supported in class bodies",
                           stmt.line)
                continue
            # We want to collect class variables in a dictionary for both real
            # non-extension classes and fake dataclass ones.
            var_non_ext = non_ext or dataclass_non_ext
            if var_non_ext:
                add_non_ext_class_attr(builder, var_non_ext, lvalue, stmt, cdef, attrs_to_cache)
                if non_ext:
                    continue
            # Variable declaration with no body
            if isinstance(stmt.rvalue, TempNode):
                continue
            # Only treat marked class variables as class variables.
            if not (is_class_var(lvalue) or stmt.is_final_def):
                continue
            typ = builder.load_native_type_object(cdef.fullname)
            value = builder.accept(stmt.rvalue)
            builder.call_c(
                py_setattr_op, [typ, builder.load_static_unicode(lvalue.name), value], stmt.line)
            if builder.non_function_scope() and stmt.is_final_def:
                builder.init_final_static(lvalue, value, cdef.name)
        elif isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
            # Docstring. Ignore
            pass
        else:
            builder.error("Unsupported statement in class body", stmt.line)

    if not non_ext:  # That is, an extension class
        generate_attr_defaults(builder, cdef)
        create_ne_from_eq(builder, cdef)
        if dataclass_non_ext:
            assert type_obj
            dataclass_finalize(builder, cdef, dataclass_non_ext, type_obj)
    else:
        # Dynamically create the class via the type constructor
        non_ext_class = load_non_ext_class(builder, ir, non_ext, cdef.line)
        non_ext_class = load_decorated_class(builder, cdef, non_ext_class)

        # Save the decorated class
        builder.add(InitStatic(non_ext_class, cdef.name, builder.module_name, NAMESPACE_TYPE))

        # Add the non-extension class to the dict
        builder.call_c(dict_set_item_op,
                       [
                           builder.load_globals_dict(),
                           builder.load_static_unicode(cdef.name),
                           non_ext_class
                       ], cdef.line)

        # Cache any cachable class attributes
        cache_class_attrs(builder, attrs_to_cache, cdef)


def allocate_class(builder: IRBuilder, cdef: ClassDef) -> Value:
    # OK AND NOW THE FUN PART
    base_exprs = cdef.base_type_exprs + cdef.removed_base_type_exprs
    if base_exprs:
        bases = [builder.accept(x) for x in base_exprs]
        tp_bases = builder.new_tuple(bases, cdef.line)
    else:
        tp_bases = builder.add(LoadErrorValue(object_rprimitive, is_borrowed=True))
    modname = builder.load_static_unicode(builder.module_name)
    template = builder.add(LoadStatic(object_rprimitive, cdef.name + "_template",
                                   builder.module_name, NAMESPACE_TYPE))
    # Create the class
    tp = builder.call_c(pytype_from_template_op,
                    [template, tp_bases, modname], cdef.line)
    # Immediately fix up the trait vtables, before doing anything with the class.
    ir = builder.mapper.type_to_ir[cdef.info]
    if not ir.is_trait and not ir.builtin_base:
        builder.add(Call(
            FuncDecl(cdef.name + '_trait_vtable_setup',
                     None, builder.module_name,
                     FuncSignature([], bool_rprimitive)), [], -1))
    # Populate a '__mypyc_attrs__' field containing the list of attrs
    builder.call_c(py_setattr_op, [
        tp, builder.load_static_unicode('__mypyc_attrs__'),
        create_mypyc_attrs_tuple(builder, builder.mapper.type_to_ir[cdef.info], cdef.line)],
        cdef.line)

    # Save the class
    builder.add(InitStatic(tp, cdef.name, builder.module_name, NAMESPACE_TYPE))

    # Add it to the dict
    builder.call_c(dict_set_item_op,
                [
                    builder.load_globals_dict(),
                    builder.load_static_unicode(cdef.name),
                    tp,
                ], cdef.line)

    return tp


def populate_non_ext_bases(builder: IRBuilder, cdef: ClassDef) -> Value:
    """Create base class tuple of a non-extension class.

    The tuple is passed to the metaclass constructor.
    """
    ir = builder.mapper.type_to_ir[cdef.info]
    bases = []
    for cls in cdef.info.mro[1:]:
        if cls.fullname == 'builtins.object':
            continue
        # Add the current class to the base classes list of concrete subclasses
        if cls in builder.mapper.type_to_ir:
            base_ir = builder.mapper.type_to_ir[cls]
            if base_ir.children is not None:
                base_ir.children.append(ir)

        base = builder.load_global_str(cls.name, cdef.line)
        bases.append(base)
    return builder.new_tuple(bases, cdef.line)


def find_non_ext_metaclass(builder: IRBuilder, cdef: ClassDef, bases: Value) -> Value:
    """Find the metaclass of a class from its defs and bases. """
    if cdef.metaclass:
        declared_metaclass = builder.accept(cdef.metaclass)
    else:
        declared_metaclass = builder.add(LoadAddress(type_object_op.type,
                                                     type_object_op.src, cdef.line))

    return builder.call_c(py_calc_meta_op, [declared_metaclass, bases], cdef.line)


def setup_non_ext_dict(builder: IRBuilder,
                       cdef: ClassDef,
                       metaclass: Value,
                       bases: Value) -> Value:
    """Initialize the class dictionary for a non-extension class.

    This class dictionary is passed to the metaclass constructor.
    """
    # Check if the metaclass defines a __prepare__ method, and if so, call it.
    has_prepare = builder.call_c(py_hasattr_op,
                                [metaclass,
                                builder.load_static_unicode('__prepare__')], cdef.line)

    non_ext_dict = Register(dict_rprimitive)

    true_block, false_block, exit_block, = BasicBlock(), BasicBlock(), BasicBlock()
    builder.add_bool_branch(has_prepare, true_block, false_block)

    builder.activate_block(true_block)
    cls_name = builder.load_static_unicode(cdef.name)
    prepare_meth = builder.py_get_attr(metaclass, '__prepare__', cdef.line)
    prepare_dict = builder.py_call(prepare_meth, [cls_name, bases], cdef.line)
    builder.assign(non_ext_dict, prepare_dict, cdef.line)
    builder.goto(exit_block)

    builder.activate_block(false_block)
    builder.assign(non_ext_dict, builder.call_c(dict_new_op, [], cdef.line), cdef.line)
    builder.goto(exit_block)
    builder.activate_block(exit_block)

    return non_ext_dict


def add_non_ext_class_attr(builder: IRBuilder,
                           non_ext: NonExtClassInfo,
                           lvalue: NameExpr,
                           stmt: AssignmentStmt,
                           cdef: ClassDef,
                           attr_to_cache: List[Lvalue]) -> None:
    """Add a class attribute to __annotations__ of a non-extension class.

    If the attribute is initialized with a value, also add it to __dict__.
    """
    # We populate __annotations__ because dataclasses uses it to determine
    # which attributes to compute on.
    # TODO: Maybe generate more precise types for annotations
    key = builder.load_static_unicode(lvalue.name)
    typ = builder.add(LoadAddress(type_object_op.type, type_object_op.src, stmt.line))
    builder.call_c(dict_set_item_op, [non_ext.anns, key, typ], stmt.line)

    # Only add the attribute to the __dict__ if the assignment is of the form:
    # x: type = value (don't add attributes of the form 'x: type' to the __dict__).
    if not isinstance(stmt.rvalue, TempNode):
        rvalue = builder.accept(stmt.rvalue)
        builder.add_to_non_ext_dict(non_ext, lvalue.name, rvalue, stmt.line)
        # We cache enum attributes to speed up enum attribute lookup since they
        # are final.
        if (
            cdef.info.bases
            and cdef.info.bases[0].type.fullname == 'enum.Enum'
            # Skip "_order_" and "__order__", since Enum will remove it
            and lvalue.name not in ('_order_', '__order__')
        ):
            attr_to_cache.append(lvalue)


def generate_attr_defaults(builder: IRBuilder, cdef: ClassDef) -> None:
    """Generate an initialization method for default attr values (from class vars)."""
    cls = builder.mapper.type_to_ir[cdef.info]
    if cls.builtin_base:
        return

    # Pull out all assignments in classes in the mro so we can initialize them
    # TODO: Support nested statements
    default_assignments = []
    for info in reversed(cdef.info.mro):
        if info not in builder.mapper.type_to_ir:
            continue
        for stmt in info.defn.defs.body:
            if (isinstance(stmt, AssignmentStmt)
                    and isinstance(stmt.lvalues[0], NameExpr)
                    and not is_class_var(stmt.lvalues[0])
                    and not isinstance(stmt.rvalue, TempNode)):
                if stmt.lvalues[0].name == '__slots__':
                    continue

                # Skip type annotated assignments in dataclasses
                if is_dataclass(cdef) and stmt.type:
                    continue

                default_assignments.append(stmt)

    if not default_assignments:
        return

    builder.enter_method(cls, '__mypyc_defaults_setup', bool_rprimitive)

    self_var = builder.self()
    for stmt in default_assignments:
        lvalue = stmt.lvalues[0]
        assert isinstance(lvalue, NameExpr)
        if not stmt.is_final_def and not is_constant(stmt.rvalue):
            builder.warning('Unsupported default attribute value', stmt.rvalue.line)

        # If the attribute is initialized to None and type isn't optional,
        # don't initialize it to anything.
        attr_type = cls.attr_type(lvalue.name)
        if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == 'builtins.None':
            if (not is_optional_type(attr_type) and not is_object_rprimitive(attr_type)
                    and not is_none_rprimitive(attr_type)):
                continue
        val = builder.coerce(builder.accept(stmt.rvalue), attr_type, stmt.line)
        builder.add(SetAttr(self_var, lvalue.name, val, -1))

    builder.add(Return(builder.true()))

    builder.leave_method()


def create_ne_from_eq(builder: IRBuilder, cdef: ClassDef) -> None:
    """Create a "__ne__" method from a "__eq__" method (if only latter exists)."""
    cls = builder.mapper.type_to_ir[cdef.info]
    if cls.has_method('__eq__') and not cls.has_method('__ne__'):
        gen_glue_ne_method(builder, cls, cdef.line)


def gen_glue_ne_method(builder: IRBuilder, cls: ClassIR, line: int) -> None:
    """Generate a "__ne__" method from a "__eq__" method. """
    builder.enter_method(cls, '__ne__', object_rprimitive)
    rhs_arg = builder.add_argument('rhs', object_rprimitive)

    # If __eq__ returns NotImplemented, then __ne__ should also
    not_implemented_block, regular_block = BasicBlock(), BasicBlock()
    eqval = builder.add(MethodCall(builder.self(), '__eq__', [rhs_arg], line))
    not_implemented = builder.add(LoadAddress(not_implemented_op.type,
                                              not_implemented_op.src, line))
    builder.add(Branch(
        builder.translate_is_op(eqval, not_implemented, 'is', line),
        not_implemented_block,
        regular_block,
        Branch.BOOL))

    builder.activate_block(regular_block)
    retval = builder.coerce(
        builder.unary_op(eqval, 'not', line), object_rprimitive, line
    )
    builder.add(Return(retval))

    builder.activate_block(not_implemented_block)
    builder.add(Return(not_implemented))

    builder.leave_method()


def load_non_ext_class(builder: IRBuilder,
                       ir: ClassIR,
                       non_ext: NonExtClassInfo,
                       line: int) -> Value:
    cls_name = builder.load_static_unicode(ir.name)

    finish_non_ext_dict(builder, non_ext, line)

    class_type_obj = builder.py_call(
        non_ext.metaclass,
        [cls_name, non_ext.bases, non_ext.dict],
        line
    )
    return class_type_obj


def load_decorated_class(builder: IRBuilder, cdef: ClassDef, type_obj: Value) -> Value:
    """Apply class decorators to create a decorated (non-extension) class object.

    Given a decorated ClassDef and a register containing a
    non-extension representation of the ClassDef created via the type
    constructor, applies the corresponding decorator functions on that
    decorated ClassDef and returns a register containing the decorated
    ClassDef.
    """
    decorators = cdef.decorators
    dec_class = type_obj
    for d in reversed(decorators):
        decorator = d.accept(builder.visitor)
        assert isinstance(decorator, Value)
        dec_class = builder.py_call(decorator, [dec_class], dec_class.line)
    return dec_class


def cache_class_attrs(builder: IRBuilder, attrs_to_cache: List[Lvalue], cdef: ClassDef) -> None:
    """Add class attributes to be cached to the global cache."""
    typ = builder.load_native_type_object(cdef.fullname)
    for lval in attrs_to_cache:
        assert isinstance(lval, NameExpr)
        rval = builder.py_get_attr(typ, lval.name, cdef.line)
        builder.init_final_static(lval, rval, cdef.name)


def create_mypyc_attrs_tuple(builder: IRBuilder, ir: ClassIR, line: int) -> Value:
    attrs = [name for ancestor in ir.mro for name in ancestor.attributes]
    if ir.inherits_python:
        attrs.append('__dict__')
    items = [builder.load_static_unicode(attr) for attr in attrs]
    return builder.new_tuple(items, line)


def finish_non_ext_dict(builder: IRBuilder, non_ext: NonExtClassInfo, line: int) -> None:
    # Add __annotations__ to the class dict.
    builder.call_c(dict_set_item_op,
                [non_ext.dict, builder.load_static_unicode('__annotations__'),
                non_ext.anns], -1)

    # We add a __doc__ attribute so if the non-extension class is decorated with the
    # dataclass decorator, dataclass will not try to look for __text_signature__.
    # https://github.com/python/cpython/blob/3.7/Lib/dataclasses.py#L957
    filler_doc_str = 'mypyc filler docstring'
    builder.add_to_non_ext_dict(
        non_ext, '__doc__', builder.load_static_unicode(filler_doc_str), line)
    builder.add_to_non_ext_dict(
        non_ext, '__module__', builder.load_static_unicode(builder.module_name), line)


def dataclass_finalize(
        builder: IRBuilder, cdef: ClassDef, non_ext: NonExtClassInfo, type_obj: Value) -> None:
    """Generate code to finish instantiating a dataclass.

    This works by replacing all of the attributes on the class
    (which will be descriptors) with whatever they would be in a
    non-extension class, calling dataclass, then switching them back.

    The resulting class is an extension class and instances of it do not
    have a __dict__ (unless something else requires it).
    All methods written explicitly in the source are compiled and
    may be called through the vtable while the methods generated
    by dataclasses are interpreted and may not be.

    (If we just called dataclass without doing this, it would think that all
    of the descriptors for our attributes are default values and generate an
    incorrect constructor. We need to do the switch so that dataclass gets the
    appropriate defaults.)
    """
    finish_non_ext_dict(builder, non_ext, cdef.line)
    dec = builder.accept(next(d for d in cdef.decorators if is_dataclass_decorator(d)))
    builder.call_c(
        dataclass_sleight_of_hand, [dec, type_obj, non_ext.dict, non_ext.anns], cdef.line)


def dataclass_non_ext_info(builder: IRBuilder, cdef: ClassDef) -> Optional[NonExtClassInfo]:
    """Set up a NonExtClassInfo to track dataclass attributes.

    In addition to setting up a normal extension class for dataclasses,
    we also collect its class attributes like a non-extension class so
    that we can hand them to the dataclass decorator.
    """
    if is_dataclass(cdef):
        return NonExtClassInfo(
            builder.call_c(dict_new_op, [], cdef.line),
            builder.add(TupleSet([], cdef.line)),
            builder.call_c(dict_new_op, [], cdef.line),
            builder.add(LoadAddress(type_object_op.type, type_object_op.src, cdef.line))
        )
    else:
        return None