Repository: pyccel/pyccel (python)

Issue:
Add Python support for set() function
### Describe the feature
I would like to be able to use the `set()` function. This is important so that empty sets can be created. The `set()` function is different to the `{,}` function as it takes an iterable.

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
if __name__ == '__main__':
    a = set([1,2,3])
    b = [4,5,6]
    c = set(b)
    d = set(c)
```

### Proposed Solution
This feature should be implemented similarly to what was done for tuples. The equivalent issues for tuples was resolved in #1589.
The pertinent code is:
https://github.com/pyccel/pyccel/blob/d64be7ff7072faa07ce19c9e70fa02ed45a25c9c/pyccel/ast/builtins.py#L596-L620


### Additional context
It is important to handle the `set` function so that this function can be used for type annotations. If the `{,}` function is used bugs such as #1892 can arise.

Pull request description:
Add Python Support for set initialisation with `set()`
Add support for the `set()` function in Python. This fixes #1893. 
In this PR, the `PythonSetFunction` class is introduced to handle calls to the `set()` function, along with the addition of related tests.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65b41003..7c653634 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file.
 -   #1844 : Add line numbers and code to errors from built-in function calls.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[DEVELOPER\] Added an improved traceback to the developer-mode errors for errors in function calls.
+-   #1893 : Add Python support for set initialisation with `set()`.
 
 ### Fixed
 
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index 7d61a5f6..1df3a2f9 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -50,6 +50,7 @@ __all__ = (
     'PythonRange',
     'PythonReal',
     'PythonSet',
+    'PythonSetFunction',
     'PythonSum',
     'PythonTuple',
     'PythonTupleFunction',
@@ -774,6 +775,36 @@ class PythonSet(TypedAstNode):
         """
         return True
 
+
+class PythonSetFunction(PyccelFunction):
+    """
+    Class representing a call to the `set` function.
+
+    Class representing a call to the `set` function. This is
+    different to the `{,}` syntax as it only takes one argument
+    and unpacks any variables.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function call.
+    """
+
+    __slots__ = ('_shape', '_class_type')
+    name = 'set'
+    def __new__(cls, arg):
+        if isinstance(arg.class_type, HomogeneousSetType):
+            return arg
+        elif isinstance(arg, (PythonList, PythonSet, PythonTuple)):
+            return PythonSet(*arg)
+        else:
+            return super().__new__(cls)
+
+    def __init__(self, copied_obj):
+        self._class_type = copied_obj.class_type
+        self._shape = copied_obj.shape
+        super().__init__(copied_obj)
+
 #==============================================================================
 class PythonMap(PyccelFunction):
     """
@@ -1234,4 +1265,5 @@ builtin_functions_dict = {
     'str'      : LiteralString,
     'type'     : PythonType,
     'tuple'    : PythonTupleFunction,
+    'set'      : PythonSetFunction
 }
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 03a203f3..4c6e2108 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -21,7 +21,7 @@ from sympy import ceiling
 from pyccel.utilities.strings import random_string
 from pyccel.ast.basic         import PyccelAstNode, TypedAstNode, ScopedAstNode
 
-from pyccel.ast.builtins import PythonPrint, PythonTupleFunction
+from pyccel.ast.builtins import PythonPrint, PythonTupleFunction, PythonSetFunction
 from pyccel.ast.builtins import PythonComplex
 from pyccel.ast.builtins import builtin_functions_dict, PythonImag, PythonReal
 from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet
@@ -64,7 +64,7 @@ from pyccel.ast.datatypes import CustomDataType, PyccelType, TupleType, VoidType
 from pyccel.ast.datatypes import PrimitiveIntegerType, HomogeneousListType, StringType, SymbolicType
 from pyccel.ast.datatypes import PythonNativeBool, PythonNativeInt, PythonNativeFloat
 from pyccel.ast.datatypes import DataTypeFactory, PrimitiveFloatingPointType
-from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType
+from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType, HomogeneousSetType
 from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType
 
 from pyccel.ast.functionalexpr import FunctionalSum, FunctionalMax, FunctionalMin, GeneratorComprehension, FunctionalFor
@@ -146,6 +146,12 @@ import pyccel.decorators as def_decorators
 errors = Errors()
 pyccel_stage = PyccelStage()
 
+type_container = {
+                   PythonTupleFunction : HomogeneousTupleType,
+                   PythonList : HomogeneousListType,
+                   PythonSetFunction : HomogeneousSetType
+                  }
+
 #==============================================================================
 
 def _get_name(var):
@@ -1920,7 +1926,7 @@ class SemanticParser(BasicParser):
                 raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
                         severity='fatal', symbol=expr)
             rank = 1
-            if len(args) == 2 and args[1] is LiteralEllipsis():
+            if len(args) == 2 and args[1] is LiteralEllipsis() or len(args) == 1:
                 syntactic_annotation = args[0]
                 if not isinstance(syntactic_annotation, SyntacticTypeAnnotation):
                     pyccel_stage.set_stage('syntactic')
@@ -1928,10 +1934,8 @@ class SemanticParser(BasicParser):
                     pyccel_stage.set_stage('semantic')
                 internal_datatypes = self._visit(syntactic_annotation)
                 type_annotations = []
-                if dtype_cls is PythonTupleFunction:
-                    class_type = HomogeneousTupleType
-                elif dtype_cls is PythonList:
-                    class_type = HomogeneousListType
+                if dtype_cls in type_container :
+                    class_type = type_container[dtype_cls]
                 else:
                     raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
                             severity='fatal', symbol=expr)


Test patch:
diff --git a/tests/epyccel/test_epyccel_sets.py b/tests/epyccel/test_epyccel_sets.py
index afd7d224..c8c37791 100644
--- a/tests/epyccel/test_epyccel_sets.py
+++ b/tests/epyccel/test_epyccel_sets.py
@@ -283,3 +283,84 @@ def test_update_tuple_as_arg(language):
     pyccel_result = epyccel_update()
     python_result =  update_tuple_as_arg()
     assert python_result == pyccel_result
+
+def test_set_with_list(language):
+    def set_With_list():
+        a = [1.6, 6.3, 7.2]
+        b = set(a)
+        return b
+
+    epyc_set_With_list = epyccel(set_With_list, language = language)
+    pyccel_result = epyc_set_With_list()
+    python_result = set_With_list()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_set_with_tuple(language):
+    def set_With_tuple():
+        a = (1j, 6j, 7j)
+        b = set(a)
+        return b
+
+    epyc_set_With_tuple = epyccel(set_With_tuple, language = language)
+    pyccel_result = epyc_set_With_tuple()
+    python_result = set_With_tuple()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_set_with_set(language):
+    def set_With_set():
+        a = {True, False, True}  #pylint: disable=duplicate-value
+        b = set(a)
+        return b
+
+    epyc_set_With_set = epyccel(set_With_set, language = language)
+    pyccel_result = epyc_set_With_set()
+    python_result = set_With_set()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_init_with_set(language):
+    def init_with_set():
+        b = set({4.6, 7.9, 2.5})
+        return b
+
+    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
+
+def test_set_init_with_list(language):
+    def init_with_list():
+        b = set([4.6, 7.9, 2.5])
+        return b
+
+    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
+
+
+def test_set_copy_from_arg1(language):
+    def copy_from_arg1(a : 'list[float]'):
+        b = set(a)
+        return b
+    a = [2.5, 1.4, 9.2]
+    epyc_copy_from_arg = epyccel(copy_from_arg1, language = language)
+    pyccel_result = epyc_copy_from_arg(a)
+    python_result = copy_from_arg1(a)
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_set_copy_from_arg2(language):
+    def copy_from_arg2(a : 'set[float]'):
+        b = set(a)
+        return b
+    a = {2.5, 1.4, 9.2}
+    epyc_copy_from_arg = epyccel(copy_from_arg2, language = language)
+    pyccel_result = epyc_copy_from_arg(a)
+    python_result = copy_from_arg2(a)
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result