Repository: pyccel/pyccel (python)

Issue:
Add Python support for dict() function
### Describe the feature
I would like to be able to use the `dict()` function. The `dict()` function is different to the `{}` function as it can behave as a copy function and/or create a dictionary using keyword arguments.

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
if __name__ == '__main__':
    a = dict({1:1.0,2:2.0})
    b = dict(a=1, b=2)
```

### Proposed Solution
This feature should be implemented similarly to what was done for tuples. The equivalent issues for tuples was resolved in #1589.
The pertinent code is:
https://github.com/pyccel/pyccel/blob/d64be7ff7072faa07ce19c9e70fa02ed45a25c9c/pyccel/ast/builtins.py#L596-L620


### Additional context
It is important to handle the `dict` function so that this function can be used for type annotations. If the `{,}` function is used bugs such as #1892 can arise.

Pull request description:
[DICT] Add Python support for dict() function
Add Python support for `dict()` function as a copy operator or with key word arguments to build a dictionary. This fixes #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] = {}`.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 973379a5..4ee292e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ All notable changes to this project will be documented in this file.
 -   #1893 : Add Python support for set initialisation with `set()`.
 -   #1877 : Add C Support for set method `pop()`.
 -   #1895 : Add Python support for dict initialisation with `{}`.
+-   #1895 : Add Python support for dict initialisation with `dict()`.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[DEVELOPER\] Added an improved traceback to the developer-mode errors for errors in function calls.
 
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index 6e22ddd3..da29bd09 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -19,7 +19,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `compile` | No |
 | **`complex`** | **Yes** |
 | `delattr` | No |
-| `dict` | No |
+| *`dict`* | Preliminary Python support |
 | `dir` | No |
 | `divmod` | No |
 | **`enumerate`** | as a loop iterable |
@@ -42,12 +42,12 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `issubclass` | No |
 | `iter` | No |
 | **`len`** | **Yes** |
-| *`list`* | implemented as a tuple |
+| *`list`* | Python-only |
 | `locals` | No |
 | **`map`** | as a loop iterable |
-| **`max`** | Fortran-only |
+| **`max`** | Full Fortran support and C support for 2 arguments |
 | `memoryview` | No |
-| **`min`** | Fortran-only |
+| **`min`** | Full Fortran support and C support for 2 arguments |
 | `next` | No |
 | `object` | No |
 | `oct` | No |
@@ -60,13 +60,13 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `repr` | No |
 | `reversed` | No |
 | `round` | No |
-| `set` | No |
+| *`set`* | Python-only |
 | `setattr` | No  |
 | `slice` | No |
 | `sorted` | No |
 | `staticmethod` | No |
 | `str` | No |
-| **`sum`** | Fortran-only |
+| **`sum`** | **Yes** |
 | `super` | No |
 | **`tuple`** | **Yes** |
 | **`type`** | **Yes** |
