Skip to content

Commit 9b0545f

Browse files
authored
Add support for inhomogeneous tuple annotations (pyccel#2066)
Add support for inhomogeneous tuple annotations
1 parent ba2a97a commit 9b0545f

File tree

5 files changed

+132
-12
lines changed

5 files changed

+132
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ All notable changes to this project will be documented in this file.
5454
- #2016 : Add support for translating arithmetic magic methods (methods cannot yet be used from Python).
5555
- #1980 : Extend The C support for min and max to more than two variables
5656
- #2081 : Add support for multi operator expressions
57+
- Add support for inhomogeneous tuple annotations.
5758
- \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
5859
- \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
5960
- \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.

docs/type_annotations.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,29 @@ 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 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.
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.
45+
46+
Tuples can be homogeneous or inhomogeneous. A homogeneous tuple is a tuple whose elements all have the same type and shape. Pyccel translates homogeneous tuples in a similar way to 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. An inhomogeneous tuple describes all other types, but comes with extra restrictions. An inhomogeneous tuple is translated to multiple objects in the target language so it can only be used if the element can be identified during the translation. This means that expressions such as `a[i]` are not possible for inhomogeneous tuples while `a[0]` is valid.
47+
48+
Homogeneous tuple type annotations are supported for local variables and function arguments (if the tuples contain scalar objects).
4549

4650
To declare a homogeneous tuple the syntax is as follows:
4751
```python
4852
a : tuple[int,...] = (1,2,3,4)
4953
```
5054

55+
Inhomogeneous tuple type annotations are supported for local variables.
56+
57+
To declare an inhomogeneous tuple the syntax is as follows:
58+
```python
59+
a : tuple[int,bool] = (1,False)
60+
```
61+
62+
It is of course possible to create an inhomogeneous tuple in place of a homogeneous tuple to benefit from code optimisations that can arise from using multiple scalars in place of an array object. This will however imply the same restrictions as any other inhomogeneous tuple. E.g:
63+
```python
64+
a : tuple[int, int] = (1,2)
65+
```
66+
5167
## Lists
5268

5369
Lists are in the process of being added to Pyccel. Homogeneous lists can be declared in Pyccel using the following syntax:

pyccel/codegen/printing/ccode.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,10 +1753,10 @@ def _print_Allocate(self, expr):
17531753
if isinstance(variable.class_type, NumpyNDArrayType):
17541754
#set dtype to the C struct types
17551755
dtype = self.find_in_ndarray_type_registry(variable.dtype)
1756-
elif isinstance(variable.class_type, HomogeneousContainerType):
1756+
elif isinstance(variable.class_type, HomogeneousContainerType) and isinstance(variable.dtype, FixedSizeNumericType):
17571757
dtype = self.find_in_ndarray_type_registry(numpy_precision_map[(variable.dtype.primitive_type, variable.dtype.precision)])
17581758
else:
1759-
raise NotImplementedError(f"Don't know how to index {variable.class_type} type")
1759+
raise NotImplementedError(f"The allocation of the type {variable.class_type} is not yet supported.")
17601760
shape_dtype = self.get_c_type(NumpyInt64Type())
17611761
shape_Assign = "("+ shape_dtype +"[]){" + shape + "}"
17621762
is_view = 'false' if variable.on_heap else 'true'
@@ -1772,9 +1772,9 @@ def _print_Allocate(self, expr):
17721772
malloc_size = ' * '.join([malloc_size, *(self._print(s) for s in expr.shape)])
17731773
return f'{var_code} = malloc({malloc_size});\n'
17741774
else:
1775-
raise NotImplementedError(f"Allocate not implemented for {variable}")
1775+
raise NotImplementedError(f"Allocate not implemented for {variable.class_type}")
17761776
else:
1777-
raise NotImplementedError(f"Allocate not implemented for {variable}")
1777+
raise NotImplementedError(f"Allocate not implemented for {variable.class_type}")
17781778

17791779
def _print_Deallocate(self, expr):
17801780
if isinstance(expr.variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):

pyccel/parser/semantic.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
from pyccel.ast.headers import FunctionHeader, MethodHeader, Header
7373
from pyccel.ast.headers import MacroFunction, MacroVariable
7474

75-
from pyccel.ast.internals import PyccelFunction, Slice, PyccelSymbol
75+
from pyccel.ast.internals import PyccelFunction, Slice, PyccelSymbol, PyccelArrayShapeElement
7676
from pyccel.ast.internals import Iterable
7777
from pyccel.ast.itertoolsext import Product
7878

@@ -1565,7 +1565,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign =
15651565
for a in args:
15661566
if isinstance(a.class_type, InhomogeneousTupleType):
15671567
new_args.extend(self.scope.collect_tuple_element(v) for v in a if v.rank>0)
1568-
else:
1568+
elif a.rank > 0:
15691569
new_expressions.append(Allocate(a,
15701570
shape=a.alloc_shape, status=status))
15711571
args = new_args
@@ -1709,6 +1709,26 @@ def _ensure_inferred_type_matches_existing(self, class_type, d_var, var, is_auga
17091709
tmp_result = PyccelAdd(var, rhs)
17101710
result_type = tmp_result.class_type
17111711
raise_error = var.class_type != result_type
1712+
elif isinstance(var.class_type, InhomogeneousTupleType) and \
1713+
isinstance(class_type, HomogeneousTupleType):
1714+
if d_var['shape'][0] == var.shape[0]:
1715+
rhs_elem = self.scope.collect_tuple_element(var[0])
1716+
self._ensure_inferred_type_matches_existing(class_type.element_type,
1717+
self._infer_type(rhs_elem), rhs_elem, is_augassign, new_expressions, rhs)
1718+
raise_error = False
1719+
else:
1720+
raise_error = True
1721+
elif isinstance(var.class_type, InhomogeneousTupleType) and \
1722+
isinstance(class_type, InhomogeneousTupleType):
1723+
for i, element_type in enumerate(class_type):
1724+
rhs_elem = self.scope.collect_tuple_element(var[i])
1725+
self._ensure_inferred_type_matches_existing(element_type,
1726+
self._infer_type(rhs_elem), rhs_elem, is_augassign, new_expressions, rhs)
1727+
raise_error = False
1728+
elif isinstance(var.class_type, HomogeneousTupleType) and \
1729+
isinstance(class_type, InhomogeneousTupleType):
1730+
# TODO: Remove isinstance(rhs, Variable) condition when tuples are saved like lists
1731+
raise_error = any(a != var.class_type.element_type for a in class_type) or not isinstance(rhs, Variable)
17121732
else:
17131733
raise_error = True
17141734

@@ -2027,7 +2047,8 @@ def _get_indexed_type(self, base, args, expr):
20272047
else:
20282048
raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
20292049
severity='fatal', symbol=expr)
2030-
if (len(args) == 2 and args[1] is LiteralEllipsis()) or len(args) == 1:
2050+
if (len(args) == 2 and args[1] is LiteralEllipsis()) or \
2051+
(len(args) == 1 and dtype_cls is not PythonTupleFunction):
20312052
syntactic_annotation = self._convert_syntactic_object_to_type_annotation(args[0])
20322053
internal_datatypes = self._visit(syntactic_annotation)
20332054
if dtype_cls in type_container:
@@ -2046,6 +2067,13 @@ def _get_indexed_type(self, base, args, expr):
20462067
type_annotations = [VariableTypeAnnotation(DictType(k.class_type, v.class_type)) \
20472068
for k,v in zip(key_types.type_list, val_types.type_list)]
20482069
return UnionTypeAnnotation(*type_annotations)
2070+
elif dtype_cls is PythonTupleFunction:
2071+
syntactic_annotations = [self._convert_syntactic_object_to_type_annotation(a) for a in args]
2072+
types = [self._visit(a).type_list for a in syntactic_annotations]
2073+
internal_datatypes = list(product(*types))
2074+
type_annotations = [VariableTypeAnnotation(InhomogeneousTupleType(*[ui.class_type for ui in u]), True)
2075+
for u in internal_datatypes]
2076+
return UnionTypeAnnotation(*type_annotations)
20492077
else:
20502078
raise errors.report("Cannot handle non-homogenous type index\n"+PYCCEL_RESTRICTION_TODO,
20512079
severity='fatal', symbol=expr)
@@ -2808,12 +2836,21 @@ def _visit_AnnotatedPyccelSymbol(self, expr):
28082836
elif isinstance(t, VariableTypeAnnotation):
28092837
class_type = t.class_type
28102838
cls_base = self.scope.find(str(class_type), 'classes') or get_cls_base(class_type)
2839+
shape = len(class_type) if isinstance(class_type, InhomogeneousTupleType) else None
28112840
v = var_class(class_type, name, cls_base = cls_base,
2812-
shape = None,
2841+
shape = shape,
28132842
is_const = t.is_const, is_optional = False,
28142843
memory_handling = array_memory_handling if class_type.rank > 0 else 'stack',
28152844
**kwargs)
28162845
possible_args.append(v)
2846+
if isinstance(class_type, InhomogeneousTupleType):
2847+
for i, t in enumerate(class_type):
2848+
pyccel_stage.set_stage('syntactic')
2849+
syntactic_elem = AnnotatedPyccelSymbol(self.scope.get_new_name( f'{name}_{i}'),
2850+
annotation = UnionTypeAnnotation(VariableTypeAnnotation(t)))
2851+
pyccel_stage.set_stage('semantic')
2852+
elem = self._visit(syntactic_elem)
2853+
self.scope.insert_symbolic_alias(IndexedElement(v, i), elem[0])
28172854
else:
28182855
errors.report(PYCCEL_RESTRICTION_TODO + '\nUnrecoginsed type annotation',
28192856
severity='fatal', symbol=expr)
@@ -3471,7 +3508,8 @@ def _visit_Assign(self, expr):
34713508
if isinstance(l.class_type, InhomogeneousTupleType) \
34723509
and not isinstance(r, (FunctionCall, PyccelFunction)):
34733510
new_lhs.extend(self.scope.collect_tuple_element(v) for v in l)
3474-
new_rhs.extend(self.scope.collect_tuple_element(v) for v in r)
3511+
new_rhs.extend(self.scope.collect_tuple_element(r[i]) \
3512+
for i in range(len(l.class_type)))
34753513
# Repeat step to handle tuples of tuples of etc.
34763514
unravelling = True
34773515
elif isinstance(l, Variable) and isinstance(l.class_type, InhomogeneousTupleType):
@@ -3509,7 +3547,8 @@ def _visit_Assign(self, expr):
35093547
bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
35103548
symbol=li, severity='error')
35113549
else:
3512-
if getattr(l, 'is_const', False) and (not isinstance(expr.lhs, AnnotatedPyccelSymbol) or len(l.get_all_user_nodes()) > 0):
3550+
if getattr(l, 'is_const', False) and (not isinstance(expr.lhs, AnnotatedPyccelSymbol) or \
3551+
any(not isinstance(u, (Allocate, PyccelArrayShapeElement)) for u in l.get_all_user_nodes())):
35133552
# If constant and not the initialising declaration of a constant variable
35143553
errors.report("Cannot modify 'const' variable",
35153554
bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),

