Skip to content

Commit 5a105d1

Browse files
authored
[mypyc] Support __setitem__, __delitem__, __len__ and __contains__ (#10451)
Previously these couldn't be used in compiled classes. __setitem__ and __delitem__ are implemented by a single wrapper function. If the value is NULL, it should act as __delitem__. This makes the implementation non-trivial. First, we need to support both a single variant and two variants being defined in a class. Second, when overriding, we need to use super(), if we only override one of them. The other dunders are straightforward. The implementation is pretty verbose. I'll look at refactoring it in a separate PR. This has some overlap with the previous PR #10211 by @sohailsomani.
1 parent 6fac701 commit 5a105d1

File tree

11 files changed

+669
-99
lines changed

11 files changed

+669
-99
lines changed

mypyc/codegen/emitclass.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from mypyc.codegen.emitfunc import native_function_header
1010
from mypyc.codegen.emitwrapper import (
1111
generate_dunder_wrapper, generate_hash_wrapper, generate_richcompare_wrapper,
12-
generate_bool_wrapper, generate_get_wrapper,
12+
generate_bool_wrapper, generate_get_wrapper, generate_len_wrapper,
13+
generate_set_del_item_wrapper, generate_contains_wrapper
1314
)
1415
from mypyc.ir.rtypes import RType, RTuple, object_rprimitive
1516
from mypyc.ir.func_ir import FuncIR, FuncDecl, FUNC_STATICMETHOD, FUNC_CLASSMETHOD
@@ -46,6 +47,13 @@ def wrapper_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
4647

4748
AS_MAPPING_SLOT_DEFS = {
4849
'__getitem__': ('mp_subscript', generate_dunder_wrapper),
50+
'__setitem__': ('mp_ass_subscript', generate_set_del_item_wrapper),
51+
'__delitem__': ('mp_ass_subscript', generate_set_del_item_wrapper),
52+
'__len__': ('mp_length', generate_len_wrapper),
53+
} # type: SlotTable
54+
55+
AS_SEQUENCE_SLOT_DEFS = {
56+
'__contains__': ('sq_contains', generate_contains_wrapper),
4957
} # type: SlotTable
5058

5159
AS_NUMBER_SLOT_DEFS = {
@@ -60,6 +68,7 @@ def wrapper_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
6068

6169
SIDE_TABLES = [
6270
('as_mapping', 'PyMappingMethods', AS_MAPPING_SLOT_DEFS),
71+
('as_sequence', 'PySequenceMethods', AS_SEQUENCE_SLOT_DEFS),
6372
('as_number', 'PyNumberMethods', AS_NUMBER_SLOT_DEFS),
6473
('as_async', 'PyAsyncMethods', AS_ASYNC_SLOT_DEFS),
6574
]
@@ -82,11 +91,19 @@ def generate_call_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
8291

8392
def generate_slots(cl: ClassIR, table: SlotTable, emitter: Emitter) -> Dict[str, str]:
8493
fields = OrderedDict() # type: Dict[str, str]
94+
generated = {} # type: Dict[str, str]
8595
# Sort for determinism on Python 3.5
86-
for name, (slot, generator) in sorted(table.items()):
96+
for name, (slot, generator) in sorted(table.items(), reverse=True):
8797
method_cls = cl.get_method_and_class(name)
8898
if method_cls and (method_cls[1] == cl or name in ALWAYS_FILL):
89-
fields[slot] = generator(cl, method_cls[0], emitter)
99+
if slot in generated:
100+
# Reuse previously generated wrapper.
101+
fields[slot] = generated[slot]
102+
else:
103+
# Generate new wrapper.
104+
name = generator(cl, method_cls[0], emitter)
105+
fields[slot] = name
106+
generated[slot] = name
90107

91108
return fields
92109

mypyc/codegen/emitwrapper.py

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
or methods in a single compilation unit.
1111
"""
1212

13-
from typing import List, Optional
13+
from typing import List, Optional, Sequence
1414

1515
from mypy.nodes import ARG_POS, ARG_OPT, ARG_NAMED_OPT, ARG_NAMED, ARG_STAR, ARG_STAR2
1616

@@ -350,6 +350,29 @@ def generate_hash_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
350350
return name
351351

352352

353+
def generate_len_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
354+
"""Generates a wrapper for native __len__ methods."""
355+
name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names))
356+
emitter.emit_line('static Py_ssize_t {name}(PyObject *self) {{'.format(
357+
name=name
358+
))
359+
emitter.emit_line('{}retval = {}{}{}(self);'.format(emitter.ctype_spaced(fn.ret_type),
360+
emitter.get_group_prefix(fn.decl),
361+
NATIVE_PREFIX,
362+
fn.cname(emitter.names)))
363+
emitter.emit_error_check('retval', fn.ret_type, 'return -1;')
364+
if is_int_rprimitive(fn.ret_type):
365+
emitter.emit_line('Py_ssize_t val = CPyTagged_AsSsize_t(retval);')
366+
else:
367+
emitter.emit_line('Py_ssize_t val = PyLong_AsSsize_t(retval);')
368+
emitter.emit_dec_ref('retval', fn.ret_type)
369+
emitter.emit_line('if (PyErr_Occurred()) return -1;')
370+
emitter.emit_line('return val;')
371+
emitter.emit_line('}')
372+
373+
return name
374+
375+
353376
def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
354377
"""Generates a wrapper for native __bool__ methods."""
355378
name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names))
@@ -370,6 +393,134 @@ def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
370393
return name
371394

372395

396+
def generate_del_item_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
397+
"""Generates a wrapper for native __delitem__.
398+
399+
This is only called from a combined __delitem__/__setitem__ wrapper.
400+
"""
401+
name = '{}{}{}'.format(DUNDER_PREFIX, '__delitem__', cl.name_prefix(emitter.names))
402+
input_args = ', '.join('PyObject *obj_{}'.format(arg.name) for arg in fn.args)
403+
emitter.emit_line('static int {name}({input_args}) {{'.format(
404+
name=name,
405+
input_args=input_args,
406+
))
407+
generate_set_del_item_wrapper_inner(fn, emitter, fn.args)
408+
return name
409+
410+
411+
def generate_set_del_item_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
412+
"""Generates a wrapper for native __setitem__ method (also works for __delitem__).
413+
414+
This is used with the mapping protocol slot. Arguments are taken as *PyObjects and we
415+
return a negative C int on error.
416+
417+
Create a separate wrapper function for __delitem__ as needed and have the
418+
__setitem__ wrapper call it if the value is NULL. Return the name
419+
of the outer (__setitem__) wrapper.
420+
"""
421+
method_cls = cl.get_method_and_class('__delitem__')
422+
del_name = None
423+
if method_cls and method_cls[1] == cl:
424+
# Generate a separate wrapper for __delitem__
425+
del_name = generate_del_item_wrapper(cl, method_cls[0], emitter)
426+
427+
args = fn.args
428+
if fn.name == '__delitem__':
429+
# Add an extra argument for value that we expect to be NULL.
430+
args = list(args) + [RuntimeArg('___value', object_rprimitive, ARG_POS)]
431+
432+
name = '{}{}{}'.format(DUNDER_PREFIX, '__setitem__', cl.name_prefix(emitter.names))
433+
input_args = ', '.join('PyObject *obj_{}'.format(arg.name) for arg in args)
434+
emitter.emit_line('static int {name}({input_args}) {{'.format(
435+
name=name,
436+
input_args=input_args,
437+
))
438+
439+
# First check if this is __delitem__
440+
emitter.emit_line('if (obj_{} == NULL) {{'.format(args[2].name))
441+
if del_name is not None:
442+
# We have a native implementation, so call it
443+
emitter.emit_line('return {}(obj_{}, obj_{});'.format(del_name,
444+
args[0].name,
445+
args[1].name))
446+
else:
447+
# Try to call superclass method instead
448+
emitter.emit_line(
449+
'PyObject *super = CPy_Super(CPyModule_builtins, obj_{});'.format(args[0].name))
450+
emitter.emit_line('if (super == NULL) return -1;')
451+
emitter.emit_line(
452+
'PyObject *result = PyObject_CallMethod(super, "__delitem__", "O", obj_{});'.format(
453+
args[1].name))
454+
emitter.emit_line('Py_DECREF(super);')
455+
emitter.emit_line('Py_XDECREF(result);')
456+
emitter.emit_line('return result == NULL ? -1 : 0;')
457+
emitter.emit_line('}')
458+
459+
method_cls = cl.get_method_and_class('__setitem__')
460+
if method_cls and method_cls[1] == cl:
461+
generate_set_del_item_wrapper_inner(fn, emitter, args)
462+
else:
463+
emitter.emit_line(
464+
'PyObject *super = CPy_Super(CPyModule_builtins, obj_{});'.format(args[0].name))
465+
emitter.emit_line('if (super == NULL) return -1;')
466+
emitter.emit_line('PyObject *result;')
467+
468+
if method_cls is None and cl.builtin_base is None:
469+
msg = "'{}' object does not support item assignment".format(cl.name)
470+
emitter.emit_line(
471+
'PyErr_SetString(PyExc_TypeError, "{}");'.format(msg))
472+
emitter.emit_line('result = NULL;')
473+
else:
474+
# A base class may have __setitem__
475+
emitter.emit_line(
476+
'result = PyObject_CallMethod(super, "__setitem__", "OO", obj_{}, obj_{});'.format(
477+
args[1].name, args[2].name))
478+
emitter.emit_line('Py_DECREF(super);')
479+
emitter.emit_line('Py_XDECREF(result);')
480+
emitter.emit_line('return result == NULL ? -1 : 0;')
481+
emitter.emit_line('}')
482+
return name
483+
484+
485+
def generate_set_del_item_wrapper_inner(fn: FuncIR, emitter: Emitter,
486+
args: Sequence[RuntimeArg]) -> None:
487+
for arg in args:
488+
generate_arg_check(arg.name, arg.type, emitter, 'goto fail;', False)
489+
native_args = ', '.join('arg_{}'.format(arg.name) for arg in args)
490+
emitter.emit_line('{}val = {}{}({});'.format(emitter.ctype_spaced(fn.ret_type),
491+
NATIVE_PREFIX,
492+
fn.cname(emitter.names),
493+
native_args))
494+
emitter.emit_error_check('val', fn.ret_type, 'goto fail;')
495+
emitter.emit_dec_ref('val', fn.ret_type)
496+
emitter.emit_line('return 0;')
497+
emitter.emit_label('fail')
498+
emitter.emit_line('return -1;')
499+
emitter.emit_line('}')
500+
501+
502+
def generate_contains_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
503+
"""Generates a wrapper for a native __contains__ method."""
504+
name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names))
505+
emitter.emit_line(
506+
'static int {name}(PyObject *self, PyObject *obj_item) {{'.
507+
format(name=name))
508+
generate_arg_check('item', fn.args[1].type, emitter, 'return -1;', False)
509+
emitter.emit_line('{}val = {}{}(self, arg_item);'.format(emitter.ctype_spaced(fn.ret_type),
510+
NATIVE_PREFIX,
511+
fn.cname(emitter.names)))
512+
emitter.emit_error_check('val', fn.ret_type, 'return -1;')
513+
if is_bool_rprimitive(fn.ret_type):
514+
emitter.emit_line('return val;')
515+
else:
516+
emitter.emit_line('int boolval = PyObject_IsTrue(val);')
517+
emitter.emit_dec_ref('val', fn.ret_type)
518+
emitter.emit_line('return boolval;')
519+
emitter.emit_line('}')
520+
521+
return name
522+
523+
373524
# Helpers
374525

375526

mypyc/irbuild/ll_builder.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,8 @@ def binary_op(self,
646646
if is_bool_rprimitive(ltype) and is_bool_rprimitive(rtype) and op in (
647647
'&', '&=', '|', '|=', '^', '^='):
648648
return self.bool_bitwise_op(lreg, rreg, op[0], line)
649+
if isinstance(rtype, RInstance) and op in ('in', 'not in'):
650+
return self.translate_instance_contains(rreg, lreg, op, line)
649651

650652
call_c_ops_candidates = binary_ops.get(op, [])
651653
target = self.matching_call_c(call_c_ops_candidates, [lreg, rreg], line)
@@ -829,6 +831,14 @@ def compare_tuples(self,
829831
self.goto_and_activate(out)
830832
return result
831833

834+
def translate_instance_contains(self, inst: Value, item: Value, op: str, line: int) -> Value:
835+
res = self.gen_method_call(inst, '__contains__', [item], None, line)
836+
if not is_bool_rprimitive(res.type):
837+
res = self.call_c(bool_op, [res], line)
838+
if op == 'not in':
839+
res = self.bool_bitwise_op(res, Integer(1, rtype=bool_rprimitive), '^', line)
840+
return res
841+
832842
def bool_bitwise_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value:
833843
if op == '&':
834844
code = IntOp.AND
@@ -1099,9 +1109,12 @@ def int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int) -> Val
10991109
def comparison_op(self, lhs: Value, rhs: Value, op: int, line: int) -> Value:
11001110
return self.add(ComparisonOp(lhs, rhs, op, line))
11011111

1102-
def builtin_len(self, val: Value, line: int,
1103-
use_pyssize_t: bool = False) -> Value:
1104-
"""Return short_int_rprimitive by default."""
1112+
def builtin_len(self, val: Value, line: int, use_pyssize_t: bool = False) -> Value:
1113+
"""Generate len(val).
1114+
1115+
Return short_int_rprimitive by default.
1116+
Return c_pyssize_t if use_pyssize_t is true (unshifted).
1117+
"""
11051118
typ = val.type
11061119
if is_list_rprimitive(typ) or is_tuple_rprimitive(typ):
11071120
elem_address = self.add(GetElementPtr(val, PyVarObject, 'ob_size'))
@@ -1128,8 +1141,22 @@ def builtin_len(self, val: Value, line: int,
11281141
offset = Integer(1, c_pyssize_t_rprimitive, line)
11291142
return self.int_op(short_int_rprimitive, size_value, offset,
11301143
IntOp.LEFT_SHIFT, line)
1131-
# generic case
1144+
elif isinstance(typ, RInstance):
1145+
# TODO: Support use_pyssize_t
1146+
assert not use_pyssize_t
1147+
length = self.gen_method_call(val, '__len__', [], int_rprimitive, line)
1148+
length = self.coerce(length, int_rprimitive, line)
1149+
ok, fail = BasicBlock(), BasicBlock()
1150+
self.compare_tagged_condition(length, Integer(0), '>=', ok, fail, line)
1151+
self.activate_block(fail)
1152+
self.add(RaiseStandardError(RaiseStandardError.VALUE_ERROR,
1153+
"__len__() should return >= 0",
1154+
line))
1155+
self.add(Unreachable())
1156+
self.activate_block(ok)
1157+
return length
11321158
else:
1159+
# generic case
11331160
if use_pyssize_t:
11341161
return self.call_c(generic_ssize_t_len_op, [val], line)
11351162
else:

mypyc/irbuild/specialize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def translate_len(
8585
return Integer(len(expr_rtype.types))
8686
else:
8787
obj = builder.accept(expr.args[0])
88-
return builder.builtin_len(obj, -1)
88+
return builder.builtin_len(obj, expr.line)
8989
return None
9090

9191

mypyc/lib-rt/CPy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ int CPyStatics_Initialize(PyObject **statics,
540540
const double *floats,
541541
const double *complex_numbers,
542542
const int *tuples);
543+
PyObject *CPy_Super(PyObject *builtins, PyObject *self);
543544

544545
#ifdef __cplusplus
545546
}

mypyc/lib-rt/misc_ops.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Misc primitive operations
1+
// Misc primitive operations + C helpers
22
//
33
// These are registered in mypyc.primitives.misc_ops.
44

@@ -631,3 +631,15 @@ int CPyStatics_Initialize(PyObject **statics,
631631
}
632632
return 0;
633633
}
634+
635+
// Call super(type(self), self)
636+
PyObject *
637+
CPy_Super(PyObject *builtins, PyObject *self) {
638+
PyObject *super_type = PyObject_GetAttrString(builtins, "super");
639+
if (!super_type)
640+
return NULL;
641+
PyObject *result = PyObject_CallFunctionObjArgs(
642+
super_type, (PyObject*)self->ob_type, self, NULL);
643+
Py_DECREF(super_type);
644+
return result;
645+
}

0 commit comments

Comments
 (0)