Repository: pyccel/pyccel
Pull request #2197 (2025-02-06 22:52:47)

Allow strings as dictionary keys in C
Allow strings as dictionary keys in C. Fortran support is left for a later PR as the gFTL docs are not very clear on how to manage this

pyccel-bot[bot]: Hello again! Thank you for this new pull request :star_struck:.

Please begin by requesting your checklist using the command `/bot checklist`

EmilyBourne: /bot checklist
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Hey @pyccel/pyccel-dev ! @EmilyBourne has just created this great new pull request! Check it out and let me know what you think!

EmilyBourne: > Well done! I think we should prohibit the use `\0` in Python source files, otherwise it would be hard to handle cases like this:
> 
> ```python
> if __name__ == "__main__":
>     a = {'a':1, 'b':2}
>     print(a.get('a\0b',4))
> ```
> 
> Pyccel erroneously prints `1` because the comparaison stops at `a`

I completely agree. I also can't think of a case where someone would need this in Python code. I have created an issue from your comment
EmilyBourne: /bot run pr_tests
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Hey @yguclu, @EmilyBourne, this PR is looking pretty good. @EmilyBourne and @jalalium think it is ready to merge. Could you add your expertise to confirm that this follows all the coding conventions and fits in Pyccel's future plans? Thanks :smile:

EmilyBourne: @jalalium re-requesting your review as fixing the merge of devel turned out to be non-trivial
pyccel-bot[bot]: Hey @yguclu, @EmilyBourne, this PR is looking pretty good. @EmilyBourne and @jalalium think it is ready to merge. Could you add your expertise to confirm that this follows all the coding conventions and fits in Pyccel's future plans? Thanks :smile:


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0da42919..90a013a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -117,6 +117,7 @@ All notable changes to this project will be documented in this file.
 -   #2198 : Fix saving an empty string in Fortran.
 -   #2195 : Fix string comparisons.
 -   Fixed returning strings from functions.
+-   #2197 : Allow strings as dictionary keys in C.
 -   Lifted the restriction on ndarrays limiting them to rank<15.
 -   #2175 : Fix the shape of multi-level containers.
 -   Catch all internal errors arising in the syntactic, semantic, printing or code generation stages.
diff --git a/docs/type_annotations.md b/docs/type_annotations.md
index 001dffd8..805f331f 100644
--- a/docs/type_annotations.md
+++ b/docs/type_annotations.md
@@ -94,14 +94,14 @@ def g(b : Final[set[bool]]):
 
 ## Dictionaries
 
-Dictionaries are in the process of being added to Pyccel. They cannot yet be used effectively however the type annotations are already supported.
+Dictionaries are in the process of being added to Pyccel.
 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.
+Strings are not yet supported as keys in Fortran.
 
 ## Strings
 
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index b8732453..45417c56 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -39,7 +39,7 @@ from pyccel.ast.datatypes import TupleType, FixedSizeNumericType, CharType
 from pyccel.ast.datatypes import CustomDataType, StringType, HomogeneousTupleType
 from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousListType, HomogeneousSetType
 from pyccel.ast.datatypes import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
-from pyccel.ast.datatypes import HomogeneousContainerType, DictType
+from pyccel.ast.datatypes import HomogeneousContainerType, DictType, FixedSizeType
 
 from pyccel.ast.internals import Slice, PrecomputedCode, PyccelArrayShapeElement
 from pyccel.ast.internals import PyccelFunction
@@ -690,10 +690,18 @@ class CCodePrinter(CodePrinter):
         class_type = assignment_var.class_type
         dtype = self.get_c_type(class_type)
         if isinstance(expr, PythonDict):
-            dict_item_strs = [(self._print(k), self._print(v)) for k,v in zip(expr.keys, expr.values)]
-            keyraw = '{' + ', '.join(f'{{{k}, {v}}}' for k,v in dict_item_strs) + '}'
+            values = [self._print(k) for k in expr.values]
+            if isinstance(class_type.key_type, StringType):
+                keys = [self._print(CStrStr(k)) for k in expr.keys]
+            else:
+                keys = [self._print(k) for k in expr.keys]
+            keyraw = '{' + ', '.join(f'{{{k}, {v}}}' for k,v in zip(keys, values)) + '}'
         else:
