Repository: pyccel/pyccel (python)

Issue:
[STR] Add C support for local string declarations
### Describe the feature
Pyccel contains some minimal support for strings. Eventually we would like to expand on this support, however as a first step C support should be brought up to the level of Fortran support in order to allow strings to be used as keys for dictionaries.

As a first step support should be added for declaring a string locally

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def f():
    a = 'abcdef'
    b = '123456'
```

### Proposed Solution
The declaration of strings should be managed via `cstr` from STC. The associated documentation can be found here:
https://github.com/stclib/STC/blob/master/docs/cstr_api.md

The generated code should be:
```c
void f() {
    cstr a;
    a = cstr_lit("abcdef");
    cstr_drop(&a);
    a = cstr_lit("123456");
    cstr_drop(&a);
}
```
In the semantic stage, this will need to be handled with the `Allocate` node. In the codegen stage, this will need to be handled in `_print_Assign`, `_print_Allocate` and `_print_Deallocate`

Pull request description:
Add C support for local string declarations
Add C support for local string declarations. This is a first step towards allowing string keys for dictionaries. Fixes #2061 

**Commit Summary**
- Handle strings in loop unravelling
- Allow extensions to depend on stc folders by using a recognisable dependency that can be swapped for the correct directory
- Fix declaration of strings in C
- Add support for `PyccelArrayShapeElement` of a string in C
- Remove double line break after deallocation
- Add support for string initialisation
- Correct the warning message for array reallocation (maybe this should be removed for non-arrays)
- Ensure strings are allocated in the semantic parser.
- Create a `cstr.c` file to compile string functions independently (avoids duplicate definitions)
- Activate tests
- Add a leak test for string reallocation

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4295340f..b657cdf2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,7 @@ All notable changes to this project will be documented in this file.
 -   #2016 : Add support for translating arithmetic magic methods (methods cannot yet be used from Python).
 -   #1980 : Extend The C support for min and max to more than two variables
 -   #2081 : Add support for multi operator expressions
+-   #2061 : Add C support for string declarations.
 -   Add support for inhomogeneous tuple annotations.
 -   #1834 : Add support for `@property` decorator.
 -   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
diff --git a/pyccel/ast/utilities.py b/pyccel/ast/utilities.py
index 34aceb13..4952f986 100644
--- a/pyccel/ast/utilities.py
+++ b/pyccel/ast/utilities.py
@@ -425,7 +425,12 @@ def collect_loops(block, indices, new_index, language_has_vectors = False, resul
                                     and not isinstance(f, (NumpyTranspose))))
     for line in block:
 
-        if (isinstance(line, Assign) and
+        if isinstance(line, Assign) and isinstance(line.lhs.class_type, StringType):
+            # Save line in top level (no for loop)
+            result.append(line)
+            current_level = 0
+
+        elif (isinstance(line, Assign) and
                 not isinstance(line.rhs, (array_creator_types, Nil)) and # not creating array
                 not line.rhs.get_attribute_nodes(array_creator_types) and # not creating array
                 not is_function_call(line.rhs)): # not a basic function call
diff --git a/pyccel/codegen/compiling/basic.py b/pyccel/codegen/compiling/basic.py
index 9e31b58c..acac6933 100644
--- a/pyccel/codegen/compiling/basic.py
+++ b/pyccel/codegen/compiling/basic.py
@@ -91,10 +91,7 @@ class CompileObj:
         self._lock_source  = FileLock(self.source+'.lock')
 
         self._flags        = list(flags)
-        if has_target_file:
-            self._includes     = set([folder, *includes])
-        else:
-            self._includes = set(includes)
+        self._includes     = set([folder, *includes])
         self._libs         = list(libs)
         self._libdirs      = set(libdirs)
         self._accelerators = set(accelerators)
@@ -117,9 +114,8 @@ class CompileObj:
         folder : str
             The new folder where the source file can be found.
         """
-        if self.has_target_file:
-            self._includes.remove(self._folder)
-            self._includes.add(folder)
+        self._includes.remove(self._folder)
+        self._includes.add(folder)
 
         self._file = os.path.join(folder, os.path.basename(self._file))
         self._lock_source  = FileLock(self.source+'.lock')
diff --git a/pyccel/codegen/pipeline.py b/pyccel/codegen/pipeline.py
index 4a8a74c1..3137e868 100644
--- a/pyccel/codegen/pipeline.py
+++ b/pyccel/codegen/pipeline.py
@@ -311,7 +311,7 @@ def execute_pyccel(fname, *,
     #         pass
     #------------------------------------------------------
     try:
-        manage_dependencies(codegen.printer, src_compiler, pyccel_dirpath, mod_obj,
+        manage_dependencies(codegen.printer.get_additional_imports(), src_compiler, pyccel_dirpath, mod_obj,
                 language, verbose, convert_only)
     except NotImplementedError as error:
         errors.report(f'{error}\n'+PYCCEL_RESTRICTION_TODO,
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index beb33aa6..b0feb494 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -247,7 +247,8 @@ c_imports = {n : Import(n, Module(n, (), ())) for n in
                  'stdio',
                  "inttypes",
                  'stdbool',
-                 'assert']}
+                 'assert',
+                 'stc/cstr']}
 
 import_header_guard_prefix = {'stc/hset'    : '_TOOLS_SET',
                               'stc/vec'   : '_TOOLS_LIST',
@@ -309,7 +310,6 @@ class CCodePrinter(CodePrinter):
                       (PrimitiveIntegerType(),8)       : LiteralString("%") + CMacro('PRId64'),
                       (PrimitiveIntegerType(),2)       : LiteralString("%") + CMacro('PRId16'),
                       (PrimitiveIntegerType(),1)       : LiteralString("%") + CMacro('PRId8'),
-                      StringType()                  : '%s',
                       }
 
     def __init__(self, filename, prefix_module = None):
@@ -1039,7 +1039,7 @@ class CCodePrinter(CodePrinter):
                      f'#include <{stc_extension_mapping[source]}.h>\n', 
                      f'#endif // {header_guard}\n\n'))
             return code
-        elif source.startswith('stc/'):
+        elif source != 'stc/cstr' and (source.startswith('stc/') or source in import_header_guard_prefix):
             code = ''
             for t in expr.target:
                 class_type = t.object.class_type
@@ -1130,6 +1130,13 @@ class CCodePrinter(CodePrinter):
                 except KeyError:
                     errors.report(f"Printing {var.dtype} type is not supported currently", severity='fatal')
                 arg = self._print(var)
+        elif isinstance(var.dtype, StringType):
+            if isinstance(var, Variable):
+                var_obj = self._print(ObjectAddress(var))
+                arg = f'cstr_str({var_obj})'
+            else:
+                arg = self._print(var)
+            arg_format = '%s'
         else:
             try:
                 arg_format = self.type_to_format[var.dtype]
@@ -1302,6 +1309,9 @@ class CCodePrinter(CodePrinter):
             i_type = f'{container_type}_{key_type}_{val_type}'
             self.add_import(Import(f'stc/{container_type}', AsName(VariableTypeAnnotation(dtype), i_type)))
             return i_type
+        elif isinstance(dtype, StringType):
+            self.add_import(c_imports['stc/cstr'])
+            return 'cstr'
         else:
             key = dtype
 
@@ -1377,11 +1387,11 @@ class CCodePrinter(CodePrinter):
         rank  = expr.rank
 
         if rank > 0:
-            if isinstance(expr.class_type, (HomogeneousSetType, HomogeneousListType, DictType)):
-                dtype = self.get_c_type(expr.class_type)
-            elif isinstance(expr.class_type, CStackArray):
-                return self.get_c_type(expr.class_type.element_type)
-            elif isinstance(expr.class_type, (HomogeneousTupleType, NumpyNDArrayType)):
+            if isinstance(class_type, (HomogeneousSetType, HomogeneousListType, DictType, StringType)):
+                dtype = self.get_c_type(class_type)
+            elif isinstance(class_type, CStackArray):
+                return self.get_c_type(class_type.element_type)
+            elif isinstance(class_type, (HomogeneousTupleType, NumpyNDArrayType)):
                 if expr.rank > 15:
                     errors.report(UNSUPPORTED_ARRAY_RANK, symbol=expr, severity='fatal')
                 self.add_import(c_imports['ndarrays'])
@@ -1699,15 +1709,18 @@ class CCodePrinter(CodePrinter):
             c_type = self.get_c_type(arg.class_type)
             arg_code = self._print(ObjectAddress(arg))
             return f'{c_type}_size({arg_code})'
+        elif isinstance(arg.class_type, StringType):
+            arg_code = self._print(ObjectAddress(arg))
+            return f'cstr_size({arg_code})'
         else:
             raise NotImplementedError(f"Don't know how to represent shape of object of type {arg.class_type}")
 
     def _print_Allocate(self, expr):
         free_code = ''
         variable = expr.variable
-        if isinstance(variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):
+        if isinstance(variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType, StringType)):
             if expr.status in ('allocated', 'unknown'):
-                free_code = f'{self._print(Deallocate(variable))}\n'
+                free_code = f'{self._print(Deallocate(variable))}'
             if expr.shape[0] is None:
                 return free_code
             size = self._print(expr.shape[0])
@@ -1718,7 +1731,7 @@ class CCodePrinter(CodePrinter):
             elif expr.alloc_type == 'resize':
                 return f'{container_type}_resize({variable_address}, {size}, {0});\n'
             return free_code
-        if isinstance(variable.class_type, (NumpyNDArrayType, HomogeneousTupleType)):
+        elif isinstance(variable.class_type, (NumpyNDArrayType, HomogeneousTupleType)):
             #free the array if its already allocated and checking if its not null if the status is unknown
             if  (expr.status == 'unknown'):
                 shape_var = DottedVariable(VoidType(), 'shape', lhs = variable)
@@ -1755,7 +1768,7 @@ class CCodePrinter(CodePrinter):
             raise NotImplementedError(f"Allocate not implemented for {variable.class_type}")
 
     def _print_Deallocate(self, expr):
-        if isinstance(expr.variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):
+        if isinstance(expr.variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType, StringType)):
             if expr.variable.is_alias:
                 return ''
             variable_address = self._print(ObjectAddress(expr.variable))
@@ -2306,11 +2319,13 @@ class CCodePrinter(CodePrinter):
             return self.copy_NumpyArray_Data(expr)
         if isinstance(rhs, (NumpyFull)):
             return self.arrayFill(expr)
-        lhs = self._print(expr.lhs)
+        lhs_code = self._print(lhs)
         if isinstance(rhs, (PythonList, PythonSet, PythonDict)):
             return self.init_stc_container(rhs, expr)
-        rhs = self._print(expr.rhs)
-        return f'{lhs} = {rhs};\n'
+        rhs_code = self._print(rhs)
+        if isinstance(rhs, LiteralString):
+            rhs_code = f'cstr_lit({rhs_code})'
+        return f'{lhs_code} = {rhs_code};\n'
 
     def _print_AliasAssign(self, expr):
         lhs_var = expr.lhs
diff --git a/pyccel/codegen/python_wrapper.py b/pyccel/codegen/python_wrapper.py
index 4918be9b..167a8cce 100644
--- a/pyccel/codegen/python_wrapper.py
+++ b/pyccel/codegen/python_wrapper.py
@@ -198,7 +198,7 @@ def create_shared_library(codegen,
     #  Compile cwrapper_ndarrays from stdlib (if necessary)
     #--------------------------------------------------------
     start_compile_libs = time.time()
-    manage_dependencies(wrapper_codegen, wrapper_compiler, pyccel_dirpath, wrapper_compile_obj,
+    manage_dependencies(wrapper_codegen.get_additional_imports(), wrapper_compiler, pyccel_dirpath, wrapper_compile_obj,
             language, verbose)
     timings['Dependency compilation'] += (time.time() - start_compile_libs)
 
diff --git a/pyccel/codegen/utilities.py b/pyccel/codegen/utilities.py
index 93067e27..ce339a8f 100644
--- a/pyccel/codegen/utilities.py
+++ b/pyccel/codegen/utilities.py
@@ -11,6 +11,7 @@ This file contains some useful functions to compile the generated fortran code
 import os
 import shutil
 from filelock import FileLock
+
 import pyccel.stdlib as stdlib_folder
 import pyccel.extensions as ext_folder
 from pyccel.errors.errors import Errors
@@ -34,8 +35,9 @@ language_extension = {'fortran':'f90', 'c':'c', 'python':'py'}
 
 #==============================================================================
 # map external libraries inside pyccel/extensions with their path
-external_libs = {"stc" : "STC/include/stc", "gFTL" : "gFTL/install/GFTL-1.13/include/v2"}
-
+external_libs = {"stc" : ("STC/include/stc", CompileObj("stc", folder="stc", has_target_file = False)),
+                 "gFTL" : ("gFTL/install/GFTL-1.13/include/v2", CompileObj("gFTL", folder="gFTL", has_target_file = False)),
+}
 #==============================================================================
 # map internal libraries to their folders inside pyccel/stdlib and their compile objects
 # The compile object folder will be in the pyccel dirpath
@@ -47,10 +49,23 @@ internal_libs = {
     "cwrapper"        : ("cwrapper", CompileObj("cwrapper.c",folder="cwrapper", accelerators=('python',))),
     "numpy_f90"       : ("numpy", CompileObj("numpy_f90.f90",folder="numpy")),
     "numpy_c"         : ("numpy", CompileObj("numpy_c.c",folder="numpy")),
-    "Set_extensions"  : ("STC_Extensions", CompileObj("Set_Extensions.h", folder="STC_Extensions", has_target_file = False)),
-    "List_extensions" : ("STC_Extensions", CompileObj("List_Extensions.h", folder="STC_Extensions", has_target_file = False)),
-    "Common_extensions" : ("STC_Extensions", CompileObj("Common_Extensions.h", folder="STC_Extensions", has_target_file = False)),
-    "gFTL_functions/Set_extensions"  : ("gFTL_functions", CompileObj("Set_Extensions.inc", folder="gFTL_functions", has_target_file = False)),
+    "Set_extensions"  : ("STC_Extensions", CompileObj("Set_Extensions.h",
+                                                      folder="STC_Extensions",
+                                                      has_target_file = False,
+                                                      dependencies = (external_libs['stc'][1],))),
+    "List_extensions" : ("STC_Extensions", CompileObj("List_Extensions.h",
+                                                      folder="STC_Extensions",
+                                                      has_target_file = False,
+                                                      dependencies = (external_libs['stc'][1],))),
+    "Common_extensions" : ("STC_Extensions", CompileObj("Common_Extensions.h",
+                                                      folder="STC_Extensions",
+                                                      has_target_file = False,
+                                                      dependencies = (external_libs['stc'][1],))),
+    "gFTL_functions/Set_extensions"  : ("gFTL_functions", CompileObj("Set_Extensions.inc",
+                                                                     folder="gFTL_functions",
+                                                                     has_target_file = False,
+                                                                     dependencies = (external_libs['gFTL'][1],))),
+    "stc/cstr" : ("STC_Extensions", CompileObj("cstr.c", folder="STC_Extensions", dependencies = (external_libs['stc'][1],)))
 }
 internal_libs["cwrapper_ndarrays"] = ("cwrapper_ndarrays", CompileObj("cwrapper_ndarrays.c",folder="cwrapper_ndarrays",
                                                              accelerators = ('python',),
@@ -121,7 +136,7 @@ def copy_internal_library(lib_folder, pyccel_dirpath, extra_files = None):
     """
     # get lib path (stdlib_path/lib_name or ext_path/lib_name)
     if lib_folder in external_libs:
-        lib_path = os.path.join(ext_path, external_libs[lib_folder])
+        lib_path = os.path.join(ext_path, external_libs[lib_folder][0])
     else:
         lib_path = os.path.join(stdlib_path, lib_folder)
 
@@ -183,12 +198,16 @@ def copy_internal_library(lib_folder, pyccel_dirpath, extra_files = None):
                         extra_file = os.path.join(lib_dest_path, filename)
                         with open(extra_file, 'w', encoding="utf-8") as f:
                             f.writelines(contents)
-    return lib_dest_path
+
+    if lib_folder in external_libs and lib_folder != 'gFTL':
+        return pyccel_dirpath
+    else:
+        return lib_dest_path
 
 #==============================================================================
 def generate_extension_modules(import_key, import_node, pyccel_dirpath,
-                               includes, libs, libdirs, dependencies,
-                               accelerators, language):
+                               compiler, includes, libs, libdirs, dependencies,
+                               accelerators, language, verbose, convert_only):
     """
     Generate any new modules that describe extensions.
 
@@ -204,6 +223,8 @@ def generate_extension_modules(import_key, import_node, pyccel_dirpath,
         be printed).
     pyccel_dirpath : str
         The folder where files are being saved.
+    compiler : Compiler
+        A compiler that can be used to compile dependencies.
     includes : iterable of strs
         Include directories paths.
     libs : iterable of strs
@@ -216,6 +237,10 @@ def generate_extension_modules(import_key, import_node, pyccel_dirpath,
         Tool used to accelerate the code (e.g. openmp openacc).
     language : str
         The language in which code is being printed.
+    verbose : bool
+        Indicates whether additional information should be printed.
+    convert_only : bool, default=False
+        Indicates if the compilation step is required or not.
 
     Returns
     -------
@@ -228,10 +253,10 @@ def generate_extension_modules(import_key, import_node, pyccel_dirpath,
     if lib_name == 'gFTL_extensions':
         lib_name = 'gFTL'
         mod = import_node.source_module
-        printer = printer_registry[language]
         filename = os.path.join(pyccel_dirpath, import_key)+'.F90'
         folder = os.path.dirname(filename)
-        code = printer(filename).doprint(mod)
+        printer = printer_registry[language](filename)
+        code = printer.doprint(mod)
         if not os.path.exists(folder):
             os.mkdir(folder)
         with FileLock(f'{folder}.lock'):
@@ -239,12 +264,12 @@ def generate_extension_modules(import_key, import_node, pyccel_dirpath,
                 f.write(code)
 
         new_dependencies.append(CompileObj(os.path.basename(filename), folder=folder,
-                            includes=(os.path.join(pyccel_dirpath, 'gFTL'), *includes),
-                            libs=libs, libdirs=libdirs, dependencies=dependencies,
+                            includes=includes,
+                            libs=libs, libdirs=libdirs,
+                            dependencies=(*dependencies, external_libs['gFTL'][1]),
                             accelerators=accelerators))
-
-    if lib_name in external_libs:
-        copy_internal_library(lib_name, pyccel_dirpath)
+        manage_dependencies({'gFTL':None}, compiler, pyccel_dirpath, new_dependencies[-1],
+                language, verbose, convert_only)
 
     return new_dependencies
 
@@ -284,7 +309,7 @@ def recompile_object(compile_obj,
                 output_folder=compile_obj.source_folder,
                 verbose=verbose)
 
-def manage_dependencies(printer, compiler, pyccel_dirpath, mod_obj, language, verbose, convert_only = False):
+def manage_dependencies(pyccel_imports, compiler, pyccel_dirpath, mod_obj, language, verbose, convert_only = False):
     """
     Manage dependencies of the code to be compiled.
 
@@ -292,8 +317,8 @@ def manage_dependencies(printer, compiler, pyccel_dirpath, mod_obj, language, ve
 
     Parameters
     ----------
-    printer : CodePrinter
-        The printer that printed the code. This object describes dependencies from imports.
+    pyccel_imports : dict[str,Import]
+        A dictionary describing imports created by Pyccel that may imply dependencies.
     compiler : Compiler
         A compiler that can be used to compile dependencies.
     pyccel_dirpath : str
@@ -307,39 +332,54 @@ def manage_dependencies(printer, compiler, pyccel_dirpath, mod_obj, language, ve
     convert_only : bool, default=False
         Indicates if the compilation step is required or not.
     """
+    # Copy any necessary external libraries
+    for import_key in pyccel_imports:
+        lib_name = str(import_key).split('/', 1)[0]
+        if lib_name in external_libs:
+            lib_dest_path = copy_internal_library(lib_name, pyccel_dirpath)
+            external_libs[lib_name][1].reset_folder(lib_dest_path)
+
     # Iterate over the internal_libs list and determine if the printer
     # requires an internal lib to be included.
     for lib_name, (stdlib_folder, stdlib) in internal_libs.items():
-        if lib_name in printer.get_additional_imports():
+        if lib_name in pyccel_imports:
 
             lib_dest_path = copy_internal_library(stdlib_folder, pyccel_dirpath)
 
+            # Pylint thinks stdlib is a str
+            if stdlib.dependencies: # pylint: disable=E1101
+                manage_dependencies({os.path.splitext(os.path.basename(d.source))[0]: None for d in stdlib.dependencies}, # pylint: disable=E1101
+                        compiler, pyccel_dirpath, stdlib, language, verbose, convert_only)
+
             # stop after copying lib to __pyccel__ directory for
             # convert only
             if convert_only:
                 continue
-
-            # Pylint determines wrong type
             stdlib.reset_folder(lib_dest_path) # pylint: disable=E1101
+
             # get the include folder path and library files
             recompile_object(stdlib,
-                              compiler = compiler,
-                              verbose  = verbose)
+                             compiler = compiler,
+                             verbose  = verbose)
 
             mod_obj.add_dependencies(stdlib)
 
     # Iterate over the external_libs list and determine if the printer
     # requires an external lib to be included.
-    for key, import_node in printer.get_additional_imports().items():
+    for key, import_node in pyccel_imports.items():
         deps = generate_extension_modules(key, import_node, pyccel_dirpath,
+                                          compiler     = compiler,
                                           includes     = mod_obj.includes,
                                           libs         = mod_obj.libs,
                                           libdirs      = mod_obj.libdirs,
                                           dependencies = mod_obj.dependencies,
                                           accelerators = mod_obj.accelerators,
-                                          language = language)
+                                          language = language,
+                                          verbose = verbose,
+                                          convert_only = convert_only)
         for d in deps:
             recompile_object(d,
-                              compiler = compiler,
-                              verbose  = verbose)
+                             compiler = compiler,
+                             verbose  = verbose)
             mod_obj.add_dependencies(d)
+
diff --git a/pyccel/errors/messages.py b/pyccel/errors/messages.py
index 8fefbfa6..e2628fa5 100644
--- a/pyccel/errors/messages.py
+++ b/pyccel/errors/messages.py
@@ -153,7 +153,7 @@ INVALID_INDICES = 'only integers and slices (`:`) are valid indices'
 UNDEFINED_INIT_METHOD = 'Undefined `__init__` method'
 FOUND_SYMBOLIC_ASSIGN = 'Found symbolic assignment [Ignored]'
 FOUND_IS_IN_ASSIGN = 'Found `is` statement in assignment [Ignored]'
-ARRAY_REALLOCATION = 'Array redefinition may cause memory reallocation at runtime'
+ARRAY_REALLOCATION = '{class_type} redefinition may cause memory reallocation at runtime'
 ARRAY_DEFINITION_IN_LOOP = 'Array definition in for loop may cause memory reallocation at each cycle. Consider creating the array before the loop'
 TEMPLATE_IN_UNIONTYPE = 'Cannot use templates in a union type'
 DUPLICATED_SIGNATURE = 'Same signature defined for the same function multiple times'
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 0a0590bd..22561c3e 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -1519,7 +1519,7 @@ class SemanticParser(BasicParser):
 
                 # We cannot allow the definition of a stack array from a shape which
                 # is unknown at the declaration
-                if not isinstance(class_type, StringType) and class_type.rank > 0 and d_lhs.get('memory_handling', None) == 'stack':
+                if class_type.rank > 0 and d_lhs.get('memory_handling', None) == 'stack':
                     for a in d_lhs['shape']:
                         if (isinstance(a, FunctionCall) and not a.funcdef.is_pure) or \
                                 any(not f.funcdef.is_pure for f in a.get_attribute_nodes(FunctionCall)):
@@ -1546,7 +1546,7 @@ class SemanticParser(BasicParser):
                 # Add memory allocation if needed
                 array_declared_in_function = (isinstance(rhs, FunctionCall) and not isinstance(rhs.funcdef, PyccelFunctionDef) \
                                             and not getattr(rhs.funcdef, 'is_elemental', False) and not isinstance(lhs.class_type, HomogeneousTupleType)) or arr_in_multirets
-                if not isinstance(lhs.class_type, StringType) and lhs.on_heap and not array_declared_in_function:
+                if lhs.on_heap and not array_declared_in_function:
                     if self.scope.is_loop:
                         # Array defined in a loop may need reallocation at every cycle
                         errors.report(ARRAY_DEFINITION_IN_LOOP, symbol=name,
@@ -1585,7 +1585,7 @@ class SemanticParser(BasicParser):
 
                 # ...
                 # Add memory deallocation
-                if isinstance(lhs.class_type, CustomDataType) or (not lhs.on_stack and not isinstance(lhs.class_type, StringType)):
+                if isinstance(lhs.class_type, CustomDataType) or not lhs.on_stack:
                     if isinstance(lhs.class_type, InhomogeneousTupleType):
                         args = [self.scope.collect_tuple_element(v) for v in lhs if v.rank>0]
                         new_args = []
@@ -1741,7 +1741,7 @@ class SemanticParser(BasicParser):
                     bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
                     severity='error')
 
-        elif not is_augassign and not isinstance(var.class_type, StringType):
+        elif not is_augassign:
 
             shape = var.shape
 
@@ -1804,7 +1804,7 @@ class SemanticParser(BasicParser):
                     if status == 'unallocated':
                         self._allocs[-1].add(var)
                     else:
-                        errors.report(ARRAY_REALLOCATION, symbol=var.name,
+                        errors.report(ARRAY_REALLOCATION.format(class_type = var.class_type), symbol=var.name,
                             severity='warning',
                             bounding_box=(self.current_ast_node.lineno,
                                 self.current_ast_node.col_offset))
diff --git a/pyccel/stdlib/STC_Extensions/cstr.c b/pyccel/stdlib/STC_Extensions/cstr.c
new file mode 100644
index 00000000..4dff6512
--- /dev/null
+++ b/pyccel/stdlib/STC_Extensions/cstr.c
@@ -0,0 +1,2 @@
+#define i_implement
+#include <stc/cstr.h>


Test patch:
diff --git a/tests/epyccel/test_arrays_multiple_assignments.py b/tests/epyccel/test_arrays_multiple_assignments.py
index 80e2d536..fc303b90 100644
--- a/tests/epyccel/test_arrays_multiple_assignments.py
+++ b/tests/epyccel/test_arrays_multiple_assignments.py
@@ -60,7 +60,8 @@ def test_reallocation_heap(language):
     # Check that the warning is correct
     warning_info = [*errors.error_info_map.values()][0][0]
     assert warning_info.symbol  == 'x'
-    assert warning_info.message == ARRAY_REALLOCATION
+    expected_msg = ARRAY_REALLOCATION.split()[1:]
+    assert warning_info.message.split()[-len(expected_msg):] == expected_msg
 
 #==============================================================================
 def test_reallocation_stack(language):
@@ -269,7 +270,8 @@ def test_Assign_after_If():
     # Check that the warning is correct
     warning_info = [*errors.error_info_map.values()][0][0]
     assert warning_info.symbol  == 'x'
-    assert warning_info.message == ARRAY_REALLOCATION
+    expected_msg = ARRAY_REALLOCATION.split()[1:]
+    assert warning_info.message.split()[-len(expected_msg):] == expected_msg
 
     assert f(True) == f2(True)
     assert f(False) == f2(False)
diff --git a/tests/epyccel/test_builtins.py b/tests/epyccel/test_builtins.py
index e74e70bb..9508149b 100644
--- a/tests/epyccel/test_builtins.py
+++ b/tests/epyccel/test_builtins.py
@@ -460,14 +460,6 @@ def test_len_dict_int_float(stc_language):
 
     assert epyc_f() == f()
 
-@pytest.mark.parametrize( 'language', (
-        pytest.param("fortran", marks = pytest.mark.fortran),
-        pytest.param("c", marks = [
-            pytest.mark.skip(reason="C has no support for strings. See #2061"),
-            pytest.mark.c]),
-        pytest.param("python", marks = pytest.mark.python)
-    )
-)
 def test_len_string(language):
     def f():
         a = 'abcdefghij'
@@ -478,14 +470,6 @@ def test_len_string(language):
 
     assert epyc_f() == f()
 
-@pytest.mark.parametrize( 'language', (
-        pytest.param("fortran", marks = pytest.mark.fortran),
-        pytest.param("c", marks = [
-            pytest.mark.skip(reason="C has no support for strings. See #2061"),
-            pytest.mark.c]),
-        pytest.param("python", marks = pytest.mark.python)
-    )
-)
 def test_len_literal_string(language):
     def f():
         b = len('abcd')
diff --git a/tests/epyccel/test_epyccel_variable_annotations.py b/tests/epyccel/test_epyccel_variable_annotations.py
index 2bfedd79..8d6bff28 100644
--- a/tests/epyccel/test_epyccel_variable_annotations.py
+++ b/tests/epyccel/test_epyccel_variable_annotations.py
@@ -395,14 +395,6 @@ def test_inhomogeneous_tuple_annotation_8(language):
     epyc_inhomogeneous_tuple_annotation = epyccel(inhomogeneous_tuple_annotation, language = language)
     assert epyc_inhomogeneous_tuple_annotation() == inhomogeneous_tuple_annotation()
 
-@pytest.mark.parametrize( 'language', (
-        pytest.param("fortran", marks = pytest.mark.fortran),
-        pytest.param("c", marks = [
-            pytest.mark.skip(reason="C has no support for strings. See #2061"),
-            pytest.mark.c]),
-        pytest.param("python", marks = pytest.mark.python)
-    )
-)
 def test_str_declaration(language):
     def str_declaration():
         a : str = 'hello here is a very long string with more than 128 characters. This used to be a Fortran limit but now I can hold lots more characters. There is no limit!'
diff --git a/tests/pyccel/scripts/print_strings.py b/tests/pyccel/scripts/print_strings.py
index 3467b21d..befb0a1f 100644
--- a/tests/pyccel/scripts/print_strings.py
+++ b/tests/pyccel/scripts/print_strings.py
@@ -49,3 +49,5 @@ if __name__ == '__main__':
     Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
     Excepteur sint occaecat cupidatat non proident,
     sunt in culpa qui officia deserunt mollit anim id est laborum""")
+    a = "Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters."
+    print(a)
diff --git a/tests/stc_containers/leaks_check.py b/tests/stc_containers/leaks_check.py
index 3e4ecf0e..b270b737 100644
--- a/tests/stc_containers/leaks_check.py
+++ b/tests/stc_containers/leaks_check.py
@@ -27,6 +27,11 @@ def dict_reassign():
     a = {1:1,2:2}
     a = {1:1,2:2,3:3,4:4,5:5}
 
+def str_reassign():
+    a = 'hello'
+    a = 'hello world'
+    a = 'hello cruel world'
+
 def conditional_list(b1: bool):
     if (b1):
         a = [1,2,3]