Skip to content

Commit ba2a97a

Browse files
authored
Add support for returning lists, sets and homogeneous tuples from C (pyccel#2070)
Generalise `_extract_HomogeneousTupleType_FunctionDefResult` to `_extract_HomogeneousContainerType_FunctionDefResult` to add support for returning lists, sets and homogeneous tuples from C. Fixes pyccel#1664 and fixes pyccel#1665 Improve docs
1 parent 1ca1bd8 commit ba2a97a

File tree

8 files changed

+152
-15
lines changed

8 files changed

+152
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ All notable changes to this project will be documented in this file.
3333
- #1874 : Add C and Fortran support for the `len()` function for the `list` container.
3434
- #1875 : Add C and Fortran support for the `len()` function for the `set` container.
3535
- #1908 : Add C and Fortran support for the `len()` function for the `dict` container.
36+
- #1665 : Add C support for returning lists from functions.
3637
- #1689 : Add C and Fortran support for list method `append()`.
3738
- #1876 : Add C support for indexing lists.
3839
- #1690 : Add C support for list method `pop()`.
40+
- #1664 : Add C support for returning sets from functions.
3941
- #2023 : Add support for iterating over a `set`.
4042
- #1877 : Add C and Fortran Support for set method `pop()`.
4143
- #1917 : Add C and Fortran support for set method `add()`.
@@ -51,6 +53,7 @@ All notable changes to this project will be documented in this file.
5153
- Add a warning about containers in lists.
5254
- #2016 : Add support for translating arithmetic magic methods (methods cannot yet be used from Python).
5355
- #1980 : Extend The C support for min and max to more than two variables
56+
- #2081 : Add support for multi operator expressions
5457
- \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
5558
- \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
5659
- \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.

docs/type_annotations.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,33 @@ In general string type hints must be used to provide Pyccel with information abo
4141

4242
## Tuples
4343

44-
Currently Pyccel supports tuples used locally in functions and in certain cases as arguments, but not as returned objects or module variables. The implementation of the type annotations (including adding the missing support) is in progress. Currently homogeneous tuple type annotations are supported for local variables and function arguments (if the tuples contain scalar objects). Internally we handle homogeneous tuples as though they were NumPy arrays. When creating multiple dimensional tuples it is therefore important to ensure that all objects have compatible sizes otherwise they will be handled as inhomogeneous tuples.
44+
Currently Pyccel supports tuples used locally in functions and in certain cases as arguments, but not as returned objects or module variables. The implementation of the type annotations (including adding the missing support) is in progress. Currently homogeneous tuple type annotations are supported for local variables and function arguments and results (if the tuples contain scalar objects). Internally we handle homogeneous tuples as though they were NumPy arrays. When creating multiple dimensional tuples it is therefore important to ensure that all objects have compatible sizes otherwise they will be handled as inhomogeneous tuples.
4545

4646
To declare a homogeneous tuple the syntax is as follows:
4747
```python
4848
a : tuple[int,...] = (1,2,3,4)
4949
```
5050

51+
## Lists
52+
53+
Lists are in the process of being added to Pyccel. Homogeneous lists can be declared in Pyccel using the following syntax:
54+
```python
55+
a : list[int] = [1, 2]
56+
b : list[bool] = [False, True]
57+
c : list[float] = []
58+
```
59+
So far lists can be declared as local variables or as results of functions.
60+
61+
## Sets
62+
63+
Sets are in the process of being added to Pyccel. Homogeneous sets can be declared in Pyccel using the following syntax:
64+
```python
65+
a : set[int] = {1, 2}
66+
b : set[bool] = {False, True}
67+
c : set[float] = {}
68+
```
69+
So far sets can be declared as local variables or as results of functions.
70+
5171
## Dictionaries
5272

5373
Dictionaries are in the process of being added to Pyccel. They cannot yet be used effectively however the type annotations are already supported.

pyccel/ast/cwrapper.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,7 @@ def C_to_Python(c_object):
11051105
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
11061106
FunctionDefArgument(Variable(PythonNativeInt(), name='i')),
11071107
FunctionDefArgument(Variable(PyccelPyObject(), name='new_item', memory_handling='alias'))],
1108-
results = [])
1108+
results = [FunctionDefResult(Variable(CNativeInt(), 'i'))])
11091109

11101110
#-------------------------------------------------------------------
11111111
# Tuple functions
@@ -1136,6 +1136,31 @@ def C_to_Python(c_object):
11361136
FunctionDefArgument(Variable(PythonNativeInt(), name='i'))],
11371137
results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
11381138