-            keyraw = '{' + ', '.join(self._print(a) for a in expr.args) + '}'
+            if isinstance(class_type.element_type, StringType):
+                args = [self._print(CStrStr(a)) for a in expr.args]
+            else:
+                args = [self._print(a) for a in expr.args]
+            keyraw = '{' + ', '.join(args) + '}'
         container_name = self._print(assignment_var)
         init = f'{container_name} = c_init({dtype}, {keyraw});\n'
         return init
@@ -1186,13 +1194,33 @@ class CCodePrinter(CodePrinter):
                        AsName(VariableTypeAnnotation(class_type), container_type),
                        ignore_at_print=True))
                 if isinstance(class_type, DictType):
+                    key_type = class_type.key_type
                     container_key_key = self.get_c_type(class_type.key_type)
                     container_val_key = self.get_c_type(class_type.value_type)
                     container_key = f'{container_key_key}_{container_val_key}'
                     type_decl = f'{container_key_key},{container_val_key}'
+                    if isinstance(key_type, FixedSizeType):
+                        decl_line = f'#define i_type {container_type},{type_decl}\n'
+                    elif isinstance(key_type, StringType):
+                        decl_line = (f'#define i_type {container_type}\n'
+                                     f'#define i_keypro cstr\n'
+                                     f'#define i_val {container_val_key}\n')
                 else:
-                    type_decl = self.get_c_type(class_type.element_type)
-                decl_line = f'#define i_type {container_type},{type_decl}\n'
+                    element_type = class_type.element_type
+                    if isinstance(element_type, FixedSizeType):
+                        type_decl = self.get_c_type(element_type)
+                        decl_line = f'#define i_type {container_type},{type_decl}\n'
+                    elif isinstance(element_type, StringType):
+                        decl_line = (f'#define i_type {container_type}\n'
+                                     f'#define i_keypro cstr\n')
+                    elif isinstance(element_type, (HomogeneousListType, HomogeneousSetType, DictType)):
+                        type_decl = self.get_c_type(element_type)
+                        decl_line = (f'#define i_type {container_type}\n'
+                                     f'#define i_keyclass {type_decl}\n')
+                    else:
+                        decl_line = ''
+                        errors.report(f"The declaration of type {class_type} is not yet implemented.",
+                                symbol=expr, severity='error')
                 if isinstance(class_type, HomogeneousListType) and isinstance(class_type.element_type, FixedSizeNumericType) \
                         and not isinstance(class_type.element_type.primitive_type, PrimitiveComplexType):
                     decl_line += '#define i_use_cmp\n'
@@ -1203,8 +1231,7 @@ class CCodePrinter(CodePrinter):
                                  decl_line,
                                  f'#include <{source}.h>\n'))
                 if source in stc_extension_mapping:
-                    code += (f'#define i_type {container_type},{type_decl}\n'
-                             f'#include <{stc_extension_mapping[source]}.h>\n')
+                    code += decl_line + f'#include <{stc_extension_mapping[source]}.h>\n'
                 code += f'#endif // {header_guard}\n\n'
             return code
 
@@ -2854,7 +2881,10 @@ class CCodePrinter(CodePrinter):
         c_type = self.get_c_type(class_type)
 
         dict_var = self._print(ObjectAddress(target))
-        key = self._print(expr.key)
+        if isinstance(class_type.key_type, StringType):
+            key = self._print(CStrStr(expr.key))
+        else:
+            key = self._print(expr.key)
 
         if expr.default_value is not None:
             default_val = self._print(expr.default_value)
@@ -2868,7 +2898,10 @@ class CCodePrinter(CodePrinter):
         c_type = self.get_c_type(class_type)
 
         dict_var = self._print(ObjectAddress(target))
-        key = self._print(expr.key)
+        if isinstance(class_type.key_type, StringType):
+            key = self._print(CStrStr(expr.key))
+        else:
+            key = self._print(expr.key)
 
         if expr.default_value is not None:
             default = self._print(expr.default_value)
