Repository: pyccel/pyccel (python)

Issue:
Add documentation for the `min_value` and `max_value` properties of datatypes
Documentation should be added [here](https://github.com/pyccel/pyccel/blob/devel/developer_docs/type_inference.md#data-types-in-pyccel.) for the new properties of `datatypes`

Pull request:
Use Fortran and C native expressions for min/max values
Use Fortran and C native expressions for min/max values instead of printing a long literal. Fixes #2247. This is preferable as it improves readability and does not encounter rounding errors (as seen on the main branch for Windows)

Patch:
diff --git a/pyccel/ast/datatypes.py b/pyccel/ast/datatypes.py
index c51b28cb..69329ae0 100644
--- a/pyccel/ast/datatypes.py
+++ b/pyccel/ast/datatypes.py
@@ -358,38 +358,6 @@ class PythonNativeInt(PythonNativeNumericType):
         else:
             return NotImplemented
 
-    @property
-    def min_value(self):
-        """
-        Get the minimum representable value for this datatype.
-
-        This property returns the smallest value that can be represented by
-        this numeric datatype. It is useful for validation, boundary checks,
-        and ensuring that generated code respects the limits of the type.
-
-        Returns
-        -------
-        int
-            The minimum value that can be represented.
-        """
-        return numpy.iinfo(int).min
-
-    @property
-    def max_value(self):
-        """
-        Get the maximum representable value for this datatype.
-
-        This property returns the smallest value that can be represented by
-        this numeric datatype. It is useful for validation, boundary checks,
-        and ensuring that generated code respects the limits of the type.
-
-        Returns
-        -------
-        int
-            The maximum value that can be represented.
-        """
-        return numpy.iinfo(int).max
-
 
 class PythonNativeFloat(PythonNativeNumericType):
     """
@@ -411,38 +379,6 @@ class PythonNativeFloat(PythonNativeNumericType):
         else:
             return NotImplemented
 
-    @property
-    def min_value(self):
-        """
-        Get the minimum representable value for this datatype.
-
-        This property returns the smallest value that can be represented by
-        this numeric datatype. It is useful for validation, boundary checks,
-        and ensuring that generated code respects the limits of the type.
-
-        Returns
-        -------
-        float
-            The minimum value that can be represented.
-        """
-        return numpy.finfo(float).min
-
-    @property
-    def max_value(self):
-        """
-        Get the maximum representable value for this datatype.
-
-        This property returns the smallest value that can be represented by
-        this numeric datatype. It is useful for validation, boundary checks,
-        and ensuring that generated code respects the limits of the type.
-
-        Returns
-        -------
-        float
-            The maximum value that can be represented.
-        """
-        return numpy.finfo(float).max
-
 
 class PythonNativeComplex(PythonNativeNumericType):
     """
diff --git a/pyccel/ast/functionalexpr.py b/pyccel/ast/functionalexpr.py
index d208c145..32b0c12a 100644
--- a/pyccel/ast/functionalexpr.py
+++ b/pyccel/ast/functionalexpr.py
@@ -8,6 +8,7 @@
 from pyccel.utilities.stage import PyccelStage
 
 from .basic import TypedAstNode
+from .datatypes import FixedSizeNumericType
 
 pyccel_stage = PyccelStage()
 
@@ -16,7 +17,9 @@ __all__ = (
     'FunctionalMax',
     'FunctionalMin',
     'FunctionalSum',
-    'GeneratorComprehension'
+    'GeneratorComprehension',
+    'MaxLimit',
+    'MinLimit',
 )
 
 #==============================================================================
@@ -191,3 +194,47 @@ class FunctionalMin(GeneratorComprehension):
     """
     __slots__ = ()
     name = 'min'
+#==============================================================================
+
+class MaxLimit(TypedAstNode):
+    """
+    A class representing the largest usable value for a given type.
+
+    A class representing the largest usable value for a given type.
+    This is particularly useful with FunctionalMin.
+
+    Parameters
+    ----------
+    class_type : FixedSizeNumericType
+        The type whose maximum limit is represented by an instance
+        of this class.
+    """
+    __slots__ = ('_class_type',)
+    _attribute_nodes = ()
+
+    def __init__(self, class_type):
+        assert class_type.rank == 0
+        self._class_type = class_type
+        super().__init__()
+#==============================================================================
+
+class MinLimit(TypedAstNode):
+    """
+    A class representing the smallest usable value for a given type.
+
+    A class representing the smallest usable value for a given type.
+    This is particularly useful with FunctionalMax.
+
+    Parameters
+    ----------
+    class_type : FixedSizeNumericType
+        The type whose minimum limit is represented by an instance
+        of this class.
+    """
+    __slots__ = ('_class_type',)
+    _attribute_nodes = ()
+
+    def __init__(self, class_type):
+        assert isinstance(class_type, FixedSizeNumericType)
+        self._class_type = class_type
+        super().__init__()
diff --git a/pyccel/ast/literals.py b/pyccel/ast/literals.py
index 92638c09..fcf292eb 100644
--- a/pyccel/ast/literals.py
+++ b/pyccel/ast/literals.py
@@ -447,7 +447,7 @@ def convert_to_literal(value, dtype = None):
         The Python value 'value' expressed as a literal
         with the specified dtype.
     """
-    from .operators import PyccelUnarySub, PyccelMinus # Imported here to avoid circular import
+    from .operators import PyccelUnarySub # Imported here to avoid circular import
 
     # Calculate the default datatype
     if dtype is None:
@@ -474,8 +474,6 @@ def convert_to_literal(value, dtype = None):
     if isinstance(primitive_type, PrimitiveIntegerType):
         if value >= 0:
             literal_val = LiteralInteger(value, dtype)
-        elif value == -9223372036854775808:
-            literal_val =  PyccelMinus(PyccelUnarySub(LiteralInteger(-value-1, dtype)), LiteralInteger(1))
         else:
             literal_val = PyccelUnarySub(LiteralInteger(-value, dtype))
     elif isinstance(primitive_type, PrimitiveFloatingPointType):
diff --git a/pyccel/ast/numpytypes.py b/pyccel/ast/numpytypes.py
index 39d81103..e4e7cf18 100644
--- a/pyccel/ast/numpytypes.py
+++ b/pyccel/ast/numpytypes.py
@@ -176,38 +176,6 @@ class NumpyFloat64Type(NumpyNumericType):
     _primitive_type = PrimitiveFloatingPointType()
     _precision = 8
 
-    @property
-    def min_value(self):
-        """
-        Get the minimum representable value for this datatype.
-
-        This property returns the smallest value that can be represented by
-        this numeric datatype. It is useful for validation, boundary checks,
-        and ensuring that generated code respects the limits of the type.
-
-        Returns
-        -------
-        float64
-            The minimum value that can be represented.
-        """
-        return np.finfo(np.float64).min
-
-    @property
-    def max_value(self):
-        """
-        Get the maximum representable value for this datatype.
-
-        This property returns the smallest value that can be represented by
-        this numeric datatype. It is useful for validation, boundary checks,
-        and ensuring that generated code respects the limits of the type.
-
-        Returns
-        -------
-        float64
-            The maximum value that can be represented.
-        """
-        return np.finfo(np.float64).max
-
 
 class NumpyFloat128Type(NumpyNumericType):
     """
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index dbaaf6ca..456603eb 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -58,7 +58,7 @@ from pyccel.ast.numpyext import NumpyAmin, NumpyAmax
 from pyccel.ast.numpyext import get_shape_of_multi_level_container
 
 from pyccel.ast.numpytypes import NumpyInt8Type, NumpyInt16Type, NumpyInt32Type, NumpyInt64Type
-from pyccel.ast.numpytypes import NumpyFloat32Type, NumpyFloat64Type, NumpyComplex64Type, NumpyComplex128Type
+from pyccel.ast.numpytypes import NumpyFloat32Type, NumpyFloat64Type, NumpyFloat128Type
 from pyccel.ast.numpytypes import NumpyNDArrayType, numpy_precision_map
 
 from pyccel.ast.operators import PyccelAdd, PyccelMul, PyccelMinus, PyccelLt, PyccelGt
@@ -78,7 +78,7 @@ from pyccel.codegen.printing.codeprinter import CodePrinter
 
 from pyccel.errors.errors   import Errors
 from pyccel.errors.messages import (PYCCEL_RESTRICTION_TODO, INCOMPATIBLE_TYPEVAR_TO_FUNC,
-                                    PYCCEL_RESTRICTION_IS_ISNOT)
+                                    PYCCEL_RESTRICTION_IS_ISNOT, PYCCEL_INTERNAL_ERROR)
 
 numpy_v1 = Version(np.__version__) < Version("2.0.0")
 
@@ -227,6 +227,7 @@ c_library_headers = (
     "complex",
     "ctype",
     "float",
+    "inttypes",
     "math",
     "stdarg",
     "stdbool",
@@ -235,23 +236,22 @@ c_library_headers = (
     "stdio",
     "stdlib",
     "string",
-    "tgmath",
-    "inttypes",
 )
 
 import_dict = {'omp_lib' : 'omp' }
 
 c_imports = {n : Import(n, Module(n, (), ())) for n in
-                ['stdlib',
-                 'math',
-                 'string',
+                ['assert',
                  'complex',
-                 'stdint',
+                 'float',
+                 'inttypes',
+                 'math',
                  'pyc_math_c',
-                 'stdio',
-                 "inttypes",
                  'stdbool',
-                 'assert',
+                 'stdint',
+                 'stdio',
+                 'stdlib',
+                 'string',
                  'stc/cstr',
                  'CSpan_extensions']}
 
@@ -2785,6 +2785,40 @@ class CCodePrinter(CodePrinter):
     def _print_EmptyNode(self, expr):
         return ''
 
+    def _print_MaxLimit(self, expr):
+        class_type = expr.class_type
+        if isinstance(class_type.primitive_type, PrimitiveIntegerType):
+            self.add_import(c_imports['stdint'])
+            return f'INT{class_type.precision*8}_MAX'
+        elif isinstance(class_type.primitive_type, PrimitiveFloatingPointType):
+            self.add_import(c_imports['float'])
+            numpy_type = numpy_precision_map[(class_type.primitive_type, class_type.precision)]
+            if numpy_type is NumpyFloat32Type():
+                return 'FLT_MAX'
+            elif numpy_type is NumpyFloat64Type():
+                return 'DBL_MAX'
+            elif numpy_type is NumpyFloat128Type():
+                return 'LDBL_MAX'
+        raise errors.report(PYCCEL_INTERNAL_ERROR,
+                symbol = expr, severity='fatal')
+
+    def _print_MinLimit(self, expr):
+        class_type = expr.class_type
+        if isinstance(class_type.primitive_type, PrimitiveIntegerType):
+            self.add_import(c_imports['stdint'])
+            return f'INT{class_type.precision*8}_MIN'
+        elif isinstance(class_type.primitive_type, PrimitiveFloatingPointType):
+            self.add_import(c_imports['float'])
+            numpy_type = numpy_precision_map[(class_type.primitive_type, class_type.precision)]
+            if numpy_type is NumpyFloat32Type():
+                return '-FLT_MAX'
+            elif numpy_type is NumpyFloat64Type():
+                return '-DBL_MAX'
+            elif numpy_type is NumpyFloat128Type():
+                return '-LDBL_MAX'
+        raise errors.report(PYCCEL_INTERNAL_ERROR,
+                symbol = expr, severity='fatal')
+
     #=================== OMP ==================
 
     def _print_OmpAnnotatedComment(self, expr):
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index fb56d9fa..ae613d48 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -58,7 +58,7 @@ from pyccel.ast.itertoolsext import Product
 
 from pyccel.ast.literals  import LiteralInteger, LiteralFloat, Literal, LiteralEllipsis
 from pyccel.ast.literals  import LiteralTrue, LiteralFalse, LiteralString
-from pyccel.ast.literals  import Nil
+from pyccel.ast.literals  import Nil, convert_to_literal
 
 from pyccel.ast.low_level_tools  import MacroDefinition, IteratorType, PairType
 from pyccel.ast.low_level_tools  import MacroUndef
@@ -3921,3 +3921,15 @@ class FCodePrinter(CodePrinter):
 
     def _print_KindSpecification(self, expr):
         return f'(kind = {self.print_kind(expr.type_specifier)})'
+
+    def _print_MinLimit(self, expr):
+        # TypeError: '<' not supported between instances of 'complex' and 'complex'
+        assert not isinstance(expr.class_type.primitive_type, PrimitiveComplexType)
+        example_of_type = convert_to_literal(0, dtype = expr.class_type)
+        return f'-Huge({self._print(example_of_type)})'
+
+    def _print_MaxLimit(self, expr):
+        # TypeError: '>' not supported between instances of 'complex' and 'complex'
+        assert not isinstance(expr.class_type.primitive_type, PrimitiveComplexType)
+        example_of_type = convert_to_literal(0, dtype = expr.class_type)
+        return f'Huge({self._print(example_of_type)})'
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index b85a5d20..ac7e81e5 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -77,6 +77,7 @@ from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType, H
 from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType, DictType, TypeAlias
 
 from pyccel.ast.functionalexpr import FunctionalSum, FunctionalMax, FunctionalMin, GeneratorComprehension, FunctionalFor
+from pyccel.ast.functionalexpr import MaxLimit, MinLimit
 
 from pyccel.ast.headers import FunctionHeader, MethodHeader, Header
 from pyccel.ast.headers import MacroFunction, MacroVariable
@@ -1968,10 +1969,10 @@ class SemanticParser(BasicParser):
             d_var = self._infer_type(PyccelAdd(result, val))
         elif isinstance(expr, FunctionalMin):
             d_var = self._infer_type(result)
-            val = convert_to_literal(d_var['class_type'].max_value)
+            val = MaxLimit(d_var['class_type'])
         elif isinstance(expr, FunctionalMax):
             d_var = self._infer_type(result)
-            val = convert_to_literal(d_var['class_type'].min_value)
+            val = MinLimit(d_var['class_type'])
 
         # Infer the final dtype of the expression
         class_type = d_var.pop('class_type')


Test patch:
diff --git a/tests/epyccel/test_epyccel_generators.py b/tests/epyccel/test_epyccel_generators.py
index 622ba6f0..1744e6bb 100644
--- a/tests/epyccel/test_epyccel_generators.py
+++ b/tests/epyccel/test_epyccel_generators.py
@@ -4,6 +4,7 @@ import numpy as np
 from numpy.random import randint
 
 from pyccel import epyccel
+from pyccel.decorators import template
 
 def test_sum_range(language):
     def f(a0 : 'int[:]'):
@@ -270,3 +271,27 @@ def test_sum_with_two_variables(language):
     f_epyc = epyccel(f, language=language)
 
     assert f() == f_epyc()
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = pytest.mark.fortran),
+        pytest.param("c", marks = [
+            pytest.mark.skip(reason="Var arg causes type promotion. See #2251."),
+            pytest.mark.c]
+        ),
+        pytest.param("python", marks = pytest.mark.python)
+    )
+)
+def test_min_max_values(language):
+    @template('T', ['int16', 'int32', 'int64', 'float32', 'float64'])
+    def f(a : 'T[:]'):
+        min_val = min(ai for ai in a)
+        max_val = max(ai for ai in a)
+        return min_val, max_val
+
+    f_epyc = epyccel(f, language=language)
+
+    for dtype in (np.int16, np.int32, np.int64, np.float32, np.float64):
+        x = randint(0, 100, size=(5,)).astype(dtype)
+        print(f(x))
+        print(f_epyc(x))
+        assert f(x) == f_epyc(x)