Repository: pyccel/pyccel (python)

Issue:
[SETS] Support sets as arguments in C 
### Describe the feature
Support passing sets as arguments to C code

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def f(a : set[int]):
    pass
```

### Proposed Solution
The cwrapper should be improved to ensure that sets can be read from Python and passed to C code.

### Prerequisites
#1659

Pull request description:
Support sets as arguments to functions
Improve the C-Python wrapper to allow passing sets as arguments. Fixes #1663

**Commit Summary**
- Add documentation for set annotation
- Add description for C-Python API set manipulation functions
- Add support for constant set arguments to C functions
- Remove include guards from `Set_extensions.h` to allow extensions to multiple set types
- Add a test for a function with a constant set argument

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a17ed43..761605df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@ All notable changes to this project will be documented in this file.
 -   #1689 : Add C and Fortran support for list method `append()`.
 -   #1876 : Add C support for indexing lists.
 -   #1690 : Add C support for list method `pop()`.
+-   #1663 : Add C support for sets as constant arguments.
 -   #1664 : Add C support for returning sets from functions.
 -   #2023 : Add support for iterating over a `set`.
 -   #1877 : Add C and Fortran Support for set method `pop()`.
diff --git a/docs/type_annotations.md b/docs/type_annotations.md
index b1420911..6c1c0a05 100644
--- a/docs/type_annotations.md
+++ b/docs/type_annotations.md
@@ -82,7 +82,15 @@ a : set[int] = {1, 2}
 b : set[bool] = {False, True}
 c : set[float] = {}
 ```
-So far sets can be declared as local variables or as results of functions.
+Sets can be declared as local variables, arguments or results of functions translated to C. An argument can be marked as constant using a string annotation or (in a module) using the `Final` qualifier:
+```python
+def f(a : 'const set[int]'):
+    pass
+
+from typing import Final
+def g(b : Final[set[bool]]):
+    pass
+```
 
 ## Dictionaries
 
diff --git a/pyccel/ast/cwrapper.py b/pyccel/ast/cwrapper.py
index 35825419..ad6d65a0 100644
--- a/pyccel/ast/cwrapper.py
+++ b/pyccel/ast/cwrapper.py
@@ -1143,7 +1143,7 @@ PyTuple_SetItem = FunctionDef(name = 'PyTuple_SetItem',
                         results = [FunctionDefResult(Variable(CNativeInt(), 'i'))])
 
 #-------------------------------------------------------------------
-#                          Set functions
+#                         Set functions
 #-------------------------------------------------------------------
 
 # https://docs.python.org/3/c-api/set.html#c.PySet_New
@@ -1159,6 +1159,36 @@ PySet_Add = FunctionDef(name = 'PySet_Add',
                     results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
                     body = [])
 
