Repository: pyccel/pyccel (python)

Issue:
[DICTS] Support dicts as results in C
### Describe the feature
Support returning dicts from C code

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def f():
    a = {1:1.0}
    return a
```

### Proposed Solution
The cwrapper should be improved to ensure that dicts can be read from C code and passed to Python.


Pull request:
Allow returning a dictionary from C
Allow returning a dictionary from C. Fixes #2059 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d0bc117..16f4b90f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,7 @@ All notable changes to this project will be documented in this file.
 -   #1754 : Add support for set method `update()`.
 -   #1744 : Add support for set method `intersection()`.
 -   #1745 : Add support for set method `intersection_update()`.
+-   #2059 : Add C support for returning dictionaries from functions.
 -   #1884 : Add support for dict method `items()`.
 -   #1936 : Add missing C output for inline decorator example in documentation
 -   #1937 : Optimise `pyccel.ast.basic.PyccelAstNode.substitute` method.
diff --git a/pyccel/ast/cwrapper.py b/pyccel/ast/cwrapper.py
index c1f50cea..9f127599 100644
--- a/pyccel/ast/cwrapper.py
+++ b/pyccel/ast/cwrapper.py
@@ -1245,6 +1245,24 @@ PyIter_Next = FunctionDef(name = 'PyIter_Next',
                         arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='iter', memory_handling='alias'))],
                         results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
 
+#-------------------------------------------------------------------
+#                         Dict functions
+#-------------------------------------------------------------------
+
+
+# https://docs.python.org/3/c-api/dict.html#c.PyDict_New
+PyDict_New = FunctionDef(name = 'PyDict_New',
+                    arguments = [],
+                    results = [FunctionDefResult(Variable(PyccelPyObject(), 'dict', memory_handling='alias'))],
+                    body = [])
+
+# https://docs.python.org/3/c-api/dict.html#c.PyDict_SetItem
+PyDict_SetItem = FunctionDef(name = 'PyDict_SetItem',
+                    arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'dict', memory_handling='alias')),
+                                 FunctionDefArgument(Variable(PyccelPyObject(), 'key', memory_handling='alias')),
+                                 FunctionDefArgument(Variable(PyccelPyObject(), 'val', memory_handling='alias'))],
+                    results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
+                    body = [])
 
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
 check_type_registry = {
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index 9bd2fdde..0b1b5a8e 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -14,6 +14,7 @@ from pyccel.ast.bind_c        import BindCClassDef, BindCClassProperty
 from pyccel.ast.builtins      import PythonTuple, PythonRange, PythonLen, PythonSet
 from pyccel.ast.builtins      import VariableIterator
 from pyccel.ast.builtin_methods.set_methods import SetAdd, SetPop
+from pyccel.ast.builtin_methods.dict_methods import DictItems
 from pyccel.ast.class_defs    import StackArrayClass
 from pyccel.ast.core          import Interface, If, IfSection, Return, FunctionCall
 from pyccel.ast.core          import FunctionDef, FunctionDefArgument, FunctionDefResult
@@ -36,6 +37,7 @@ from pyccel.ast.cwrapper      import PyTuple_GetItem, PyTuple_SetItem
 from pyccel.ast.cwrapper      import PySet_New, PySet_Add
 from pyccel.ast.cwrapper      import PySet_Size, PySet_Check, PySet_GetIter, PySet_Clear
 from pyccel.ast.cwrapper      import PyIter_Next
+from pyccel.ast.cwrapper      import PyDict_New, PyDict_SetItem
 from pyccel.ast.c_concepts    import ObjectAddress, PointerCast, CStackArray, CNativeInt
 from pyccel.ast.datatypes     import VoidType, PythonNativeInt, CustomDataType, DataTypeFactory
 from pyccel.ast.datatypes     import FixedSizeNumericType, HomogeneousTupleType, PythonNativeBool
@@ -2903,3 +2905,54 @@ class CToPythonWrapper(Wrapper):
             body.append(Deallocate(c_res))
 
         return {'c_results': c_results, 'py_result': py_res, 'body': body}
+
+    def _extract_DictType_FunctionDefResult(self, orig_var, is_bind_c, funcdef):
+        """
+        Get the code which translates a `Variable` containing a dictionary to a PyObject.
+
+        Get the code which translates a `Variable` containing a dictionary to a PyObject.
+
+        Parameters
+        ----------
+        orig_var : Variable | IndexedElement
+            An object representing the variable or an element of the variable from the
+            FunctionDefResult being wrapped.
+        is_bind_c : bool
+            True if the result was saved in a BindCFunctionDefResult. False otherwise.
+        funcdef : FunctionDef
+            The function being wrapped.
+
+        Returns
+        -------
+        dict
+            A dictionary describing the objects necessary to collect the result.
+        """
+        name = getattr(orig_var, 'name', 'tmp')
+        py_res = self.get_new_PyObject(f'{name}_obj', orig_var.dtype)
+        if is_bind_c:
+            raise NotImplementedError("Can't return a dict from Fortran")
+        else:
+            c_res = orig_var.clone(self.scope.get_new_name(name), is_argument = False)
+            c_results = [c_res]
+            iterable = DictItems(c_res)
+
+        iterable.set_loop_counter(Variable(PythonNativeInt(), self.scope.get_new_name()))
+        self.scope.insert_variable(c_res)
+
+        for_scope = self.scope.create_new_loop_scope()
+        self.scope = for_scope
+        key_elem, val_elem = iterable.get_python_iterable_item()
+        key_extraction = self._extract_FunctionDefResult(key_elem, is_bind_c, funcdef)
+        value_extraction = self._extract_FunctionDefResult(val_elem, is_bind_c, funcdef)
+        elem_set = PyDict_SetItem(py_res, key_extraction['py_result'], value_extraction['py_result'])
+        for_body = [*key_extraction['body'], *value_extraction['body'],
+                    If(IfSection(PyccelEq(elem_set, PyccelUnarySub(LiteralInteger(1))),
+                                         [Return([self._error_exit_code])]))]
+        self.exit_scope()
+        for_loop = For((key_extraction['c_results'][0], value_extraction['c_results'][0]),
+                        iterable, for_body, for_scope)
+
+        body = [AliasAssign(py_res, PyDict_New()),
+                for_loop]
+
+        return {'c_results': c_results, 'py_result': py_res, 'body': body}


Test patch:
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index 51b360f4..ffaa7f07 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -16,11 +16,11 @@ from pyccel import epyccel
 def python_only_language(request):
     return request.param
 
-def test_dict_init(python_only_language):
+def test_dict_init(stc_language):
     def dict_init():
         a = {1:1.0, 2:2.0}
         return a
-    epyc_dict_init = epyccel(dict_init, language = python_only_language)
+    epyc_dict_init = epyccel(dict_init, language = stc_language)
     pyccel_result = epyc_dict_init()
     python_result = dict_init()
     assert isinstance(python_result, type(pyccel_result))
@@ -36,11 +36,11 @@ def test_dict_str_keys(python_only_language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_dict_empty_init(python_only_language):
+def test_dict_empty_init(stc_language):
     def dict_empty_init():
         a : 'dict[int, float]' = {}
         return a
-    epyc_dict_empty_init = epyccel(dict_empty_init, language = python_only_language)
+    epyc_dict_empty_init = epyccel(dict_empty_init, language = stc_language)
     pyccel_result = epyc_dict_empty_init()
     python_result = dict_empty_init()
     assert isinstance(python_result, type(pyccel_result))