Skip to content

Commit 50b45ce

Browse files
committed
[mypyc] Add a mypyc_attr to support interpreted children
This operates by generating a "shadow vtable" containing pointers to glue methods that dispatch to the appropriate method via the C API. We then install those shadow vtables in interpreted subclasses so that overridden methods will be called. This does not support directly inheriting from traits, which I think will require generating vtables dynamically (and maybe some more nonsense too.) Closes #296. (Though I will file a follow-up for traits.)
1 parent 23a719f commit 50b45ce

File tree

8 files changed

+276
-49
lines changed

8 files changed

+276
-49
lines changed

mypy-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
typing_extensions>=3.7.4
2-
mypy_extensions>=0.4.0,<0.5.0
2+
mypy_extensions>=0.4.3,<0.5.0
33
typed_ast>=1.4.0,<1.5.0

mypyc/emitclass.py

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def emit_line() -> None:
202202
fields['tp_basicsize'] = base_size
203203

204204
if generate_full:
205-
emitter.emit_line('static PyObject *{}(void);'.format(setup_name))
205+
emitter.emit_line('static PyObject *{}(PyTypeObject *type);'.format(setup_name))
206206
assert cl.ctor is not None
207207
emitter.emit_line(native_function_header(cl.ctor, emitter) + ';')
208208

@@ -216,7 +216,15 @@ def emit_line() -> None:
216216
generate_dealloc_for_class(cl, dealloc_name, clear_name, emitter)
217217
emit_line()
218218
generate_native_getters_and_setters(cl, emitter)
219-
vtable_name = generate_vtables(cl, vtable_setup_name, vtable_name, emitter)
219+
220+
if cl.allow_interpreted_children:
221+
shadow_vtable_name = generate_vtables(
222+
cl, vtable_setup_name + "_shadow", vtable_name + "_shadow", emitter, shadow=True
223+
) # type: Optional[str]
224+
emit_line()
225+
else:
226+
shadow_vtable_name = None
227+
vtable_name = generate_vtables(cl, vtable_setup_name, vtable_name, emitter, shadow=False)
220228
emit_line()
221229
if needs_getseters:
222230
generate_getseter_declarations(cl, emitter)
@@ -241,7 +249,8 @@ def emit_line() -> None:
241249

242250
emitter.emit_line()
243251
if generate_full:
244-
generate_setup_for_class(cl, setup_name, defaults_fn, vtable_name, emitter)
252+
generate_setup_for_class(
253+
cl, setup_name, defaults_fn, vtable_name, shadow_vtable_name, emitter)
245254
emitter.emit_line()
246255
generate_constructor_for_class(
247256
cl, cl.ctor, init_fn, setup_name, vtable_name, emitter)
@@ -344,7 +353,8 @@ def generate_native_getters_and_setters(cl: ClassIR,
344353
def generate_vtables(base: ClassIR,
345354
vtable_setup_name: str,
346355
vtable_name: str,
347-
emitter: Emitter) -> str:
356+
emitter: Emitter,
357+
shadow: bool) -> str:
348358
"""Emit the vtables and vtable setup functions for a class.
349359
350360
This includes both the primary vtable and any trait implementation vtables.
@@ -359,8 +369,9 @@ def generate_vtables(base: ClassIR,
359369
"""
360370

361371
def trait_vtable_name(trait: ClassIR) -> str:
362-
return '{}_{}_trait_vtable'.format(
363-
base.name_prefix(emitter.names), trait.name_prefix(emitter.names))
372+
return '{}_{}_trait_vtable{}'.format(
373+
base.name_prefix(emitter.names), trait.name_prefix(emitter.names),
374+
'_shadow' if shadow else '')
364375

