Repository: pyccel/pyccel (python)

Issue:
[SETS] Add Fortran support for pop()
### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def foo():
    a = {1,2,1}
    b = a.pop()
    c = a.pop()
    return b,c
```

### Proposed Solution
This feature should be implemented using gFTL. Unfortunately the gFTL implementation of sets does not appear to be very developed for the moment so this issue will probably require a corresponding issue and maybe PR in gFTL.


Pull request description:
Add Fortran support for set method `pop`
Add Fortran support for set method `pop`. Fixes #2010 

**Commit Summary**
- Add `MacroUndef` class to avoid macros polluting namespaces
- Simplify code using `_get_comparison_operator` to avoid duplication
- Add `_build_gFTL_extension_module` to create extensions for gFTL types
- Add support for `set.pop`
- Fix Python printing so expressions that can be used in expressions don't end in `\n`
- Activate `set.pop` tests
- Activate `test_init_` functions which don't require specific language support

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29cd110b..ab1f34cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,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()`.
--   #1877 : Add C Support for set method `pop()`.
+-   #1877 : Add C and Fortran Support for set method `pop()`.
 -   #1917 : Add C and Fortran support for set method `add()`.
 -   #1918 : Add C and Fortran support for set method `clear()`.
 -   #1936 : Add missing C output for inline decorator example in documentation
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index bfebd900..2d95cf5c 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -107,7 +107,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `isdisjoint` | No |
 | `issubset` | No |
 | `issuperset` | No |
-| `pop` | C and Python |
+| **`pop`** | **Yes** |
 | `remove` | Python-only |
 | `symmetric_difference` | No |
 | `symmetric_difference_update` | No |
diff --git a/pyccel/ast/low_level_tools.py b/pyccel/ast/low_level_tools.py
index a7ce8e82..8029e409 100644
--- a/pyccel/ast/low_level_tools.py
+++ b/pyccel/ast/low_level_tools.py
@@ -12,7 +12,8 @@ from .datatypes import PyccelType
 
 __all__ = ('IteratorType',
            'PairType',
-           'MacroDefinition')
+           'MacroDefinition',
+           'MacroUndef')
 
 #------------------------------------------------------------------------------
 class IteratorType(PyccelType, metaclass=ArgumentSingleton):
@@ -127,3 +128,32 @@ class MacroDefinition(PyccelAstNode):
         """
         return self._obj
 
+#------------------------------------------------------------------------------
+class MacroUndef(PyccelAstNode):
+    """
+    A class for undefining a macro in a file.
+
+    A class for undefining a macro in a file.
+
+    Parameters
+    ----------
+    macro_name : str
+        The name of the macro.
+    """
+    _attribute_nodes = ()
+    __slots__ = ('_macro_name',)
+
+    def __init__(self, macro_name):
+        assert isinstance(macro_name, str)
+        self._macro_name = macro_name
+        super().__init__()
+
+    @property
+    def macro_name(self):
+        """
+        The name of the macro being undefined.
+
+        The name of the macro being undefined.
+        """
+        return self._macro_name
+
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index ca5c3de8..d05b7dda 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -52,6 +52,7 @@ from pyccel.ast.literals  import LiteralTrue, LiteralFalse, LiteralString
 from pyccel.ast.literals  import Nil
 
 from pyccel.ast.low_level_tools  import MacroDefinition, IteratorType, PairType
+from pyccel.ast.low_level_tools  import MacroUndef
 
 from pyccel.ast.mathext  import math_constants
 
@@ -265,6 +266,7 @@ class FCodePrinter(CodePrinter):
 
         self.prefix_module = prefix_module
 
+        self._generated_gFTL_types = {}
         self._generated_gFTL_extensions = {}
 
     def print_constant_imports(self):
@@ -522,6 +524,41 @@ class FCodePrinter(CodePrinter):
                         scope.rename_function(f, suggested_name)
                     f.cls_name = scope.get_new_name(f'{name}_{f.name}')
 
+    def _get_comparison_operator(self, element_type, imports_and_macros):
+        """
+        Get an AST node describing a comparison operator between two objects of the same type.
+
+        Get an AST node describing a comparison operator between two objects of the same type.
+        This is useful when defining gFTL modules.
+
+        Parameters
+        ----------
+        element_type : PyccelType
+            The data type to be compared.
+
+        imports_and_macros : list
+            A list of imports or macros for the gFTL module.
+
+        Returns
+        -------
+        PyccelAstNode
+            An AST node describing a comparison operator.
+        """
+        tmpVar_x = Variable(element_type, 'x')
+        tmpVar_y = Variable(element_type, 'y')
+        if isinstance(element_type.primitive_type, PrimitiveComplexType):
+            complex_tool_import = Import('pyc_tools_f90', Module('pyc_tools_f90',(),()))
+            self.add_import(complex_tool_import)
+            imports_and_macros.append(complex_tool_import)
+            compare_func = FunctionDef('complex_comparison',
+                                       [FunctionDefArgument(tmpVar_x), FunctionDefArgument(tmpVar_y)],
+                                       [FunctionDefResult(Variable(PythonNativeBool(), 'c'))], [])
+            lt_def = FunctionCall(compare_func, [tmpVar_x, tmpVar_y])
+        else:
+            lt_def = PyccelAssociativeParenthesis(PyccelLt(tmpVar_x, tmpVar_y))
+
+        return lt_def
+
     def _build_gFTL_module(self, expr_type):
         """
         Build the gFTL module to create container types.
