Skip to content

Commit 0e6fc86

Browse files
authored
Allow homogeneous tuples as function arguments (pyccel#1850)
Allow 1D homogeneous tuples as function arguments. Fixes pyccel#738. Homogeneous tuples as arguments already worked in the low level code as homogeneous tuples are handled as arrays, however the wrapper was missing which meant that such functions couldn't be called from Python. In order to handle multi-level tuples some reorganisation of the wrapper was necessary. Namely the introduction of the `_extract_X_FunctionDefArgument` functions. However Python multi-level tuples may have different lengths in the elements. It proved difficult to check the size of the elements in a generic way so this was left for later work (the obvious solution is to use lists to support this). **Commit Summary** - Update docs - Update `BindCFunctionDefArgument` to handle tuples as strideless arrays - Add descriptions of more tuple functions from `Python.h`. Group these functions together in `ast.cwrapper` - Make `dtype` a searchable attribute of `SyntacticTypeAnnotation` - Fix filter for used template type annotations so it also searches through indexed elements to find objects in a tuple annotation. - Add tests for tuples as arguments - Rename `_get_check_function`->`_get_type_check_condition`. - Allow `_get_type_check_condition` to return a condition which is not a function call (e.g. a Variable) - Add `body` argument to `_get_type_check_condition` to allow additional commands to be run before determining if the type is correct. - Add type checks for homogeneous tuples - Introduce `_extract_X_FunctionDefArgument` functions to describe container unpacking recursively - Add `_extract_HomogeneousTupleType_FunctionDefArgument` function to unpack 1D homogeneous tuples. - Ensure errors are raised for multi-D tuples.
1 parent 53ed828 commit 0e6fc86

File tree

13 files changed

+650
-165
lines changed

13 files changed

+650
-165
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ All notable changes to this project will be documented in this file.
4343
- #1937 : Optimise `pyccel.ast.basic.PyccelAstNode.substitute` method.
4444
- #1544 : Add support for `typing.TypeAlias`.
4545
- #1583 : Allow inhomogeneous tuples in classes.
46+
- #738 : Add support for homogeneous tuples with scalar elements as arguments.
4647
- Add a warning about containers in lists.
4748
- \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
4849
- \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.

developer_docs/wrapper_stage.md

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,15 +376,17 @@ PyObject* func_name(PyObject* self, PyObject* args, PyObject* kwargs);
376376

377377
The arguments and keyword arguments are unpacked into individual `PyObject` pointers.
378378
Each of these objects is checked to verify the type. If the type does not match the expected type then an error is raised as described in the [C-API documentation](https://docs.python.org/3/c-api/intro.html#exceptions).
379-
If the type does match then the value is unpacked into a C object. This is done using custom functions defined in `pyccel/stdlib/cwrapper/` or `pyccel/stdlib/cwrapper_ndarrays/` (see these files for more details).
379+
If the type does match then the value is unpacked into a C object. This is done using custom functions defined in `pyccel/stdlib/cwrapper/` or `pyccel/stdlib/cwrapper_ndarrays/` (see these files for more details) or using functions provided by `Python.h`.
380+
381+
In order to create all the nodes necessary to describe the unpacking of the arguments we use functions named `_extract_X_FunctionDefArgument` where `X` is the type of the object being extracted from the `FunctionDefArgument`. This allows such functions to call each other recursively. This is notably useful for container types (tuples, lists, etc) whose elements may themselves be container types. The types of scalars are checked in the same way regardless of whether they are arguments or elements of a container so this also reduces code duplication.
380382

381383
Once C objects have been retrieved the function is called normally.
382384

383385
Finally all the arguments are packed into a Python tuple stored in a `PyObject` and are returned.
384386

385387
The wrapper is attached to the module via a `PyMethodDef` (see C-API [docs](https://docs.python.org/3/c-api/structures.html#c.PyMethodDef)).
386388

387-
#### Example
389+
#### Example 1
388390

389391
The following Python code:
390392
```python
@@ -543,6 +545,77 @@ PyObject* f_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
543545
}
544546
```
545547

548+
#### Example 2 : function with tuple arguments
549+
550+
The following Python code:
551+
```python
552+
def get_first_element_of_tuple(a : 'tuple[int,...]'):
553+
return a[0]
554+
```
555+
556+
leads to C code with the following prototype (as homogeneous tuples are treated like arrays):
557+
```c
558+
int64_t get_first_element_of_tuple(t_ndarray a);
559+
```
560+
561+
which is then wrapped as follows:
562+
```c
563+
static PyObject* get_first_element_of_tuple_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
564+
{
565+
PyObject* a_obj;
566+
t_ndarray a = {.shape = NULL};
567+
int Dummy_0000;
568+
int64_t a_size;
569+
PyObject* Dummy_0001;
570+
bool is_homog_tuple;
571+
int64_t size;
572+
int Dummy_0002;
573+
PyObject* Dummy_0003;
574+
int64_t Out_0001;
575+
PyObject* Out_0001_obj;
576+
static char *kwlist[] = {
577+
"a",
578+
NULL
579+
};
580+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &a_obj))
581+
{
582+
return NULL;
583+
}
584+
if (PyTuple_Check(a_obj))
585+
{
586+
size = PyTuple_Size(a_obj);
587+
is_homog_tuple = 1;
588+
for (Dummy_0002 = INT64_C(0); Dummy_0002 < size; Dummy_0002 += INT64_C(1))
589+
{
590+
Dummy_0003 = PyTuple_GetItem(a_obj, Dummy_0002);
591+
is_homog_tuple = is_homog_tuple && PyIs_NativeInt(Dummy_0003);
592+
}
593+
}
594+
else
595+
{
596+
is_homog_tuple = 0;
597+
}
598+
if (is_homog_tuple)
599+
{
600+
a_size = PyTuple_Size(a_obj);
601+
a = array_create(1, (int64_t[]){a_size}, nd_int64, false, order_c);
602+
for (Dummy_0000 = INT64_C(0); Dummy_0000 < a_size; Dummy_0000 += INT64_C(1))
603+
{
604+
Dummy_0001 = PyTuple_GetItem(a_obj, Dummy_0000);
605+
GET_ELEMENT(a, nd_int64, (int64_t)Dummy_0000) = PyInt64_to_Int64(Dummy_0001);
606+
}
607+
}
608+
else
609+
{
610+
PyErr_SetString(PyExc_TypeError, "Expected an argument of type tuple[int, ...] for argument a");
611+
return NULL;
612+
}
613+
Out_0001 = get_first_element_of_tuple(a);
614+
Out_0001_obj = Int64_to_PyLong(&Out_0001);
615+
return Out_0001_obj;
616+
}
617+
```
618+
546619
### Interfaces
547620

548621
Interfaces are functions which accept more than one type.

docs/type_annotations.md

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

4242
## Tuples
4343

44-
Currently tuples are supported locally in Pyccel but cannot be passed as arguments or returned. The implementation of the type annotations (as a first step to adding the missing support) is in progress. Currently homogeneous tuple type annotations are supported for local variables. See [Container types in Pyccel](./containers.md#tuples) for more information about tuple handling. 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 (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

pyccel/ast/bind_c.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
from pyccel.ast.core import FunctionDef, ClassDef
1414
from pyccel.ast.core import FunctionDefArgument, FunctionDefResult
1515
from pyccel.ast.datatypes import FixedSizeType, PythonNativeInt
16+
from pyccel.ast.numpytypes import NumpyNDArrayType
1617
from pyccel.ast.variable import Variable
18+
from pyccel.errors.errors import Errors
1719
from pyccel.utilities.metaclasses import Singleton
1820

21+
errors = Errors()
22+
1923
__all__ = (
2024
'BindCArrayVariable',
2125
'BindCClassDef',
@@ -203,9 +207,15 @@ def __init__(self, var, scope, original_arg_var, wrapping_bound_argument, **kwar
203207
shape = [scope.get_temporary_variable(PythonNativeInt(),
204208
name=f'{name}_shape_{i+1}')
205209
for i in range(self._rank)]
206-
strides = [scope.get_temporary_variable(PythonNativeInt(),
207-
name=f'{name}_stride_{i+1}')
208-
for i in range(self._rank)]
210+
if isinstance(original_arg_var.class_type, NumpyNDArrayType):
211+
strides = [scope.get_temporary_variable(PythonNativeInt(),
212+
name=f'{name}_stride_{i+1}')
213+
for i in range(self._rank)]
214+
else:
215+
if original_arg_var.rank > 1:
216+
errors.report("Wrapping multi-level tuples is not yet supported",
217+
severity='fatal', symbol=original_arg_var)
218+
strides = []
209219
self._shape = shape
210220
self._strides = strides
211221
self._original_arg_var = original_arg_var

pyccel/ast/cwrapper.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -971,21 +971,6 @@ def declarations(self):
971971
arguments = [FunctionDefArgument(Variable(StringType(), name='_'))],
972972
results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
973973

974-
# https://docs.python.org/3/c-api/list.html#c.PyList_GetItem
975-
PyList_GetItem = FunctionDef(name = 'PyList_GetItem',
976-
body = [],
977-
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
978-
FunctionDefArgument(Variable(CNativeInt(), name='i'))],
979-
results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
980-
981-
# https://docs.python.org/3/c-api/list.html#c.PyList_SetItem
982-
PyList_SetItem = FunctionDef(name = 'PyList_SetItem',
983-
body = [],
984-
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
985-
FunctionDefArgument(Variable(CNativeInt(), name='i')),
986-
FunctionDefArgument(Variable(PyccelPyObject(), name='new_item', memory_handling='alias'))],
987-
results = [])
988-
989974
#-------------------------------------------------------------------
990975

991976
#using the documentation of PyArg_ParseTuple() and Py_BuildValue https://docs.python.org/3/c-api/arg.html
@@ -1084,28 +1069,73 @@ def C_to_Python(c_object):
10841069
results = [FunctionDefResult(Variable(PythonNativeBool(), 'r'))],
10851070
body = [])
10861071

1072+
#-------------------------------------------------------------------
1073+
# List functions
1074+
#-------------------------------------------------------------------
1075+
1076+
# https://docs.python.org/3/c-api/list.html#c.PyList_New
10871077
PyList_New = FunctionDef(name = 'PyList_New',
10881078
arguments = [FunctionDefArgument(Variable(PythonNativeInt(), 'size'), value = LiteralInteger(0))],
10891079
results = [FunctionDefResult(Variable(PyccelPyObject(), 'r', memory_handling='alias'))],
10901080
body = [])
10911081

1082+
# https://docs.python.org/3/c-api/list.html#c.PyList_Append
10921083
PyList_Append = FunctionDef(name = 'PyList_Append',
10931084
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'list', memory_handling='alias')),
10941085
FunctionDefArgument(Variable(PyccelPyObject(), 'item', memory_handling='alias'))],
10951086
results = [FunctionDefResult(Variable(CNativeInt(), 'i'))],
10961087
body = [])
10971088

1089+
# https://docs.python.org/3/c-api/list.html#c.PyList_GetItem
10981090
PyList_GetItem = FunctionDef(name = 'PyList_GetItem',
10991091
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'list', memory_handling='alias')),
11001092
FunctionDefArgument(Variable(PythonNativeInt(), 'i'))],
11011093
results = [FunctionDefResult(Variable(PyccelPyObject(), 'item', memory_handling='alias'))],
11021094
body = [])
11031095

