Repository: GridTools/gt4py
Pull request #1184 (2023-02-23 09:56:11)

Implementation for min and max in domain
```python
@program
    def program_domain(a: Field[[IDim, JDim], float64]):
        fieldop_domain(a, out=a, domain={IDim: (minimum(1, 2), 9), JDim: (4, 6)})
```


Patch:
diff --git a/src/gt4py/next/ffront/past_passes/type_deduction.py b/src/gt4py/next/ffront/past_passes/type_deduction.py
index c01adcf1..3c8c362b 100644
--- a/src/gt4py/next/ffront/past_passes/type_deduction.py
+++ b/src/gt4py/next/ffront/past_passes/type_deduction.py
@@ -44,7 +44,7 @@ def _is_integral_scalar(expr: past.Expr) -> bool:
     return isinstance(expr.type, ts.ScalarType) and type_info.is_integral(expr.type)
 
 
-def _validate_call_params(new_func: past.Name, new_kwargs: dict):
+def _validate_operator_call(new_func: past.Name, new_kwargs: dict):
     """
     Perform checks for domain and output field types.
 
@@ -54,13 +54,10 @@ def _validate_call_params(new_func: past.Name, new_kwargs: dict):
     """
     if not isinstance(
         new_func.type,
-        (
-            ts_ffront.FieldOperatorType,
-            ts_ffront.ScanOperatorType,
-        ),
+        (ts_ffront.FieldOperatorType, ts_ffront.ScanOperatorType),
     ):
         raise GTTypeError(
-            f"Only calls `FieldOperator`s and `ScanOperators` "
+            f"Only calls `FieldOperator`s and `ScanOperator`s "
             f"allowed in `Program`, but got `{new_func.type}`."
         )
 
@@ -189,12 +186,16 @@ class ProgramTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTranslator):
         new_kwargs = self.visit(node.kwargs, **kwargs)
 
         try:
-            _validate_call_params(new_func, new_kwargs)
+            is_operator = isinstance(
+                new_func.type, (ts_ffront.FieldOperatorType, ts_ffront.ScanOperatorType)
+            )
+            if is_operator:
+                _validate_operator_call(new_func, new_kwargs)
             arg_types = [arg.type for arg in new_args]
             kwarg_types = {
                 name: expr.type
                 for name, expr in new_kwargs.items()
-                if name != "out" and name != "domain"
+                if not is_operator and name != "out" and name != "domain"
             }
 
             type_info.accepts_args(
@@ -203,16 +204,29 @@ class ProgramTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTranslator):
                 with_kwargs=kwarg_types,
                 raise_exception=True,
             )
-
-            return_type = type_info.return_type(
-                new_func.type, with_args=arg_types, with_kwargs=kwarg_types
-            )
-            if return_type != new_kwargs["out"].type:
-                raise GTTypeError(
-                    f"Expected keyword argument `out` to be of "
-                    f"type {return_type}, but got "
-                    f"{new_kwargs['out'].type}."
+            return_type = ts.VoidType()
+            if is_operator:
+                operator_return_type = type_info.return_type(
+                    new_func.type, with_args=arg_types, with_kwargs=kwarg_types
+                )
+                if operator_return_type != new_kwargs["out"].type:
+                    raise GTTypeError(
+                        f"Expected keyword argument `out` to be of "
+                        f"type {operator_return_type}, but got "
+                        f"{new_kwargs['out'].type}."
+                    )
+            elif new_func.id in ["minimum", "maximum"]:
+                if new_args[0].type != new_args[1].type:
+                    raise GTTypeError(
+                        f"First and second argument in {new_func.id} must be the same type."
+                        f"Got `{new_args[0].type}` and `{new_args[1].type}`."
+                    )
+                return_type = new_args[0].type
+            else:
+                raise AssertionError(
+                    "Only calls `FieldOperator`s, `ScanOperator`s or minimum and maximum builtins allowed"
                 )
+
         except GTTypeError as ex:
             raise ProgramTypeError.from_past_node(
                 node, msg=f"Invalid call to `{node.func.id}`."
@@ -222,7 +236,7 @@ class ProgramTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTranslator):
             func=new_func,
             args=new_args,
             kwargs=new_kwargs,
-            type=ts.VoidType(),
+            type=return_type,
             location=node.location,
         )
 