@@ -541,13 +578,12 @@ class FCodePrinter(CodePrinter):
             The import which allows the new type to be accessed.
         """
         # Get the type used in the dict for compatible types (e.g. float vs float64)
-        matching_expr_type = next((t for t in self._generated_gFTL_extensions if expr_type == t), None)
+        matching_expr_type = next((t for t in self._generated_gFTL_types if expr_type == t), None)
         if matching_expr_type:
-            module = self._generated_gFTL_extensions[matching_expr_type]
+            module = self._generated_gFTL_types[matching_expr_type]
             mod_name = module.name
         else:
             if isinstance(expr_type, HomogeneousListType):
-                include = Import(LiteralString('vector/template.inc'), Module('_', (), ()))
                 element_type = expr_type.element_type
                 if isinstance(element_type, FixedSizeNumericType):
                     imports_and_macros = [MacroDefinition('T', element_type.primitive_type),
@@ -558,50 +594,32 @@ class FCodePrinter(CodePrinter):
                     imports_and_macros.append(MacroDefinition('T_rank', element_type.rank))
                 elif not isinstance(element_type, FixedSizeNumericType):
                     raise NotImplementedError("Support for lists of types defined in other modules is not yet implemented")
-                imports_and_macros.append(MacroDefinition('Vector', expr_type))
-                imports_and_macros.append(MacroDefinition('VectorIterator', IteratorType(expr_type)))
+                imports_and_macros.extend([MacroDefinition('Vector', expr_type),
+                                           MacroDefinition('VectorIterator', IteratorType(expr_type)),
+                                           Import(LiteralString('vector/template.inc'), Module('_', (), ())),
+                                           MacroUndef('Vector'),
+                                           MacroUndef('VectorIterator'),])
             elif isinstance(expr_type, HomogeneousSetType):
-                include = Import(LiteralString('set/template.inc'), Module('_', (), ()))
                 element_type = expr_type.element_type
                 imports_and_macros = []
                 if isinstance(element_type, FixedSizeNumericType):
-                    tmpVar_x = Variable(element_type, 'x')
-                    tmpVar_y = Variable(element_type, 'y')
-                    if isinstance(element_type.primitive_type, PrimitiveComplexType):
-                        complex_tool_import = Import('pyc_tools_f90', Module('pyc_tools_f90',(),()))
-                        self.add_import(complex_tool_import)
-                        imports_and_macros.append(complex_tool_import)
-                        compare_func = FunctionDef('complex_comparison',
-                                                   [FunctionDefArgument(tmpVar_x), FunctionDefArgument(tmpVar_y)],
-                                                   [FunctionDefResult(Variable(PythonNativeBool(), 'c'))], [])
-                        lt_def = FunctionCall(compare_func, [tmpVar_x, tmpVar_y])
-                    else:
-                        lt_def = PyccelAssociativeParenthesis(PyccelLt(tmpVar_x, tmpVar_y))
+                    lt_def = self._get_comparison_operator(element_type, imports_and_macros)
                     imports_and_macros.extend([MacroDefinition('T', element_type.primitive_type),
                               MacroDefinition('T_KINDLEN(context)', KindSpecification(element_type)),
                               MacroDefinition('T_LT(x,y)', lt_def)])
                 else:
                     raise NotImplementedError("Support for sets of types which define their own < operator is not yet implemented")
-                imports_and_macros.append(MacroDefinition('Set', expr_type))
-                imports_and_macros.append(MacroDefinition('SetIterator', IteratorType(expr_type)))
+                imports_and_macros.extend([MacroDefinition('Set', expr_type),
+                                           MacroDefinition('SetIterator', IteratorType(expr_type)),
+                                           Import(LiteralString('set/template.inc'), Module('_', (), ())),
+                                           MacroUndef('Set'),
+                                           MacroUndef('SetIterator')])
             elif isinstance(expr_type, DictType):
-                include = Import(LiteralString('map/template.inc'), Module('_', (), ()))
                 key_type = expr_type.key_type
                 value_type = expr_type.value_type
                 imports_and_macros = []
                 if isinstance(key_type, FixedSizeNumericType):
-                    tmpVar_x = Variable(key_type, 'x')
-                    tmpVar_y = Variable(key_type, 'y')
-                    if isinstance(key_type.primitive_type, PrimitiveComplexType):
-                        complex_tool_import = Import('pyc_tools_f90', Module('pyc_tools_f90',(),()))
-                        self.add_import(complex_tool_import)
-                        imports_and_macros.append(complex_tool_import)
-                        compare_func = FunctionDef('complex_comparison',
-                                                   [FunctionDefArgument(tmpVar_x), FunctionDefArgument(tmpVar_y)],
-                                                   [FunctionDefResult(Variable(PythonNativeBool(), 'c'))], [])
-                        lt_def = FunctionCall(compare_func, [tmpVar_x, tmpVar_y])
-                    else:
-                        lt_def = PyccelAssociativeParenthesis(PyccelLt(tmpVar_x, tmpVar_y))
+                    lt_def = self._get_comparison_operator(key_type, imports_and_macros)
                     imports_and_macros.extend([MacroDefinition('Key', key_type.primitive_type),
                                    MacroDefinition('Key_KINDLEN(context)', KindSpecification(key_type)),
                                    MacroDefinition('Key_LT(x,y)', lt_def)])
@@ -612,15 +630,68 @@ class FCodePrinter(CodePrinter):
                                    MacroDefinition('T_KINDLEN(context)', KindSpecification(value_type))])
                 else:
                     raise NotImplementedError(f"Support for dictionary values of type {value_type} not yet implemented")
-                imports_and_macros.append(MacroDefinition('Pair', PairType(key_type, value_type)))
-                imports_and_macros.append(MacroDefinition('Map', expr_type))
-                imports_and_macros.append(MacroDefinition('MapIterator', IteratorType(expr_type)))
+                imports_and_macros.extend([MacroDefinition('Pair', PairType(key_type, value_type)),
+                                           MacroDefinition('Map', expr_type),
+                                           MacroDefinition('MapIterator', IteratorType(expr_type)),
+                                           Import(LiteralString('map/template.inc'), Module('_', (), ())),
+                                           MacroUndef('Pair'),
+                                           MacroUndef('Map'),
+                                           MacroUndef('MapIterator')])
             else:
                 raise NotImplementedError(f"Unkown gFTL import for type {expr_type}")
 
             typename = self._print(expr_type)
             mod_name = f'{typename}_mod'
-            module = Module(mod_name, (), (), scope = Scope(), imports = [*imports_and_macros, include],
+            module = Module(mod_name, (), (), scope = Scope(), imports = imports_and_macros,
+                                       is_external = True)
+
+            self._generated_gFTL_types[expr_type] = module
+
+        return Import(f'gFTL_extensions/{mod_name}', module)
+
+    def _build_gFTL_extension_module(self, expr_type):
+        """
+        Build the gFTL module to create container extension functions.
+
+        Create a module which will import the gFTL include files
+        in order to create container types (e.g lists, sets, etc).
+        The name of the module is derived from the name of the type.
+
+        Parameters
+        ----------
+        expr_type : DataType
+            The data type for which extensions are required.
+
+        Returns
+        -------
+        Import
+            The import which allows the new type to be accessed.
+        """
+        # Get the type used in the dict for compatible types (e.g. float vs float64)
+        matching_expr_type = next((t for t in self._generated_gFTL_types if expr_type == t), None)
+        matching_expr_extensions = next((t for t in self._generated_gFTL_extensions if expr_type == t), None)
+        typename = self._print(expr_type)
+        mod_name = f'{typename}_extensions_mod'
+        if matching_expr_extensions:
+            module = self._generated_gFTL_extensions[matching_expr_extensions]
+        else:
+            if matching_expr_type is None:
+                matching_expr_type = self._build_gFTL_module(expr_type)
+                self.add_import(matching_expr_type)
+
+            type_module = matching_expr_type.source_module
+
+            if isinstance(expr_type, HomogeneousSetType):
+                set_filename = LiteralString('set/template.inc')
+                imports_and_macros = [Import(LiteralString('Set_extensions.inc'), Module('_', (), ())) \
+                                        if getattr(i, 'source', None) == set_filename else i \
+                                        for i in type_module.imports]
+                imports_and_macros.insert(0, matching_expr_type)
+                self.add_import(Import('gFTL_functions/Set_extensions', Module('_', (), ()), ignore_at_print = True))
+            else:
+                raise NotImplementedError(f"Unkown gFTL import for type {expr_type}")
+
+            module = Module(mod_name, (), (), scope = Scope(), imports = imports_and_macros,
                                        is_external = True)
 
             self._generated_gFTL_extensions[expr_type] = module
@@ -1253,6 +1324,14 @@ class FCodePrinter(CodePrinter):
         var = self._print(expr.set_variable)
         return f'call {var} % clear()\n'
 
+    def _print_SetPop(self, expr):
+        var = expr.set_variable
+        expr_type = var.class_type
+        var_code = self._print(expr.set_variable)
+        type_name = self._print(expr_type)
+        self.add_import(self._build_gFTL_extension_module(expr_type))
+        return f'{type_name}_pop({var_code})\n'
+
     #========================== Numpy Elements ===============================#
 
     def _print_NumpySum(self, expr):
@@ -3596,5 +3675,9 @@ class FCodePrinter(CodePrinter):
         obj = self._print(expr.object)
         return f'#define {name} {obj}\n'
 
+    def _print_MacroUndef(self, expr):
+        name = expr.macro_name
+        return f'#undef {name}\n'
+
     def _print_KindSpecification(self, expr):
         return f'(kind = {self.print_kind(expr.type_specifier)})'
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index d5b3cdc7..d9584819 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -11,7 +11,7 @@ from pyccel.ast.builtins   import PythonMin, PythonMax, PythonType, PythonBool,
 from pyccel.ast.builtins   import PythonComplex, DtypePrecisionToCastFunction
 from pyccel.ast.core       import CodeBlock, Import, Assign, FunctionCall, For, AsName, FunctionAddress
 from pyccel.ast.core       import IfSection, FunctionDef, Module, PyccelFunctionDef
-from pyccel.ast.datatypes  import HomogeneousTupleType, HomogeneousListType
+from pyccel.ast.datatypes  import HomogeneousTupleType, VoidType
 from pyccel.ast.functionalexpr import FunctionalFor
 from pyccel.ast.literals   import LiteralTrue, LiteralString, LiteralInteger
 from pyccel.ast.numpyext   import numpy_target_swap
@@ -841,9 +841,9 @@ class PythonCodePrinter(CodePrinter):
         key = self._print(expr.key)
         if expr.default_value:
             val = self._print(expr.default_value)
-            return f"{dict_obj}.pop({key}, {val})\n"
+            return f"{dict_obj}.pop({key}, {val})"
         else:
-            return f"{dict_obj}.pop({key})\n"
+            return f"{dict_obj}.pop({key})"
 
     def _print_DictGet(self, expr):
         dict_obj = self._print(expr.dict_obj)
@@ -871,7 +871,11 @@ class PythonCodePrinter(CodePrinter):
         name = expr.name
         args = "" if len(expr.args) == 0 or expr.args[-1] is None \
             else ', '.join(self._print(a) for a in expr.args)
-        return f"{set_var}.{name}({args})\n"
+        code = f"{set_var}.{name}({args})"
+        if expr.class_type is VoidType():
+            return f'{code}\n'
+        else:
+            return code
 
     def _print_Nil(self, expr):
         return 'None'
diff --git a/pyccel/codegen/utilities.py b/pyccel/codegen/utilities.py
index 0f6111f7..90dbaf1b 100644
--- a/pyccel/codegen/utilities.py
+++ b/pyccel/codegen/utilities.py
@@ -46,6 +46,7 @@ internal_libs = {
     "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)),
+    "gFTL_functions/Set_extensions"  : ("gFTL_functions", CompileObj("Set_Extensions.inc", folder="gFTL_functions", has_target_file = False)),
 }
 internal_libs["cwrapper_ndarrays"] = ("cwrapper_ndarrays", CompileObj("cwrapper_ndarrays.c",folder="cwrapper_ndarrays",
                                                              accelerators = ('python',),
diff --git a/pyccel/stdlib/gFTL_functions/Set_extensions.inc b/pyccel/stdlib/gFTL_functions/Set_extensions.inc
new file mode 100644
index 00000000..876d4a29
--- /dev/null
+++ b/pyccel/stdlib/gFTL_functions/Set_extensions.inc
@@ -0,0 +1,25 @@
+#include <set/header.inc>
+
+contains
+
+#define __IDENTITY(x) x
+#define __guard __set_guard
+#include "parameters/T/copy_set_T_to_internal_T.inc"
+#include "parameters/T/define_derived_macros.inc"
+
+  function __IDENTITY(Set)_pop(my_set) result(result)
+    class(Set), intent(inout) :: my_set
+    __T_declare_dummy__ :: result
+  
+    type(SetIterator) :: iter1
+    type(SetIterator) :: iter2
+  
+    iter1 = my_set%begin()
+  
+    result = iter1%of()
+  
+    iter2 = my_set%erase(iter1)
+  
+  end function __IDENTITY(Set)_pop
+
+#include <set/tail.inc>
diff --git a/pyproject.toml b/pyproject.toml
index c14a98ec..3b0a4728 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -58,6 +58,7 @@ include = [
   "pyccel/stdlib/**/*.h",
   "pyccel/stdlib/**/*.c",
   "pyccel/stdlib/**/*.f90",
+  "pyccel/stdlib/**/*.inc",
   "pyccel/extensions/STC/include"
 ]
 exclude = [


Test patch:
diff --git a/tests/epyccel/test_epyccel_sets.py b/tests/epyccel/test_epyccel_sets.py
index 72b7bf78..ddfcaa97 100644
--- a/tests/epyccel/test_epyccel_sets.py
+++ b/tests/epyccel/test_epyccel_sets.py
@@ -290,27 +290,29 @@ def test_set_with_set(python_only_language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_init_with_set(python_only_language):
+def test_init_with_set(language):
     def init_with_set():
         b = set({4.6, 7.9, 2.5})
-        return b
+        return len(b), b.pop(), b.pop(), b.pop()
 
-    epyc_init_with_set = epyccel(init_with_set, language = python_only_language)
+    epyc_init_with_set = epyccel(init_with_set, language = language)
     pyccel_result = epyc_init_with_set()
     python_result = init_with_set()
     assert isinstance(python_result, type(pyccel_result))
-    assert python_result == pyccel_result
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
 
-def test_set_init_with_list(python_only_language):
+def test_set_init_with_list(language):
     def init_with_list():
         b = set([4.6, 7.9, 2.5])
-        return b
+        return len(b), b.pop(), b.pop(), b.pop()
 
-    epyc_init_with_list = epyccel(init_with_list, language = python_only_language)
+    epyc_init_with_list = epyccel(init_with_list, language = language)
     pyccel_result = epyc_init_with_list()
     python_result = init_with_list()
     assert isinstance(python_result, type(pyccel_result))
-    assert python_result == pyccel_result
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
 
 
 def test_set_copy_from_arg1(python_only_language):
@@ -335,39 +337,39 @@ def test_set_copy_from_arg2(python_only_language):
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
 
-def test_Pop_int(stc_language):
+def test_Pop_int(language):
     def Pop_int():
         se = {2, 4, 9}
         el1 = se.pop()
         el2 = se.pop()
         el3 = se.pop()
         return el1, el2, el3
-    epyccel_remove = epyccel(Pop_int, language = stc_language)
+    epyccel_remove = epyccel(Pop_int, language = language)
     pyccel_result = set(epyccel_remove())
     python_result = set(Pop_int())
     assert python_result == pyccel_result
 
-def test_Pop_float(stc_language):
+def test_Pop_float(language):
     def Pop_float():
         se = {2.3 , 4.1, 9.5}
         el1 = se.pop()
         el2 = se.pop()
         el3 = se.pop()
         return el1, el2, el3
-    epyccel_remove = epyccel(Pop_float, language = stc_language)
+    epyccel_remove = epyccel(Pop_float, language = language)
     pyccel_result = set(epyccel_remove())
     python_result = set(Pop_float())
     assert python_result == pyccel_result
 
 
-def test_Pop_complex(stc_language):
+def test_Pop_complex(language):
     def Pop_complex():
         se = {4j , 1j, 7j}
         el1 = se.pop()
         el2 = se.pop()
         el3 = se.pop()
         return el1, el2, el3
-    epyccel_remove = epyccel(Pop_complex, language = stc_language)
+    epyccel_remove = epyccel(Pop_complex, language = language)
     pyccel_result = set(epyccel_remove())
     python_result = set(Pop_complex())
     assert python_result == pyccel_result