Skip to content

Commit 804d153

Browse files
authored
Add wrapping support for arithmetic magic methods (pyccel#2128)
Add wrapping support for arithmetic magic methods to allow them to be called from Python. Fixes pyccel#2024
1 parent e963fcb commit 804d153

File tree

10 files changed

+326
-16
lines changed

10 files changed

+326
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ All notable changes to this project will be documented in this file.
5454
- #1583 : Allow inhomogeneous tuples in classes.
5555
- #738 : Add support for homogeneous tuples with scalar elements as arguments.
5656
- Add a warning about containers in lists.
57-
- #2016 : Add support for translating arithmetic magic methods (methods cannot yet be used from Python).
57+
- #2016 : Add support for translating arithmetic magic methods.
5858
- #1980 : Extend The C support for min and max to more than two variables
5959
- #2081 : Add support for multi operator expressions
6060
- #2061 : Add C support for string declarations.

docs/classes.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
Pyccel strives to provide robust support for object-oriented programming concepts commonly used by developers. In Pyccel, classes are a fundamental building block for creating structured and reusable code. This documentation outlines key features and considerations when working with classes in Pyccel.
44

5+
## Contents
6+
7+
1. [Constructor Method](#constructor-method)
8+
2. [Destructor Method](#destructor-method)
9+
3. [Class Methods](#class-methods)
10+
4. [Class Properties](#class-properties)
11+
5. [Magic Methods](#magic-methods)
12+
6. [Limitations](#limitations)
13+
514
## Constructor Method
615

716
- The Constructor Method, `__init__`, is used to initialise the object's attributes.
@@ -188,7 +197,7 @@ MyClass Object created!
188197
==158858== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
189198
```
190199
191-
## Class properties
200+
## Class Properties
192201
193202
Pyccel now supports class properties (to retrieve a constant value only).
194203
@@ -415,6 +424,42 @@ int main()
415424
call obj % free()
416425
```
417426
427+
## Magic Methods
428+
429+
Pyccel supports a subset of magic methods that are listed here:
430+
431+
- `__add__`
432+
- `__sub__`
433+
- `__mul__`
434+
- `__truediv__`
435+
- `__pow__`
436+
- `__lshift__`
437+
- `__rshift__`
438+
- `__and__`
439+
- `__or__`
440+
- `__iadd__`
441+
- `__isub__`
442+
- `__imul__`
443+
- `__itruediv__`
444+
- `__ipow__`
445+
- `__ilshift__`
446+
- `__irshift__`
447+
- `__iand__`
448+
- `__ior__`
449+
450+
Additionally the following methods are supported in the translation but are lacking the wrapper support that would allow them to be called from Python code:
451+
452+
- `__radd__`
453+
- `__rsub__`
454+
- `__rmul__`
455+
- `__rtruediv__`
456+
- `__rpow__`
457+
- `__rlshift__`
458+
- `__rrshift__`
459+
- `__rand__`
460+
- `__ror__`
461+
- `__contains__`
462+
418463
## Limitations
419464
420-
It's important to note that Pyccel does not support class inheritance, magic methods or static class variables. For our first implementation, the focus of Pyccel is primarily on core class functionality and memory management.
465+
It's important to note that Pyccel does not support class inheritance, or static class variables. For our first implementation, the focus of Pyccel is primarily on core class functionality and memory management.

pyccel/ast/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from .basic import PyccelAstNode, TypedAstNode, iterable, ScopedAstNode
1515

16-
from .bitwise_operators import PyccelBitOr, PyccelBitAnd
16+
from .bitwise_operators import PyccelBitOr, PyccelBitAnd, PyccelLShift, PyccelRShift
1717

1818
from .builtins import PythonBool, PythonTuple
1919

@@ -750,6 +750,8 @@ class AugAssign(Assign):
750750
'%' : PyccelMod,
751751
'|' : PyccelBitOr,
752752
'&' : PyccelBitAnd,
753+
'<<': PyccelLShift,
754+
'>>': PyccelRShift,
753755
}
754756

755757
def __init__(

pyccel/ast/cwrapper.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,8 @@ class definition.
718718
wrapped.
719719
"""
720720
__slots__ = ('_original_class', '_struct_name', '_type_name', '_type_object',
721-
'_new_func', '_properties')
721+
'_new_func', '_properties', '_number_magic_methods')
722+
_attribute_nodes = ClassDef._attribute_nodes + ('_number_magic_methods',)
722723

723724
def __init__(self, original_class, struct_name, type_name, scope, **kwargs):
724725
self._original_class = original_class
@@ -727,6 +728,7 @@ def __init__(self, original_class, struct_name, type_name, scope, **kwargs):
727728
self._type_object = Variable(PyccelPyClassType(), type_name)
728729
self._new_func = None
729730
self._properties = ()
731+
self._number_magic_methods = ()
730732
variables = [Variable(VoidType(), 'instance', memory_handling='alias'),
731733
Variable(PyccelPyObject(), 'referenced_objects', memory_handling='alias'),
732734
Variable(PythonNativeBool(), 'is_alias')]
@@ -819,6 +821,33 @@ def properties(self):
819821
"""
820822
return self._properties
821823

824+
def add_new_magic_number_method(self, method):
825+
"""
826+
Add a new magic number method to the current class.
827+
828+
Add a new magic method to the current ClassDef describing
829+
a number method.
830+
831+
Parameters
832+
----------
833+
method : FunctionDef
834+
The Method that will be added.
835+
"""
836+
837+
if not isinstance(method, PyFunctionDef):
838+
raise TypeError("Method must be FunctionDef")
839+
method.set_current_user_node(self)
840+
self._number_magic_methods += (method,)
841+
842+
@property
843+
def number_magic_methods(self):
844+
"""
845+
Get the magic methods describing number methods.
846+
847+
Get the magic methods describing number methods such as __add__.
848+
"""
849+
return self._number_magic_methods
850+
822851
#-------------------------------------------------------------------
823852

824853
class PyGetSetDefElement(PyccelAstNode):

pyccel/codegen/printing/cwrappercode.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ def _print_ModuleHeader(self, expr):
285285
"};\n")
286286
sig_methods = c.methods + (c.new_func,) + tuple(f for i in c.interfaces for f in i.functions) + \
287287
tuple(i.interface_func for i in c.interfaces) + \
288-
tuple(getset for p in c.properties for getset in (p.getter, p.setter) if getset)
288+
tuple(getset for p in c.properties for getset in (p.getter, p.setter) if getset) + \
289+
c.number_magic_methods
289290
function_signatures += '\n'+''.join(self.function_signature(f)+';\n' for f in sig_methods)
290291
macro_defs += f'#define {type_name} (*(PyTypeObject*){API_var.name}[{i}])\n'
291292

@@ -395,7 +396,7 @@ def _print_PyClassDef(self, expr):
395396
getters = tuple(p.getter for p in expr.properties)
396397
setters = tuple(p.setter for p in expr.properties if p.setter)
397398
print_methods = expr.methods + (expr.new_func,) + expr.interfaces + \
398-
getters + setters
399+
expr.number_magic_methods + getters + setters
399400
functions = '\n'.join(self._print(f) for f in print_methods)
400401
init_string = ''
401402
del_string = ''
@@ -434,6 +435,44 @@ def _print_PyClassDef(self, expr):
434435
'},\n')
435436
for name, (wrapper_name, doc_string) in funcs.items())
436437

438+
number_magic_method_name = self.scope.get_new_name(f'{expr.name}_number_methods')
439+
number_magic_methods = {self.get_python_name(original_scope, f.original_function): f for f in expr.number_magic_methods}
440+
441+
number_magic_methods_def = f"static PyNumberMethods {number_magic_method_name} = {{\n"
442+
if '__add__' in number_magic_methods:
443+
number_magic_methods_def += f" .nb_add = (binaryfunc){number_magic_methods['__add__'].name},\n"
444+
if '__sub__' in number_magic_methods:
445+
number_magic_methods_def += f" .nb_subtract = (binaryfunc){number_magic_methods['__sub__'].name},\n"
446+
if '__mul__' in number_magic_methods:
447+
number_magic_methods_def += f" .nb_multiply = (binaryfunc){number_magic_methods['__mul__'].name},\n"
448+
if '__truediv__' in number_magic_methods:
449+
number_magic_methods_def += f" .nb_true_divide = (binaryfunc){number_magic_methods['__truediv__'].name},\n"
450+
if '__lshift__' in number_magic_methods:
451+
number_magic_methods_def += f" .nb_lshift = (binaryfunc){number_magic_methods['__lshift__'].name},\n"
452+
if '__rshift__' in number_magic_methods:
453+
number_magic_methods_def += f" .nb_rshift = (binaryfunc){number_magic_methods['__rshift__'].name},\n"
454+
if '__and__' in number_magic_methods:
455+
number_magic_methods_def += f" .nb_and = (binaryfunc){number_magic_methods['__and__'].name},\n"
456+
if '__or__' in number_magic_methods:
457+
number_magic_methods_def += f" .nb_or = (binaryfunc){number_magic_methods['__or__'].name},\n"
458+
if '__iadd__' in number_magic_methods:
459+
number_magic_methods_def += f" .nb_inplace_add = (binaryfunc){number_magic_methods['__iadd__'].name},\n"
460+
if '__isub__' in number_magic_methods:
461+
number_magic_methods_def += f" .nb_inplace_subtract = (binaryfunc){number_magic_methods['__isub__'].name},\n"
462+
if '__imul__' in number_magic_methods:
463+
number_magic_methods_def += f" .nb_inplace_multiply = (binaryfunc){number_magic_methods['__imul__'].name},\n"
464+
if '__itruediv__' in number_magic_methods:
465+
number_magic_methods_def += f" .nb_inplace_true_divide = (binaryfunc){number_magic_methods['__itruediv__'].name},\n"
466+
if '__ilshift__' in number_magic_methods:
467+
number_magic_methods_def += f" .nb_inplace_lshift = (binaryfunc){number_magic_methods['__ilshift__'].name},\n"
468+
if '__irshift__' in number_magic_methods:
469+
number_magic_methods_def += f" .nb_inplace_rshift = (binaryfunc){number_magic_methods['__irshift__'].name},\n"
470+
if '__iand__' in number_magic_methods:
471+
number_magic_methods_def += f" .nb_inplace_and = (binaryfunc){number_magic_methods['__iand__'].name},\n"
472+
if '__ior__' in number_magic_methods:
473+
number_magic_methods_def += f" .nb_inplace_or = (binaryfunc){number_magic_methods['__ior__'].name},\n"
474+
number_magic_methods_def += '};\n'
475+
437476
method_def_name = self.scope.get_new_name(f'{expr.name}_methods')
438477
method_def = (f'static PyMethodDef {method_def_name}[] = {{\n'
439478
f'{method_def_funcs}'
@@ -448,6 +487,7 @@ def _print_PyClassDef(self, expr):
448487
type_code = (f"static PyTypeObject {type_name} = {{\n"
449488
" PyVarObject_HEAD_INIT(NULL, 0)\n"
450489
f" .tp_name = \"{self._module_name}.{name}\",\n"
490+
f" .tp_as_number = &{number_magic_method_name},\n"
451491
f" .tp_doc = PyDoc_STR({docstring}),\n"
452492
f" .tp_basicsize = sizeof(struct {struct_name}),\n"
453493
" .tp_itemsize = 0,\n"
@@ -458,7 +498,7 @@ def _print_PyClassDef(self, expr):
458498
f" .tp_getset = {property_def_name},\n"
459499
"};\n")
460500

461-
return '\n'.join((method_def, property_def, type_code, functions))
501+
return '\n'.join((method_def, number_magic_methods_def, property_def, type_code, functions))
462502

463503
def _print_PyModInitFunc(self, expr):
464504
decs = ''.join(self._print(d) for d in expr.declarations)

pyccel/codegen/printing/pycode.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from pyccel.ast.utilities import builtin_import_registry as pyccel_builtin_import_registry
2222
from pyccel.ast.utilities import decorators_mod
2323

24+
from pyccel.parser.semantic import magic_method_map
25+
2426
from pyccel.codegen.printing.codeprinter import CodePrinter
2527

2628
from pyccel.errors.errors import Errors
@@ -299,11 +301,12 @@ def _print_FunctionDef(self, expr):
299301

300302
body = ''.join([docstring, functions, interfaces, imports, body])
301303

302-
code = ('def {name}({args}):\n'
303-
'{body}\n').format(
304-
name=name,
305-
args=args,
306-
body=body)
304+
# Put back return removed in semantic stage
305+
if name.startswith('__i') and ('__'+name[3:]) in magic_method_map.values():
306+
body += f' return {expr.arguments[0].name}\n'
307+
308+
code = (f'def {name}({args}):\n'
309+
f'{body}\n')
307310
decorators = expr.decorators.copy()
308311
if decorators:
309312
if decorators['template']:

pyccel/codegen/wrapper/c_to_python_wrapper.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@
6464
cwrapper_ndarray_imports = [Import('cwrapper_ndarrays', Module('cwrapper_ndarrays', (), ())),
6565
Import('ndarrays', Module('ndarrays', (), ()))]
6666

67+
magic_binary_funcs = ('__add__',
68+
'__sub__',
69+
'__mul__',
70+
'__truediv__',
71+
'__pow__',
72+
'__lshift__',
73+
'__rshift__',
74+
'__and__',
75+
'__or__',
76+
'__iadd__',
77+
'__isub__',
78+
'__imul__',
79+
'__itruediv__',
80+
'__ipow__',
81+
'__ilshift__',
82+
'__irshift__',
83+
'__iand__',
84+
'__ior__',
85+
)
86+
6787
class CToPythonWrapper(Wrapper):
6888
"""
6989
Class for creating a wrapper exposing C code to Python.
@@ -1409,7 +1429,7 @@ def _wrap_Interface(self, expr):
14091429

14101430
def _wrap_FunctionDef(self, expr):
14111431
"""
1412-
Build a `PyFunctionDef` form a `FunctionDef`.
1432+
Build a `PyFunctionDef` from a `FunctionDef`.
14131433
14141434
Create a `PyFunctionDef` which wraps a C-compatible `FunctionDef`.
14151435
The `PyFunctionDef` should take three arguments (`self`, `args`,
@@ -1431,6 +1451,7 @@ def _wrap_FunctionDef(self, expr):
14311451
func_name = self.scope.get_new_name(expr.name+'_wrapper')
14321452
func_scope = self.scope.new_child_scope(func_name)
14331453
self.scope = func_scope
1454+
original_func_name = original_func.scope.get_python_name(original_func.name)
14341455

14351456
possible_class_base = expr.get_user_nodes((ClassDef,))
14361457
if possible_class_base:
@@ -1477,7 +1498,7 @@ def _wrap_FunctionDef(self, expr):
14771498
func_args = [FunctionDefArgument(a) for a in func_args]
14781499
body = []
14791500
else:
1480-
if in_interface:
1501+
if in_interface or original_func_name in magic_binary_funcs:
14811502
func_args = [FunctionDefArgument(a) for a in self._get_python_argument_variables(python_args)]
14821503
body = []
14831504
else:
@@ -1490,7 +1511,12 @@ def _wrap_FunctionDef(self, expr):
14901511

14911512
# Get the code required to wrap the C-compatible results into Python objects
14921513
# This function creates variables so it must be called before extracting them from the scope.
1493-
if len(python_results) == 0:
1514+
if original_func_name in magic_binary_funcs and original_func_name.startswith('__i'):
1515+
res = func_args[0].var.clone(self.scope.get_new_name(func_args[0].var.name), is_argument=False)
1516+
wrapped_results = {'c_results': [], 'py_result': res, 'body': []}
1517+
body.append(AliasAssign(res, func_args[0].var))
1518+
body.append(Py_INCREF(res))
1519+
elif len(python_results) == 0:
14941520
wrapped_results = {'c_results': [], 'py_result': Py_None, 'body': []}
14951521
elif len(python_results) == 1:
14961522
wrapped_results = self._extract_FunctionDefResult(original_func.results[0].var, is_bind_c_function_def, expr)
@@ -2014,6 +2040,8 @@ def _wrap_ClassDef(self, expr):
20142040
wrapped_class.add_new_method(self._get_class_destructor(f, orig_cls_dtype, wrapped_class.scope))
20152041
elif python_name == '__init__':
20162042
wrapped_class.add_new_method(self._get_class_initialiser(f, orig_cls_dtype))
2043+
elif python_name in magic_binary_funcs:
2044+
wrapped_class.add_new_magic_number_method(self._wrap(f))
20172045
elif 'property' in f.decorators:
20182046
wrapped_class.add_property(self._wrap(f))
20192047
else:

pyccel/parser/syntactic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@ def _visit_AugAssign(self, stmt):
444444
return AugAssign(lhs, '|', rhs)
445445
elif isinstance(stmt.op, ast.BitAnd):
446446
return AugAssign(lhs, '&', rhs)
447+
elif isinstance(stmt.op, ast.LShift):
448+
return AugAssign(lhs, '<<', rhs)
449+
elif isinstance(stmt.op, ast.RShift):
450+
return AugAssign(lhs, '>>', rhs)
447451
else:
448452
return errors.report(PYCCEL_RESTRICTION_TODO, symbol = stmt,
449453
severity='error')

0 commit comments

Comments
 (0)