1096+
# https://docs.python.org/3/c-api/list.html#c.PyList_Size
11041097
PyList_Size = FunctionDef(name = 'PyList_Size',
11051098
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'list', memory_handling='alias'))],
11061099
results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
11071100
body = [])
11081101

1102+
# https://docs.python.org/3/c-api/list.html#c.PyList_SetItem
1103+
PyList_SetItem = FunctionDef(name = 'PyList_SetItem',
1104+
body = [],
1105+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
1106+
FunctionDefArgument(Variable(PythonNativeInt(), name='i')),
1107+
FunctionDefArgument(Variable(PyccelPyObject(), name='new_item', memory_handling='alias'))],
1108+
results = [])
1109+
1110+
#-------------------------------------------------------------------
1111+
# Tuple functions
1112+
#-------------------------------------------------------------------
1113+
1114+
# https://docs.python.org/3/c-api/tuple.html#c.PyTuple_New
1115+
PyTuple_New = FunctionDef(name = 'PyTuple_New',
1116+
arguments = [FunctionDefArgument(Variable(PythonNativeInt(), 'size'), value = LiteralInteger(0))],
1117+
results = [FunctionDefResult(Variable(PyccelPyObject(), 'tuple', memory_handling='alias'))],
1118+
body = [])
1119+
1120+
# https://docs.python.org/3/c-api/tuple.html#c.PyTuple_Check
1121+
PyTuple_Check = FunctionDef(name = 'PyTuple_Check',
1122+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'tuple', memory_handling='alias'))],
1123+
results = [FunctionDefResult(Variable(CNativeInt(), 'i'))],
1124+
body = [])
1125+
1126+
# https://docs.python.org/3/c-api/tuple.html#c.PyTuple_Size
1127+
PyTuple_Size = FunctionDef(name = 'PyTuple_Size',
1128+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'tuple', memory_handling='alias'))],
1129+
results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
1130+
body = [])
1131+
1132+
# https://docs.python.org/3/c-api/tuple.html#c.PyTuple_GetItem
1133+
PyTuple_GetItem = FunctionDef(name = 'PyTuple_GetItem',
1134+
body = [],
1135+
arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='tuple', memory_handling='alias')),
1136+
FunctionDefArgument(Variable(PythonNativeInt(), name='i'))],
1137+
results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
1138+
11091139

