Repository: GridTools/gt4py
Pull request #1196 (2023-03-06 12:20:40)

Small fixes to FOAST -> ITIR lowering
Just some fixes for minor bugs I found while working on the temporaries.
 - Fix literal dtype used for unary expressions
 - Use `maximum` and `minimum` for `max_over`, `min_over`
 - User proper initial values for `max_over`, `min_over` on other dtypes than `float64`


Patch:
diff --git a/src/gt4py/next/ffront/experimental.py b/src/gt4py/next/ffront/experimental.py
new file mode 100644
index 00000000..7dbd4331
--- /dev/null
+++ b/src/gt4py/next/ffront/experimental.py
@@ -0,0 +1,40 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2023, ETH Zurich
+# All rights reserved.
+#
+# This file is part of the GT4Py project and the GridTools framework.
+# GT4Py is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or any later
+# version. See the LICENSE.txt file at the top-level directory of this
+# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from dataclasses import dataclass
+
+from gt4py.next.type_system import type_specifications as ts
+
+
+@dataclass
+class BuiltInFunction:
+    __gt_type: ts.FunctionType
+
+    def __call__(self, *args, **kwargs):
+        """Act as an empty place holder for the built in function."""
+
+    def __gt_type__(self):
+        return self.__gt_type
+
+
+as_offset = BuiltInFunction(
+    ts.FunctionType(
+        args=[
+            ts.DeferredType(constraint=ts.OffsetType),
+            ts.DeferredType(constraint=ts.FieldType),
+        ],
+        kwargs={},
+        returns=ts.DeferredType(constraint=ts.OffsetType),
+    )
+)
diff --git a/src/gt4py/next/ffront/fbuiltins.py b/src/gt4py/next/ffront/fbuiltins.py
index 2f7f1f7b..3b135410 100644
--- a/src/gt4py/next/ffront/fbuiltins.py
+++ b/src/gt4py/next/ffront/fbuiltins.py
@@ -18,6 +18,7 @@ from dataclasses import dataclass
 from numpy import float32, float64, int32, int64
 
 from gt4py.next.common import Dimension, DimensionKind, Field
+from gt4py.next.ffront.experimental import as_offset  # noqa F401
 from gt4py.next.iterator import runtime
 from gt4py.next.type_system import type_specifications as ts
 
@@ -192,6 +193,7 @@ FUN_BUILTIN_NAMES = [
     "broadcast",
     "where",
     "astype",
+    "as_offset",
 ] + MATH_BUILTIN_NAMES
 
 BUILTIN_NAMES = TYPE_BUILTIN_NAMES + FUN_BUILTIN_NAMES
diff --git a/src/gt4py/next/ffront/foast_passes/type_deduction.py b/src/gt4py/next/ffront/foast_passes/type_deduction.py
index d316d6bd..84d1022c 100644
--- a/src/gt4py/next/ffront/foast_passes/type_deduction.py
+++ b/src/gt4py/next/ffront/foast_passes/type_deduction.py
@@ -718,6 +718,35 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
             location=node.location,
         )
 
+    def _visit_as_offset(self, node: foast.Call, **kwargs) -> foast.Call:
+        arg_0 = node.args[0].type
+        arg_1 = node.args[1].type
+        assert isinstance(arg_0, ts.OffsetType)
+        assert isinstance(arg_1, ts.FieldType)
+        if not type_info.is_integral(arg_1):
+            raise FieldOperatorTypeDeductionError.from_foast_node(
+                node,
+                msg=f"Incompatible argument in call to `{node.func.id}`. "
+                f"Excepted integer for offset field dtype, but got {arg_1.dtype}"
+                f"{node.location}",
+            )
+
+        if arg_0.source not in arg_1.dims:
+            raise FieldOperatorTypeDeductionError.from_foast_node(
+                node,
+                msg=f"Incompatible argument in call to `{node.func.id}`. "
+                f"{arg_0.source} not in list of offset field dimensions {arg_1.dims}. "
+                f"{node.location}",
+            )
+
+        return foast.Call(
+            func=node.func,
+            args=node.args,
+            kwargs=node.kwargs,
+            type=arg_0,
+            location=node.location,
+        )
+
     def _visit_where(self, node: foast.Call, **kwargs) -> foast.Call:
         mask_type = cast(ts.FieldType, node.args[0].type)
         true_branch_type = node.args[1].type
