Repository: pyccel/pyccel (python)

Issue:
isdisjoint : Returns True if two sets have no elements in common.
### Feature Description

**Add support for isdisjoint() method for sets in the Python translation.**
`isdisjoint(other)` :   Returns True if the calling set and the specified other set have no common elements, otherwise returns False.

### Test Code

This Code Should Work 

```python
set1 = {1, 2, 3}
set2 = {4, 5, 6}
disjoint = set1.isdisjoint(set2)
print(disjoint)
>>> True
```

### Proposed Solution
See description in #1531 

Pull request:
Add support for `set.isdisjoint`
Add support for `set.isdisjoint`. Fixes #1746

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 487ea44b..077291f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,7 @@ All notable changes to this project will be documented in this file.
 -   #1754 : Add support for set method `update()`.
 -   #1744 : Add support for set method `intersection()`.
 -   #1745 : Add support for set method `intersection_update()`.
+-   #1745 : Add support for set method `isdisjoint()`.
 -   #2059 : Add C support for returning dictionaries from functions.
 -   #2164 : Add support for dict indexing.
 -   #1880 : Add support for dict method `clear()`.
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index 7db19f07..7ddd157c 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -104,7 +104,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | **`discard`** | **Yes** |
 | **`intersection`** | **Yes** |
 | **`intersection_update`** | **Yes** |
-| `isdisjoint` | No |
+| **`isdisjoint`** | **Yes** |
 | `issubset` | No |
 | `issuperset` | No |
 | **`pop`** | **Yes** |
