Skip to content

Commit 1d78e0b

Browse files
authored
[DICT] Add Python support for dict() function (pyccel#1897)
Add Python support for `dict()` function as a copy operator or with key word arguments to build a dictionary. This fixes pyccel#1894 . The introduction of the `dict` function allows variable annotations to be supported. `__eq__` and `__hash__` functions are added to container types. This allows the initialisation of empty containers with syntax like `a : dict[str,int] = {}`.
1 parent dfe48c7 commit 1d78e0b

File tree

9 files changed

+192
-28
lines changed

9 files changed

+192
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file.
2424
- #1893 : Add Python support for set initialisation with `set()`.
2525
- #1877 : Add C Support for set method `pop()`.
2626
- #1895 : Add Python support for dict initialisation with `{}`.
27+
- #1895 : Add Python support for dict initialisation with `dict()`.
2728
- \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
2829
- \[DEVELOPER\] Added an improved traceback to the developer-mode errors for errors in function calls.
2930

docs/builtin-functions.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
1919
| `compile` | No |
2020
| **`complex`** | **Yes** |
2121
| `delattr` | No |
22-
| `dict` | No |
22+
| *`dict`* | Preliminary Python support |
2323
| `dir` | No |
2424
| `divmod` | No |
2525
| **`enumerate`** | as a loop iterable |
@@ -42,12 +42,12 @@ Python contains a limited number of builtin functions defined [here](https://doc
4242
| `issubclass` | No |
4343
| `iter` | No |
4444
| **`len`** | **Yes** |
45-
| *`list`* | implemented as a tuple |
45+
| *`list`* | Python-only |
4646
| `locals` | No |
4747
| **`map`** | as a loop iterable |
48-
| **`max`** | Fortran-only |
48+
| **`max`** | Full Fortran support and C support for 2 arguments |
4949
| `memoryview` | No |
50-
| **`min`** | Fortran-only |
50+
| **`min`** | Full Fortran support and C support for 2 arguments |
5151
| `next` | No |
5252
| `object` | No |
5353
| `oct` | No |
@@ -60,13 +60,13 @@ Python contains a limited number of builtin functions defined [here](https://doc
6060
| `repr` | No |
6161
| `reversed` | No |
6262
| `round` | No |
63-
| `set` | No |
63+
| *`set`* | Python-only |
6464
| `setattr` | No |
6565
| `slice` | No |
6666
| `sorted` | No |
6767
| `staticmethod` | No |
6868
| `str` | No |
69-
| **`sum`** | Fortran-only |
69+
| **`sum`** | **Yes** |
7070
| `super` | No |
7171
| **`tuple`** | **Yes** |
7272
| **`type`** | **Yes** |

docs/type_annotations.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ To declare a homogeneous tuple the syntax is as follows:
4848
a : tuple[int,...] = (1,2,3,4)
4949
```
5050

51+
## Dictionaries
52+
53+
Dictionaries are in the process of being added to Pyccel. They cannot yet be used effectively however the type annotations are already supported.
54+
Homogeneous dictionaries can be declared in Pyccel using the following syntax:
55+
```python
56+
a : dict[int,float] = {1: 1.0, 2: 2.0}
57+
b : dict[int,bool] = {1: False, 4: True}
58+
c : dict[int,complex] = {}
59+
```
60+
So far strings are supported as keys however as Pyccel is still missing support for non-literal strings it remains to be seen how such cases will be handled in low-level languages.
61+
5162
## Handling multiple types
5263

5364
The basic type annotations indicate only one type however it is common to need a function to be able to handle multiple types, e.g. integers and floats. In this case it is possible to provide a union type.

pyccel/ast/builtins.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
'PythonComplexProperty',
4040
'PythonConjugate',
4141
'PythonDict',
42+
'PythonDictFunction',
4243
'PythonEnumerate',
4344
'PythonFloat',
4445
'PythonImag',
@@ -882,6 +883,56 @@ def values(self):
882883
"""
883884
return self._values
884885

886+
#==============================================================================
887+
class PythonDictFunction(PyccelFunction):
888+
"""
889+
Class representing a call to the `dict` function.
890+
891+
Class representing a call to the `dict` function. This is
892+
different to the `{}` syntax as it is either a cast function or it
893+
uses arguments to create the dictionary. In the case of a cast function
894+
an instance of PythonDict is returned to express this concept. In the
895+
case of a copy this class stores the description of the copy operator.
896+
897+
Parameters
898+
----------
899+
*args : TypedAstNode
900+
The arguments passed to the function call. If args are provided
901+
then only one argument should be provided. This object is copied
902+
unless it is a temporary PythonDict in which case it is returned
903+
directly.
904+
**kwargs : dict[TypedAstNode]
905+
The keyword arguments passed to the function call. If kwargs are
906+
provided then no args should be provided and a PythonDict object
907+
will be created.
908+
"""
909+
__slots__ = ('_shape', '_class_type')
910+
name = 'dict'
911+
912+
def __new__(cls, *args, **kwargs):
913+
if len(args) == 0:
914+
keys = [LiteralString(k) for k in kwargs]
915+
values = list(kwargs.values())
916+
return PythonDict(keys, values)
917+
elif len(args) != 1:
918+
raise NotImplementedError("Unrecognised dict calling convention")
919+
else:
920+
return super().__new__(cls)
921+
922+
def __init__(self, copied_obj):
923+
self._class_type = copied_obj.class_type
924+
self._shape = copied_obj.shape
925+
super().__init__(copied_obj)
926+
927+
@property
928+
def copied_obj(self):
929+
"""
930+
The object being copied.
931+
932+
The object being copied to create a new dict instance.
933+
"""
934+
return self._args[0]
935+
885936
#==============================================================================
886937
class PythonMap(PyccelFunction):
887938
"""
@@ -1325,22 +1376,22 @@ def print_string(self):
13251376
builtin_functions_dict = {
13261377
'abs' : PythonAbs,
13271378
'bool' : PythonBool,
1328-
'range' : PythonRange,
1329-
'zip' : PythonZip,
1379+
'complex' : PythonComplex,
1380+
'dict' : PythonDictFunction,
13301381
'enumerate': PythonEnumerate,
1331-
'int' : PythonInt,
13321382
'float' : PythonFloat,
1333-
'complex' : PythonComplex,
1334-
'bool' : PythonBool,
1335-
'sum' : PythonSum,
1383+
'int' : PythonInt,
13361384
'len' : PythonLen,
13371385
'list' : PythonList,
1386+
'map' : PythonMap,
13381387
'max' : PythonMax,
13391388
'min' : PythonMin,
13401389
'not' : PyccelNot,
1341-
'map' : PythonMap,
1390+
'range' : PythonRange,
1391+
'set' : PythonSetFunction,
13421392
'str' : LiteralString,
1343-
'type' : PythonType,
1393+
'sum' : PythonSum,
13441394
'tuple' : PythonTupleFunction,
1345-
'set' : PythonSetFunction
1395+
'type' : PythonType,
1396+
'zip' : PythonZip,
13461397
}

pyccel/ast/datatypes.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,12 @@ def order(self):
656656
"""
657657
return self._order # pylint: disable=no-member
658658

659+
def __eq__(self, other):
660+
return isinstance(other, self.__class__) and self.element_type == other.element_type
661+
662+
def __hash__(self):
663+
return hash((self.__class__, self.element_type))
664+
659665
class StringType(HomogeneousContainerType, metaclass = Singleton):
660666
"""
661667
Class representing Python's native string type.
@@ -715,6 +721,12 @@ def rank(self):
715721
"""
716722
return 1
717723

724+
def __eq__(self, other):
725+
return isinstance(other, self.__class__)
726+
727+
def __hash__(self):
728+
return hash(self.__class__)
729+
718730
class HomogeneousTupleType(HomogeneousContainerType, TupleType, metaclass = ArgumentSingleton):
719731
"""
720732
Class representing the homogeneous tuple type.
@@ -1069,6 +1081,13 @@ def order(self):
10691081
"""
10701082
return None
10711083

1084+
def __eq__(self, other):
1085+
return isinstance(other, self.__class__) and self.key_type == other.key_type \
1086+
and self.value_type == other.value_type
1087+
1088+
def __hash__(self):
1089+
return hash((self.__class__, self._key_type, self._value_type))
1090+
10721091
#==============================================================================
10731092

10741093
def DataTypeFactory(name, argnames = (), *, BaseClass=CustomDataType):

pyccel/ast/numpytypes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,13 @@ def __repr__(self):
425425
order_str = f'(order={self._order})' if self._order else ''
426426
return f'{self.element_type}[{dims}]{order_str}'
427427

428+
def __hash__(self):
429+
return hash((self.element_type, self.rank, self.order))
430+
431+
def __eq__(self, other):
432+
return isinstance(other, NumpyNDArrayType) and self.element_type == other.element_type \
433+
and self.rank == other.rank and self.order == other.order
434+
428435
#==============================================================================
429436

430437
numpy_precision_map = {

pyccel/parser/semantic.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from pyccel.ast.basic import PyccelAstNode, TypedAstNode, ScopedAstNode
2323

2424
from pyccel.ast.builtins import PythonPrint, PythonTupleFunction, PythonSetFunction
25-
from pyccel.ast.builtins import PythonComplex, PythonDict
25+
from pyccel.ast.builtins import PythonComplex, PythonDict, PythonDictFunction
2626
from pyccel.ast.builtins import builtin_functions_dict, PythonImag, PythonReal
2727
from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet
2828
from pyccel.ast.builtins import (PythonRange, PythonZip, PythonEnumerate,
@@ -65,7 +65,7 @@
6565
from pyccel.ast.datatypes import PythonNativeBool, PythonNativeInt, PythonNativeFloat
6666
from pyccel.ast.datatypes import DataTypeFactory, PrimitiveFloatingPointType
6767
from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType, HomogeneousSetType, HomogeneousListType
68-
from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType
68+
from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType, DictType
6969

7070
from pyccel.ast.functionalexpr import FunctionalSum, FunctionalMax, FunctionalMin, GeneratorComprehension, FunctionalFor
7171

@@ -149,7 +149,7 @@
149149
type_container = {
150150
PythonTupleFunction : HomogeneousTupleType,
151151
PythonList : HomogeneousListType,
152-
PythonSetFunction : HomogeneousSetType
152+
PythonSetFunction : HomogeneousSetType,
153153
}
154154

155155
#==============================================================================
@@ -1640,7 +1640,7 @@ def _ensure_inferred_type_matches_existing(self, class_type, d_var, var, is_auga
16401640
if raise_error:
16411641
name = var.name
16421642
rhs_str = str(rhs)
1643-
errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(repr(var.class_type), repr(class_type)),
1643+
errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(var.class_type, class_type),
16441644
symbol=f'{name}={rhs_str}',
16451645
bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
16461646
severity='error')
@@ -1880,6 +1880,30 @@ def _find_superclasses(self, expr):
18801880

18811881
return list(parent.values())
18821882

1883+
def _convert_syntactic_object_to_type_annotation(self, syntactic_annotation):
1884+
"""
1885+
Convert an arbitrary syntactic object to a type annotation.
1886+
1887+
Convert an arbitrary syntactic object to a type annotation. This means that
1888+
the syntactic object is wrapped in a SyntacticTypeAnnotation (if necessary).
1889+
This ensures that a type annotation is obtained instead of e.g. a function.
1890+
1891+
Parameters
1892+
----------
1893+
syntactic_annotation : PyccelAstNode
1894+
A syntactic object that needs to be visited as a type annotation.
1895+
1896+
Returns
1897+
-------
1898+
SyntacticTypeAnnotation
1899+
A syntactic object that will be recognised as a type annotation.
1900+
"""
1901+
if not isinstance(syntactic_annotation, SyntacticTypeAnnotation):
1902+
pyccel_stage.set_stage('syntactic')
1903+
syntactic_annotation = SyntacticTypeAnnotation(dtype=syntactic_annotation)
1904+
pyccel_stage.set_stage('semantic')
1905+
return syntactic_annotation
1906+
18831907
def _get_indexed_type(self, base, args, expr):
18841908
"""
18851909
Extract a type annotation from an IndexedElement.
@@ -1933,21 +1957,24 @@ def _get_indexed_type(self, base, args, expr):
19331957
else:
19341958
raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
19351959
severity='fatal', symbol=expr)
1936-
if len(args) == 2 and args[1] is LiteralEllipsis() or len(args) == 1:
1937-
syntactic_annotation = args[0]
1938-
if not isinstance(syntactic_annotation, SyntacticTypeAnnotation):
1939-
pyccel_stage.set_stage('syntactic')
1940-
syntactic_annotation = SyntacticTypeAnnotation(dtype=syntactic_annotation)
1941-
pyccel_stage.set_stage('semantic')
1960+
if (len(args) == 2 and args[1] is LiteralEllipsis()) or len(args) == 1:
1961+
syntactic_annotation = self._convert_syntactic_object_to_type_annotation(args[0])
19421962
internal_datatypes = self._visit(syntactic_annotation)
1943-
type_annotations = []
19441963
if dtype_cls in type_container :
19451964
class_type = type_container[dtype_cls]
19461965
else:
19471966
raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
19481967
severity='fatal', symbol=expr)
1949-
for u in internal_datatypes.type_list:
1950-
type_annotations.append(VariableTypeAnnotation(class_type(u.class_type), u.is_const))
1968+
type_annotations = [VariableTypeAnnotation(class_type(u.class_type), u.is_const)
1969+
for u in internal_datatypes.type_list]
1970+
return UnionTypeAnnotation(*type_annotations)
1971+
elif len(args) == 2 and dtype_cls is PythonDictFunction:
1972+
syntactic_key_annotation = self._convert_syntactic_object_to_type_annotation(args[0])
1973+
syntactic_val_annotation = self._convert_syntactic_object_to_type_annotation(args[1])
1974+
key_types = self._visit(syntactic_key_annotation)
1975+
val_types = self._visit(syntactic_val_annotation)
1976+
type_annotations = [VariableTypeAnnotation(DictType(k.class_type, v.class_type)) \
1977+
for k,v in zip(key_types.type_list, val_types.type_list)]
19511978
return UnionTypeAnnotation(*type_annotations)
19521979
else:
19531980
raise errors.report("Cannot handle non-homogenous type index\n"+PYCCEL_RESTRICTION_TODO,

tests/epyccel/test_epyccel_dicts.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,36 @@ def dict_str_keys():
3535
python_result = dict_str_keys()
3636
assert isinstance(python_result, type(pyccel_result))
3737
assert python_result == pyccel_result
38+
39+
def test_dict_empty_init(language):
40+
def dict_empty_init():
41+
a : 'dict[int, float]' = {}
42+
return a
43+
epyc_dict_empty_init = epyccel(dict_empty_init, language = language)
44+
pyccel_result = epyc_dict_empty_init()
45+
python_result = dict_empty_init()
46+
assert isinstance(python_result, type(pyccel_result))
47+
assert python_result == pyccel_result
48+
49+
def test_dict_copy(language):
50+
def dict_copy():
51+
a = {1:1.0,2:2.0}
52+
b = dict(a)
53+
return b
54+
55+
epyc_dict_copy = epyccel(dict_copy, language = language)
56+
pyccel_result = epyc_dict_copy()
57+
python_result = dict_copy()
58+
assert isinstance(python_result, type(pyccel_result))
59+
assert python_result == pyccel_result
60+
61+
def test_dict_kwarg_init(language):
62+
def kwarg_init():
63+
b = dict(a=1, b=2) #pylint: disable=use-dict-literal
64+
return b
65+
66+
epyc_kwarg_init = epyccel(kwarg_init, language = language)
67+
pyccel_result = epyc_kwarg_init()
68+
python_result = kwarg_init()
69+
assert isinstance(python_result, type(pyccel_result))
70+
assert python_result == pyccel_result

tests/epyccel/test_epyccel_variable_annotations.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,18 @@ def homogeneous_list_annotation():
271271
epyc_homogeneous_list_annotation = epyccel(homogeneous_list_annotation, language=stc_language)
272272
assert epyc_homogeneous_list_annotation() == homogeneous_list_annotation()
273273
assert isinstance(epyc_homogeneous_list_annotation(), type(homogeneous_list_annotation()))
274+
275+
@pytest.mark.parametrize('lang',
276+
[pytest.param("python", marks = pytest.mark.python)])
277+
def test_dict_empty_init(lang):
278+
def dict_empty_init():
279+
# Not valid in Python 3.8
280+
a : dict[int, float] #pylint: disable=unsubscriptable-object
281+
a = {1:1.0, 2:2.0}
282+
return a
283+
284+
epyc_dict_empty_init = epyccel(dict_empty_init, language = lang)
285+
pyccel_result = epyc_dict_empty_init()
286+
python_result = dict_empty_init()
287+
assert isinstance(python_result, type(pyccel_result))
288+
assert python_result == pyccel_result

0 commit comments

Comments
 (0)