Repository: GridTools/gt4py
Pull request #1106 (2022-12-22 17:20:03)

Remove ellipsis as (generic) field dimension 
*Edit @tehrengruber*
Very early in the development of the frontend the ellipsis, i.e. `...`, was used to signify that a field defined on any dimensions is permitted as an input to a field operator or program. The concept was never fully supported and at least partially broken. While fixing typing errors this became a burden and it was decided to remove the concept until we introduce a more powerful type system that is capable of this in a proper way.
```python
def field_op(in_field: Field[..., float64]) -> Field[..., float64]:
  ...
```


Patch:
diff --git a/src/functional/ffront/decorator.py b/src/functional/ffront/decorator.py
index 7b2b4a77..790760de 100644
--- a/src/functional/ffront/decorator.py
+++ b/src/functional/ffront/decorator.py
@@ -406,7 +406,7 @@ def program(
 
     Examples:
         >>> @program  # noqa: F821 # doctest: +SKIP
-        ... def program(in_field: Field[..., float64], out_field: Field[..., float64]): # noqa: F821
+        ... def program(in_field: Field[[TDim], float64], out_field: Field[[TDim], float64]): # noqa: F821
         ...     field_op(in_field, out=out_field)
         >>> program(in_field, out=out_field) # noqa: F821 # doctest: +SKIP
 
@@ -414,7 +414,7 @@ def program(
         >>> # not passing it will result in embedded execution by default
         >>> # the above is equivalent to
         >>> @program(backend="roundtrip")  # noqa: F821 # doctest: +SKIP
-        ... def program(in_field: Field[..., float64], out_field: Field[..., float64]): # noqa: F821
+        ... def program(in_field: Field[[TDim], float64], out_field: Field[[TDim], float64]): # noqa: F821
         ...     field_op(in_field, out=out_field)
         >>> program(in_field, out=out_field) # noqa: F821 # doctest: +SKIP
     """
@@ -625,14 +625,14 @@ def field_operator(
 
     Examples:
         >>> @field_operator  # doctest: +SKIP
-        ... def field_op(in_field: Field[..., float64]) -> Field[..., float64]: # noqa: F821
+        ... def field_op(in_field: Field[[TDim], float64]) -> Field[[TDim], float64]: # noqa: F821
         ...     ...
         >>> field_op(in_field, out=out_field)  # noqa: F821 # doctest: +SKIP
 
         >>> # the backend can optionally be passed if already decided
         >>> # not passing it will result in embedded execution by default
         >>> @field_operator(backend="roundtrip")  # doctest: +SKIP
-        ... def field_op(in_field: Field[..., float64]) -> Field[..., float64]: # noqa: F821
+        ... def field_op(in_field: Field[[TDim], float64]) -> Field[[TDim], float64]: # noqa: F821
         ...     ...
     """
 
diff --git a/src/functional/ffront/foast_passes/type_deduction.py b/src/functional/ffront/foast_passes/type_deduction.py
index d05268f5..90f9fd69 100644
--- a/src/functional/ffront/foast_passes/type_deduction.py
+++ b/src/functional/ffront/foast_passes/type_deduction.py
@@ -15,7 +15,7 @@ from typing import Optional, cast
 
 import functional.ffront.field_operator_ast as foast
 from eve import NodeTranslator, NodeVisitor, traits
-from functional.common import Dimension, DimensionKind, GTSyntaxError, GTTypeError
+from functional.common import DimensionKind, GTSyntaxError, GTTypeError
 from functional.ffront import dialect_ast_enums, fbuiltins, type_info, type_specifications as ts
 from functional.ffront.foast_passes.utils import compute_assign_indices
 from functional.ffront.type_translation import from_value
@@ -101,11 +101,8 @@ def promote_to_mask_type(
     >>> promote_to_mask_type(ts.FieldType(dims=[I], dtype=bool_type), ts.FieldType(dims=[I,J], dtype=dtype))
     FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>), Dimension(value='J', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<ScalarKind.FLOAT64: 1064>, shape=None))
     """
-    # TODO: This code does not handle ellipses for dimensions. Fix it.
-    assert not isinstance(input_type, ts.FieldType) or input_type.dims is not ...
-    assert mask_type.dims is not ...
     if isinstance(input_type, ts.ScalarType) or not all(
-        item in cast(list[Dimension], input_type.dims) for item in mask_type.dims
+        item in input_type.dims for item in mask_type.dims
     ):
         return_dtype = input_type.dtype if isinstance(input_type, ts.FieldType) else input_type
         return type_info.promote(input_type, ts.FieldType(dims=mask_type.dims, dtype=return_dtype))  # type: ignore
@@ -141,10 +138,11 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
     ---------
     >>> import ast
     >>> import typing
-    >>> from functional.common import Field
+    >>> from functional.common import Field, Dimension
     >>> from functional.ffront.source_utils import SourceDefinition, get_closure_vars_from_function
     >>> from functional.ffront.func_to_foast import FieldOperatorParser
-    >>> def example(a: "Field[..., float]", b: "Field[..., float]"):
+    >>> IDim = Dimension("IDim")
+    >>> def example(a: "Field[[IDim], float]", b: "Field[[IDim], float]"):
     ...     return a + b
 
     >>> source_definition = SourceDefinition.from_function(example)
@@ -158,7 +156,7 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
 
     >>> typed_fieldop = FieldOperatorTypeDeduction.apply(untyped_fieldop)
     >>> assert typed_fieldop.body[0].value.type == ts.FieldType(dtype=ts.ScalarType(
-    ...     kind=ts.ScalarKind.FLOAT64), dims=Ellipsis)
+    ...     kind=ts.ScalarKind.FLOAT64), dims=[IDim])
     """
 
     @classmethod
diff --git a/src/functional/ffront/foast_pretty_printer.py b/src/functional/ffront/foast_pretty_printer.py
index f58f9955..027d289e 100644
--- a/src/functional/ffront/foast_pretty_printer.py
+++ b/src/functional/ffront/foast_pretty_printer.py
@@ -199,14 +199,15 @@ def pretty_format(node: foast.LocatedNode) -> str:
     """
     Pretty print (to string) an `foast.LocatedNode`.
 
-    >>> from functional.common import Field
+    >>> from functional.common import Field, Dimension
     >>> from functional.ffront.decorator import field_operator
+    >>> IDim = Dimension("IDim")
     >>> @field_operator
-    ... def field_op(a: Field[..., int]) -> Field[..., int]:
+    ... def field_op(a: Field[[IDim], int]) -> Field[[IDim], int]:
     ...     return a+1
     >>> print(pretty_format(field_op.foast_node))
     @field_operator
-    def field_op(a: Field[..., int64]) -> Field[..., int64]:
+    def field_op(a: Field[[IDim], int64]) -> Field[[IDim], int64]:
       return a + 1
     """
     return _PrettyPrinter().apply(node)
diff --git a/src/functional/ffront/foast_to_itir.py b/src/functional/ffront/foast_to_itir.py
index 1d47e8ca..73491be0 100644
--- a/src/functional/ffront/foast_to_itir.py
+++ b/src/functional/ffront/foast_to_itir.py
@@ -20,7 +20,7 @@ from typing import Any, Callable, Optional, cast
 import numpy as np
 
 from eve import NodeTranslator
-from functional.common import Dimension, DimensionKind
+from functional.common import DimensionKind
 from functional.ffront import (
     dialect_ast_enums,
     fbuiltins,
@@ -34,10 +34,7 @@ from functional.iterator import ir as itir
 
 
 def is_local_kind(symbol_type: ts.FieldType) -> bool:
-    assert isinstance(symbol_type, ts.FieldType)
-    if symbol_type.dims == ...:
-        return False
-    return any(dim.kind == DimensionKind.LOCAL for dim in cast(list[Dimension], symbol_type.dims))
+    return any(dim.kind == DimensionKind.LOCAL for dim in symbol_type.dims)
 
 
 class ITIRTypeKind(enum.Enum):
@@ -111,8 +108,9 @@ def to_value(node: foast.Expr) -> Callable[[itir.Expr], itir.Expr]:
     ---------
     >>> from functional.ffront.func_to_foast import FieldOperatorParser
     >>> from functional.ffront.fbuiltins import float64
-    >>> from functional.common import Field
-    >>> def foo(a: Field[..., "float64"]):
+    >>> from functional.common import Field, Dimension
+    >>> IDim = Dimension("IDim")
+    >>> def foo(a: Field[[IDim], "float64"]):
     ...    b = 5
     ...    return a, b
 
@@ -141,9 +139,10 @@ class FieldOperatorLowering(NodeTranslator):
     --------
     >>> from functional.ffront.func_to_foast import FieldOperatorParser
     >>> from functional.ffront.fbuiltins import float64
-    >>> from functional.common import Field
+    >>> from functional.common import Field, Dimension
     >>>
-    >>> def fieldop(inp: Field[..., "float64"]):
+    >>> IDim = Dimension("IDim")
+    >>> def fieldop(inp: Field[[IDim], "float64"]):
     ...    return inp
     >>>
     >>> parsed = FieldOperatorParser.apply_to_function(fieldop)
diff --git a/src/functional/ffront/func_to_foast.py b/src/functional/ffront/func_to_foast.py
index ccd890e1..5f08811e 100644
--- a/src/functional/ffront/func_to_foast.py
+++ b/src/functional/ffront/func_to_foast.py
@@ -57,9 +57,10 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
     Parse a function into a Field Operator AST (FOAST), which can
     be lowered into Iterator IR (ITIR)
 
-    >>> from functional.common import Field
+    >>> from functional.common import Field, Dimension
     >>> float64 = float
-    >>> def field_op(inp: Field[..., float64]):
+    >>> IDim = Dimension("IDim")
+    >>> def field_op(inp: Field[[IDim], float64]):
     ...     return inp
     >>> foast_tree = FieldOperatorParser.apply_to_function(field_op)
     >>> foast_tree  # doctest: +ELLIPSIS
@@ -72,7 +73,7 @@ class FieldOperatorParser(DialectParser[foast.FunctionDefinition]):
 
     If a syntax error is encountered, it will point to the location in the source code.
 
-    >>> def wrong_syntax(inp: Field[..., int]):
+    >>> def wrong_syntax(inp: Field[[IDim], int]):
     ...     for i in [1, 2, 3]: # for is not part of the field operator syntax
     ...         tmp = inp
     ...     return tmp
diff --git a/src/functional/ffront/past_to_itir.py b/src/functional/ffront/past_to_itir.py
index 1fc47d6b..2bc5d88f 100644
--- a/src/functional/ffront/past_to_itir.py
+++ b/src/functional/ffront/past_to_itir.py
@@ -196,10 +196,9 @@ class ProgramLowering(traits.VisitorWithSymbolTableTrait, NodeTranslator):
 
         assert isinstance(out_field.type, ts.TypeSpec)
         out_field_types = type_info.primitive_constituents(out_field.type).to_list()
-        out_field_types0_dims = cast(ts.FieldType, out_field_types[0]).dims
+        out_dims = cast(ts.FieldType, out_field_types[0]).dims
         if any(
-            not isinstance(out_field_type, ts.FieldType)
-            or out_field_type.dims != out_field_types0_dims
+            not isinstance(out_field_type, ts.FieldType) or out_field_type.dims != out_dims
             for out_field_type in out_field_types
         ):
             raise AssertionError(
@@ -208,7 +207,7 @@ class ProgramLowering(traits.VisitorWithSymbolTableTrait, NodeTranslator):
                 f" caught in type deduction already."
             )
 
-        for dim_i, dim in enumerate(out_field_types0_dims):  # type: ignore[arg-type] # mypy does not deduce type correctly
+        for dim_i, dim in enumerate(out_dims):
             # an expression for the size of a dimension
             dim_size = itir.SymRef(id=_size_arg_from_field(out_field.id, dim_i))
             # bounds
diff --git a/src/functional/ffront/type_info.py b/src/functional/ffront/type_info.py
index 62431cb1..7b5636e5 100644
--- a/src/functional/ffront/type_info.py
+++ b/src/functional/ffront/type_info.py
@@ -16,7 +16,7 @@ from functools import reduce
 from typing import Iterable, Iterator, cast
 
 import functional.ffront.type_specifications as ts
-from functional.common import GTTypeError
+from functional.common import Dimension, GTTypeError
 from functional.type_system.type_info import (  # noqa: F401
     accepts_args as accepts_args,
     apply_to_primitive_constituents as apply_to_primitive_constituents,
@@ -117,7 +117,8 @@ def function_signature_incompatibilities_scanop(
                 # The structure of the scan passes argument and the requested
                 # argument type differ. As such we can not extract the dimensions
                 # and just return a generic field shown in the error later on.
-                return ts.FieldType(dims=..., dtype=dtype)
+                # TODO: we want some generic field type here, but our type system does not support it yet.
+                return ts.FieldType(dims=[Dimension("...")], dtype=dtype)
 
         promoted_args.append(
             apply_to_primitive_constituents(scan_pass_arg, _as_field, with_path_arg=True)  # type: ignore[arg-type]
diff --git a/src/functional/type_system/type_info.py b/src/functional/type_system/type_info.py
index 4befca47..758fe9bd 100644
--- a/src/functional/type_system/type_info.py
+++ b/src/functional/type_system/type_info.py
@@ -13,7 +13,6 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 import functools
-from types import EllipsisType
 from typing import Callable, Iterator, Type, TypeGuard, cast
 
 from eve.utils import XIterable, xiter
@@ -265,8 +264,7 @@ def extract_dims(symbol_type: ts.TypeSpec) -> list[Dimension]:
         case ts.ScalarType():
             return []
         case ts.FieldType(dims):
-            # TODO: This code does not handle ellipses for dimensions. Fix it.
-            return dims  # type: ignore
+            return dims
     raise GTTypeError(f"Can not extract dimensions from {symbol_type}!")
 
 
@@ -362,9 +360,7 @@ def promote(*types: ts.FieldType | ts.ScalarType) -> ts.FieldType | ts.ScalarTyp
     raise TypeError("Expected a FieldType or ScalarType.")
 
 
-def promote_dims(
-    *dims_list: list[Dimension] | EllipsisType,
-) -> list[Dimension] | EllipsisType:
+def promote_dims(*dims_list: list[Dimension]) -> list[Dimension]:
     """
     Find a unique ordering of multiple (individually ordered) lists of dimensions.
 
@@ -395,9 +391,6 @@ def promote_dims(
     #  (contrary to successors) we also use this directionality here.
     graph: dict[Dimension, set[Dimension]] = {}
     for dims in dims_list:
-        if dims == Ellipsis:
-            return Ellipsis
-        dims = cast(list[Dimension], dims)
         if len(dims) == 0:
             continue
         # create a vertex for each dimension
diff --git a/src/functional/type_system/type_specifications.py b/src/functional/type_system/type_specifications.py
index 7640aafb..50312f23 100644
--- a/src/functional/type_system/type_specifications.py
+++ b/src/functional/type_system/type_specifications.py
@@ -13,7 +13,6 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from dataclasses import dataclass
-from types import EllipsisType
 from typing import Optional
 
 import numpy as np
@@ -109,7 +108,7 @@ class TupleType(DataType):
 
 @dataclass(frozen=True)
 class FieldType(DataType, CallableType):
-    dims: list[func_common.Dimension] | EllipsisType
+    dims: list[func_common.Dimension]
     dtype: ScalarType
 
     def __str__(self):


Test patch:
diff --git a/tests/functional_tests/ffront_tests/test_error_line_number.py b/tests/functional_tests/ffront_tests/test_error_line_number.py
index f761f96a..2ff0a906 100644
--- a/tests/functional_tests/ffront_tests/test_error_line_number.py
+++ b/tests/functional_tests/ffront_tests/test_error_line_number.py
@@ -1,23 +1,25 @@
 import pytest
 
-from functional.common import Field
+from functional.common import Dimension, Field
 from functional.ffront.func_to_foast import FieldOperatorParser, FieldOperatorSyntaxError
 
 
 # NOTE: This test is sensitive to filename and the line number of the marked statement
 
+TDim = Dimension("TDim")  # Meaningless dimension, used for tests.
+
 
 def test_invalid_syntax_error_empty_return():
     """Field operator syntax errors point to the file, line and column."""
 
-    def wrong_syntax(inp: Field[..., float]):
+    def wrong_syntax(inp: Field[[TDim], float]):
         return  # <-- this line triggers error
 
     with pytest.raises(
         FieldOperatorSyntaxError,
         match=(
             r"Invalid Field Operator Syntax: "
-            r"Empty return not allowed \(test_error_line_number.py, line 14\)"
+            r"Empty return not allowed \(test_error_line_number.py, line 16\)"
         ),
     ):
         _ = FieldOperatorParser.apply_to_function(wrong_syntax)
diff --git a/tests/functional_tests/ffront_tests/test_foast_lowering.py b/tests/functional_tests/ffront_tests/test_foast_lowering.py
index a7ed81a8..57dfb0ff 100644
--- a/tests/functional_tests/ffront_tests/test_foast_lowering.py
+++ b/tests/functional_tests/ffront_tests/test_foast_lowering.py
@@ -15,6 +15,8 @@ from __future__ import annotations
 
 from types import SimpleNamespace
 
+import pytest
+
 from functional.common import DimensionKind, Field
 from functional.ffront import itir_makers as im, type_specifications as ts, type_translation
 from functional.ffront.fbuiltins import (
@@ -36,6 +38,7 @@ Vertex = Dimension("Vertex")
 Cell = Dimension("Cell")
 V2EDim = Dimension("V2E", DimensionKind.LOCAL)
 V2E = FieldOffset("V2E", source=Edge, target=(Vertex, V2EDim))
+TDim = Dimension("TDim")  # Meaningless dimension, used for tests.
 
 
 def debug_itir(tree):
@@ -49,7 +52,7 @@ def debug_itir(tree):
 
 
 def test_copy():
-    def copy_field(inp: Field[..., float64]):
+    def copy_field(inp: Field[[TDim], float64]):
         return inp
 
     # ast_passes
@@ -139,7 +142,7 @@ def test_negative_shift():
 
 
 def test_temp_assignment():
-    def copy_field(inp: Field[..., float64]):
+    def copy_field(inp: Field[[TDim], float64]):
         tmp = inp
         inp = tmp
         tmp2 = inp
@@ -156,7 +159,7 @@ def test_temp_assignment():
 
 
 def test_unary_ops():
-    def unary(inp: Field[..., float64]):
+    def unary(inp: Field[[TDim], float64]):
         tmp = +inp
         tmp = -tmp
         return tmp
@@ -180,7 +183,9 @@ def test_unary_ops():
 def test_unpacking():
     """Unpacking assigns should get separated."""
 
-    def unpacking(inp1: Field[..., float64], inp2: Field[..., float64]) -> Field[..., float64]:
+    def unpacking(
+        inp1: Field[[TDim], float64], inp2: Field[[TDim], float64]
+    ) -> Field[[TDim], float64]:
         tmp1, tmp2 = inp1, inp2  # noqa
         return tmp1
 
@@ -204,8 +209,10 @@ def test_unpacking():
 
 
 def test_annotated_assignment():
-    def copy_field(inp: Field[..., float64]):
-        tmp: Field[..., float64] = inp
+    pytest.skip("Annotated assignments are not properly supported at the moment.")
+
+    def copy_field(inp: Field[[TDim], float64]):
+        tmp: Field[[TDim], float64] = inp
         return tmp
 
     parsed = FieldOperatorParser.apply_to_function(copy_field)
@@ -220,14 +227,14 @@ def test_call():
     # create something that appears to the lowering like a field operator.
     #  we could also create an actual field operator, but we want to avoid
     #  using such heavy constructs for testing the lowering.
-    field_type = type_translation.from_type_hint(Field[..., float64])
+    field_type = type_translation.from_type_hint(Field[[TDim], float64])
     identity = SimpleNamespace(
         __gt_type__=lambda: ts.FieldOperatorType(
             definition=ts.FunctionType(args=[field_type], kwargs={}, returns=field_type)
         )
     )
 
-    def call(inp: Field[..., float64]) -> Field[..., float64]:
+    def call(inp: Field[[TDim], float64]) -> Field[[TDim], float64]:
         return identity(inp)
 
     parsed = FieldOperatorParser.apply_to_function(call)
@@ -241,7 +248,7 @@ def test_call():
 def test_temp_tuple():
     """Returning a temp tuple should work."""
 
-    def temp_tuple(a: Field[..., float64], b: Field[..., int64]):
+    def temp_tuple(a: Field[[TDim], float64], b: Field[[TDim], int64]):
         tmp = a, b
         return tmp
 
@@ -257,7 +264,7 @@ def test_temp_tuple():
 
 
 def test_unary_not():
-    def unary_not(cond: Field[..., "bool"]):
+    def unary_not(cond: Field[[TDim], "bool"]):
         return not cond
 
     parsed = FieldOperatorParser.apply_to_function(unary_not)
@@ -269,7 +276,7 @@ def test_unary_not():
 
 
 def test_binary_plus():
-    def plus(a: Field[..., float64], b: Field[..., float64]):
+    def plus(a: Field[[TDim], float64], b: Field[[TDim], float64]):
         return a + b
 
     parsed = FieldOperatorParser.apply_to_function(plus)
@@ -316,7 +323,7 @@ def test_add_scalar_literals():
 
 
 def test_binary_mult():
-    def mult(a: Field[..., float64], b: Field[..., float64]):
+    def mult(a: Field[[TDim], float64], b: Field[[TDim], float64]):
         return a * b
 
     parsed = FieldOperatorParser.apply_to_function(mult)
@@ -330,7 +337,7 @@ def test_binary_mult():
 
 
 def test_binary_minus():
-    def minus(a: Field[..., float64], b: Field[..., float64]):
+    def minus(a: Field[[TDim], float64], b: Field[[TDim], float64]):
         return a - b
 
     parsed = FieldOperatorParser.apply_to_function(minus)
@@ -344,7 +351,7 @@ def test_binary_minus():
 
 
 def test_binary_div():
-    def division(a: Field[..., float64], b: Field[..., float64]):
+    def division(a: Field[[TDim], float64], b: Field[[TDim], float64]):
         return a / b
 
     parsed = FieldOperatorParser.apply_to_function(division)
@@ -358,7 +365,7 @@ def test_binary_div():
 
 
 def test_binary_and():
-    def bit_and(a: Field[..., "bool"], b: Field[..., "bool"]):
+    def bit_and(a: Field[[TDim], "bool"], b: Field[[TDim], "bool"]):
         return a & b
 
     parsed = FieldOperatorParser.apply_to_function(bit_and)
@@ -386,7 +393,7 @@ def test_scalar_and():
 
 
 def test_binary_or():
-    def bit_or(a: Field[..., "bool"], b: Field[..., "bool"]):
+    def bit_or(a: Field[[TDim], "bool"], b: Field[[TDim], "bool"]):
         return a | b
 
     parsed = FieldOperatorParser.apply_to_function(bit_or)
@@ -412,7 +419,7 @@ def test_compare_scalars():
 
 
 def test_compare_gt():
-    def comp_gt(a: Field[..., float64], b: Field[..., float64]):
+    def comp_gt(a: Field[[TDim], float64], b: Field[[TDim], float64]):
         return a > b
 
     parsed = FieldOperatorParser.apply_to_function(comp_gt)
@@ -426,7 +433,7 @@ def test_compare_gt():
 
 
 def test_compare_lt():
-    def comp_lt(a: Field[..., float64], b: Field[..., float64]):
+    def comp_lt(a: Field[[TDim], float64], b: Field[[TDim], float64]):
         return a < b
 
     parsed = FieldOperatorParser.apply_to_function(comp_lt)
@@ -440,7 +447,7 @@ def test_compare_lt():
 
 
 def test_compare_eq():
-    def comp_eq(a: Field[..., "int64"], b: Field[..., "int64"]):
+    def comp_eq(a: Field[[TDim], "int64"], b: Field[[TDim], "int64"]):
         return a == b
 
     parsed = FieldOperatorParser.apply_to_function(comp_eq)
diff --git a/tests/functional_tests/ffront_tests/test_foast_parsing.py b/tests/functional_tests/ffront_tests/test_foast_parsing.py
index 39e97c66..00eb16af 100644
--- a/tests/functional_tests/ffront_tests/test_foast_parsing.py
+++ b/tests/functional_tests/ffront_tests/test_foast_parsing.py
@@ -79,6 +79,8 @@ OR = itir.SymRef(id=or_.fun.__name__)
 XOR = itir.SymRef(id=xor_.fun.__name__)
 LIFT = itir.SymRef(id=lift.fun.__name__)
 
+TDim = Dimension("TDim")  # Meaningless dimension, used for tests.
+
 
 # --- Parsing ---
 def test_untyped_arg():
@@ -110,13 +112,13 @@ def test_mistyped_arg():
 def test_return_type():
     """Return type annotation should be stored on the FieldOperator."""
 
-    def rettype(inp: Field[..., float64]) -> Field[..., float64]:
+    def rettype(inp: Field[[TDim], float64]) -> Field[[TDim], float64]:
         return inp
 
     parsed = FieldOperatorParser.apply_to_function(rettype)
 
     assert parsed.body[-1].value.type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
 
@@ -124,7 +126,7 @@ def test_return_type():
 def test_invalid_syntax_no_return():
     """Field operators must end with a return statement."""
 
-    def no_return(inp: Field[..., "float64"]):
+    def no_return(inp: Field[[TDim], "float64"]):
         tmp = inp  # noqa
 
     with pytest.raises(
@@ -137,7 +139,7 @@ def test_invalid_syntax_no_return():
 def test_invalid_assign_to_expr():
     """Assigning to subscripts disallowed until a usecase can be found."""
 
-    def invalid_assign_to_expr(inp1: Field[..., "float64"], inp2: Field[..., "float64"]):
+    def invalid_assign_to_expr(inp1: Field[[TDim], "float64"], inp2: Field[[TDim], "float64"]):
         tmp = inp1
         tmp[-1] = inp2
         return tmp
@@ -147,7 +149,7 @@ def test_invalid_assign_to_expr():
 
 
 def test_temp_assignment():
-    def copy_field(inp: Field[..., "float64"]):
+    def copy_field(inp: Field[[TDim], "float64"]):
         tmp = inp
         inp = tmp
         tmp2 = inp
@@ -156,14 +158,16 @@ def test_temp_assignment():
     parsed = FieldOperatorParser.apply_to_function(copy_field)
 
     assert parsed.annex.symtable["tmp__0"].type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
 
 
 def test_clashing_annotated_assignment():
-    def clashing(inp: Field[..., "float64"]):
-        tmp: Field[..., "int64"] = inp
+    pytest.skip("Annotated assignments are not properly supported at the moment.")
+
+    def clashing(inp: Field[[TDim], "float64"]):
+        tmp: Field[[TDim], "int64"] = inp
         return tmp
 
     with pytest.raises(FieldOperatorTypeDeductionError, match="type inconsistency"):
@@ -171,31 +175,31 @@ def test_clashing_annotated_assignment():
 
 
 def test_binary_pow():
-    def power(inp: Field[..., "float64"]):
+    def power(inp: Field[[TDim], "float64"]):
         return inp**3
 
     parsed = FieldOperatorParser.apply_to_function(power)
 
     assert parsed.body[-1].value.type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
 
 
 def test_binary_mod():
-    def modulo(inp: Field[..., "int64"]):
+    def modulo(inp: Field[[TDim], "int64"]):
         return inp % 3
 
     parsed = FieldOperatorParser.apply_to_function(modulo)
 
     assert parsed.body[-1].value.type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.INT64, shape=None),
     )
 
 
 def test_bool_and():
-    def bool_and(a: Field[..., "bool"], b: Field[..., "bool"]):
+    def bool_and(a: Field[[TDim], "bool"], b: Field[[TDim], "bool"]):
         return a and b
 
     with pytest.raises(
@@ -206,7 +210,7 @@ def test_bool_and():
 
 
 def test_bool_or():
-    def bool_or(a: Field[..., "bool"], b: Field[..., "bool"]):
+    def bool_or(a: Field[[TDim], "bool"], b: Field[[TDim], "bool"]):
         return a or b
 
     with pytest.raises(
@@ -217,25 +221,25 @@ def test_bool_or():
 
 
 def test_bool_xor():
-    def bool_xor(a: Field[..., "bool"], b: Field[..., "bool"]):
+    def bool_xor(a: Field[[TDim], "bool"], b: Field[[TDim], "bool"]):
         return a ^ b
 
     parsed = FieldOperatorParser.apply_to_function(bool_xor)
 
     assert parsed.body[-1].value.type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL, shape=None),
     )
 
 
 def test_unary_tilde():
-    def unary_tilde(a: Field[..., "bool"]):
+    def unary_tilde(a: Field[[TDim], "bool"]):
         return ~a
 
     parsed = FieldOperatorParser.apply_to_function(unary_tilde)
 
     assert parsed.body[-1].value.type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL, shape=None),
     )
 
@@ -251,8 +255,8 @@ def test_scalar_cast():
 
 def test_conditional_wrong_mask_type():
     def conditional_wrong_mask_type(
-        a: Field[..., float64],
-    ) -> Field[..., float64]:
+        a: Field[[TDim], float64],
+    ) -> Field[[TDim], float64]:
         return where(a, a, a)
 
     msg = r"Expected a field with dtype bool."
@@ -262,10 +266,10 @@ def test_conditional_wrong_mask_type():
 
 def test_conditional_wrong_arg_type():
     def conditional_wrong_arg_type(
-        mask: Field[..., bool],
-        a: Field[..., float32],
-        b: Field[..., float64],
-    ) -> Field[..., float64]:
+        mask: Field[[TDim], bool],
+        a: Field[[TDim], float32],
+        b: Field[[TDim], float64],
+    ) -> Field[[TDim], float64]:
         return where(mask, a, b)
 
     msg = r"Could not promote scalars of different dtype \(not implemented\)."
@@ -324,13 +328,13 @@ def test_no_implicit_broadcast_in_field_op_call():
 
 
 def test_astype():
-    def astype_fieldop(a: Field[..., "int64"]) -> Field[..., float64]:
+    def astype_fieldop(a: Field[[TDim], "int64"]) -> Field[[TDim], float64]:
         return astype(a, float64)
 
     parsed = FieldOperatorParser.apply_to_function(astype_fieldop)
 
     assert parsed.body[-1].value.type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
 
@@ -344,7 +348,7 @@ def test_closure_symbols():
     nonlocals_unreferenced = FrozenNamespace()
     nonlocals = FrozenNamespace(float_value=2.3, np_value=np.float32(3.4))
 
-    def operator_with_refs(inp: Field[..., "float64"], inp2: Field[..., "float32"]):
+    def operator_with_refs(inp: Field[[TDim], "float64"], inp2: Field[[TDim], "float32"]):
         a = inp + nonlocals.float_value
         b = inp2 + nonlocals.np_value
         return a, b
diff --git a/tests/functional_tests/ffront_tests/test_type_deduction.py b/tests/functional_tests/ffront_tests/test_type_deduction.py
index 1daf9c0a..c117e83d 100644
--- a/tests/functional_tests/ffront_tests/test_type_deduction.py
+++ b/tests/functional_tests/ffront_tests/test_type_deduction.py
@@ -34,6 +34,9 @@ from functional.ffront.func_to_foast import FieldOperatorParser
 from functional.type_system import type_info, type_specifications as ts
 
 
+TDim = Dimension("TDim")  # Meaningless dimension, used for tests.
+
+
 def type_info_cases() -> list[tuple[Optional[ts.TypeSpec], dict]]:
     return [
         (
@@ -66,7 +69,7 @@ def type_info_cases() -> list[tuple[Optional[ts.TypeSpec], dict]]:
             },
         ),
         (
-            ts.FieldType(dims=Ellipsis, dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL)),
+            ts.FieldType(dims=[TDim], dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL)),
             {
                 "is_concrete": True,
                 "type_class": ts.FieldType,
@@ -239,7 +242,7 @@ def callable_type_info_cases():
             {},
             [
                 r"Expected 0-th argument to be of type tuple\[Field\[\[I, J, K\], int64\], "
-                r"Field\[..., int64\]\], but got tuple\[Field\[\[I, J, K\], int64\]\]."
+                r"Field\[\[\.\.\.\], int64\]\], but got tuple\[Field\[\[I, J, K\], int64\]\]."
             ],
             ts.FieldType(dims=[IDim, JDim, KDim], dtype=float_type),
         ),
@@ -290,19 +293,19 @@ def test_return_type(
 
 def test_unpack_assign():
     def unpack_explicit_tuple(
-        a: Field[..., float64], b: Field[..., float64]
-    ) -> tuple[Field[..., float64], Field[..., float64]]:
+        a: Field[[TDim], float64], b: Field[[TDim], float64]
+    ) -> tuple[Field[[TDim], float64], Field[[TDim], float64]]:
         tmp_a, tmp_b = (a, b)
         return tmp_a, tmp_b
 
     parsed = FieldOperatorParser.apply_to_function(unpack_explicit_tuple)
 
     assert parsed.annex.symtable["tmp_a__0"].type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
     assert parsed.annex.symtable["tmp_b__0"].type == ts.FieldType(
-        dims=Ellipsis,
+        dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
 
@@ -353,7 +356,7 @@ def test_dimension_promotion(
 
 
 def test_assign_tuple():
-    def temp_tuple(a: Field[..., float64], b: Field[..., int64]):
+    def temp_tuple(a: Field[[TDim], float64], b: Field[[TDim], int64]):
         tmp = a, b
         return tmp
 
@@ -362,11 +365,11 @@ def test_assign_tuple():
     assert parsed.annex.symtable["tmp__0"].type == ts.TupleType(
         types=[
             ts.FieldType(
-                dims=Ellipsis,
+                dims=[TDim],
                 dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
             ),
             ts.FieldType(
-                dims=Ellipsis,
+                dims=[TDim],
                 dtype=ts.ScalarType(kind=ts.ScalarKind.INT64, shape=None),
             ),
         ]
@@ -376,12 +379,12 @@ def test_assign_tuple():
 def test_adding_bool():
     """Expect an error when using arithmetic on bools."""
 
-    def add_bools(a: Field[..., bool], b: Field[..., bool]):
+    def add_bools(a: Field[[TDim], bool], b: Field[[TDim], bool]):
         return a + b
 
     with pytest.raises(
         FieldOperatorTypeDeductionError,
-        match=(r"Type Field\[\.\.\., bool\] can not be used in operator `\+`!"),
+        match=(r"Type Field\[\[TDim\], bool\] can not be used in operator `\+`!"),
     ):
         _ = FieldOperatorParser.apply_to_function(add_bools)
 
@@ -404,34 +407,34 @@ def test_binop_nonmatching_dims():
 
 
 def test_bitopping_float():
-    def float_bitop(a: Field[..., float], b: Field[..., float]):
+    def float_bitop(a: Field[[TDim], float], b: Field[[TDim], float]):
         return a & b
 
     with pytest.raises(
         FieldOperatorTypeDeductionError,
-        match=(r"Type Field\[\.\.\., float64\] can not be used in operator `\&`! "),
+        match=(r"Type Field\[\[TDim\], float64\] can not be used in operator `\&`! "),
     ):
         _ = FieldOperatorParser.apply_to_function(float_bitop)
 
 
 def test_signing_bool():
-    def sign_bool(a: Field[..., bool]):
+    def sign_bool(a: Field[[TDim], bool]):
         return -a
 
     with pytest.raises(
         FieldOperatorTypeDeductionError,
-        match=r"Incompatible type for unary operator `\-`: `Field\[\.\.\., bool\]`!",
+        match=r"Incompatible type for unary operator `\-`: `Field\[\[TDim\], bool\]`!",
     ):
         _ = FieldOperatorParser.apply_to_function(sign_bool)
 
 
 def test_notting_int():
-    def not_int(a: Field[..., int64]):
+    def not_int(a: Field[[TDim], int64]):
         return not a
 
     with pytest.raises(
         FieldOperatorTypeDeductionError,
-        match=r"Incompatible type for unary operator `not`: `Field\[\.\.\., int64\]`!",
+        match=r"Incompatible type for unary operator `not`: `Field\[\[TDim\], int64\]`!",
     ):
         _ = FieldOperatorParser.apply_to_function(not_int)
 
@@ -498,7 +501,7 @@ def test_remap_reduce_sparse(remap_setup):
 
 
 def test_mismatched_literals():
-    def mismatched_lit() -> Field[..., "float32"]:
+    def mismatched_lit() -> Field[[TDim], "float32"]:
         return float32("1.0") + float64("1.0")
 
     with pytest.raises(
@@ -664,7 +667,7 @@ def test_astype_dtype():
 
 
 def test_mod_floats():
-    def modulo_floats(inp: Field[..., float]):
+    def modulo_floats(inp: Field[[TDim], float]):
         return inp % 3.0
 
     with pytest.raises(
diff --git a/tests/functional_tests/type_system_tests/test_type_translation.py b/tests/functional_tests/type_system_tests/test_type_translation.py
index 9f0e3227..9f582c1a 100644
--- a/tests/functional_tests/type_system_tests/test_type_translation.py
+++ b/tests/functional_tests/type_system_tests/test_type_translation.py
@@ -85,8 +85,8 @@ def test_invalid_scalar_kind():
             ),
         ),
         (
-            common.Field[..., float],
-            ts.FieldType(dims=..., dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)),
+            common.Field[[IDim], float],
+            ts.FieldType(dims=[IDim], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)),
         ),
         (
             common.Field[[IDim, JDim], float],
@@ -152,9 +152,9 @@ def test_invalid_symbol_types():
         type_translation.from_type_hint(common.Field[[int, int], int])
 
     with pytest.raises(type_translation.TypingError, match="Field dtype argument"):
-        type_translation.from_type_hint(common.Field[..., str])
+        type_translation.from_type_hint(common.Field[[IDim], str])
     with pytest.raises(type_translation.TypingError, match="Field dtype argument"):
-        type_translation.from_type_hint(common.Field[..., None])
+        type_translation.from_type_hint(common.Field[[IDim], None])
 
     # Functions
     with pytest.raises(