tests/epyccel/test_epyccel_variable_annotations.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,13 +324,77 @@ def dict_empty_init():
324324
def test_dict_complex_float(language):
325325
def dict_int_float():
326326
# Not valid in Python 3.8
327-
a : dict[complex, float] #pylint: disable=unsubscriptable-object,unused-variable
327+
a : dict[complex, float] #pylint: disable=unsubscriptable-object
328328
a = {1j:1.0, -1j:2.0}
329329
return len(a)
330330

331331
epyc_dict_int_float = epyccel(dict_int_float, language = language)
332332
assert epyc_dict_int_float() == dict_int_float()
333333

334+
def test_inhomogeneous_tuple_annotation_1(language):
335+
def inhomogeneous_tuple_annotation():
336+
a : tuple[int, bool] = (1, True) #pylint: disable=unsubscriptable-object
337+
return a[0], a[1]
338+
339+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
340+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
341+
342+
def test_inhomogeneous_tuple_annotation_2(language):
343+
def inhomogeneous_tuple_annotation():
344+
a : tuple[int] = (1,) #pylint: disable=unsubscriptable-object
345+
return a[0]
346+
347+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
348+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
349+
350+
def test_inhomogeneous_tuple_annotation_3(language):
351+
def inhomogeneous_tuple_annotation():
352+
a : tuple[int,int,int] = (1,2,3) #pylint: disable=unsubscriptable-object
353+
return a[0], a[1], a[2]
354+
355+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
356+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
357+
358+
def test_inhomogeneous_tuple_annotation_4(language):
359+
def inhomogeneous_tuple_annotation():
360+
a : tuple[tuple[float,bool],tuple[int,complex]] = ((1.0, False), (1,2+3j)) #pylint: disable=unsubscriptable-object
361+
return a[0][0], a[0][1], a[1][0], a[1][1]
362+
363+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
364+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
365+
366+
def test_inhomogeneous_tuple_annotation_5(language):
367+
def inhomogeneous_tuple_annotation():
368+
a : tuple[tuple[int, float]] = ((1,0.2),) #pylint: disable=unsubscriptable-object
369+
return a[0][0], a[0][1]
370+
371+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
372+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
373+
374+
def test_inhomogeneous_tuple_annotation_6(language):
375+
def inhomogeneous_tuple_annotation():
376+
a : tuple[tuple[tuple[int, float]]] = (((1,0.2),),) #pylint: disable=unsubscriptable-object
377+
return a[0][0][0], a[0][0][1]
378+
379+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
380+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
381+
382+
def test_inhomogeneous_tuple_annotation_7(language):
383+
def inhomogeneous_tuple_annotation():
384+
a : tuple[tuple[tuple[int, float]], int] = (((1,0.2),),1) #pylint: disable=unsubscriptable-object
385+
return a[0][0][0], a[0][0][1], a[1]
386+
387+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
388+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
389+
390+
def test_inhomogeneous_tuple_annotation_8(language):
391+
def inhomogeneous_tuple_annotation():
392+
a : tuple[tuple[tuple[tuple[int, float]], int]] = ((((1,0.2),),1),) #pylint: disable=unsubscriptable-object
393+
return a[0][0][0][0], a[0][0][0][1], a[0][1]
394+
395+
epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
396+
assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
397+
334398
@pytest.mark.parametrize( 'language', (
335399
pytest.param("fortran", marks = pytest.mark.fortran),
336400
pytest.param("c", marks = [

0 commit comments

Comments
 (0)