diff --git a/pyccel/ast/builtin_methods/set_methods.py b/pyccel/ast/builtin_methods/set_methods.py
index c902af6a..e1a7c229 100644
--- a/pyccel/ast/builtin_methods/set_methods.py
+++ b/pyccel/ast/builtin_methods/set_methods.py
@@ -9,7 +9,7 @@ always available.
 
 This module contains objects which describe these methods within Pyccel's AST.
 """
-from pyccel.ast.datatypes import VoidType
+from pyccel.ast.datatypes import VoidType, PythonNativeBool
 from pyccel.ast.internals import PyccelFunction
 from pyccel.ast.basic import TypedAstNode
 
@@ -20,6 +20,7 @@ __all__ = (
     'SetDiscard',
     'SetIntersection',
     'SetIntersectionUpdate',
+    'SetIsDisjoint',
     'SetMethod',
     'SetPop',
     'SetUnion',
@@ -282,3 +283,29 @@ class SetIntersectionUpdate(SetMethod):
             if class_type != o.class_type:
                 raise TypeError(f"Only arguments of type {class_type} are supported for the functions intersection and .intersection_update")
         super().__init__(set_obj, *others)
+
+#==============================================================================
+
+class SetIsDisjoint(SetMethod):
+    """
+    Represents a call to the .isdisjoint() method.
+
+    Represents a call to the .isdisjoint() method. This method checks if two
+    sets have a null intersection.
+
+    Parameters
+    ----------
+    set_obj : TypedAstNode
+        The set object which the method is called from.
+    other_set_obj : TypedAstNode
+        The set object which is passed as an argument to the method.
+    """
+    __slots__ = ()
+    name = 'isdisjoint'
+    _class_type = PythonNativeBool()
+    _shape = None
+
+    def __init__(self, set_obj, other_set_obj):
+        if set_obj.class_type != other_set_obj.class_type:
+            raise TypeError("Is disjoint can only be used to compare sets of the same type.")
+        super().__init__(set_obj, other_set_obj)
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 935e586a..ab48b59c 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -8,7 +8,8 @@ This module contains all types which define a python class which is automaticall
 
 from pyccel.ast.builtin_methods.set_methods  import (SetAdd, SetClear, SetCopy, SetPop,
                                                      SetDiscard, SetUpdate, SetUnion,
-                                                     SetIntersection, SetIntersectionUpdate)
+                                                     SetIntersection, SetIntersectionUpdate,
+                                                     SetIsDisjoint)
 from pyccel.ast.builtin_methods.list_methods import (ListAppend, ListInsert, ListPop,
                                                      ListClear, ListExtend, ListRemove,
                                                      ListCopy, ListSort)
@@ -169,6 +170,7 @@ SetClass = ClassDef('set',
             PyccelFunctionDef('union', func_class = SetUnion),
             PyccelFunctionDef('intersection', func_class = SetIntersection),
             PyccelFunctionDef('intersection_update', func_class = SetIntersectionUpdate),
+            PyccelFunctionDef('isdisjoint', func_class = SetIsDisjoint),
             PyccelFunctionDef('update', func_class = SetUpdate),
             PyccelFunctionDef('__or__', func_class = SetUnion),
             PyccelFunctionDef('__and__', func_class = SetIntersection),
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index cfd47de9..debf1a20 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -2867,6 +2867,7 @@ class CCodePrinter(CodePrinter):
         var_type = self.get_c_type(dtype)
         self.add_import(Import('stc/hset', AsName(VariableTypeAnnotation(dtype), var_type)))
         set_var = self._print(ObjectAddress(expr.set_variable))
+        # See pyccel/stdlib/STC_Extensions/Set_extensions.h for the definition
         return f'{var_type}_pop({set_var})'
 
     def _print_SetClear(self, expr):
@@ -2895,6 +2896,7 @@ class CCodePrinter(CodePrinter):
         self.add_import(Import('stc/hset', AsName(VariableTypeAnnotation(class_type), var_type)))
         set_var = self._print(ObjectAddress(expr.set_variable))
         args = ', '.join([str(len(expr.args)), *(self._print(ObjectAddress(a)) for a in expr.args)])
+        # See pyccel/stdlib/STC_Extensions/Set_extensions.h for the definition
         return f'{var_type}_union({set_var}, {args})'
 
     def _print_SetIntersectionUpdate(self, expr):
@@ -2902,6 +2904,7 @@ class CCodePrinter(CodePrinter):
         var_type = self.get_c_type(class_type)
         self.add_import(Import('stc/hset', AsName(VariableTypeAnnotation(class_type), var_type)))
         set_var = self._print(ObjectAddress(expr.set_variable))
+        # See pyccel/stdlib/STC_Extensions/Set_extensions.h for the definition
         return ''.join(f'{var_type}_intersection_update({set_var}, {self._print(ObjectAddress(a))});\n' \
                 for a in expr.args)
 
@@ -2911,6 +2914,13 @@ class CCodePrinter(CodePrinter):
         arg_val = self._print(expr.args[0])
         return f'{var_type}_erase({set_var}, {arg_val});\n'
 
+    def _print_SetIsDisjoint(self, expr):
+        var_type = self.get_c_type(expr.set_variable.class_type)
+        set_var = self._print(ObjectAddress(expr.set_variable))
+        arg_val = self._print(ObjectAddress(expr.args[0]))
+        # See pyccel/stdlib/STC_Extensions/Set_extensions.h for the definition
+        return f'{var_type}_is_disjoint({set_var}, {arg_val});\n'
+
     def _print_PythonSet(self, expr):
         tmp_var = self.scope.get_temporary_variable(expr.class_type, shape = expr.shape,
                     memory_handling='heap')
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 1647b69b..53067817 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -1427,6 +1427,7 @@ class FCodePrinter(CodePrinter):
         var_code = self._print(expr.set_variable)
         type_name = self._print(expr_type)
         self.add_import(self._build_gFTL_extension_module(expr_type))
+        # See pyccel/stdlib/gFTL_functions/Set_extensions.inc for the definition
         return f'{type_name}_pop({var_code})\n'
 
     def _print_SetCopy(self, expr):
@@ -1472,6 +1473,7 @@ class FCodePrinter(CodePrinter):
         var_code = self._print(expr.set_variable)
         type_name = self._print(expr_type)
         self.add_import(self._build_gFTL_extension_module(expr_type))
+        # See pyccel/stdlib/gFTL_functions/Set_extensions.inc for the definition
         return ''.join(f'call {type_name}_intersection_update({var_code}, {self._print(arg)})\n' \
                 for arg in expr.args)
 
@@ -1481,6 +1483,16 @@ class FCodePrinter(CodePrinter):
         success = self.scope.get_temporary_variable(PythonNativeInt())
         return f'{success} = {var} % erase_value({val})\n'
 
+    def _print_SetIsDisjoint(self, expr):
+        var = expr.set_variable
+        expr_type = var.class_type
+        var_code = self._print(expr.set_variable)
+        arg_code = self._print(expr.args[0])
+        type_name = self._print(expr_type)
+        self.add_import(self._build_gFTL_extension_module(expr_type))
+        # See pyccel/stdlib/gFTL_functions/Set_extensions.inc for the definition
+        return f'{type_name}_is_disjoint({var_code}, {arg_code})'
+
    #========================== Dict Methods ================================#
 
     def _print_DictClear(self, expr):
diff --git a/pyccel/stdlib/STC_Extensions/Set_extensions.h b/pyccel/stdlib/STC_Extensions/Set_extensions.h
index 9077b343..a1e0f337 100644
--- a/pyccel/stdlib/STC_Extensions/Set_extensions.h
+++ b/pyccel/stdlib/STC_Extensions/Set_extensions.h
@@ -56,6 +56,29 @@ static inline void _c_MEMB(_intersection_update)(Self* self, Self* other) {
         }
     }
 }
+
+/**
+ * This function represents a call to the .isdisjoint() method.
+ * @param self : The set instance on which the function is called.
+ * @param other : The other set which may or may not be disjoint.
+ * @return True if the two sets have no elements in common, false otherwise.
+ */
+static inline bool _c_MEMB(_is_disjoint)(Self* self, Self* other) {
+    Self* shortest = _c_MEMB(_size)(self) < _c_MEMB(_size)(other) ? self : other;
+    Self* longest = self == shortest ? other : self;
+    _c_MEMB(_iter) itr = _c_MEMB(_begin)(shortest);
+    while (itr.ref)
+    {
+        i_key val = (*itr.ref);
+        if (_c_MEMB(_contains)(longest, val)) {
+            return false;
+        } else {
+            _c_MEMB(_next)(&itr);
+        }
+    }
+    return true;
+}
+
 #if defined (i_use_cmp) 
 // Function to get the minimum element from the set
 static inline i_key _c_MEMB(_min)(const Self* self) {
diff --git a/pyccel/stdlib/gFTL_functions/Set_extensions.inc b/pyccel/stdlib/gFTL_functions/Set_extensions.inc
index 361180e1..757d284e 100644
--- a/pyccel/stdlib/gFTL_functions/Set_extensions.inc
+++ b/pyccel/stdlib/gFTL_functions/Set_extensions.inc
@@ -1,3 +1,6 @@
+
+use, intrinsic :: ISO_C_Binding, only : b1 => C_BOOL
+
 #include <set/header.inc>
 
 contains
@@ -41,6 +44,39 @@ contains
 
   end subroutine __IDENTITY(Set)_intersection_update
 
+  function __IDENTITY(Set)_is_disjoint(this, other_set) result(Out)
+    class(Set), target, intent(in) :: this
+    class(Set), target, intent(in) :: other_set
+    logical(b1) :: Out
+
+    class(Set), pointer :: shortest
+    class(Set), pointer :: longest
+    type(SetIterator) :: iter
+    type(SetIterator) :: last
+
+    if (this % size() > other_set % size()) then
+      shortest => other_set
+      longest => this
+    else
+      shortest => this
+      longest => other_set
+    endif
+
+    iter = shortest%begin()
+    last = shortest%end()
+    do while (iter /= last)
+      if (longest % count(iter%of()) /= 0) then
+        Out = .false.
+        return
+      else
+        call iter%next()
+      end if
+    end do
+
+    Out = .true.
+
+  end function __IDENTITY(Set)_is_disjoint
+
   ! Sets in gFTL are BSTs, therefore the min is the first element
    function __IDENTITY(Set)_min(my_set) result(result)
     class(Set), intent(in) :: my_set


Test patch:
diff --git a/tests/epyccel/test_epyccel_sets.py b/tests/epyccel/test_epyccel_sets.py
index bd2f9bfd..8dbdfc56 100644
--- a/tests/epyccel/test_epyccel_sets.py
+++ b/tests/epyccel/test_epyccel_sets.py
@@ -756,3 +756,36 @@ def test_set_min_max(language):
     python_result = set_min_max()
     assert python_result == pyccel_result
     assert isinstance(python_result, type(pyccel_result))
+
+def test_set_is_disjoint(stc_language):
+    def set_is_disjoint(a : set[int], b : set[int]):
+        return a.isdisjoint(b)
+
+    epyccel_func = epyccel(set_is_disjoint, language = stc_language)
+    example_set1 = {1,2,3,4}
+    example_set2 = {5,6,7,8}
+    example_set3 = {7,8,2}
+    assert set_is_disjoint(example_set1, example_set2) == epyccel_func(example_set1, example_set2)
+    assert set_is_disjoint(example_set1, example_set3) == epyccel_func(example_set1, example_set3)
+    assert set_is_disjoint(example_set3, example_set2) == epyccel_func(example_set3, example_set2)
+
+def test_set_is_disjoint_fortran():
+    def set_is_disjoint1():
+        a = {1,2,3,4}
+        b = {5,6,7,8}
+        return a.isdisjoint(b)
+    def set_is_disjoint2():
+        a = {1,2,3,4}
+        b = {7,8,2}
+        return a.isdisjoint(b)
+    def set_is_disjoint3():
+        a = {7,8,2}
+        b = {5,6,7,8}
+        return a.isdisjoint(b)
+
+    epyccel_func1 = epyccel(set_is_disjoint1, language = 'fortran')
+    epyccel_func2 = epyccel(set_is_disjoint2, language = 'fortran')
+    epyccel_func3 = epyccel(set_is_disjoint3, language = 'fortran')
+    assert set_is_disjoint1() == epyccel_func1()
+    assert set_is_disjoint2() == epyccel_func2()
+    assert set_is_disjoint3() == epyccel_func3()