Repository: pyccel/pyccel (python)

Issue:
union : Returns a new set with elements from the set and all others.
### Feature Description

**Add support for union() method for sets in the Python translation.**
`union(*others)` :  Returns a new set containing all unique elements present in the calling set as well as all specified other sets.

### Test Code

This Code Should Work 

```python
set1 = {1, 2}
set2 = {3, 4}
union_set = set1.union(set2)
print(union_set)  
>>> {1, 2, 3, 4}
```

### Proposed Solution
See description in #1531 

Pull request description:
Add support for set method `union`
Add support for set method `union`. Fixes #1753 

**Commit Summary**
- Add a class to represent `SetUnion`
- Remove unused `prefix_code` variable from `CCodePrinter._print_Assign`
- Add C printing for union by adding a function to the extensions
- Add Fortran printing for union by using `copy` and `update`
- Correct error about unknown shape which is irrelevant for objects whose shape can change
- Add tests for `union`
- Fix AST tree for `DottedName`

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3e452d4..88e4d794 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ All notable changes to this project will be documented in this file.
 -   #1917 : Add C and Fortran support for set method `add()`.
 -   #1918 : Add support for set method `clear()`.
 -   #1918 : Add support for set method `copy()`.
+-   #1753 : Add support for set method `union()`.
 -   #1936 : Add missing C output for inline decorator example in documentation
 -   #1937 : Optimise `pyccel.ast.basic.PyccelAstNode.substitute` method.
 -   #1544 : Add support for `typing.TypeAlias`.
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index 87e656be..dcffd58c 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -111,7 +111,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `remove` | Python-only |
 | `symmetric_difference` | No |
 | `symmetric_difference_update` | No |
-| `union` | No |
+| **`union`** | Yes |
 | `update` | Python-only |
 
 ## Dictionary methods
diff --git a/pyccel/ast/builtin_methods/set_methods.py b/pyccel/ast/builtin_methods/set_methods.py
index c4b8af42..a203fedb 100644
--- a/pyccel/ast/builtin_methods/set_methods.py
+++ b/pyccel/ast/builtin_methods/set_methods.py
@@ -21,6 +21,7 @@ __all__ = (
     'SetMethod',
     'SetPop',
     'SetRemove',
+    'SetUnion',
     'SetUpdate'
 )
 
@@ -227,6 +228,36 @@ class SetUpdate(SetMethod):
     """
     __slots__ = ()
     name = 'update'
+    _shape = None
+    _class_type = VoidType()
 
     def __init__(self, set_obj, iterable) -> None:
         super().__init__(set_obj, iterable)
+
+#==============================================================================
+class SetUnion(SetMethod):
+    """
+    Represents a call to the set method .union.
+
+    Represents a call to the set method .union. This method builds a new set
+    by including all elements which appear in at least one of the iterables
+    (the set object and the arguments).
+
+    Parameters
+    ----------
+    set_obj : TypedAstNode
+        The set object which the method is called from.
+    *others : TypedAstNode
+        The iterables which will be combined with this set.
+    """
+    __slots__ = ('_other','_class_type', '_shape')
+    name = 'union'
+
+    def __init__(self, set_obj, *others):
+        self._class_type = set_obj.class_type
+        element_type = self._class_type.element_type
+        for o in others:
+            if element_type != o.class_type.element_type:
+                raise TypeError(f"Argument of type {o.class_type} cannot be used to build set of type {self._class_type}")
+        self._shape = (None,)*self._class_type.rank
+        super().__init__(set_obj, *others)
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 40dc7c87..c7c744d1 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -6,7 +6,8 @@
 This module contains all types which define a python class which is automatically recognised by pyccel
 """
 
-from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetClear, SetCopy, SetPop, SetRemove, SetDiscard, SetUpdate
+from pyccel.ast.builtin_methods.set_methods  import (SetAdd, SetClear, SetCopy, SetPop, SetRemove,
+                                                     SetDiscard, SetUpdate, SetUnion)
 from pyccel.ast.builtin_methods.list_methods import (ListAppend, ListInsert, ListPop,
                                                      ListClear, ListExtend, ListRemove,
                                                      ListCopy, ListSort)