+# https://docs.python.org/3/c-api/set.html#c.PySet_Check
+PySet_Check = FunctionDef(name = 'PySet_Check',
+                    arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'set', memory_handling='alias'))],
+                    results = [FunctionDefResult(Variable(CNativeInt(), 'i'))],
+                    body = [])
+
+# https://docs.python.org/3/c-api/set.html#c.PySet_Size
+PySet_Size = FunctionDef(name = 'PySet_Size',
+                    arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'set', memory_handling='alias'))],
+                    results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
+                    body = [])
+
+# https://docs.python.org/3/c-api/object.html#c.PyObject_GetIter
+PySet_GetIter = FunctionDef(name = 'PyObject_GetIter',
+                        body = [],
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='iter', memory_handling='alias'))],
+                        results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
+
+# https://docs.python.org/3/c-api/set.html#c.PySet_Clear
+PySet_Clear = FunctionDef(name = 'PySet_Clear',
+                        body = [],
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='set', memory_handling='alias'))],
+                        results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))])
+
+# https://docs.python.org/3/c-api/iter.html#c.PyIter_Check
+PyIter_Next = FunctionDef(name = 'PyIter_Next',
+                        body = [],
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='iter', memory_handling='alias'))],
+                        results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
+
 
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
 check_type_registry = {
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 04c9ae51..9034867c 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -325,6 +325,34 @@ class CCodePrinter(CodePrinter):
         self._current_module = None
         self._in_header = False
 
+    def sort_imports(self, imports):
+        """
+        Sort imports to avoid any errors due to bad ordering.
+
+        Sort imports. This is important so that types exist before they are used to create
+        container types. E.g. it is important that complex or inttypes be imported before
+        vec_int or vec_double_complex is declared.
+
+        Parameters
+        ----------
+        imports : list[Import]
+            A list of the imports.
+
+        Returns
+        -------
+        list[Import]
+            A sorted list of the imports.
+        """
+        import_src = [str(i.source) for i in imports]
+        stc_imports = [i for i in import_src if i.startswith('stc/')]
+        dependent_imports = [i for i in import_src if i in import_header_guard_prefix]
+        non_stc_imports = [i for i in import_src if i not in chain(stc_imports, dependent_imports)]
+        stc_imports.sort()
+        dependent_imports.sort()
+        non_stc_imports.sort()
+        sorted_imports = [imports[import_src.index(name)] for name in chain(non_stc_imports, stc_imports, dependent_imports)]
+        return sorted_imports
+
     def _format_code(self, lines):
         return self.indent_code(lines)
 
@@ -364,13 +392,16 @@ class CCodePrinter(CodePrinter):
             return True
         if isinstance(a, FunctionCall):
             a = a.funcdef.results[0].var
-        if isinstance(getattr(a, 'dtype', None), CustomDataType) and a.is_argument:
-            return True
-
         if not isinstance(a, Variable):
             return False
-        return (a.is_alias and not isinstance(a.class_type, (HomogeneousTupleType, NumpyNDArrayType))) \
-                or a.is_optional or \
+        if isinstance(a.class_type, (HomogeneousTupleType, NumpyNDArrayType)):
+            return a.is_optional or any(a is bi for b in self._additional_args for bi in b)
+
+        if isinstance(a.class_type, (CustomDataType, HomogeneousContainerType, DictType)) \
+                and a.is_argument and not a.is_const:
+            return True
+
+        return a.is_alias or a.is_optional or \
                 any(a is bi for b in self._additional_args for bi in b)
 
     #========================== Numpy Elements ===============================#
@@ -745,7 +776,7 @@ class CCodePrinter(CodePrinter):
             op = '<' if isinstance(expr, PythonMin) else '>'
             return f"({arg1} {op} {arg2} ? {arg1} : {arg2})"
         elif len(arg) > 2 and isinstance(arg.dtype.primitive_type, (PrimitiveFloatingPointType, PrimitiveIntegerType)):
-            key = self.get_declare_type(arg[0])
+            key = self.get_c_type(arg[0].class_type)
             self.add_import(Import('stc/common', AsName(VariableTypeAnnotation(arg.dtype), key)))
             self.add_import(Import('Common_extensions',
                                    AsName(VariableTypeAnnotation(arg.dtype), key),
@@ -851,6 +882,7 @@ class CCodePrinter(CodePrinter):
 
         # Print imports last to be sure that all additional_imports have been collected
         imports = [*expr.module.imports, *self._additional_imports.values()]
+        imports = self.sort_imports(imports)
         imports = ''.join(self._print(i) for i in imports)
 
         self._in_header = False
@@ -2658,6 +2690,7 @@ class CCodePrinter(CodePrinter):
         decs = ''.join(self._print(Declare(v)) for v in variables)
 
         imports = [*expr.imports, *self._additional_imports.values()]
+        imports = self.sort_imports(imports)
         imports = ''.join(self._print(i) for i in imports)
 
         self.exit_scope()
@@ -2724,18 +2757,18 @@ class CCodePrinter(CodePrinter):
         return f'{var_type}_pop({set_var})'
 
     def _print_SetClear(self, expr):
-        var_type = self.get_declare_type(expr.set_variable)
+        var_type = self.get_c_type(expr.set_variable.class_type)
         set_var = self._print(ObjectAddress(expr.set_variable))
         return f'{var_type}_clear({set_var});\n'
 
     def _print_SetAdd(self, expr):
-        var_type = self.get_declare_type(expr.set_variable)
+        var_type = self.get_c_type(expr.set_variable.class_type)
         set_var = self._print(ObjectAddress(expr.set_variable))
         arg = self._print(expr.args[0])
         return f'{var_type}_push({set_var}, {arg});\n'
 
     def _print_SetCopy(self, expr):
-        var_type = self.get_declare_type(expr.set_variable)
+        var_type = self.get_c_type(expr.set_variable.class_type)
         set_var = self._print(expr.set_variable)
         return f'{var_type}_clone({set_var})'
 
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index 27b4eac9..13f0c0c7 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -11,8 +11,9 @@ import warnings
 from pyccel.ast.bind_c        import BindCFunctionDef, BindCPointer, BindCFunctionDefArgument
 from pyccel.ast.bind_c        import BindCModule, BindCVariable, BindCFunctionDefResult
 from pyccel.ast.bind_c        import BindCClassDef, BindCClassProperty
-from pyccel.ast.builtins      import PythonTuple, PythonRange, PythonLen
-from pyccel.ast.builtin_methods.set_methods import SetPop
+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.class_defs    import StackArrayClass
 from pyccel.ast.core          import Interface, If, IfSection, Return, FunctionCall
 from pyccel.ast.core          import FunctionDef, FunctionDefArgument, FunctionDefResult
@@ -33,6 +34,8 @@ from pyccel.ast.cwrapper      import PySys_GetObject, PyUnicode_FromString, PyGe
 from pyccel.ast.cwrapper      import PyTuple_Size, PyTuple_Check, PyTuple_New
 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.c_concepts    import ObjectAddress, PointerCast, CStackArray, CNativeInt
 from pyccel.ast.datatypes     import VoidType, PythonNativeInt, CustomDataType, DataTypeFactory
 from pyccel.ast.datatypes     import FixedSizeNumericType, HomogeneousTupleType, PythonNativeBool
@@ -342,6 +345,32 @@ class CToPythonWrapper(Wrapper):
             tuple_checks = IfSection(tuple_check, [size_assign, Assign(type_check_condition, LiteralTrue()), internal_type_check])
             default_value = IfSection(LiteralTrue(), [Assign(type_check_condition, LiteralFalse())])
             body.append(If(tuple_checks, default_value))
+
+        elif isinstance(arg.class_type, HomogeneousSetType):
+            # Create type check result variable
+            type_check_condition = self.scope.get_temporary_variable(PythonNativeBool(), 'is_homog_set')
+
+            # Check if the object is a set
+            set_check = PySet_Check(py_obj)
+
+            # If the set is an object check that the elements have the right type
+            for_scope = self.scope.create_new_loop_scope()
+            size_var = self.scope.get_temporary_variable(PythonNativeInt(), 'size')
+            idx = self.scope.get_temporary_variable(CNativeInt())
+            indexed_py_obj = self.scope.get_temporary_variable(PyccelPyObject(), memory_handling='alias')
+            iter_obj = self.scope.get_temporary_variable(PyccelPyObject(), 'iter', memory_handling='alias')
+
+            size_assign = Assign(size_var, PySet_Size(py_obj))
+            iter_assign = AliasAssign(iter_obj, PySet_GetIter(py_obj))
+            indexed_init = AliasAssign(indexed_py_obj, PyIter_Next(iter_obj))
+            for_body = [indexed_init]
+            internal_type_check_condition, _ = self._get_type_check_condition(indexed_py_obj, arg[0], False, for_body)
+            for_body.append(Assign(type_check_condition, PyccelAnd(type_check_condition, internal_type_check_condition)))
+            internal_type_check = For((idx,), PythonRange(size_var), for_body, scope = for_scope)
+
+            set_checks = IfSection(set_check, [size_assign, iter_assign, Assign(type_check_condition, LiteralTrue()), internal_type_check])
+            default_value = IfSection(LiteralTrue(), [Assign(type_check_condition, LiteralFalse())])
+            body.append(If(set_checks, default_value))
         else:
             errors.report(f"Can't check the type of an array of {arg.class_type}\n"+PYCCEL_RESTRICTION_TODO,
                     symbol=arg, severity='fatal')
@@ -1138,6 +1167,50 @@ class CToPythonWrapper(Wrapper):
         else:
             return Assign(results, func(*args))
 
+    def connect_pointer_targets(self, orig_var, python_res, funcdef, is_bind_c):
+        """
+        Get the code to connect pointers to their targets.
+
+        Get the code to connect pointers to their targets. The connection is done via reference
+        counting to ensure that the target is not cleaned by the garbage collector before the
+        pointer.
+
+        Parameters
+        ----------
+        orig_var : Variable
+            The result of the function being wrapped.
+        python_res : Variable
+            The Python accessible result of the function being wrapped.
+        funcdef : FunctionDef
+            The function being wrapped.
+        is_bind_c : bool
+            True if the code is translated from a C-compatible language. False if the
+            translated code is in C.
+
+        Returns
+        -------
+        list
+            Any nodes which must be printed to increase reference counts.
+        """
+        python_args = funcdef.bind_c_arguments if is_bind_c else funcdef.arguments
+        arg_targets = funcdef.result_pointer_map.get(orig_var, ())
+        n_targets = len(arg_targets)
+        if n_targets == 1:
+            collect_arg = self._python_object_map[python_args[arg_targets[0]]]
+            return self._incref_return_pointer(collect_arg, python_res, orig_var)
+        elif n_targets > 1:
+            if isinstance(orig_var.class_type, NumpyNDArrayType):
+                raise errors.report((f"Can't determine the pointer target for the return object {orig_var}. "
+                            "Please avoid calling this function to prevent accidental creation of dangling pointers."),
+                        symbol = getattr(funcdef, 'original_function', funcdef), severity='warning')
+            else:
+                body = []
+                for t in arg_targets:
+                    collect_arg = self._python_object_map[python_args[t]]
+                    body.extend(self._incref_return_pointer(collect_arg, python_res, orig_var))
+                return body
+        return []
+
     #--------------------------------------------------------------------------------------------------------------------------------------------
 
     def _wrap_Module(self, expr):
@@ -1451,6 +1524,7 @@ class CToPythonWrapper(Wrapper):
                 else:
                     body.append(Deallocate(v))
         body.extend(wrapped_results['body'])
+        body.extend(ai for arg in wrapped_args for ai in arg['clean_up'])
 
         # Pack the Python compatible results of the function into one argument.
         if python_result_variable is Py_None:
@@ -1553,7 +1627,7 @@ class CToPythonWrapper(Wrapper):
         else:
             body.extend(cast)
 
-        return {'body': body, 'args': arg_vars}
+        return {'body': body, 'args': arg_vars, 'clean_up': arg_extraction.get('clean_up', ())}
 
     def _wrap_Variable(self, expr):
         """
@@ -2022,7 +2096,7 @@ class CToPythonWrapper(Wrapper):
 
         Extract the C-compatible FunctionDefArgument from the PythonObject.
         The C-compatible argument is extracted from collect_arg which holds a Python
-        oject into arg_var.
+        object into arg_var.
 
         The extraction is done by finding the appropriate function
         _extract_X_FunctionDefArgument for the object expr. X is the class type of the
@@ -2076,7 +2150,7 @@ class CToPythonWrapper(Wrapper):
 
         Extract the C-compatible scalar FunctionDefArgument from the PythonObject.
         The C-compatible argument is extracted from collect_arg which holds a Python
-        oject into arg_var.
+        object into arg_var.
 
         The extraction is done by calling a function from the C-Python API. These functions
         are indexed in the dictionary `py_to_c_registry`.
@@ -2140,7 +2214,7 @@ class CToPythonWrapper(Wrapper):
 
         Extract the C-compatible class FunctionDefArgument from the PythonObject.
         The C-compatible argument is extracted from collect_arg which holds a Python
-        oject into arg_var.
+        object into arg_var.
 
         The extraction is done by accessing the pointer from the `instance` attribute of the
         Pyccel generated class definition.
@@ -2207,7 +2281,7 @@ class CToPythonWrapper(Wrapper):
 
         Extract the C-compatible NumPy array FunctionDefArgument from the PythonObject.
         The C-compatible argument is extracted from collect_arg which holds a Python
-        oject into arg_var.
+        object into arg_var.
 
         The extraction is done by calling the function `pyarray_to_ndarray` from the stdlib.
 
@@ -2274,7 +2348,7 @@ class CToPythonWrapper(Wrapper):
 
         Extract the C-compatible homogeneous tuple FunctionDefArgument from the PythonObject.
         The C-compatible argument is extracted from collect_arg which holds a Python
-        oject into arg_var.
+        object into arg_var.
 
         The extraction is done by allocating an array and filling the elements with values
         extracted from the indexed Python tuple in collect_arg.
@@ -2352,49 +2426,100 @@ class CToPythonWrapper(Wrapper):
 
         return {'body': body, 'args': arg_vars}
 
-    def connect_pointer_targets(self, orig_var, python_res, funcdef, is_bind_c):
+    def _extract_HomogeneousSetType_FunctionDefArgument(self, orig_var, collect_arg, bound_argument,
+            is_bind_c_argument, *, arg_var = None):
         """
-        Get the code to connect pointers to their targets.
+        Extract the C-compatible homogeneous tuple FunctionDefArgument from the PythonObject.
 
-        Get the code to connect pointers to their targets. The connection is done via reference
-        counting to ensure that the target is not cleaned by the garbage collector before the
-        pointer.
+        Extract the C-compatible homogeneous tuple FunctionDefArgument from the PythonObject.
+        The C-compatible argument is extracted from collect_arg which holds a Python
+        object into arg_var.
+
+        The extraction is done by allocating an array and filling the elements with values
+        extracted from the indexed Python tuple in collect_arg.
 
         Parameters
         ----------
-        orig_var : Variable
-            The result of the function being wrapped.
-        python_res : Variable
-            The Python accessible result of the function being wrapped.
-        funcdef : FunctionDef
-            The function being wrapped.
-        is_bind_c : bool
-            True if the code is translated from a C-compatible language. False if the
-            translated code is in C.
+        orig_var : Variable | IndexedElement
+            An object representing the variable or an element of the variable from the
+            FunctionDefArgument being wrapped.
+
+        collect_arg : Variable
+            A variable with type PythonObject* holding the Python argument from which the
+            C-compatible argument should be collected.
+
+        bound_argument : bool
+            True if the argument is the self argument of a class method. False otherwise.
+            This should always be False for this function.
+
+        is_bind_c_argument : bool
+            True if the argument was saved in a BindCFunctionDefArgument. False otherwise.
+
+        arg_var : Variable | IndexedElement, optional
+            A variable or an element of the variable representing the argument that
+            will be passed to the low-level function call.
 
         Returns
         -------
-        list
-            Any nodes which must be printed to increase reference counts.
+        list[PyccelAstNode]
+            A list of expressions which extract the argument from collect_arg into arg_var.
         """
-        python_args = funcdef.bind_c_arguments if is_bind_c else funcdef.arguments
-        arg_targets = funcdef.result_pointer_map.get(orig_var, ())
-        n_targets = len(arg_targets)
-        if n_targets == 1:
-            collect_arg = self._python_object_map[python_args[arg_targets[0]]]
-            return self._incref_return_pointer(collect_arg, python_res, orig_var)
-        elif n_targets > 1:
-            if isinstance(orig_var.class_type, NumpyNDArrayType):
-                raise errors.report((f"Can't determine the pointer target for the return object {orig_var}. "
-                            "Please avoid calling this function to prevent accidental creation of dangling pointers."),
-                        symbol = getattr(funcdef, 'original_function', funcdef), severity='warning')
-            else:
-                body = []
-                for t in arg_targets:
-                    collect_arg = self._python_object_map[python_args[t]]
-                    body.extend(self._incref_return_pointer(collect_arg, python_res, orig_var))
-                return body
-        return []
+        assert arg_var is None
+
+        if orig_var.is_optional:
+            errors.report("Optionals are not yet supported",
+                    severity='fatal', symbol=orig_var)
+
+        assert not bound_argument
+
+        size_var = self.scope.get_temporary_variable(PythonNativeInt(), self.scope.get_new_name(f'{orig_var.name}_size'))
+
+        if is_bind_c_argument:
+            raise errors.report("Fortran set interface is not yet implemented", severity='fatal', symbol=orig_var)
+        else:
+            arg_var = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False,
+                                    memory_handling='heap', new_class = Variable)
+            self._wrapping_arrays = True
+            self.scope.insert_variable(arg_var, orig_var.name)
+            arg_vars = [arg_var]
+            body = [Assign(arg_var, PythonSet())]
+            insert_func = SetAdd
+
+        idx = self.scope.get_temporary_variable(CNativeInt())
+        indexed_orig_var = self.scope.get_temporary_variable(orig_var.class_type.element_type)
+        indexed_collect_arg = self.scope.get_temporary_variable(PyccelPyObject(), memory_handling='alias')
+
+        iter_obj = self.scope.get_temporary_variable(PyccelPyObject(), 'iter', memory_handling='alias')
+
+        body += [Assign(size_var, PySet_Size(collect_arg)),
+                 AliasAssign(iter_obj, PySet_GetIter(collect_arg))]
+
+        for_scope = self.scope.create_new_loop_scope()
+        self.scope = for_scope
+        for_body = [AliasAssign(indexed_collect_arg, PyIter_Next(iter_obj))]
+        for_body += self._extract_FunctionDefArgument(indexed_orig_var, indexed_collect_arg,
+                                    bound_argument, is_bind_c_argument, arg_var = indexed_orig_var)['body']
+        for_body.append(insert_func(arg_var, indexed_orig_var))
+        self.exit_scope()
+
+        body.append(For((idx,), PythonRange(size_var), for_body, scope = for_scope))
+
+        clean_up = []
+        if not orig_var.is_const:
+            element_extraction = self._extract_FunctionDefResult(IndexedElement(orig_var, idx),
+                                            is_bind_c_argument, None)
+            elem_set = PySet_Add(collect_arg, element_extraction['py_result'])
+            for_body = [*element_extraction['body'],
+                    If(IfSection(PyccelEq(elem_set, PyccelUnarySub(LiteralInteger(1))),
+                                             [Return([self._error_exit_code])]))]
+
+            loop_iterator = VariableIterator(arg_var)
+            loop_iterator.set_loop_counter(idx)
+            clean_up = [If(IfSection(PyccelEq(PySet_Clear(collect_arg), PyccelUnarySub(LiteralInteger(1))),
+                                             [Return([self._error_exit_code])])),
+                    For((element_extraction['c_results'][0],), loop_iterator, for_body, for_scope)]
+
+        return {'body': body, 'args': arg_vars, 'clean_up': clean_up}
 
     def _extract_FunctionDefResult(self, orig_var, is_bind_c, funcdef = None):
         """
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index b920cda5..dcf6b5ec 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -4126,8 +4126,10 @@ class SemanticParser(BasicParser):
                 return [ann]
 
         # Filter out unused templates
-        templatable_args = [unpack(a.annotation) for a in expr.arguments if isinstance(a.annotation, (SyntacticTypeAnnotation, UnionTypeAnnotation))]
-        arg_annotations = [annot for a in templatable_args for annot in a if isinstance(annot, SyntacticTypeAnnotation)]
+        templatable_args = [unpack(a.annotation) for a in expr.arguments \
+                if isinstance(a.annotation, (SyntacticTypeAnnotation, UnionTypeAnnotation, TypingFinal))]
+        arg_annotations = [annot for a in templatable_args for annot in a \
+                if isinstance(annot, (SyntacticTypeAnnotation, TypingFinal))]
         used_type_names = set(t for a in arg_annotations for t in a.get_attribute_nodes(PyccelSymbol))
         templates = {t: v for t,v in templates.items() if t in used_type_names}
 
diff --git a/pyccel/stdlib/STC_Extensions/Set_extensions.h b/pyccel/stdlib/STC_Extensions/Set_extensions.h
index 8b3c8bb0..9b083eb0 100644
--- a/pyccel/stdlib/STC_Extensions/Set_extensions.h
+++ b/pyccel/stdlib/STC_Extensions/Set_extensions.h
@@ -1,5 +1,3 @@
-#ifndef SET_EXTENSIONS_H
-#define SET_EXTENSIONS_H
 #include <stdarg.h>
 
 // This function represents a call to the .pop() method.
@@ -43,4 +41,3 @@ static inline i_type _c_MEMB(_union)(i_type* self, int n, ...) {
 #undef i_type
 #undef i_key
 #include <stc/priv/template2.h>
-#endif


Test patch:
diff --git a/tests/epyccel/test_epyccel_sets.py b/tests/epyccel/test_epyccel_sets.py
index acb045b2..4775215b 100644
--- a/tests/epyccel/test_epyccel_sets.py
+++ b/tests/epyccel/test_epyccel_sets.py
@@ -1,6 +1,7 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
 import pytest
 from pyccel import epyccel
+from pyccel.decorators import template
 
 @pytest.fixture( params=[
         pytest.param("fortran", marks = [
@@ -667,6 +668,36 @@ def test_set_iter_prod(language):
     assert python_result == pyccel_result
     assert isinstance(python_result, type(pyccel_result))
 
+def test_set_const_arg(stc_language):
+    @template('T', ['int', 'float', 'complex'])
+    def set_arg(arg : 'const set[T]', my_sum : 'T'):
+        for ai in arg:
+            my_sum += ai
+        return my_sum
+
+    epyccel_func = epyccel(set_arg, language = stc_language)
+    int_arg = {1,2,3,4,5,6,7}
+    float_arg = {1.5, 2.5, 3.5, 4.5, 6.7}
+    complex_arg = {1+0j,4j,2.5+2j}
+    for arg in (int_arg, float_arg, complex_arg):
+        start = type(next(iter(arg)))(0)
+        pyccel_result = epyccel_func(arg, start)
+        python_result = set_arg(arg, start)
+        assert python_result == pyccel_result
+        assert isinstance(pyccel_result, type(python_result))
+
+def test_set_arg(stc_language):
+    def set_arg(arg : 'set[int]', n : int):
+        arg.update(range(n))
+
+    epyccel_func = epyccel(set_arg, language = stc_language)
+    arg_pyc = {7,8,9,10}
+    arg_pyt = arg_pyc.copy()
+    n = 6
+    epyccel_func(arg_pyc, n)
+    set_arg(arg_pyt, n)
+    assert arg_pyc == arg_pyt
+
 def test_set_return(stc_language):
     def set_return():
         a = {1,2,3,4,5}