@@ -2881,8 +2914,12 @@ class CCodePrinter(CodePrinter):
     def _print_DictGetItem(self, expr):
         dict_obj = expr.dict_obj
         dict_obj_code = self._print(ObjectAddress(dict_obj))
-        container_type = self.get_c_type(dict_obj.class_type)
-        key = self._print(expr.key)
+        class_type = dict_obj.class_type
+        container_type = self.get_c_type(class_type)
+        if isinstance(class_type.key_type, StringType):
+            key = self._print(CStrStr(expr.key))
+        else:
+            key = self._print(expr.key)
         assign = expr.get_user_nodes(Assign)
         if assign:
             assert len(assign) == 1


Test patch:
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index 32a967b5..e89c0f30 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -120,11 +120,22 @@ def test_pop_falsy_bool_default_element(stc_language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_pop_str_keys(python_only_language):
+def test_pop_str_keys(stc_language):
     def pop_str_keys():
         a = {'a':1, 'b':2}
         return a.pop('a')
-    epyc_str_keys = epyccel(pop_str_keys, language = python_only_language)
+    epyc_str_keys = epyccel(pop_str_keys, language = stc_language)
+    pyccel_result = epyc_str_keys()
+    python_result = pop_str_keys()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_pop_non_literal_str_keys(stc_language):
+    def pop_str_keys():
+        a = {'a':1, 'b':2}
+        my_str = 'a'
+        return a.pop(my_str)
+    epyc_str_keys = epyccel(pop_str_keys, language = stc_language)
     pyccel_result = epyc_str_keys()
     python_result = pop_str_keys()
     assert isinstance(python_result, type(pyccel_result))
@@ -215,11 +226,11 @@ def test_get_str_keys(python_only_language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_get_default_str_keys(python_only_language):
+def test_get_default_str_keys(stc_language):
     def get_default_str_keys():
         a = {'a':1, 'b':2}
         return a.get('c', 4)
-    epyc_str_keys = epyccel(get_default_str_keys, language = python_only_language)
+    epyc_str_keys = epyccel(get_default_str_keys, language = stc_language)
     pyccel_result = epyc_str_keys()
     python_result = get_default_str_keys()
     assert isinstance(python_result, type(pyccel_result))
@@ -255,11 +266,11 @@ def test_getitem_element(language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_getitem_str_keys(python_only_language):
+def test_getitem_str_keys(stc_language):
     def getitem_str_keys():
         a = {'a':1, 'b':2}
         return a['a']
-    epyc_str_keys = epyccel(getitem_str_keys, language = python_only_language)
+    epyc_str_keys = epyccel(getitem_str_keys, language = stc_language)
     pyccel_result = epyc_str_keys()
     python_result = getitem_str_keys()
     assert isinstance(python_result, type(pyccel_result))
diff --git a/tests/epyccel/test_epyccel_lists.py b/tests/epyccel/test_epyccel_lists.py
index d9acf0ff..bdc24b16 100644
--- a/tests/epyccel/test_epyccel_lists.py
+++ b/tests/epyccel/test_epyccel_lists.py
@@ -688,6 +688,7 @@ def test_mutable_indexing(stc_language):
     epyc_f = epyccel(f, language=stc_language)
     assert f() == epyc_f()
 
+@pytest.mark.xfail(reason="No way to tell from type if b is a list of pointers or a list of values")
 def test_mutable_multi_level_indexing(stc_language):
     def f():
         a = [1,2,3,4]
@@ -777,7 +778,7 @@ def test_list_contains(language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_dict_ptr(language):
+def test_list_ptr(language):
     def list_ptr():
         a = [1, 3, 4, 7, 10, 3]
         b = a
@@ -801,3 +802,13 @@ def test_list_return(language):
     assert python_result == pyccel_result
     assert isinstance(python_result, type(pyccel_result))
     assert isinstance(python_result.pop(), type(pyccel_result.pop()))
+
+def test_list_str(stc_language):
+    def list_str():
+        a = ['hello', 'world', '!']
+        return len(a)
+
+    epyccel_func = epyccel(list_str, language = stc_language)
+    pyccel_result = epyccel_func()
+    python_result = list_str()
+    assert python_result == pyccel_result