1139+
# https://docs.python.org/3/c-api/tuple.html#c.PyTuple_SetItem
1140+
PyTuple_SetItem = FunctionDef(name = 'PyTuple_SetItem',
1141+
body = [],
1142+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
1143+
FunctionDefArgument(Variable(PythonNativeInt(), name='i')),
1144+
FunctionDefArgument(Variable(PyccelPyObject(), name='new_item', memory_handling='alias'))],
1145+
results = [FunctionDefResult(Variable(CNativeInt(), 'i'))])
1146+
1147+
#-------------------------------------------------------------------
1148+
# Set functions
1149+
#-------------------------------------------------------------------
1150+
1151+
# https://docs.python.org/3/c-api/set.html#c.PySet_New
1152+
PySet_New = FunctionDef(name = 'PySet_New',
1153+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'iterable', memory_handling='alias'), value = Nil())],
1154+
results = [FunctionDefResult(Variable(PyccelPyObject(), 'set', memory_handling='alias'))],
1155+
body = [])
1156+
1157+
# https://docs.python.org/3/c-api/set.html#c.PySet_Add
1158+
PySet_Add = FunctionDef(name = 'PySet_Add',
1159+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'set', memory_handling='alias')),
1160+
FunctionDefArgument(Variable(PyccelPyObject(), 'key', memory_handling='alias'))],
1161+
results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
1162+
body = [])
1163+
11391164

