Repository: pyccel/pyccel (python)

Issue:
`TypeError` in Fortran printer when creating `range` from `max` 
### Describe the bug
The Fortran printer raises an internal `TypeError` when a `range` is created from an expression that contains `max`.

### To Reproduce
Provide code to reproduce the behavior:
```python
def sum_max_range(a : int, b : int) -> int:
    k = 0
    c = max(a, b)
    for i in range(c):
        k += i
    return k


if __name__ == "__main__":
    k = sum_max_range(0, 0); print(k); assert k == 0
    k = sum_max_range(2, 2); print(k); assert k == 1
    k = sum_max_range(0, 4); print(k); assert k == 6
    k = sum_max_range(2, 5); print(k); assert k == 10
    k = sum_max_range(3, 1); print(k); assert k == 3
```

### Language
Fortran

### Additional context
This code works with the C backend.


Pull request description:
Fix class type of builtin reduction functions
The builtin reduction classes (min/max/sum) save the wrong class type when the argument is a tuple/list. This prevents the functions being used in mathematical expressions (as is the case when printing a range in Fortran). This PR corrects the class type. Fixes #1678 

Patch:
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index 804fb484..c7644091 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -18,7 +18,7 @@ from .basic     import PyccelAstNode, TypedAstNode
 from .datatypes import (NativeInteger, NativeBool, NativeFloat,
                         NativeComplex, NativeGeneric)
 from .datatypes import NativeHomogeneousTuple, NativeInhomogeneousTuple
-from .datatypes import NativeHomogeneousList
+from .datatypes import NativeHomogeneousList, NativeTuple
 from .internals import PyccelInternalFunction, Slice, get_final_precision
 from .literals  import LiteralInteger, LiteralFloat, LiteralComplex, Nil
 from .literals  import Literal, LiteralImaginaryUnit, convert_to_literal
@@ -1015,7 +1015,10 @@ class PythonSum(PyccelInternalFunction):
             raise TypeError(f'Unknown type of {type(arg)}.' )
         self._dtype = arg.dtype
         self._precision = -1
-        self._class_type = arg.dtype
+        if isinstance(arg.class_type, (NativeHomogeneousList, NativeTuple)):
+            self._class_type = arg.dtype
+        else:
+            self._class_type = arg.class_type
         super().__init__(arg)
 
     @property
@@ -1059,7 +1062,10 @@ class PythonMax(PyccelInternalFunction):
                              f"types ({types}). Please cast arguments to the desired dtype")
         self._dtype     = x.dtype
         self._precision = x.precision
-        self._class_type = x.class_type
+        if isinstance(x.class_type, (NativeHomogeneousList, NativeTuple)):
+            self._class_type = x.dtype
+        else:
+            self._class_type = x.class_type
         super().__init__(x)
 
 
@@ -1094,7 +1100,10 @@ class PythonMin(PyccelInternalFunction):
                               f"types ({types}). Please cast arguments to the desired dtype")
         self._dtype     = x.dtype
         self._precision = x.precision
-        self._class_type = x.class_type
+        if isinstance(x.class_type, (NativeHomogeneousList, NativeTuple)):
+            self._class_type = x.dtype
+        else:
+            self._class_type = x.class_type
         super().__init__(x)
 
 #==============================================================================


Test patch:
diff --git a/tests/epyccel/test_builtins.py b/tests/epyccel/test_builtins.py
index a3e423a0..9e08c656 100644
--- a/tests/epyccel/test_builtins.py
+++ b/tests/epyccel/test_builtins.py
@@ -175,6 +175,19 @@ def test_min_tuple(language):
     assert epyc_f(*int_args) == f(*int_args)
     assert np.isclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)
 
+def test_min_expr(language):
+    @template('T', [int, float])
+    def f(x : 'T', y : 'T'):
+        return min((x, y))+3, min(x, y)+4
+
+    epyc_f = epyccel(f, language=language)
+
+    int_args = [randint(min_int, max_int) for _ in range(2)]
+    float_args = [uniform(min_float/2, max_float/2) for _ in range(2)]
+
+    assert np.array_equal(epyc_f(*int_args), f(*int_args))
+    assert np.allclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)
+
 def test_max_2_args_i(language):
     def f(x : 'int', y : 'int'):
         return max(x, y)
@@ -261,6 +274,19 @@ def test_max_tuple(language):
     assert epyc_f(*int_args) == f(*int_args)
     assert np.isclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)
 
+def test_max_expr(language):
+    @template('T', [int, float])
+    def f(x : 'T', y : 'T'):
+        return max((x, y))+3, max(x, y)+4
+
+    epyc_f = epyccel(f, language=language)
+
+    int_args = [randint(min_int, max_int) for _ in range(2)]
+    float_args = [uniform(min_float/2, max_float/2) for _ in range(2)]
+
+    assert np.array_equal(epyc_f(*int_args), f(*int_args))
+    assert np.allclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)
+
 @pytest.mark.parametrize( 'language', (
         pytest.param("fortran", marks = pytest.mark.fortran),
         pytest.param("c", marks = [
@@ -277,7 +303,7 @@ def test_sum_matching_types(language):
 
     epyc_f = epyccel(f, language=language)
 
-    int_args = [randint(min_int/2, max_int/2) for _ in range(2)]
+    int_args = [randint(min_int//2, max_int//2) for _ in range(2)]
     float_args = [uniform(min_float/2, max_float/2) for _ in range(2)]
     complex_args = [uniform(min_float/2, max_float/2) + 1j*uniform(min_float/2, max_float/2)
                     for _ in range(2)]
@@ -285,3 +311,25 @@ def test_sum_matching_types(language):
     assert epyc_f(*int_args) == f(*int_args)
     assert np.isclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)
     assert np.isclose(epyc_f(*complex_args), f(*complex_args), rtol=RTOL, atol=ATOL)
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = pytest.mark.fortran),
+        pytest.param("c", marks = [
+            pytest.mark.skip(reason="sum not implemented in C"),
+            pytest.mark.c]
+        ),
+        pytest.param("python", marks = pytest.mark.python)
+    )
+)
+def test_sum_expr(language):
+    @template('T', [int, float])
+    def f(x : 'T', y : 'T'):
+        return sum((x, y))+3
+
+    epyc_f = epyccel(f, language=language)
+
+    int_args = [randint(min_int//3, max_int//3) for _ in range(2)]
+    float_args = [uniform(min_float/2, max_float/2) for _ in range(2)]
+
+    assert epyc_f(*int_args) == f(*int_args)
+    assert np.allclose(epyc_f(*float_args), f(*float_args), rtol=RTOL, atol=ATOL)