diff --git a/src/gt4py/next/ffront/past_to_itir.py b/src/gt4py/next/ffront/past_to_itir.py
index 1fdf27df..8fc6d309 100644
--- a/src/gt4py/next/ffront/past_to_itir.py
+++ b/src/gt4py/next/ffront/past_to_itir.py
@@ -14,7 +14,7 @@
 
 from __future__ import annotations
 
-from typing import Any, Generator, Optional, cast
+from typing import Optional, cast
 
 from gt4py.eve import NodeTranslator, concepts, traits
 from gt4py.next.common import Dimension, DimensionKind, GridType, GTTypeError
@@ -249,17 +249,17 @@ class ProgramLowering(traits.VisitorWithSymbolTableTrait, NodeTranslator):
         dim_i: int,
         dim: Dimension,
         node_domain: past.Dict,
-    ) -> Generator[Any, None, None]:
+    ) -> list[itir.FunCall]:
+        assert len(node_domain.values_[dim_i].elts) == 2
         keys_dims_types = cast(ts.DimensionType, node_domain.keys_[dim_i].type).dim
-        if keys_dims_types == dim:
-            assert len(node_domain.values_[dim_i].elts) == 2
-            return (self.visit(bound) for bound in node_domain.values_[dim_i].elts)
-        else:
+        if keys_dims_types != dim:
             raise GTTypeError(
                 f"Dimensions in out field and field domain are not equivalent"
                 f"Expected {dim}, but got {keys_dims_types} "
             )
 
+        return [self.visit(bound) for bound in node_domain.values_[dim_i].elts]
+
     @staticmethod
     def _compute_field_slice(node: past.Subscript):
         out_field_name: past.Name = node.value
@@ -355,3 +355,14 @@ class ProgramLowering(traits.VisitorWithSymbolTableTrait, NodeTranslator):
             fun=itir.SymRef(id=node.op.value),
             args=[self.visit(node.left, **kwargs), self.visit(node.right, **kwargs)],
         )
+
+    def visit_Call(self, node: past.Call, **kwargs) -> itir.FunCall:
+        if node.func.id in ["maximum", "minimum"] and len(node.args) == 2:
+            return itir.FunCall(
+                fun=itir.SymRef(id=node.func.id),
+                args=[self.visit(node.args[0]), self.visit(node.args[1])],
+            )
+        else:
+            raise AssertionError(
+                "Only `minimum` and `maximum` builtins supported supported currently."
+            )
diff --git a/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py b/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py
index f60d2601..73c6705a 100644
--- a/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py
+++ b/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py
@@ -175,7 +175,7 @@ def _collect_offset_definitions(
                 connectivity.origin_axis,
                 connectivity.neighbor_axis,
             ]:
-                if not dim.kind == common.DimensionKind.HORIZONTAL:
+                if dim.kind != common.DimensionKind.HORIZONTAL:
                     raise NotImplementedError()
                 offset_definitions[dim.value] = TagDefinition(
                     name=Sym(id=dim.value), alias=_horizontal_dimension


Test patch:
diff --git a/tests/next_tests/ffront_tests/test_execution.py b/tests/next_tests/ffront_tests/test_execution.py
index 6dab8528..6ead6f93 100644
--- a/tests/next_tests/ffront_tests/test_execution.py
+++ b/tests/next_tests/ffront_tests/test_execution.py
@@ -17,20 +17,21 @@
 from functools import reduce
 
 import numpy as np
-import pytest as pytest
+import pytest
 
 from gt4py.next.ffront.decorator import field_operator, program, scan_operator
 from gt4py.next.ffront.experimental import as_offset
 from gt4py.next.ffront.fbuiltins import (
     Dimension,
     Field,
-    FieldOffset,
     astype,
     broadcast,
     float32,
     float64,
     int32,
     int64,
+    maximum,
+    minimum,
     neighbor_sum,
     where,
 )
@@ -726,7 +727,7 @@ def test_domain(fieldview_backend):
 
     @program
     def program_domain(a: Field[[IDim, JDim], float64]):
-        fieldop_domain(a, out=a, domain={IDim: (1, 9), JDim: (4, 6)})
+        fieldop_domain(a, out=a, domain={IDim: (minimum(1, 2), 9), JDim: (4, maximum(5, 6))})
 
     program_domain(a_IJ_float, offset_provider={})