11401165
# Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
11411166
check_type_registry = {

pyccel/codegen/printing/cwrappercode.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def _print_ModuleHeader(self, expr):
267267
imports = [*module_imports, *mod.imports]
268268
for i in imports:
269269
self.add_import(i)
270+
self.invalidate_stc_headers(imports)
270271
imports = ''.join(self._print(i) for i in imports)
271272

272273
function_signatures = ''.join(self.function_signature(f, print_arg_names = False) + ';\n' for f in mod.external_funcs)
@@ -373,6 +374,7 @@ def _print_PyModule(self, expr):
373374

374375
pymod_name = f'{expr.name}_wrapper'
375376
imports = [Import(pymod_name, Module(pymod_name,(),())), *self._additional_imports.values()]
377+
self.invalidate_stc_headers(imports)
376378
imports = ''.join(self._print(i) for i in imports)
377379

378380
self.exit_scope()

pyccel/codegen/wrapper/c_to_python_wrapper.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from pyccel.ast.bind_c import BindCFunctionDef, BindCPointer, BindCFunctionDefArgument
1212
from pyccel.ast.bind_c import BindCModule, BindCVariable, BindCFunctionDefResult
1313
from pyccel.ast.bind_c import BindCClassDef, BindCClassProperty
14-
from pyccel.ast.builtins import PythonTuple, PythonRange
14+
from pyccel.ast.builtins import PythonTuple, PythonRange, PythonLen
15+
from pyccel.ast.builtin_methods.set_methods import SetPop
1516
from pyccel.ast.class_defs import StackArrayClass
1617
from pyccel.ast.core import Interface, If, IfSection, Return, FunctionCall
1718
from pyccel.ast.core import FunctionDef, FunctionDefArgument, FunctionDefResult
@@ -29,10 +30,13 @@
2930
from pyccel.ast.cwrapper import PyList_New, PyList_Append, PyList_GetItem, PyList_SetItem
3031
from pyccel.ast.cwrapper import PyccelPyTypeObject, PyCapsule_New, PyCapsule_Import
3132
from pyccel.ast.cwrapper import PySys_GetObject, PyUnicode_FromString, PyGetSetDefElement
32-
from pyccel.ast.cwrapper import PyTuple_Size, PyTuple_Check, PyTuple_GetItem
33+
from pyccel.ast.cwrapper import PyTuple_Size, PyTuple_Check, PyTuple_New
34+
from pyccel.ast.cwrapper import PyTuple_GetItem, PyTuple_SetItem
35+
from pyccel.ast.cwrapper import PySet_New, PySet_Add
3336
from pyccel.ast.c_concepts import ObjectAddress, PointerCast, CStackArray, CNativeInt
3437
from pyccel.ast.datatypes import VoidType, PythonNativeInt, CustomDataType, DataTypeFactory
3538
from pyccel.ast.datatypes import FixedSizeNumericType, HomogeneousTupleType, PythonNativeBool
39+
from pyccel.ast.datatypes import HomogeneousSetType, HomogeneousListType
3640
from pyccel.ast.datatypes import TupleType
3741
from pyccel.ast.literals import Nil, LiteralTrue, LiteralString, LiteralInteger
3842
from pyccel.ast.literals import LiteralFalse, convert_to_literal
@@ -761,6 +765,7 @@ def _build_module_import_function(self, expr):
761765

762766
ok_code = LiteralInteger(0, dtype=CNativeInt())
763767
error_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
768+
self._error_exit_code = error_code
764769

765770
# Create variables to temporarily modify the Python path so the file will be discovered
766771
current_path = func_scope.get_temporary_variable(PyccelPyObject(), 'current_path', memory_handling='alias')
@@ -769,14 +774,19 @@ def _build_module_import_function(self, expr):
769774
body = [AliasAssign(current_path, PySys_GetObject(LiteralString("path"))),
770775
AliasAssign(stash_path, PyList_GetItem(current_path, LiteralInteger(0, dtype=CNativeInt()))),
771776
Py_INCREF(stash_path),
772-
PyList_SetItem(current_path, LiteralInteger(0, dtype=CNativeInt()),
773-
PyUnicode_FromString(LiteralString(self._file_location))),
777+
If(IfSection(PyccelEq(PyList_SetItem(current_path, LiteralInteger(0, dtype=CNativeInt()),
778+
PyUnicode_FromString(LiteralString(self._file_location))),
779+
PyccelUnarySub(LiteralInteger(1))),
780+
[Return([self._error_exit_code])])),
774781
AliasAssign(API_var, PyCapsule_Import(self.scope.get_python_name(mod_name))),
775-
PyList_SetItem(current_path, LiteralInteger(0, dtype=CNativeInt()), stash_path),
782+
If(IfSection(PyccelEq(PyList_SetItem(current_path, LiteralInteger(0, dtype=CNativeInt()), stash_path),
783+
PyccelUnarySub(LiteralInteger(1))),
784+
[Return([self._error_exit_code])])),
776785
Return([IfTernaryOperator(PyccelIsNot(API_var, Nil()), ok_code, error_code)])]
777786

778787
result = func_scope.get_temporary_variable(CNativeInt())
779788
self.exit_scope()
789+
self._error_exit_code = Nil()
780790
import_func = FunctionDef(func_name, (), (FunctionDefResult(result),), body, is_static=True, scope = func_scope)
781791

782792
return API_var, import_func
@@ -2470,7 +2480,7 @@ def _extract_FixedSizeType_FunctionDefResult(self, orig_var, is_bind_c, funcdef)
24702480
dict
24712481
A dictionary describing the objects necessary to collect the result.
24722482
"""
2473-
name = orig_var.name
2483+
name = getattr(orig_var, 'name', 'tmp')
24742484
py_res = self.get_new_PyObject(f'{name}_obj', orig_var.dtype)
24752485
c_res = Variable(orig_var.class_type, self.scope.get_new_name(name))
24762486
self.scope.insert_variable(c_res)
@@ -2571,13 +2581,14 @@ def _extract_InhomogeneousTupleType_FunctionDefResult(self, orig_var, is_bind_c,
25712581
body.extend(Py_DECREF(r) for r in py_result_vars)
25722582
return {'c_results': c_result_vars, 'py_result': py_res, 'body': body, 'setup': setup}
25732583

2574-
def _extract_HomogeneousTupleType_FunctionDefResult(self, orig_var, is_bind_c, funcdef):
2584+
def _extract_HomogeneousContainerType_FunctionDefResult(self, orig_var, is_bind_c, funcdef):
25752585
"""
2576-
Get the code which translates a `Variable` containing a homogeneous tuple to a PyObject.
2586+
Get the code which translates a `Variable` containing a homogeneous container to a PyObject.
25772587
2578-
Get the code which translates a `Variable` containing a homogeneous tuple to a PyObject.
2579-
For now this function does not handle homogeneous tuples but merely allows multiple
2580-
returns of the same type to be outputted neatly.
2588+
Get the code which translates a `Variable` containing a homogeneous container to a PyObject.
2589+
This function handles lists, sets, and tuples.
2590+
The current implementation for tuples is not working correctly as Pyccel does not handle
2591+
function calls to functions returning tuples correctly.
25812592
25822593
Parameters
25832594
----------
@@ -2596,6 +2607,46 @@ def _extract_HomogeneousTupleType_FunctionDefResult(self, orig_var, is_bind_c, f
25962607
"""
25972608
if isinstance(orig_var, PythonTuple):
25982609
return self._extract_InhomogeneousTupleType_FunctionDefResult(orig_var, is_bind_c, funcdef)
2610+
2611+
if is_bind_c:
2612+
raise NotImplementedError("Support for returning sets from Fortran code is not yet available")
2613+
2614+
name = getattr(orig_var, 'name', 'tmp')
2615+
py_res = self.get_new_PyObject(f'{name}_obj', orig_var.dtype)
2616+
c_res = orig_var.clone(self.scope.get_new_name(name), is_argument = False)
2617+
loop_size = Variable(PythonNativeInt(), self.scope.get_new_name(f'{name}_size'))
2618+
idx = Variable(PythonNativeInt(), self.scope.get_new_name())
2619+
self.scope.insert_variable(c_res)
2620+
self.scope.insert_variable(loop_size)
2621+
self.scope.insert_variable(idx)
2622+
2623+
for_scope = self.scope.create_new_loop_scope()
2624+
self.scope = for_scope
2625+
element_extraction = self._extract_FunctionDefResult(IndexedElement(orig_var, idx), is_bind_c, funcdef)
2626+
self.exit_scope()
2627+
2628+
class_type = orig_var.class_type
2629+
if isinstance(class_type, HomogeneousSetType):
2630+
element = SetPop(c_res)
2631+
elem_set = PySet_Add(py_res, element_extraction['py_result'])
2632+
init = PySet_New()
2633+
elif isinstance(class_type, HomogeneousListType):
2634+
element = IndexedElement(c_res, idx)
2635+
elem_set = PyList_SetItem(py_res, idx, element_extraction['py_result'])
2636+
init = PyList_New(loop_size)
2637+
elif isinstance(class_type, HomogeneousTupleType):
2638+
element = IndexedElement(c_res, idx)
2639+
elem_set = PyTuple_SetItem(py_res, idx, element_extraction['py_result'])
2640+
init = PyTuple_New(loop_size)
25992641
else:
2600-
return errors.report(f"Wrapping function results is not implemented for type {orig_var.class_type}. " + PYCCEL_RESTRICTION_TODO, symbol=orig_var,
2601-
severity='fatal')
2642+
raise NotImplementedError(f"Don't know how to return an object of type {class_type}")
2643+
2644+
for_body = [Assign(element_extraction['c_results'][0], element),
2645+
*element_extraction['body'],
2646+
If(IfSection(PyccelEq(elem_set, PyccelUnarySub(LiteralInteger(1))),
2647+
[Return([self._error_exit_code])]))]
2648+
body = [Assign(loop_size, PythonLen(c_res)),
2649+
AliasAssign(py_res, init),
2650+
For((idx,), PythonRange(loop_size), for_body, for_scope)]
2651+
2652+
return {'c_results': [c_res], 'py_result': py_res, 'body': body}

tests/epyccel/test_epyccel_lists.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,3 +769,15 @@ def list_ptr():
769769
python_result = list_ptr()
770770
assert isinstance(python_result, type(pyccel_result))
771771
assert python_result == pyccel_result
772+
773+
def test_list_return(stc_language):
774+
def list_return():
775+
a = [1,2,3,4,5]
776+
return a
777+
778+
epyccel_func = epyccel(list_return, language = stc_language)
779+
pyccel_result = epyccel_func()
780+
python_result = list_return()
781+
assert python_result == pyccel_result
782+
assert isinstance(python_result, type(pyccel_result))
783+
assert isinstance(python_result.pop(), type(pyccel_result.pop()))

tests/epyccel/test_epyccel_sets.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,17 @@ def set_iter_prod():
562562
python_result = set_iter_prod()
563563
assert python_result == pyccel_result
564564
assert isinstance(python_result, type(pyccel_result))
565+
566+
def test_set_return(stc_language):
567+
def set_return():
568+
a = {1,2,3,4,5}
569+
b = {4,5,6}
570+
c = a.union(b) # Use union to avoid #2084
571+
return c
572+
573+
epyccel_func = epyccel(set_return, language = stc_language)
574+
pyccel_result = epyccel_func()
575+
python_result = set_return()
576+
assert python_result == pyccel_result
577+
assert isinstance(python_result, type(pyccel_result))
578+
assert isinstance(python_result.pop(), type(pyccel_result.pop()))

tests/epyccel/test_tuples.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,13 @@ def my_tuple(a : 'tuple[tuple[int,...],...]'):
185185
epyc_func = epyccel(my_tuple, language=language)
186186

187187
assert my_tuple(tuple_arg) == epyc_func(tuple_arg)
188+
189+
@pytest.mark.xfail(reason="Returning tuples from functions requires a reorganisation of the return system. See #337")
190+
def test_homogeneous_tuples_result(stc_language):
191+
def my_tuple() -> 'tuple[int, ...]':
192+
a = (1,2,3,4,5)
193+
return a
194+
195+
epyc_func = epyccel(my_tuple, language=stc_language)
196+
197+
assert my_tuple() == epyc_func()

0 commit comments

Comments
 (0)