@@ -163,6 +164,7 @@ SetClass = ClassDef('set',
             PyccelFunctionDef('discard', func_class = SetDiscard),
             PyccelFunctionDef('pop', func_class = SetPop),
             PyccelFunctionDef('remove', func_class = SetRemove),
+            PyccelFunctionDef('union', func_class = SetUnion),
             PyccelFunctionDef('update', func_class = SetUpdate),
         ])
 
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index e9fc379a..c6b823c3 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -1970,7 +1970,7 @@ class FunctionCall(TypedAstNode):
             self._interface = None
             self._funcdef   = func
             self._arguments = tuple(args)
-            self._func_name = func
+            self._func_name = getattr(func, 'name', func)
             super().__init__()
             return
 
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 2f67fefe..ade1b1bc 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -561,7 +561,7 @@ class DottedName(PyccelAstNode):
     pyccel.stdlib.parallel
     """
     __slots__ = ('_name',)
-    _attribute_nodes = ()
+    _attribute_nodes = ('_name',)
 
     def __new__(cls, *args):
         if len(args) == 1:
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 3472a59b..1681849a 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -992,7 +992,7 @@ class CCodePrinter(CodePrinter):
         else:
             source = expr.source
         if isinstance(source, DottedName):
-            source = source.name[-1]
+            source = source.name[-1].python_value
         else:
             source = self._print(source)
         if source.startswith('stc/') or source in import_header_guard_prefix:
@@ -2228,22 +2228,21 @@ class CCodePrinter(CodePrinter):
         return f'{lhs_code} {op}= {rhs_code};\n'
 
     def _print_Assign(self, expr):
-        prefix_code = ''
         lhs = expr.lhs
         rhs = expr.rhs
         if isinstance(rhs, FunctionCall) and isinstance(rhs.class_type, TupleType):
             self._temporary_args = [ObjectAddress(a) for a in lhs]
-            return prefix_code+'{};\n'.format(self._print(rhs))
+            return f'{self._print(rhs)};\n'
         # Inhomogenous tuples are unravelled and therefore do not exist in the c printer
         if isinstance(rhs, (NumpyArray, PythonTuple)):
-            return prefix_code+self.copy_NumpyArray_Data(expr)
+            return self.copy_NumpyArray_Data(expr)
         if isinstance(rhs, (NumpyFull)):
-            return prefix_code+self.arrayFill(expr)
+            return self.arrayFill(expr)
         lhs = self._print(expr.lhs)
         if isinstance(rhs, (PythonList, PythonSet, PythonDict)):
-            return prefix_code+self.init_stc_container(rhs, expr)
+            return self.init_stc_container(rhs, expr)
         rhs = self._print(expr.rhs)
-        return prefix_code+f'{lhs} = {rhs};\n'
+        return f'{lhs} = {rhs};\n'
 
     def _print_AliasAssign(self, expr):
         lhs_var = expr.lhs
@@ -2632,6 +2631,18 @@ class CCodePrinter(CodePrinter):
         set_var = self._print(expr.set_variable)
         return f'{var_type}_clone({set_var})'
 
+    def _print_SetUnion(self, expr):
+        assign_base = expr.get_direct_user_nodes(lambda n: isinstance(n, Assign))
+        if not assign_base:
+            errors.report("The result of the union call must be saved into a variable",
+                    severity='error', symbol=expr)
+        class_type = expr.set_variable.class_type
+        var_type = self.get_c_type(class_type)
+        self.add_import(Import('Set_extensions', 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)])
+        return f'{var_type}_union({set_var}, {args})'
+
     #=================== MACROS ==================
 
     def _print_MacroShape(self, expr):
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 763ef73d..b407d966 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -24,6 +24,8 @@ from pyccel.ast.builtins import PythonInt, PythonType, PythonPrint, PythonRange
 from pyccel.ast.builtins import PythonTuple, DtypePrecisionToCastFunction
 from pyccel.ast.builtins import PythonBool, PythonList, PythonSet
 
+from pyccel.ast.builtin_methods.set_methods import SetUnion
+
 from pyccel.ast.core import FunctionDef, FunctionDefArgument, FunctionDefResult
 from pyccel.ast.core import SeparatorComment, Comment
 from pyccel.ast.core import ConstructorCall
@@ -262,7 +264,7 @@ class FCodePrinter(CodePrinter):
         self._constantImports = {}
         self._current_class    = None
 
-        self._additional_code = None
+        self._additional_code = ''
 
         self.prefix_module = prefix_module
 
@@ -425,8 +427,6 @@ class FCodePrinter(CodePrinter):
             # Everything before the return node needs handling before the line
             # which calls the inline function is executed
             code = self._print(body)
-            if (not self._additional_code):
-                self._additional_code = ''
             self._additional_code += code
 
             # Collect statements from results to return object
@@ -888,7 +888,7 @@ class FCodePrinter(CodePrinter):
 
         source = expr.source
         if isinstance(source, DottedName):
-            source = source.name[-1]
+            source = source.name[-1].python_value
         elif isinstance(source, LiteralString):
             source = source.python_value
         else:
@@ -1276,14 +1276,12 @@ class FCodePrinter(CodePrinter):
     def _print_DottedVariable(self, expr):
         if isinstance(expr.lhs, FunctionCall):
             base = expr.lhs.funcdef.results[0].var
-            if (not self._additional_code):
-                self._additional_code = ''
             var_name = self.scope.get_new_name()
             var = base.clone(var_name)
 
             self.scope.insert_variable(var)
 
-            self._additional_code = self._additional_code + self._print(Assign(var,expr.lhs)) + '\n'
+            self._additional_code += self._print(Assign(var,expr.lhs)) + '\n'
             return self._print(var) + '%' +self._print(expr.name)
         else:
             return self._print(expr.lhs) + '%' +self._print(expr.name)
@@ -1337,6 +1335,31 @@ class FCodePrinter(CodePrinter):
         type_name = self._print(expr.class_type)
         return f'{type_name}({var_code})'
 
+    def _print_SetUnion(self, expr):
+        assign_base = expr.get_direct_user_nodes(lambda n: isinstance(n, Assign))
+        var = expr.set_variable
+        if not assign_base:
+            result = self._print(self.scope.get_temporary_variable(var))
+        else:
+            result = self._print(assign_base[0].lhs)
+        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))
+        args_insert = []
+        for arg in expr.args:
+            a = self._print(arg)
+            if arg.class_type == expr_type:
+                args_insert.append(f'call {result} % merge({a})\n')
+            else:
+                errors.report(PYCCEL_RESTRICTION_TODO, severity = 'error', symbol = expr)
+        code = f'{result} = {type_name}({var_code})\n' + ''.join(args_insert)
+        if assign_base:
+            return code
+        else:
+            self._additional_code += code
+            return result
+
     #========================== Numpy Elements ===============================#
 
     def _print_NumpySum(self, expr):
@@ -1675,12 +1698,10 @@ class FCodePrinter(CodePrinter):
             errors.report(FORTRAN_ALLOCATABLE_IN_EXPRESSION,
                           symbol=expr, severity='fatal')
 
-        if (not self._additional_code):
-            self._additional_code = ''
         var = self.scope.get_temporary_variable(expr.dtype, memory_handling = 'stack',
                 shape = expr.shape)
 
-        self._additional_code = self._additional_code + self._print(Assign(var,expr)) + '\n'
+        self._additional_code += self._print(Assign(var,expr)) + '\n'
         return self._print(var)
 
     def _print_NumpyRandint(self, expr):
@@ -1978,9 +1999,9 @@ class FCodePrinter(CodePrinter):
         body_stmts = []
         for b in body_exprs :
             line = self._print(b)
-            if (self._additional_code):
+            if self._additional_code:
                 body_stmts.append(self._additional_code)
-                self._additional_code = None
+                self._additional_code = ''
             body_stmts.append(line)
         return ''.join(self._print(b) for b in body_stmts)
 
@@ -2000,7 +2021,7 @@ class FCodePrinter(CodePrinter):
     def _print_Assign(self, expr):
         rhs = expr.rhs
 
-        if isinstance(rhs, FunctionCall):
+        if isinstance(rhs, (FunctionCall, SetUnion)):
             return self._print(rhs)
 
         lhs_code = self._print(expr.lhs)
@@ -3397,17 +3418,13 @@ class FCodePrinter(CodePrinter):
             args = args[1:]
             if isinstance(class_variable, FunctionCall):
                 base = class_variable.funcdef.results[0].var
-                if (not self._additional_code):
-                    self._additional_code = ''
                 var = self.scope.get_temporary_variable(base)
 
-                self._additional_code = self._additional_code + self._print(Assign(var, class_variable)) + '\n'
+                self._additional_code += self._print(Assign(var, class_variable)) + '\n'
                 f_name = f'{self._print(var)} % {f_name}'
             else:
                 f_name = f'{self._print(class_variable)} % {f_name}'
 
-        if (not self._additional_code):
-            self._additional_code = ''
         if parent_assign:
             lhs = parent_assign[0].lhs
             if len(func_results) == 1:
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index d9584819..2b7ac599 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -524,7 +524,8 @@ class PythonCodePrinter(CodePrinter):
         return ''
 
     def _print_DottedName(self, expr):
-        return '.'.join(self._print(n) for n in expr.name)
+        # A DottedName can only contain LiteralStrings or PyccelSymbols at the printing stage
+        return '.'.join(str(n) for n in expr.name)
 
     def _print_FunctionCall(self, expr):
         func = expr.funcdef
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 8270790f..54e0a4a5 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -29,7 +29,7 @@ from pyccel.ast.builtins import (PythonRange, PythonZip, PythonEnumerate,
                                  PythonTuple, Lambda, PythonMap)
 
 from pyccel.ast.builtin_methods.list_methods import ListMethod, ListAppend
-from pyccel.ast.builtin_methods.set_methods  import SetMethod, SetAdd
+from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetUnion
 
 from pyccel.ast.core import Comment, CommentBlock, Pass
 from pyccel.ast.core import If, IfSection
@@ -1095,6 +1095,7 @@ class SemanticParser(BasicParser):
                             new_expr = FunctionCall(func, args)
                         new_expr.set_current_ast(expr.python_ast)
                         pyccel_stage.set_stage('semantic')
+                        new_expr.set_current_user_node(expr.current_user_node)
                         expr = new_expr
                     return getattr(self, annotation_method)(expr)
 
@@ -1566,7 +1567,7 @@ class SemanticParser(BasicParser):
                 know_lhs_shape = (lhs.rank == 0) or all(sh is not None for sh in lhs.alloc_shape) \
                         or isinstance(lhs.class_type, StringType)
 
-                if not know_lhs_shape:
+                if isinstance(class_type, (NumpyNDArrayType, HomogeneousTupleType)) and not know_lhs_shape:
                     msg = f"Cannot infer shape of right-hand side for expression {lhs} = {rhs}"
                     errors.report(PYCCEL_RESTRICTION_TODO+'\n'+msg,
                         bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
@@ -3158,13 +3159,7 @@ class SemanticParser(BasicParser):
                     symbol=expr, severity='error')
 
         # Checking for the result of _visit_ListExtend
-        if isinstance(rhs, For) or (isinstance(rhs, CodeBlock) and
-            isinstance(rhs.body[0], (ListMethod, SetMethod))):
-            return rhs
-        if isinstance(rhs, ConstructorCall):
-            return rhs
-
-        elif isinstance(rhs, CodeBlock) and len(rhs.body)>1 and isinstance(rhs.body[1], FunctionalFor):
+        if isinstance(rhs, (For, CodeBlock, ConstructorCall)):
             return rhs
 
         elif isinstance(rhs, FunctionCall):
@@ -5032,7 +5027,7 @@ class SemanticParser(BasicParser):
         elements of the iterable. If not, it attempts to construct a syntactic `For` 
         loop to iterate over the iterable object and added its elements to the set 
         object. Finally, it passes to a `_visit()` call for semantic parsing.
-    
+
         Parameters
         ----------
         expr : DottedName
@@ -5067,3 +5062,47 @@ class SemanticParser(BasicParser):
             pyccel_stage.set_stage('semantic')
             return self._visit(for_obj)
 
+    def _build_SetUnion(self, expr):
+        """
+        Method to navigate the syntactic DottedName node of a `set.union()` call.
+
+        The purpose of this `_build` method is to construct new nodes from a syntactic
+        DottedName node. It creates a SetUnion node if the type of the arguments matches
+        the type of the original set. Otherwise it uses `set.copy` and `set.update` to
+        handle iterators.
+
+        Parameters
+        ----------
+        expr : DottedName
+            The syntactic DottedName node that represent the call to `.union()`.
+
+        Returns
+        -------
+        SetUnion | CodeBlock
+            The nodes describing the union operator.
+        """
+        syntactic_set_obj = expr.name[0]
+        syntactic_args = expr.name[1].args
+        set_obj = self._visit(expr.name[0])
+        args = [self._visit(a.value) for a in expr.name[1].args]
+        class_type = set_obj.class_type
+        if all(a.class_type == class_type for a in args):
+            return SetUnion(set_obj, *args)
+        else:
+            element_type = class_type.element_type
+            if any(a.class_type.element_type != element_type for a in args):
+                errors.report(("Containers containing objects of a different type cannot be used as "
+                               f"arguments to {class_type}.union"),
+                        severity='fatal', symbol=expr)
+
+            lhs = expr.get_user_nodes(Assign)[0].lhs
+            pyccel_stage.set_stage('syntactic')
+            body = [Assign(lhs, DottedName(syntactic_set_obj, FunctionCall('copy', ())),
+                           python_ast = expr.python_ast)]
+            update_calls = [DottedName(lhs, FunctionCall('update', (s_a,))) for s_a in syntactic_args]
+            for c in update_calls:
+                c.set_current_ast(expr.python_ast)
+            body += [Assign(PyccelSymbol('_', is_temp=True), c, python_ast = expr.python_ast)
+                     for c in update_calls]
+            pyccel_stage.set_stage('semantic')
+            return CodeBlock([self._visit(b) for b in body])
diff --git a/pyccel/parser/syntactic.py b/pyccel/parser/syntactic.py
index d3674fec..339f0167 100644
--- a/pyccel/parser/syntactic.py
+++ b/pyccel/parser/syntactic.py
@@ -1102,6 +1102,9 @@ class SyntaxParser(BasicParser):
                 func = FunctionCall(func, args)
         elif isinstance(func, DottedName):
             func_attr = FunctionCall(func.name[-1], args)
+            for n in func.name:
+                if isinstance(n, PyccelAstNode):
+                    n.clear_syntactic_user_nodes()
             func = DottedName(*func.name[:-1], func_attr)
         else:
             raise NotImplementedError(f' Unknown function type {type(func)}')
diff --git a/pyccel/stdlib/STC_Extensions/Set_extensions.h b/pyccel/stdlib/STC_Extensions/Set_extensions.h
index 6f5eb20f..51f4fa33 100644
--- a/pyccel/stdlib/STC_Extensions/Set_extensions.h
+++ b/pyccel/stdlib/STC_Extensions/Set_extensions.h
@@ -1,6 +1,6 @@
 #ifndef SET_EXTENSIONS_H
 #define SET_EXTENSIONS_H
-
+#include <stdarg.h>
 
 #define _c_MEMB(name) c_JOIN(i_type, name)
 
@@ -19,6 +19,29 @@ static inline i_key _c_MEMB(_pop)(i_type* self) {
     return *(itr.ref); // Return the element that is being popped.
 }
 
+/*
+ * This function represents a call to the .union() method.
+ * @param self : The set instance
+ * @param n : The number of variadic arguments passed to the method.
+ * @param ... : The variadic arguments. These are the other sets in which elements may be found
+ */
+static inline i_type _c_MEMB(_union)(i_type* self, int n, ...) {
+    i_type union_result = _c_MEMB(_clone)(*self);
+
+    va_list args;
+    va_start(args, n);
+
+    for (int i=0; i<n; ++i) {
+        i_type* other = va_arg(args, i_type*);
+        c_foreach (elem, i_type, *other)
+            _c_MEMB(_insert)(&union_result, (*elem.ref));
+    }
+
+    va_end(args);
+
+    return union_result;
+}
+
 #undef i_type
 #undef i_key
 #endif


Test patch:
diff --git a/tests/epyccel/test_epyccel_sets.py b/tests/epyccel/test_epyccel_sets.py
index c85a0032..1f550b86 100644
--- a/tests/epyccel/test_epyccel_sets.py
+++ b/tests/epyccel/test_epyccel_sets.py
@@ -219,7 +219,7 @@ def test_update_boolean_tuple(python_only_language):
 def test_update_complex_list(python_only_language):
     def update_complex_list():
         a = {1j, 2 + 3j, 0 + 0j}
-        b = {4j, 5j, 1 + 6j}
+        b = [4j, 5j, 1 + 6j]
         a.update(b)
         return a
     epyccel_update = epyccel(update_complex_list, language=python_only_language)
@@ -377,3 +377,90 @@ def test_Pop_complex(language):
     pyccel_result = set(epyccel_remove())
     python_result = set(Pop_complex())
     assert python_result == pyccel_result
+
+def test_set_union_int(language):
+    def union_int():
+        a = {1,2,3,4}
+        b = {5,6,7,2}
+        c = a.union(b)
+        return len(c), c.pop(), c.pop(), c.pop(), c.pop(), c.pop(), c.pop(), c.pop()
+
+    epyccel_func = epyccel(union_int, language = language)
+    pyccel_result = epyccel_func()
+    python_result = union_int()
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
+
+def test_set_union_no_args(language):
+    def union_int():
+        a = {1,2,3,4}
+        c = a.union()
+        a.add(5)
+        return len(c), c.pop(), c.pop(), c.pop(), c.pop()
+
+    epyccel_func = epyccel(union_int, language = language)
+    pyccel_result = epyccel_func()
+    python_result = union_int()
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
+
+def test_set_union_2_args(language):
+    def union_int():
+        a = {1,2,3,4}
+        b = {5,6,7}
+        c = {8,9,10,4}
+        d = a.union(b, c)
+        return len(d), d.pop(), d.pop(), d.pop(), d.pop(), d.pop(), d.pop(), d.pop(), d.pop(), d.pop(), d.pop()
+
+    epyccel_func = epyccel(union_int, language = language)
+    pyccel_result = epyccel_func()
+    python_result = union_int()
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = pytest.mark.fortran),
+        pytest.param("c", marks = [
+            pytest.mark.skip(reason="Can't use a pointer to a temporary object."),
+            pytest.mark.c]
+        ),
+        pytest.param("python", marks = pytest.mark.python)
+    )
+)
+def test_temporary_set_union(language):
+    def union_int():
+        a = {1,2}
+        b = {2}
+        d = a.union(b).pop()
+        return d
+
+    epyccel_func = epyccel(union_int, language = language)
+    pyccel_result = epyccel_func()
+    python_result = union_int()
+    assert python_result == pyccel_result
+
+def test_set_union_list(language):
+    def union_list():
+        a = {1.2, 2.3}
+        b = [1.2, 5.0]
+        d = a.union(b)
+        return len(d), d.pop(), d.pop(), d.pop()
+
+    epyccel_func = epyccel(union_list, language = language)
+    pyccel_result = epyccel_func()
+    python_result = union_list()
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
+
+def test_set_union_tuple(language):
+    def union_tuple():
+        a = {True}
+        b = (False,)
+        d = a.union(b)
+        return len(d), d.pop(), d.pop()
+
+    epyccel_func = epyccel(union_tuple, language = language)
+    pyccel_result = epyccel_func()
+    python_result = union_tuple()
+    assert python_result[0] == pyccel_result[0]
+    assert set(python_result[1:]) == set(pyccel_result[1:])
diff --git a/tests/errors/semantic/blocking/SET_UNION_TYPES.py b/tests/errors/semantic/blocking/SET_UNION_TYPES.py
new file mode 100644
index 00000000..792f04bc
--- /dev/null
+++ b/tests/errors/semantic/blocking/SET_UNION_TYPES.py
@@ -0,0 +1,7 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+def f():
+    a = {1,2,4}
+    b = {1.3,4.6, 9.2}
+    c = a.union(b)
+    return c