diff --git a/src/gt4py/next/ffront/foast_to_itir.py b/src/gt4py/next/ffront/foast_to_itir.py
index 0b2a2cc4..a9065669 100644
--- a/src/gt4py/next/ffront/foast_to_itir.py
+++ b/src/gt4py/next/ffront/foast_to_itir.py
@@ -17,8 +17,6 @@ import itertools
 from dataclasses import dataclass, field
 from typing import Any, Callable
 
-import numpy as np
-
 from gt4py.eve import NodeTranslator
 from gt4py.next.common import DimensionKind
 from gt4py.next.ffront import (
@@ -318,9 +316,12 @@ class FieldOperatorLowering(NodeTranslator):
             return self._lift_if_field(node)(
                 im.call_("not_")(to_value(node.operand)(self.visit(node.operand, **kwargs)))
             )
+
+        dtype = type_info.extract_dtype(node.type)
+        assert type_info.is_arithmetic(dtype)
         return self._lift_if_field(node)(
             im.call_(node.op.value)(
-                im.literal_("0", "int"),
+                im.literal_("0", dtype.kind.name.lower()),
                 to_value(node.operand)(self.visit(node.operand, **kwargs)),
             )
         )
@@ -357,10 +358,19 @@ class FieldOperatorLowering(NodeTranslator):
     def _visit_shift(self, node: foast.Call, **kwargs) -> itir.FunCall:
         match node.args[0]:
             case foast.Subscript(value=foast.Name(id=offset_name), index=int(offset_index)):
-                return im.shift_(offset_name, offset_index)(self.visit(node.func, **kwargs))
+                shift_offset = im.shift_(offset_name, offset_index)
             case foast.Name(id=offset_name):
-                return im.shift_(offset_name)(self.visit(node.func, **kwargs))
-        raise FieldOperatorLoweringError("Unexpected shift arguments!")
+                shift_offset = im.shift_(offset_name)
+            case foast.Call(func=foast.Name(id="as_offset")):
+                func_args = node.args[0]
+                offset_dim = func_args.args[0]
+                assert isinstance(offset_dim, foast.Name)
+                shift_offset = im.shift_(
+                    offset_dim.id, im.deref_(self.visit(func_args.args[1], **kwargs))
+                )
+            case _:
+                raise FieldOperatorLoweringError("Unexpected shift arguments!")
+        return shift_offset(self.visit(node.func, **kwargs))
 
     def _make_reduction_expr(
         self,
@@ -388,20 +398,23 @@ class FieldOperatorLowering(NodeTranslator):
         )
 
     def _visit_max_over(self, node: foast.Call, **kwargs) -> itir.FunCall:
-        # TODO(tehrengruber): replace greater_ with max_ builtin as soon as itir supports it
-        init_expr = itir.Literal(value=str(np.finfo(np.float64).min), type="float64")
+        dtype = type_info.extract_dtype(node.type)
+        min_value, _ = type_info.arithmetic_bounds(dtype)
+        init_expr = itir.Literal(value=str(min_value), type=dtype.kind.name.lower())
         return self._make_reduction_expr(
             node,
-            lambda expr: im.call_("if_")(im.greater_("acc", expr), "acc", expr),
+            lambda expr: im.call_("maximum")("acc", expr),
             init_expr,
             **kwargs,
         )
 
     def _visit_min_over(self, node: foast.Call, **kwargs) -> itir.FunCall:
-        init_expr = itir.Literal(value=str(np.finfo(np.float64).max), type="float64")
+        dtype = type_info.extract_dtype(node.type)
+        _, max_value = type_info.arithmetic_bounds(dtype)
+        init_expr = itir.Literal(value=str(max_value), type=dtype.kind.name.lower())
         return self._make_reduction_expr(
             node,
-            lambda expr: im.call_("if_")(im.less_("acc", expr), "acc", expr),
+            lambda expr: im.call_("minimum")("acc", expr),
             init_expr,
             **kwargs,
         )
@@ -548,7 +561,11 @@ class InsideReductionLowering(FieldOperatorLowering):
         if node.op is dialect_ast_enums.UnaryOperator.NOT:
             return im.call_(node.op.value)(self.visit(node.operand, **kwargs))
 
-        return im.call_(node.op.value)(im.literal_("0", "int"), self.visit(node.operand, **kwargs))
+        dtype = type_info.extract_dtype(node.type)
+        assert type_info.is_arithmetic(dtype)
+        return im.call_(node.op.value)(
+            im.literal_("0", dtype.kind.name.lower()), self.visit(node.operand, **kwargs)
+        )
 
     def _visit_shift(self, node: foast.Call, **kwargs) -> itir.SymRef:  # type: ignore[override]
         uid = f"{node.func.id}__{self._sequential_id()}"
diff --git a/src/gt4py/next/ffront/itir_makers.py b/src/gt4py/next/ffront/itir_makers.py
index f54af3b7..9c9ae206 100644
--- a/src/gt4py/next/ffront/itir_makers.py
+++ b/src/gt4py/next/ffront/itir_makers.py
@@ -75,7 +75,7 @@ def ensure_expr(literal_or_expr: Union[str, int, itir.Expr]) -> itir.Expr:
     return literal_or_expr
 
 
-def ensure_offset(str_or_offset: Union[str, itir.OffsetLiteral]) -> itir.OffsetLiteral:
+def ensure_offset(str_or_offset: Union[str, int, itir.OffsetLiteral]) -> itir.OffsetLiteral:
     """
     Convert Python literals into an OffsetLiteral and let OffsetLiterals pass unchanged.
 
@@ -87,7 +87,7 @@ def ensure_offset(str_or_offset: Union[str, itir.OffsetLiteral]) -> itir.OffsetL
     >>> ensure_offset(itir.OffsetLiteral(value="J"))
     OffsetLiteral(value='J')
     """
-    if isinstance(str_or_offset, str):
+    if isinstance(str_or_offset, (str, int)):
         return itir.OffsetLiteral(value=str_or_offset)
     return str_or_offset
 
@@ -258,7 +258,7 @@ def shift_(offset, value=None):
     Examples
     --------
     >>> shift_("i", 0)("a")
-    FunCall(fun=FunCall(fun=SymRef(id=SymbolRef('shift')), args=[OffsetLiteral(value='i'), Literal(value='0', type='int')]), args=[SymRef(id=SymbolRef('a'))])
+    FunCall(fun=FunCall(fun=SymRef(id=SymbolRef('shift')), args=[OffsetLiteral(value='i'), OffsetLiteral(value=0)]), args=[SymRef(id=SymbolRef('a'))])
 
     >>> shift_("V2E")("b")
     FunCall(fun=FunCall(fun=SymRef(id=SymbolRef('shift')), args=[OffsetLiteral(value='V2E')]), args=[SymRef(id=SymbolRef('b'))])
@@ -266,7 +266,7 @@ def shift_(offset, value=None):
     offset = ensure_offset(offset)
     args = [offset]
     if value is not None:
-        value = ensure_expr(value)
+        value = ensure_offset(value)
         args.append(value)
     return call_(call_("shift")(*args))
 
diff --git a/src/gt4py/next/type_system/type_info.py b/src/gt4py/next/type_system/type_info.py
index 7796bdfe..1936841e 100644
--- a/src/gt4py/next/type_system/type_info.py
+++ b/src/gt4py/next/type_system/type_info.py
@@ -15,6 +15,8 @@
 import functools
 from typing import Callable, Iterator, Type, TypeGuard, cast
 
+import numpy as np
+
 from gt4py.eve.utils import XIterable, xiter
 from gt4py.next.common import Dimension, GTTypeError
 from gt4py.next.type_system import type_specifications as ts
@@ -227,6 +229,16 @@ def is_arithmetic(symbol_type: ts.TypeSpec) -> bool:
     return is_floating_point(symbol_type) or is_integral(symbol_type)
 
 
+def arithmetic_bounds(arithmetic_type: ts.ScalarType):
+    assert is_arithmetic(arithmetic_type)
+    return {
+        ts.ScalarKind.FLOAT32: (np.finfo(np.float32).min, np.finfo(np.float32).max),
+        ts.ScalarKind.FLOAT64: (np.finfo(np.float64).min, np.finfo(np.float64).max),
+        ts.ScalarKind.INT32: (np.iinfo(np.int32).min, np.iinfo(np.int32).max),
+        ts.ScalarKind.INT64: (np.iinfo(np.int64).min, np.iinfo(np.int64).max),
+    }[arithmetic_type.kind]
+
+
 def is_type_or_tuple_of_type(type_: ts.TypeSpec, expected_type: type | tuple) -> bool:
     """
     Return True if ``type_`` matches any of the expected.


Test patch:
diff --git a/tests/next_tests/ffront_tests/test_execution.py b/tests/next_tests/ffront_tests/test_execution.py
index 7d855815..6dab8528 100644
--- a/tests/next_tests/ffront_tests/test_execution.py
+++ b/tests/next_tests/ffront_tests/test_execution.py
@@ -20,22 +20,21 @@ import numpy as np
 import pytest as 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,
-    max_over,
-    min_over,
     neighbor_sum,
     where,
 )
 from gt4py.next.ffront.foast_passes.type_deduction import FieldOperatorTypeDeductionError
-from gt4py.next.iterator.builtins import float32
 from gt4py.next.iterator.embedded import (
     NeighborTableOffsetProvider,
     index_field,
@@ -144,9 +143,6 @@ def test_tuples(fieldview_backend):
     b_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
     out_I_float = np_as_located_field(IDim)(np.zeros((size), dtype=float64))
 
-    if fieldview_backend == gtfn_cpu.run_gtfn:
-        pytest.skip("Tuples are not supported yet.")
-
     @field_operator
     def tuples(
         inp1: Field[[IDim], float64], inp2: Field[[IDim], float64]
@@ -336,6 +332,50 @@ def test_astype_float(fieldview_backend):
     assert np.allclose(c_int32.array(), out_int_32)
 
 
+def test_offset_field(fieldview_backend):
+    a_I_arr = np.random.randn(size, size).astype("float64")
+    a_I_float = np_as_located_field(IDim, KDim)(a_I_arr)
+    a_I_float_1 = np_as_located_field(IDim, KDim)(
+        np.append(np.insert(a_I_arr, size, 0, axis=1), [np.array([0] * (size + 1))], axis=0)
+    )
+    offset_field_arr = np.asarray(np.ones((size - 1, size - 1)), dtype=int64)
+    offset_field_comp = np.append(
+        np.insert(offset_field_arr, size - 1, 0, axis=1), [np.array([0] * size)], axis=0
+    )
+    offset_field = np_as_located_field(IDim, KDim)(offset_field_comp)
+    out_I_float = np_as_located_field(IDim, KDim)(np.zeros((size, size), dtype=float64))
+    out_I_float_1 = np_as_located_field(IDim, KDim)(np.zeros((size, size), dtype=float64))
+
+    @field_operator(backend=fieldview_backend)
+    def offset_index_field_fo(
+        a: Field[[IDim, KDim], float64],
+        offset_field: Field[[IDim, KDim], int64],
+    ) -> Field[[IDim, KDim], float64]:
+        a_i = a(as_offset(Ioff, offset_field))
+        a_i_k = a_i(as_offset(Koff, offset_field))
+        return a_i_k
+
+    offset_index_field_fo(
+        a_I_float,
+        offset_field,
+        out=out_I_float,
+        offset_provider={"Ioff": IDim, "Koff": KDim},
+    )
+
+    @field_operator(backend=fieldview_backend)
+    def offset_index_int_fo(a: Field[[IDim, KDim], float64]) -> Field[[IDim, KDim], float64]:
+        a_i = a(Ioff[1])
+        a_i_k = a_i(Koff[1])
+        return a_i_k
+
+    offset_index_int_fo(
+        a_I_float_1, out=out_I_float_1, offset_provider={"Ioff": IDim, "Koff": KDim}
+    )
+    assert np.allclose(
+        out_I_float.array()[: size - 1, : size - 1], out_I_float_1.array()[: size - 1, : size - 1]
+    )
+
+
 def test_nested_tuple_return(fieldview_backend):
     a_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
     b_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
@@ -384,7 +424,7 @@ def test_tuple_return_2(reduction_setup, fieldview_backend):
 
 
 @pytest.mark.xfail(raises=NotImplementedError)
-def test_tuple_with_local_field_in_reduction_shifted(reduction_setup):
+def test_tuple_with_local_field_in_reduction_shifted(reduction_setup, fieldview_backend):
     rs = reduction_setup
     Edge = rs.Edge
     Vertex = rs.Vertex
@@ -400,7 +440,7 @@ def test_tuple_with_local_field_in_reduction_shifted(reduction_setup):
     b = np_as_located_field(Vertex)(2 * np.ones((num_vertices,)))
     out = np_as_located_field(Edge)(np.zeros((num_edges,)))
 
-    @field_operator
+    @field_operator(backend=fieldview_backend)
     def reduce_tuple_element(
         edge_field: Field[[Edge], float64], vertex_field: Field[[Vertex], float64]
     ) -> Field[[Edge], float64]:
@@ -857,6 +897,9 @@ def test_implicit_broadcast_mixed_dims(fieldview_backend):
 
 
 def test_tuple_unpacking(fieldview_backend):
+    if fieldview_backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
+        pytest.skip("Tuple arguments are not supported in gtfn yet.")
+
     size = 10
     inp = np_as_located_field(IDim)(np.ones((size)))
     out1 = np_as_located_field(IDim)(np.ones((size)))
@@ -864,7 +907,7 @@ def test_tuple_unpacking(fieldview_backend):
     out3 = np_as_located_field(IDim)(np.ones((size)))
     out4 = np_as_located_field(IDim)(np.ones((size)))
 
-    @field_operator
+    @field_operator(backend=fieldview_backend)
     def unpack(
         inp: Field[[IDim], float64],
     ) -> tuple[
@@ -887,6 +930,9 @@ def test_tuple_unpacking(fieldview_backend):
 
 
 def test_tuple_unpacking_star_multi(fieldview_backend):
+    if fieldview_backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
+        pytest.skip("Tuple arguments are not supported in gtfn yet.")
+
     size = 10
     inp = np_as_located_field(IDim)(np.ones((size)))
     out = tuple(np_as_located_field(IDim)(np.ones(size) * i) for i in range(3 * 4))
@@ -906,7 +952,7 @@ def test_tuple_unpacking_star_multi(fieldview_backend):
         Field[[IDim], float64],
     ]
 
-    @field_operator
+    @field_operator(backend=fieldview_backend)
     def unpack(
         inp: Field[[IDim], float64],
     ) -> OutType:
@@ -928,7 +974,7 @@ def test_tuple_unpacking_too_many_values(fieldview_backend):
         match=(r"Could not deduce type: Too many values to unpack \(expected 3\)"),
     ):
 
-        @field_operator
+        @field_operator(backend=fieldview_backend)
         def _star_unpack() -> tuple[int, float64, int]:
             a, b, c = (1, 2.0, 3, 4, 5, 6, 7.0)
             return a, b, c
@@ -939,7 +985,7 @@ def test_tuple_unpacking_too_many_values(fieldview_backend):
         FieldOperatorTypeDeductionError, match=(r"Assignment value must be of type tuple!")
     ):
 
-        @field_operator
+        @field_operator(backend=fieldview_backend)
         def _invalid_unpack() -> tuple[int, float64, int]:
             a, b, c = 1
             return a
diff --git a/tests/next_tests/ffront_tests/test_foast_lowering.py b/tests/next_tests/ffront_tests/test_foast_lowering.py
index 8856a38c..fb2affba 100644
--- a/tests/next_tests/ffront_tests/test_foast_lowering.py
+++ b/tests/next_tests/ffront_tests/test_foast_lowering.py
@@ -165,11 +165,15 @@ def test_unary_ops():
 
     reference = im.let(
         "tmp__0",
-        im.lift_(im.lambda__("inp")(im.plus_(0, im.deref_("inp"))))("inp"),
+        im.lift_(im.lambda__("inp")(im.plus_(im.literal_("0", "float64"), im.deref_("inp"))))(
+            "inp"
+        ),
     )(
         im.let(
             "tmp__1",
-            im.lift_(im.lambda__("tmp__0")(im.minus_(0, im.deref_("tmp__0"))))("tmp__0"),
+            im.lift_(
+                im.lambda__("tmp__0")(im.minus_(im.literal_("0", "float64"), im.deref_("tmp__0")))
+            )("tmp__0"),
         )("tmp__1")
     )
 
diff --git a/tests/next_tests/ffront_tests/test_gt4py_builtins.py b/tests/next_tests/ffront_tests/test_gt4py_builtins.py
index 548267ae..bf88097a 100644
--- a/tests/next_tests/ffront_tests/test_gt4py_builtins.py
+++ b/tests/next_tests/ffront_tests/test_gt4py_builtins.py
@@ -80,7 +80,7 @@ def test_minover_execution(reduction_setup, fieldview_backend):
     Vertex, V2EDim = rs.Vertex, rs.V2EDim
     in_field = np_as_located_field(Vertex, V2EDim)(rs.v2e_table)
 
-    @field_operator
+    @field_operator(backend=fieldview_backend)
     def minover_fieldoperator(input: Field[[Vertex, V2EDim], int64]) -> Field[[Vertex], int64]:
         return min_over(input, axis=V2EDim)
 
@@ -101,7 +101,7 @@ def test_minover_execution_float(reduction_setup, fieldview_backend):
     in_field = np_as_located_field(Vertex, V2EDim)(in_array)
     out_field = np_as_located_field(Vertex)(np.zeros(rs.num_vertices))
 
-    @field_operator
+    @field_operator(backend=fieldview_backend)
     def minover_fieldoperator(input: Field[[Vertex, V2EDim], float64]) -> Field[[Vertex], float64]:
         return min_over(input, axis=V2EDim)
 
@@ -323,7 +323,7 @@ def test_conditional_shifted(fieldview_backend):
     out_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
     mask = np_as_located_field(IDim)(np.zeros((size,), dtype=bool))
 
-    @field_operator()
+    @field_operator
     def conditional_shifted(
         mask: Field[[IDim], bool], a: Field[[IDim], float64], b: Field[[IDim], float64]
     ) -> Field[[IDim], float64]:
diff --git a/tests/next_tests/ffront_tests/test_type_deduction.py b/tests/next_tests/ffront_tests/test_type_deduction.py
index 55efd4f1..110d8937 100644
--- a/tests/next_tests/ffront_tests/test_type_deduction.py
+++ b/tests/next_tests/ffront_tests/test_type_deduction.py
@@ -18,6 +18,7 @@ import pytest
 
 import gt4py.next.ffront.type_specifications
 from gt4py.next.common import DimensionKind, GTTypeError
+from gt4py.next.ffront.experimental import as_offset
 from gt4py.next.ffront.fbuiltins import (
     Dimension,
     Field,
@@ -684,3 +685,33 @@ def test_undefined_symbols():
 
     with pytest.raises(FieldOperatorTypeDeductionError, match="Undeclared symbol"):
         _ = FieldOperatorParser.apply_to_function(return_undefined)
+
+
+def test_as_offset_dim():
+    ADim = Dimension("ADim")
+    BDim = Dimension("BDim")
+    Boff = FieldOffset("Boff", source=BDim, target=(BDim,))
+
+    def as_offset_dim(a: Field[[ADim, BDim], float], b: Field[[ADim], int]):
+        return a(as_offset(Boff, b))
+
+    with pytest.raises(
+        FieldOperatorTypeDeductionError,
+        match=f"not in list of offset field dimensions",
+    ):
+        _ = FieldOperatorParser.apply_to_function(as_offset_dim)
+
+
+def test_as_offset_dtype():
+    ADim = Dimension("ADim")
+    BDim = Dimension("BDim")
+    Boff = FieldOffset("Boff", source=BDim, target=(BDim,))
+
+    def as_offset_dtype(a: Field[[ADim, BDim], float], b: Field[[BDim], float]):
+        return a(as_offset(Boff, b))
+
+    with pytest.raises(
+        FieldOperatorTypeDeductionError,
+        match=f"Excepted integer for offset field dtype",
+    ):
+        _ = FieldOperatorParser.apply_to_function(as_offset_dtype)