365376
# Emit array definitions with enough space for all the entries
366377
emitter.emit_line('static CPyVTableItem {}[{}];'.format(
@@ -376,13 +387,16 @@ def trait_vtable_name(trait: ClassIR) -> str:
376387
emitter.emit_line('{}{}(void)'.format(NATIVE_PREFIX, vtable_setup_name))
377388
emitter.emit_line('{')
378389

390+
if base.allow_interpreted_children and not shadow:
391+
emitter.emit_line('{}{}_shadow();'.format(NATIVE_PREFIX, vtable_setup_name))
392+
379393
subtables = []
380394
for trait, vtable in base.trait_vtables.items():
381395
name = trait_vtable_name(trait)
382-
generate_vtable(vtable, name, emitter, [])
396+
generate_vtable(vtable, name, emitter, [], shadow)
383397
subtables.append((trait, name))
384398

385-
generate_vtable(base.vtable_entries, vtable_name, emitter, subtables)
399+
generate_vtable(base.vtable_entries, vtable_name, emitter, subtables, shadow)
386400

387401
emitter.emit_line('return 1;')
388402
emitter.emit_line('}')
@@ -393,7 +407,8 @@ def trait_vtable_name(trait: ClassIR) -> str:
393407
def generate_vtable(entries: VTableEntries,
394408
vtable_name: str,
395409
emitter: Emitter,
396-
subtables: List[Tuple[ClassIR, str]]) -> None:
410+
subtables: List[Tuple[ClassIR, str]],
411+
shadow: bool) -> None:
397412
emitter.emit_line('CPyVTableItem {}_scratch[] = {{'.format(vtable_name))
398413
if subtables:
399414
emitter.emit_line('/* Array of trait vtables */')
@@ -404,10 +419,11 @@ def generate_vtable(entries: VTableEntries,
404419

405420
for entry in entries:
406421
if isinstance(entry, VTableMethod):
422+
method = entry.shadow_method if shadow and entry.shadow_method else entry.method
407423
emitter.emit_line('(CPyVTableItem){}{}{},'.format(
408424
emitter.get_group_prefix(entry.method.decl),
409425
NATIVE_PREFIX,
410-
entry.method.cname(emitter.names)))
426+
method.cname(emitter.names)))
411427
else:
412428
cl, attr, is_setter = entry
413429
namer = native_setter_name if is_setter else native_getter_name
@@ -425,18 +441,27 @@ def generate_setup_for_class(cl: ClassIR,
425441
func_name: str,
426442
defaults_fn: Optional[FuncIR],
427443
vtable_name: str,
444+
shadow_vtable_name: Optional[str],
428445
emitter: Emitter) -> None:
429446
"""Generate a native function that allocates an instance of a class."""
430447
emitter.emit_line('static PyObject *')
431-
emitter.emit_line('{}(void)'.format(func_name))
448+
emitter.emit_line('{}(PyTypeObject *type)'.format(func_name))
432449
emitter.emit_line('{')
433450
emitter.emit_line('{} *self;'.format(cl.struct_name(emitter.names)))
434-
emitter.emit_line('self = ({struct} *){type_struct}->tp_alloc({type_struct}, 0);'.format(
435-
struct=cl.struct_name(emitter.names),
436-
type_struct=emitter.type_struct_name(cl)))
451+
emitter.emit_line('self = ({struct} *)type->tp_alloc(type, 0);'.format(
452+
struct=cl.struct_name(emitter.names)))
437453
emitter.emit_line('if (self == NULL)')
438454
emitter.emit_line(' return NULL;')
439-
emitter.emit_line('self->vtable = {};'.format(vtable_name))
455+
456+
if shadow_vtable_name:
457+
emitter.emit_line('if (type != {}) {{'.format(emitter.type_struct_name(cl)))
458+
emitter.emit_line('self->vtable = {};'.format(shadow_vtable_name))
459+
emitter.emit_line('} else {')
460+
emitter.emit_line('self->vtable = {};'.format(vtable_name))
461+
emitter.emit_line('}')
462+
else:
463+
emitter.emit_line('self->vtable = {};'.format(vtable_name))
464+
440465
for base in reversed(cl.base_mro):
441466
for attr, rtype in base.attributes.items():
442467
emitter.emit_line('self->{} = {};'.format(
@@ -464,7 +489,7 @@ def generate_constructor_for_class(cl: ClassIR,
464489
"""Generate a native function that allocates and initializes an instance of a class."""
465490
emitter.emit_line('{}'.format(native_function_header(fn, emitter)))
466491
emitter.emit_line('{')
467-
emitter.emit_line('PyObject *self = {}();'.format(setup_name))
492+
emitter.emit_line('PyObject *self = {}({});'.format(setup_name, emitter.type_struct_name(cl)))
468493
emitter.emit_line('if (self == NULL)')
469494
emitter.emit_line(' return NULL;')
470495
args = ', '.join(['self'] + [REG_PREFIX + arg.name for arg in fn.sig.args])
@@ -525,13 +550,15 @@ def generate_new_for_class(cl: ClassIR,
525550
'{}(PyTypeObject *type, PyObject *args, PyObject *kwds)'.format(func_name))
526551
emitter.emit_line('{')
527552
# TODO: Check and unbox arguments
528-
emitter.emit_line('if (type != {}) {{'.format(emitter.type_struct_name(cl)))
529-
emitter.emit_line(
530-
'PyErr_SetString(PyExc_TypeError, "interpreted classes cannot inherit from compiled");')
531-
emitter.emit_line('return NULL;')
532-
emitter.emit_line('}')
553+
if not cl.allow_interpreted_children:
554+
emitter.emit_line('if (type != {}) {{'.format(emitter.type_struct_name(cl)))
555+
emitter.emit_line(
556+
'PyErr_SetString(PyExc_TypeError, "interpreted classes cannot inherit from compiled");'
557+
)
558+
emitter.emit_line('return NULL;')
559+
emitter.emit_line('}')
533560

534-
emitter.emit_line('return {}();'.format(setup_name))
561+
emitter.emit_line('return {}(type);'.format(setup_name))
535562
emitter.emit_line('}')
536563

537564

mypyc/genops.py

Lines changed: 106 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,55 @@ def is_dataclass(cdef: ClassDef) -> bool:
239239
return any(is_dataclass_decorator(d) for d in cdef.decorators)
240240

241241

242-
def is_extension_class(cdef: ClassDef) -> bool:
242+
def get_mypyc_attr_literal(e: Expression) -> Any:
243+
"""Convert an expression from a mypyc_attr decorator to a value.
244+
245+
Supports a pretty limited range."""
246+
if isinstance(e, (StrExpr, IntExpr, FloatExpr)):
247+
return e.value
248+
elif isinstance(e, RefExpr) and e.fullname == 'builtins.True':
249+
return True
250+
elif isinstance(e, RefExpr) and e.fullname == 'builtins.False':
251+
return False
252+
elif isinstance(e, RefExpr) and e.fullname == 'builtins.None':
253+
return None
254+
return NotImplemented
255+
256+
257+
def get_mypyc_attr_call(d: Expression) -> Optional[CallExpr]:
258+
"""Check if an expression is a call to mypyc_attr and return it if so."""
259+
if (
260+
isinstance(d, CallExpr)
261+
and isinstance(d.callee, RefExpr)
262+
and d.callee.fullname == 'mypy_extensions.mypyc_attr'
263+
):
264+
return d
265+
return None
266+
267+
268+
def get_mypyc_attrs(stmt: Union[ClassDef, Decorator]) -> Dict[str, Any]:
269+
"""Collect all the mypyc_attr attributes on a class definition or a function."""
270+
attrs = {} # type: Dict[str, Any]
271+
for dec in stmt.decorators:
272+
d = get_mypyc_attr_call(dec)
273+
if d:
274+
for name, arg in zip(d.arg_names, d.args):
275+
if name is None:
276+
if isinstance(arg, StrExpr):
277+
attrs[arg.value] = True
278+
else:
279+
attrs[name] = get_mypyc_attr_literal(arg)
280+
281+
return attrs
282+
243283

244-
if any(not is_trait_decorator(d) and not is_dataclass_decorator(d)
245-
for d in cdef.decorators):
284+
def is_extension_class(cdef: ClassDef) -> bool:
285+
if any(
286+
not is_trait_decorator(d)
287+
and not is_dataclass_decorator(d)
288+
and not get_mypyc_attr_call(d)
289+
for d in cdef.decorators
290+
):
246291
return False
247292
elif (cdef.info.metaclass_type and cdef.info.metaclass_type.type.fullname not in (
248293
'abc.ABCMeta', 'typing.TypingMeta', 'typing.GenericMeta')):
@@ -285,10 +330,11 @@ def specialize_parent_vtable(cls: ClassIR, parent: ClassIR) -> VTableEntries:
285330
# TODO: emit a wrapper for __init__ that raises or something
286331
if (is_same_method_signature(orig_parent_method.sig, child_method.sig)
287332
or orig_parent_method.name == '__init__'):
288-
entry = VTableMethod(entry.cls, entry.name, child_method)
333+
entry = VTableMethod(entry.cls, entry.name, child_method, entry.shadow_method)
289334
else:
290335
entry = VTableMethod(entry.cls, entry.name,
291-
defining_cls.glue_methods[(entry.cls, entry.name)])
336+
defining_cls.glue_methods[(entry.cls, entry.name)],
337+
entry.shadow_method)
292338
else:
293339
# If it is an attribute from a trait, we need to find out
294340
# the real class it got mixed in at and point to that.
@@ -346,7 +392,8 @@ def compute_vtable(cls: ClassIR) -> None:
346392
# TODO: don't generate a new entry when we overload without changing the type
347393
if fn == cls.get_method(fn.name):
348394
cls.vtable[fn.name] = len(entries)
349-
entries.append(VTableMethod(t, fn.name, fn))
395+
shadow = cls.glue_methods.get((cls, fn.name))
396+
entries.append(VTableMethod(t, fn.name, fn, shadow))
350397

351398
# Compute vtables for all of the traits that the class implements
352399
if not cls.is_trait:
@@ -546,6 +593,10 @@ def prepare_class_def(path: str, module_name: str, cdef: ClassDef,
546593
ir = mapper.type_to_ir[cdef.info]
547594
info = cdef.info
548595

596+
attrs = get_mypyc_attrs(cdef)
597+
if attrs.get("allow_interpreted_children") is True:
598+
ir.allow_interpreted_children = True
599+
549600
# We sort the table for determinism here on Python 3.5
550601
for name, node in sorted(info.names.items()):
551602
# Currenly all plugin generated methods are dummies and not included.
@@ -1169,20 +1220,26 @@ def handle_ext_method(self, cdef: ClassDef, fdef: FuncDef) -> None:
11691220
and not is_same_method_signature(class_ir.method_decls[name].sig,
11701221
cls.method_decls[name].sig)):
11711222

1223+
if cls is class_ir and not cls.allow_interpreted_children:
1224+
continue
1225+
11721226
# TODO: Support contravariant subtyping in the input argument for
11731227
# property setters. Need to make a special glue method for handling this,
11741228
# similar to gen_glue_property.
11751229

1176-
if fdef.is_property:
1177-
f = self.gen_glue_property(cls.method_decls[name].sig, func_ir, class_ir,
1178-
cls, fdef.line)
1179-
else:
1180-
f = self.gen_glue_method(cls.method_decls[name].sig, func_ir, class_ir,
1181-
cls, fdef.line)
1182-
1230+
f = self.gen_glue(cls.method_decls[name].sig, func_ir, class_ir, cls, fdef)
11831231
class_ir.glue_methods[(cls, name)] = f
11841232
self.functions.append(f)
11851233

1234+
# If the class allows interpreted children, create glue
1235+
# methods that dispatch via the Python API. These will go in a
1236+
# "shadow vtable" that will be assigned to interpreted
1237+
# children.
1238+
if class_ir.allow_interpreted_children:
1239+
f = self.gen_glue(func_ir.sig, func_ir, class_ir, class_ir, fdef, do_py_ops=True)
1240+
class_ir.glue_methods[(class_ir, name)] = f
1241+
self.functions.append(f)
1242+
11861243
def handle_non_ext_method(
11871244
self, non_ext: NonExtClassInfo, cdef: ClassDef, fdef: FuncDef) -> None:
11881245
# Perform the function of visit_method for methods inside non-extension classes.
@@ -1481,6 +1538,11 @@ def visit_class_def(self, cdef: ClassDef) -> None:
14811538
if any(ir.base_mro[i].base != ir. base_mro[i + 1] for i in range(len(ir.base_mro) - 1)):
14821539
self.error("Non-trait MRO must be linear", cdef.line)
14831540

1541+
if ir.allow_interpreted_children and any(
1542+
not parent.allow_interpreted_children for parent in ir.mro
1543+
):
1544+
self.error("Parents must allow interpreted children also", cdef.line)
1545+
14841546
# Currently, we only create non-extension classes for classes that are
14851547
# decorated or inherit from Enum. Classes decorated with @trait do not
14861548
# apply here, and are handled in a different way.
@@ -1707,8 +1769,24 @@ def visit_import_all(self, node: ImportAll) -> None:
17071769
return
17081770
self.gen_import(node.id, node.line)
17091771

1772+
def gen_glue(self, sig: FuncSignature, target: FuncIR,
1773+
cls: ClassIR, base: ClassIR, fdef: FuncItem,
1774+
*,
1775+
do_py_ops: bool = False
1776+
) -> FuncIR:
1777+
if fdef.is_property:
1778+
return self.gen_glue_property(
1779+
cls.method_decls[fdef.name].sig, target, cls, base, fdef.line, do_py_ops
1780+
)
1781+
else:
1782+
return self.gen_glue_method(
1783+
cls.method_decls[fdef.name].sig, target, cls, base, fdef.line, do_py_ops
1784+
)
1785+
17101786
def gen_glue_method(self, sig: FuncSignature, target: FuncIR,
1711-
cls: ClassIR, base: ClassIR, line: int) -> FuncIR:
1787+
cls: ClassIR, base: ClassIR, line: int,
1788+
do_pycall: bool,
1789+
) -> FuncIR:
17121790
"""Generate glue methods that mediate between different method types in subclasses.
17131791
17141792
For example, if we have:
@@ -1745,7 +1823,11 @@ def f(self, x: object) -> int: ...
17451823
arg_names = [arg.name for arg in rt_args]
17461824
arg_kinds = [concrete_arg_kind(arg.kind) for arg in rt_args]
17471825

1748-
retval = self.call(target.decl, args, arg_kinds, arg_names, line)
1826+
if do_pycall:
1827+
retval = self.py_method_call(
1828+
args[0], target.name, args[1:], line, arg_kinds[1:], arg_names[1:])
1829+
else:
1830+
retval = self.call(target.decl, args, arg_kinds, arg_names, line)
17491831
retval = self.coerce(retval, sig.ret_type, line)
17501832
self.add(Return(retval))
17511833

@@ -1758,7 +1840,8 @@ def f(self, x: object) -> int: ...
17581840
blocks, env)
17591841

17601842
def gen_glue_property(self, sig: FuncSignature, target: FuncIR, cls: ClassIR, base: ClassIR,
1761-
line: int) -> FuncIR:
1843+
line: int,
1844+
do_pygetattr: bool) -> FuncIR:
17621845
"""Similarly to methods, properties of derived types can be covariantly subtyped. Thus,
17631846
properties also require glue. However, this only requires the return type to change.
17641847
Further, instead of a method call, an attribute get is performed."""
@@ -1767,7 +1850,10 @@ def gen_glue_property(self, sig: FuncSignature, target: FuncIR, cls: ClassIR, ba
17671850
rt_arg = RuntimeArg(SELF_NAME, RInstance(cls))
17681851
arg = self.read(self.add_self_to_env(cls), line)
17691852
self.ret_types[-1] = sig.ret_type
1770-
retval = self.add(GetAttr(arg, target.name, line))
1853+
if do_pygetattr:
1854+
retval = self.py_get_attr(arg, target.name, line)
1855+
else:
1856+
retval = self.add(GetAttr(arg, target.name, line))
17711857
retbox = self.coerce(retval, sig.ret_type, line)
17721858
self.add(Return(retbox))
17731859

@@ -3103,7 +3189,7 @@ def py_call(self,
31033189
arg_values: List[Value],
31043190
line: int,
31053191
arg_kinds: Optional[List[int]] = None,
3106-
arg_names: Optional[List[Optional[str]]] = None) -> Value:
3192+
arg_names: Optional[Sequence[Optional[str]]] = None) -> Value:
31073193
"""Use py_call_op or py_call_with_kwargs_op for function call."""
31083194
# If all arguments are positional, we can use py_call_op.
31093195
if (arg_kinds is None) or all(kind == ARG_POS for kind in arg_kinds):
@@ -3152,8 +3238,8 @@ def py_method_call(self,
31523238
method_name: str,
31533239
arg_values: List[Value],
31543240
line: int,
3155-
arg_kinds: Optional[List[int]] = None,
3156-
arg_names: Optional[List[Optional[str]]] = None) -> Value:
3241+
arg_kinds: Optional[List[int]],
3242+
arg_names: Optional[Sequence[Optional[str]]]) -> Value:
31573243
if (arg_kinds is None) or all(kind == ARG_POS for kind in arg_kinds):
31583244
method_name_reg = self.load_static_unicode(method_name)
31593245
return self.primitive_op(py_method_call_op, [obj, method_name_reg] + arg_values, line)

0 commit comments

Comments
 (0)