diff --git a/docs/type_annotations.md b/docs/type_annotations.md
index c37b3f22..33f6f30b 100644
--- a/docs/type_annotations.md
+++ b/docs/type_annotations.md
@@ -48,6 +48,17 @@ To declare a homogeneous tuple the syntax is as follows:
 a : tuple[int,...] = (1,2,3,4)
 ```
 
+## Dictionaries
+
+Dictionaries are in the process of being added to Pyccel. They cannot yet be used effectively however the type annotations are already supported.
+Homogeneous dictionaries can be declared in Pyccel using the following syntax:
+```python
+a : dict[int,float] = {1: 1.0, 2: 2.0}
+b : dict[int,bool] = {1: False, 4: True}
+c : dict[int,complex] = {}
+```
+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.
+
 ## Handling multiple types
 
 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.
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index e4765c6d..5bb3f66b 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -39,6 +39,7 @@ __all__ = (
     'PythonComplexProperty',
     'PythonConjugate',
     'PythonDict',
+    'PythonDictFunction',
     'PythonEnumerate',
     'PythonFloat',
     'PythonImag',
@@ -882,6 +883,56 @@ class PythonDict(PyccelFunction):
         """
         return self._values
 
+#==============================================================================
+class PythonDictFunction(PyccelFunction):
+    """
+    Class representing a call to the `dict` function.
+
+    Class representing a call to the `dict` function. This is
+    different to the `{}` syntax as it is either a cast function or it
+    uses arguments to create the dictionary. In the case of a cast function
+    an instance of PythonDict is returned to express this concept. In the
+    case of a copy this class stores the description of the copy operator.
+
+    Parameters
+    ----------
+    *args : TypedAstNode
+        The arguments passed to the function call. If args are provided
+        then only one argument should be provided. This object is copied
+        unless it is a temporary PythonDict in which case it is returned
+        directly.
+    **kwargs : dict[TypedAstNode]
+        The keyword arguments passed to the function call. If kwargs are
+        provided then no args should be provided and a PythonDict object
+        will be created.
+    """
+    __slots__ = ('_shape', '_class_type')
+    name = 'dict'
+
+    def __new__(cls, *args, **kwargs):
+        if len(args) == 0:
+            keys = [LiteralString(k) for k in kwargs]
+            values = list(kwargs.values())
+            return PythonDict(keys, values)
+        elif len(args) != 1:
+            raise NotImplementedError("Unrecognised dict calling convention")
+        else:
+            return super().__new__(cls)
+
+    def __init__(self, copied_obj):
+        self._class_type = copied_obj.class_type
+        self._shape = copied_obj.shape
+        super().__init__(copied_obj)
+
+    @property
+    def copied_obj(self):
+        """
+        The object being copied.
+
+        The object being copied to create a new dict instance.
+        """
+        return self._args[0]
+
 #==============================================================================
 class PythonMap(PyccelFunction):
     """
@@ -1325,22 +1376,22 @@ DtypePrecisionToCastFunction = {
 builtin_functions_dict = {
     'abs'      : PythonAbs,
     'bool'     : PythonBool,
-    'range'    : PythonRange,
-    'zip'      : PythonZip,
+    'complex'  : PythonComplex,
+    'dict'     : PythonDictFunction,
     'enumerate': PythonEnumerate,
-    'int'      : PythonInt,
     'float'    : PythonFloat,
-    'complex'  : PythonComplex,
-    'bool'     : PythonBool,
-    'sum'      : PythonSum,
+    'int'      : PythonInt,
     'len'      : PythonLen,
     'list'     : PythonList,
+    'map'      : PythonMap,
     'max'      : PythonMax,
     'min'      : PythonMin,
     'not'      : PyccelNot,
-    'map'      : PythonMap,
+    'range'    : PythonRange,
+    'set'      : PythonSetFunction,
     'str'      : LiteralString,
-    'type'     : PythonType,
+    'sum'      : PythonSum,
     'tuple'    : PythonTupleFunction,
-    'set'      : PythonSetFunction
+    'type'     : PythonType,
+    'zip'      : PythonZip,
 }
diff --git a/pyccel/ast/datatypes.py b/pyccel/ast/datatypes.py
index a4c5a44a..c6b4af3c 100644
--- a/pyccel/ast/datatypes.py
+++ b/pyccel/ast/datatypes.py
@@ -656,6 +656,12 @@ class HomogeneousContainerType(ContainerType):
         """
         return self._order # pylint: disable=no-member
 
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.element_type == other.element_type
+
+    def __hash__(self):
+        return hash((self.__class__, self.element_type))
+
 class StringType(HomogeneousContainerType, metaclass = Singleton):
     """
     Class representing Python's native string type.
@@ -715,6 +721,12 @@ class StringType(HomogeneousContainerType, metaclass = Singleton):
         """
         return 1
 
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+    def __hash__(self):
+        return hash(self.__class__)
+
 class HomogeneousTupleType(HomogeneousContainerType, TupleType, metaclass = ArgumentSingleton):
     """
     Class representing the homogeneous tuple type.
@@ -1069,6 +1081,13 @@ class DictType(ContainerType, metaclass = ArgumentSingleton):
         """
         return None
 
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.key_type == other.key_type \
+                and self.value_type == other.value_type
+
+    def __hash__(self):
+        return hash((self.__class__, self._key_type, self._value_type))
+
 #==============================================================================
 
 def DataTypeFactory(name, argnames = (), *, BaseClass=CustomDataType):
diff --git a/pyccel/ast/numpytypes.py b/pyccel/ast/numpytypes.py
index 8bc1df82..d6e36e75 100644
--- a/pyccel/ast/numpytypes.py
+++ b/pyccel/ast/numpytypes.py
@@ -425,6 +425,13 @@ class NumpyNDArrayType(HomogeneousContainerType, metaclass = ArgumentSingleton):
         order_str = f'(order={self._order})' if self._order else ''
         return f'{self.element_type}[{dims}]{order_str}'
 
+    def __hash__(self):
+        return hash((self.element_type, self.rank, self.order))
+
+    def __eq__(self, other):
+        return isinstance(other, NumpyNDArrayType) and self.element_type == other.element_type \
+                and self.rank == other.rank and self.order == other.order
+
 #==============================================================================
 
 numpy_precision_map = {
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index b5c78c59..f6e9f34f 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -22,7 +22,7 @@ from pyccel.utilities.strings import random_string
 from pyccel.ast.basic         import PyccelAstNode, TypedAstNode, ScopedAstNode
 
 from pyccel.ast.builtins import PythonPrint, PythonTupleFunction, PythonSetFunction
-from pyccel.ast.builtins import PythonComplex, PythonDict
+from pyccel.ast.builtins import PythonComplex, PythonDict, PythonDictFunction
 from pyccel.ast.builtins import builtin_functions_dict, PythonImag, PythonReal
 from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet
 from pyccel.ast.builtins import (PythonRange, PythonZip, PythonEnumerate,
@@ -65,7 +65,7 @@ from pyccel.ast.datatypes import PrimitiveIntegerType, StringType, SymbolicType
 from pyccel.ast.datatypes import PythonNativeBool, PythonNativeInt, PythonNativeFloat
 from pyccel.ast.datatypes import DataTypeFactory, PrimitiveFloatingPointType
 from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType, HomogeneousSetType, HomogeneousListType
-from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType
+from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType, DictType
 
 from pyccel.ast.functionalexpr import FunctionalSum, FunctionalMax, FunctionalMin, GeneratorComprehension, FunctionalFor
 
@@ -149,7 +149,7 @@ pyccel_stage = PyccelStage()
 type_container = {
                    PythonTupleFunction : HomogeneousTupleType,
                    PythonList : HomogeneousListType,
-                   PythonSetFunction : HomogeneousSetType
+                   PythonSetFunction : HomogeneousSetType,
                   }
 
 #==============================================================================
@@ -1640,7 +1640,7 @@ class SemanticParser(BasicParser):
             if raise_error:
                 name = var.name
                 rhs_str = str(rhs)
-                errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(repr(var.class_type), repr(class_type)),
+                errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(var.class_type, class_type),
                     symbol=f'{name}={rhs_str}',
                     bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
                     severity='error')
@@ -1880,6 +1880,30 @@ class SemanticParser(BasicParser):
 
         return list(parent.values())
 
+    def _convert_syntactic_object_to_type_annotation(self, syntactic_annotation):
+        """
+        Convert an arbitrary syntactic object to a type annotation.
+
+        Convert an arbitrary syntactic object to a type annotation. This means that
+        the syntactic object is wrapped in a SyntacticTypeAnnotation (if necessary).
+        This ensures that a type annotation is obtained instead of e.g. a function.
+
+        Parameters
+        ----------
+        syntactic_annotation : PyccelAstNode
+            A syntactic object that needs to be visited as a type annotation.
+
+        Returns
+        -------
+        SyntacticTypeAnnotation
+            A syntactic object that will be recognised as a type annotation.
+        """
+        if not isinstance(syntactic_annotation, SyntacticTypeAnnotation):
+            pyccel_stage.set_stage('syntactic')
+            syntactic_annotation = SyntacticTypeAnnotation(dtype=syntactic_annotation)
+            pyccel_stage.set_stage('semantic')
+        return syntactic_annotation
+
     def _get_indexed_type(self, base, args, expr):
         """
         Extract a type annotation from an IndexedElement.
@@ -1933,21 +1957,24 @@ class SemanticParser(BasicParser):
             else:
                 raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
                         severity='fatal', symbol=expr)
-            if len(args) == 2 and args[1] is LiteralEllipsis() or len(args) == 1:
-                syntactic_annotation = args[0]
-                if not isinstance(syntactic_annotation, SyntacticTypeAnnotation):
-                    pyccel_stage.set_stage('syntactic')
-                    syntactic_annotation = SyntacticTypeAnnotation(dtype=syntactic_annotation)
-                    pyccel_stage.set_stage('semantic')
+            if (len(args) == 2 and args[1] is LiteralEllipsis()) or len(args) == 1:
+                syntactic_annotation = self._convert_syntactic_object_to_type_annotation(args[0])
                 internal_datatypes = self._visit(syntactic_annotation)
-                type_annotations = []
                 if dtype_cls in type_container :
                     class_type = type_container[dtype_cls]
                 else:
                     raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
                             severity='fatal', symbol=expr)
-                for u in internal_datatypes.type_list:
-                    type_annotations.append(VariableTypeAnnotation(class_type(u.class_type), u.is_const))
+                type_annotations = [VariableTypeAnnotation(class_type(u.class_type), u.is_const)
+                                    for u in internal_datatypes.type_list]
+                return UnionTypeAnnotation(*type_annotations)
+            elif len(args) == 2 and dtype_cls is PythonDictFunction:
+                syntactic_key_annotation = self._convert_syntactic_object_to_type_annotation(args[0])
+                syntactic_val_annotation = self._convert_syntactic_object_to_type_annotation(args[1])
+                key_types = self._visit(syntactic_key_annotation)
+                val_types = self._visit(syntactic_val_annotation)
+                type_annotations = [VariableTypeAnnotation(DictType(k.class_type, v.class_type)) \
+                                    for k,v in zip(key_types.type_list, val_types.type_list)]
                 return UnionTypeAnnotation(*type_annotations)
             else:
                 raise errors.report("Cannot handle non-homogenous type index\n"+PYCCEL_RESTRICTION_TODO,


Test patch:
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index 8262ae6f..a5cca5e1 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -35,3 +35,36 @@ def test_dict_str_keys(language):
     python_result = dict_str_keys()
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
+
+def test_dict_empty_init(language):
+    def dict_empty_init():
+        a : 'dict[int, float]' = {}
+        return a
+    epyc_dict_empty_init = epyccel(dict_empty_init, language = language)
+    pyccel_result = epyc_dict_empty_init()
+    python_result = dict_empty_init()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_dict_copy(language):
+    def dict_copy():
+        a = {1:1.0,2:2.0}
+        b = dict(a)
+        return b
+
+    epyc_dict_copy = epyccel(dict_copy, language = language)
+    pyccel_result = epyc_dict_copy()
+    python_result = dict_copy()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_dict_kwarg_init(language):
+    def kwarg_init():
+        b = dict(a=1, b=2) #pylint: disable=use-dict-literal
+        return b
+
+    epyc_kwarg_init = epyccel(kwarg_init, language = language)
+    pyccel_result = epyc_kwarg_init()
+    python_result = kwarg_init()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
diff --git a/tests/epyccel/test_epyccel_variable_annotations.py b/tests/epyccel/test_epyccel_variable_annotations.py
index c8710a59..82416614 100644
--- a/tests/epyccel/test_epyccel_variable_annotations.py
+++ b/tests/epyccel/test_epyccel_variable_annotations.py
@@ -271,3 +271,18 @@ def test_homogeneous_list_annotation_embedded_complex(stc_language):
     epyc_homogeneous_list_annotation = epyccel(homogeneous_list_annotation, language=stc_language)
     assert epyc_homogeneous_list_annotation() == homogeneous_list_annotation()
     assert isinstance(epyc_homogeneous_list_annotation(), type(homogeneous_list_annotation()))
+
+@pytest.mark.parametrize('lang',
+        [pytest.param("python", marks = pytest.mark.python)])
+def test_dict_empty_init(lang):
+    def dict_empty_init():
+        # Not valid in Python 3.8
+        a : dict[int, float] #pylint: disable=unsubscriptable-object
+        a = {1:1.0, 2:2.0}
+        return a
+
+    epyc_dict_empty_init = epyccel(dict_empty_init, language = lang)
+    pyccel_result = epyc_dict_empty_init()
+    python_result = dict_empty_init()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result