11101140
# Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
11111141
check_type_registry = {

pyccel/ast/datatypes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ def __init__(self, element_type):
768768
super().__init__()
769769

770770
def __str__(self):
771-
return f'{self._name}[{self._element_type}, ...]'
771+
return f'tuple[{self._element_type}, ...]'
772772

773773
class HomogeneousListType(HomogeneousContainerType, metaclass = ArgumentSingleton):
774774
"""

pyccel/ast/type_annotations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ class SyntacticTypeAnnotation(PyccelAstNode):
262262
The order requested in the type annotation.
263263
"""
264264
__slots__ = ('_dtype', '_order')
265-
_attribute_nodes = ()
265+
_attribute_nodes = ('_dtype',)
266266
def __init__(self, dtype, order = None):
267267
if not isinstance(dtype, (str, DottedName, IndexedElement)):
268268
raise ValueError("Syntactic datatypes should be strings")

pyccel/codegen/printing/ccode.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,7 +1652,7 @@ def _print_Allocate(self, expr):
16521652
variable = expr.variable
16531653
if isinstance(variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):
16541654
return ''
1655-
if variable.rank > 0:
1655+
if isinstance(variable.class_type, (NumpyNDArrayType, HomogeneousTupleType)):
16561656
#free the array if its already allocated and checking if its not null if the status is unknown
16571657
if (expr.status == 'unknown'):
16581658
shape_var = DottedVariable(VoidType(), 'shape', lhs = variable)
@@ -1679,7 +1679,10 @@ def _print_Allocate(self, expr):
16791679
var_code = self._print(ObjectAddress(variable))
16801680
if expr.like:
16811681
declaration_type = self.get_declare_type(expr.like)
1682-
return f'{var_code} = malloc(sizeof({declaration_type}));\n'
1682+
malloc_size = f'sizeof({declaration_type})'
1683+
if variable.rank:
1684+
malloc_size = ' * '.join([malloc_size, *(self._print(s) for s in expr.shape)])
1685+
return f'{var_code} = malloc({malloc_size});\n'
16831686
else:
16841687
raise NotImplementedError(f"Allocate not implemented for {variable}")
16851688
else:

pyccel/codegen/printing/cwrappercode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def is_c_pointer(self, a):
8686
--------
8787
CCodePrinter.is_c_pointer : The extended function.
8888
"""
89-
if isinstance(a.dtype, (WrapperCustomDataType, BindCPointer)):
89+
if isinstance(a.class_type, (WrapperCustomDataType, BindCPointer, CStackArray)):
9090
return True
9191
elif isinstance(a, (PyBuildValueNode, PyCapsule_New, PyCapsule_Import, PyModule_Create, LiteralString)):
9292
return True

0 commit comments

Comments
 (0)