Repository: GridTools/gt4py
Pull request #1734 (2024-11-12 14:14:06)

feat[next][dace]: Use offset_type to represent neighborhood information for local dimensions
This PR adopts the `offset_type` design concept implemented in #1703 for Embedded-GTIR and applies it to the DaCe-GTIR backend.

The only functional change is that the if-builtin is now expected to return the exact same data type, including the same `offset_type` if a local dimension is present in the result field. This change required updates to `test_gtir_reduce_with_cond_neighbors`.


Patch:
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py
index bb37440f..69aedf44 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py
@@ -44,15 +44,50 @@ class FieldopData:
 
     Args:
         dc_node: DaCe access node to the data storage.
-        gt_dtype: GT4Py type definition, which includes the field domain information.
-        local_offset: Provides information about the local dimension in`FieldType` data.
-            Set to 'None' for scalar data. Can be 'None' for `FieldType` data with
-            only global (horizontal or vertical) dimensions.
+        gt_type: GT4Py type definition, which includes the field domain information.
     """
 
     dc_node: dace.nodes.AccessNode
-    gt_dtype: ts.FieldType | ts.ScalarType
-    local_offset: Optional[str]
+    gt_type: ts.FieldType | ts.ScalarType
+
+    def get_local_view(
+        self, domain: FieldopDomain
+    ) -> gtir_dataflow.IteratorExpr | gtir_dataflow.MemletExpr:
+        """Helper method to access a field in local view, given a field operator domain."""
+        if isinstance(self.gt_type, ts.ScalarType):
+            return gtir_dataflow.MemletExpr(
+                dc_node=self.dc_node, gt_dtype=self.gt_type, subset=sbs.Indices([0])
+            )
+
+        if isinstance(self.gt_type, ts.FieldType):
+            indices: dict[gtx_common.Dimension, gtir_dataflow.DataExpr] = {
+                dim: gtir_dataflow.SymbolExpr(dace_gtir_utils.get_map_variable(dim), INDEX_DTYPE)
+                for dim, _, _ in domain
+            }
+            local_dims = [
+                dim for dim in self.gt_type.dims if dim.kind == gtx_common.DimensionKind.LOCAL
+            ]
+
+            if len(local_dims) == 0:
+                return gtir_dataflow.IteratorExpr(
+                    self.dc_node, self.gt_type.dtype, self.gt_type.dims, indices
+                )
+
+            elif len(local_dims) == 1:
+                field_dtype = itir_ts.ListType(
+                    element_type=self.gt_type.dtype, offset_type=local_dims[0]
+                )
+                field_dims = [
+                    dim for dim in self.gt_type.dims if dim.kind != gtx_common.DimensionKind.LOCAL
+                ]
+                return gtir_dataflow.IteratorExpr(self.dc_node, field_dtype, field_dims, indices)
+
+            else:
+                raise ValueError(
+                    f"Unexpected data field {self.dc_node.data} with more than one local dimension."
+                )
+
+        raise NotImplementedError(f"Node type {type(self.gt_type)} not supported.")
 
 
 FieldopDomain: TypeAlias = list[
@@ -111,31 +146,13 @@ def _parse_fieldop_arg(
 ) -> gtir_dataflow.IteratorExpr | gtir_dataflow.MemletExpr:
     """Helper method to visit an expression passed as argument to a field operator."""
 
-    arg = sdfg_builder.visit(
-        node,
-        sdfg=sdfg,
-        head_state=state,
-    )
+    arg = sdfg_builder.visit(node, sdfg=sdfg, head_state=state)
 
     # arguments passed to field operator should be plain fields, not tuples of fields
     if not isinstance(arg, FieldopData):
         raise ValueError(f"Received {node} as argument to field operator, expected a field.")
 
-    if isinstance(arg.gt_dtype, ts.ScalarType):
-        return gtir_dataflow.MemletExpr(arg.dc_node, sbs.Indices([0]))
-    elif isinstance(arg.gt_dtype, ts.FieldType):
-        indices: dict[gtx_common.Dimension, gtir_dataflow.DataExpr] = {
-            dim: gtir_dataflow.SymbolExpr(dace_gtir_utils.get_map_variable(dim), INDEX_DTYPE)
-            for dim, _, _ in domain
-        }
-        dims = arg.gt_dtype.dims + (
-            # we add an extra anonymous dimension in the iterator definition to enable
-            # dereferencing elements in `ListType`
-            [gtx_common.Dimension("")] if isinstance(arg.gt_dtype.dtype, itir_ts.ListType) else []
-        )
-        return gtir_dataflow.IteratorExpr(arg.dc_node, dims, indices, arg.local_offset)
-    else:
-        raise NotImplementedError(f"Node type {type(arg.gt_dtype)} not supported.")
+    return arg.get_local_view(domain)
 
 
 def _get_field_shape(
@@ -178,20 +195,27 @@ def _create_temporary_field(
     if isinstance(output_desc, dace.data.Array):
         assert isinstance(node_type.dtype, itir_ts.ListType)
         assert isinstance(node_type.dtype.element_type, ts.ScalarType)
-        field_dtype = node_type.dtype.element_type
+        assert output_desc.dtype == dace_utils.as_dace_type(node_type.dtype.element_type)
         # extend the array with the local dimensions added by the field operator (e.g. `neighbors`)
         field_shape.extend(output_desc.shape)
     elif isinstance(output_desc, dace.data.Scalar):
-        field_dtype = node_type.dtype
+        assert output_desc.dtype == dace_utils.as_dace_type(node_type.dtype)
     else:
         raise ValueError(f"Cannot create field for dace type {output_desc}.")
 
     # allocate local temporary storage
-    temp_name, _ = sdfg.add_temp_transient(field_shape, dace_utils.as_dace_type(field_dtype))
+    temp_name, _ = sdfg.add_temp_transient(field_shape, output_desc.dtype)
     field_node = state.add_access(temp_name)
-    field_type = ts.FieldType(field_dims, node_type.dtype)
 
-    return FieldopData(field_node, field_type, local_offset=dataflow_output.result.local_offset)
+    if isinstance(dataflow_output.result.gt_dtype, ts.ScalarType):
+        field_dtype = dataflow_output.result.gt_dtype
+    else:
+        assert isinstance(dataflow_output.result.gt_dtype.element_type, ts.ScalarType)
+        field_dtype = dataflow_output.result.gt_dtype.element_type
+        assert dataflow_output.result.gt_dtype.offset_type is not None
+        field_dims.append(dataflow_output.result.gt_dtype.offset_type)
+
+    return FieldopData(field_node, ts.FieldType(field_dims, field_dtype))
 
 
 def extract_domain(node: gtir.Node) -> FieldopDomain:
@@ -273,7 +297,6 @@ def translate_as_fieldop(
 
     if isinstance(node.type.dtype, itir_ts.ListType):
         assert isinstance(output_desc, dace.data.Array)
-        assert set(output_desc.offset) == {0}
         # additional local dimension for neighbors
         # TODO(phimuell): Investigate if we should swap the two.
         output_subset = sbs.Range.from_indices(domain_indices) + sbs.Range.from_array(output_desc)
@@ -383,7 +406,7 @@ def translate_broadcast_scalar(
         external_edges=True,
     )
 
-    return FieldopData(output_node, ts.FieldType(field_dims, gt_dtype), local_offset=None)
+    return FieldopData(output_node, ts.FieldType(field_dims, gt_dtype))
 
 
 def translate_if(
@@ -439,14 +462,14 @@ def translate_if(
         head_state=false_state,
     )
 
-    def make_temps(output_data: FieldopData) -> FieldopData:
-        desc = output_data.dc_node.desc(sdfg)
-        data_name, _ = sdfg.add_temp_transient_like(desc)
-        data_node = state.add_access(data_name)
+    def construct_output(inner_data: FieldopData) -> FieldopData:
+        inner_desc = inner_data.dc_node.desc(sdfg)
+        outer, _ = sdfg.add_temp_transient_like(inner_desc)
+        outer_node = state.add_access(outer)
 
-        return FieldopData(data_node, output_data.gt_dtype, output_data.local_offset)
+        return FieldopData(outer_node, inner_data.gt_type)
 
-    result_temps = gtx_utils.tree_map(make_temps)(true_br_args)
+    result_temps = gtx_utils.tree_map(construct_output)(true_br_args)
 
     fields: Iterable[tuple[FieldopData, FieldopData, FieldopData]] = zip(
         gtx_utils.flatten_nested_tuple((true_br_args,)),
@@ -456,7 +479,10 @@ def translate_if(
     )
 
     for true_br, false_br, temp in fields:
-        assert true_br.gt_dtype == false_br.gt_dtype
+        if true_br.gt_type != false_br.gt_type:
+            raise ValueError(
+                f"Different type of result fields on if-branches '{true_br.gt_type}' vs '{false_br.gt_type}'."
+            )
         true_br_node = true_br.dc_node
         false_br_node = false_br.dc_node
 
@@ -482,40 +508,31 @@ def _get_data_nodes(
     sdfg: dace.SDFG,
     state: dace.SDFGState,
     sdfg_builder: gtir_sdfg.SDFGBuilder,
-    sym_name: str,
-    sym_type: ts.DataType,
+    data_name: str,
+    data_type: ts.DataType,
 ) -> FieldopResult:
-    if isinstance(sym_type, ts.FieldType):
-        sym_node = state.add_access(sym_name)
-        local_dims = [dim for dim in sym_type.dims if dim.kind == gtx_common.DimensionKind.LOCAL]
-        if len(local_dims) > 1:
-            raise ValueError(f"Field {sym_name} has more than one local dimension.")
-        elif len(local_dims) == 1:
-            # we ensure that the name of the local dimension corresponds to a valid
-            # connectivity-based offset provider
-            local_offset = next(iter(local_dims)).value
-            assert isinstance(
-                sdfg_builder.get_offset_provider(local_offset), gtx_common.Connectivity
-            )
-        else:
-            local_offset = None
-        return FieldopData(sym_node, sym_type, local_offset)
-    elif isinstance(sym_type, ts.ScalarType):
-        if sym_name in sdfg.symbols:
-            sym_node = _get_symbolic_value(
-                sdfg, state, sdfg_builder, sym_name, sym_type, temp_name=f"__{sym_name}"
+    if isinstance(data_type, ts.FieldType):
+        data_node = state.add_access(data_name)
+        return FieldopData(data_node, data_type)
+
+    elif isinstance(data_type, ts.ScalarType):
+        if data_name in sdfg.symbols:
+            data_node = _get_symbolic_value(
+                sdfg, state, sdfg_builder, data_name, data_type, temp_name=f"__{data_name}"
             )
         else:
-            sym_node = state.add_access(sym_name)
-        return FieldopData(sym_node, sym_type, local_offset=None)
-    elif isinstance(sym_type, ts.TupleType):
-        tuple_fields = dace_gtir_utils.get_tuple_fields(sym_name, sym_type)
+            data_node = state.add_access(data_name)
+        return FieldopData(data_node, data_type)
+
+    elif isinstance(data_type, ts.TupleType):
+        tuple_fields = dace_gtir_utils.get_tuple_fields(data_name, data_type)
         return tuple(
             _get_data_nodes(sdfg, state, sdfg_builder, fname, ftype)
             for fname, ftype in tuple_fields
         )
+
     else:
-        raise NotImplementedError(f"Symbol type {type(sym_type)} not supported.")
+        raise NotImplementedError(f"Symbol type {type(data_type)} not supported.")
 
 
 def _get_symbolic_value(
@@ -562,7 +579,7 @@ def translate_literal(
     data_type = node.type
     data_node = _get_symbolic_value(sdfg, state, sdfg_builder, node.value, data_type)
 
-    return FieldopData(data_node, data_type, local_offset=None)
+    return FieldopData(data_node, data_type)
 
 
 def translate_make_tuple(
@@ -646,7 +663,7 @@ def translate_scalar_expr(
                 sdfg=sdfg,
                 head_state=state,
             )
-            if not (isinstance(arg, FieldopData) and isinstance(arg.gt_dtype, ts.ScalarType)):
+            if not (isinstance(arg, FieldopData) and isinstance(arg.gt_type, ts.ScalarType)):
                 raise ValueError(f"Invalid argument to scalar expression {arg_expr}.")
             param = f"__arg{i}"
             args.append(arg.dc_node)
@@ -691,7 +708,7 @@ def translate_scalar_expr(
         dace.Memlet(data=temp_name, subset="0"),
     )
 
-    return FieldopData(temp_node, node.type, local_offset=None)
+    return FieldopData(temp_node, node.type)
 
 
 def translate_symbol_ref(
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py
index cf91d15a..73b6e2ed 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py
@@ -29,6 +29,12 @@ from gt4py.next.program_processors.runners.dace_fieldview import (
 from gt4py.next.type_system import type_info as ti, type_specifications as ts
 
 
+# Magic local dimension for the result of a `make_const_list`.
+# A clean implementation will probably involve to tag the `make_const_list`
+# with the neighborhood it is meant to be used with.
+_CONST_DIM = gtx_common.Dimension(value="_CONST_DIM", kind=gtx_common.DimensionKind.LOCAL)
+
+
 @dataclasses.dataclass(frozen=True)
 class ValueExpr:
     """
@@ -41,15 +47,12 @@ class ValueExpr:
     the result of a field operator, basically the data storage outside a global map.
 
     Args:
-        dc_node: Access node to the data storage, can be either a scalar or a local list.
-        gt_dtype: GT4Py type definition, which includes the field domain information.
-        local_offset: Provides information about the local dimension in`FieldType` data.
-            For a more detailed explanation see `gtir_builtin_translators.FieldopData`.
+        dc_node: Access node to the data container, can be either a scalar or a local list.
+        gt_dtype: GT4Py data type, which includes the `offset_type` local dimension for lists.
     """
 
     dc_node: dace.nodes.AccessNode
     gt_dtype: itir_ts.ListType | ts.ScalarType
-    local_offset: Optional[str] = None
 
 
 @dataclasses.dataclass(frozen=True)
@@ -58,15 +61,14 @@ class MemletExpr:
     Scalar or array data access through a memlet.
 
     Args:
-        dc_node: Access node to the data storage, can be either a scalar or a local list.
+        dc_node: Access node to the data container, can be either a scalar or a local list.
+        gt_dtype: GT4Py data type, which includes the `offset_type` local dimension for lists.
         subset: Represents the subset to use in memlet to access the above data.
-        local_offset: Provides information about the local dimension in`FieldType` data.
-            For a more detailed explanation see `gtir_builtin_translators.FieldopData`.
     """
 
     dc_node: dace.nodes.AccessNode
+    gt_dtype: itir_ts.ListType | ts.ScalarType
     subset: sbs.Indices | sbs.Range
-    local_offset: Optional[str] = None
 
 
 @dataclasses.dataclass(frozen=True)
@@ -87,19 +89,17 @@ class IteratorExpr:
 
     Args:
         field: Access node to the field this iterator operates on.
+        gt_dtype: GT4Py data type, which includes the `offset_type` local dimension for lists.
         dimensions: Field domain represented as a sorted list of dimensions, needed
             to order the map index variables and dereference an element in the field.
         indices: Maps each dimension to an index value, which could be either a symbolic value
             or the result of a tasklet computation like neighbors connectivity or dynamic offset.
-        local_offset: Provides information about the local dimension in`FieldType` data.
-            For a more detailed explanation see `gtir_builtin_translators.FieldopData`.
-
     """
 
     field: dace.nodes.AccessNode
+    gt_dtype: itir_ts.ListType | ts.ScalarType
     dimensions: list[gtx_common.Dimension]
     indices: dict[gtx_common.Dimension, DataExpr]
-    local_offset: Optional[str] = None
 
 
 class DataflowInputEdge(Protocol):
@@ -383,18 +383,18 @@ class LambdaToDataflow(eve.NodeVisitor):
         dc_dtype: dace.typeclass,
         src_node: dace.nodes.Tasklet,
         src_connector: str,
-        local_offset: Optional[str] = None,
         use_array: bool = False,
     ) -> ValueExpr:
-        temp_name = self.sdfg.temp_data_name()
+        data_type = dace_utils.as_itir_type(dc_dtype)
         if use_array:
             # In some cases, such as result data with list-type annotation, we want
             # that output data is represented as an array (single-element 1D array)
             # in order to allow for composition of array shape in external memlets.
-            self.sdfg.add_array(temp_name, (1,), dc_dtype, transient=True)
+            temp_name, _ = self.sdfg.add_temp_transient((1,), dc_dtype)
         else:
+            temp_name = self.sdfg.temp_data_name()
             self.sdfg.add_scalar(temp_name, dc_dtype, transient=True)
-        data_type = dace_utils.as_itir_type(dc_dtype)
+
         temp_node = self.state.add_access(temp_name)
         self._add_edge(
             src_node,
@@ -403,7 +403,14 @@ class LambdaToDataflow(eve.NodeVisitor):
             None,
             dace.Memlet(data=temp_name, subset="0"),
         )
-        return ValueExpr(temp_node, data_type, local_offset)
+        return ValueExpr(
+            dc_node=temp_node,
+            gt_dtype=(
+                itir_ts.ListType(element_type=data_type, offset_type=_CONST_DIM)
+                if use_array
+                else data_type
+            ),
+        )
 
     def _visit_deref(self, node: gtir.FunCall) -> DataExpr:
         """
@@ -435,81 +442,87 @@ class LambdaToDataflow(eve.NodeVisitor):
             # deref a zero-dimensional field
             assert len(arg_expr.dimensions) == 0
             assert isinstance(node.type, ts.ScalarType)
-            return MemletExpr(arg_expr.field, subset="0")
+            return MemletExpr(arg_expr.field, arg_expr.gt_dtype, subset="0")
+
         # default case: deref a field with one or more dimensions
-        assert len(field_desc.shape) == len(arg_expr.dimensions)
         if all(isinstance(index, SymbolExpr) for index in arg_expr.indices.values()):
             # when all indices are symblic expressions, we can perform direct field access through a memlet
+            if isinstance(arg_expr.gt_dtype, itir_ts.ListType):
+                assert len(field_desc.shape) == len(arg_expr.dimensions) + 1
+                assert arg_expr.gt_dtype.offset_type is not None
+                field_dims = [*arg_expr.dimensions, arg_expr.gt_dtype.offset_type]
+            else:
+                assert len(field_desc.shape) == len(arg_expr.dimensions)
+                field_dims = arg_expr.dimensions
+
             field_subset = sbs.Range(
                 (arg_expr.indices[dim].value, arg_expr.indices[dim].value, 1)  # type: ignore[union-attr]
                 if dim in arg_expr.indices
                 else (0, size - 1, 1)
-                for dim, size in zip(arg_expr.dimensions, field_desc.shape)
+                for dim, size in zip(field_dims, field_desc.shape)
             )
-            return MemletExpr(arg_expr.field, field_subset, arg_expr.local_offset)
+            return MemletExpr(arg_expr.field, arg_expr.gt_dtype, field_subset)
 
-        else:
-            # we use a tasklet to dereference an iterator when one or more indices are the result of some computation,
-            # either indirection through connectivity table or dynamic cartesian offset.
-            assert all(dim in arg_expr.indices for dim in arg_expr.dimensions)
-            field_indices = [(dim, arg_expr.indices[dim]) for dim in arg_expr.dimensions]
-            index_connectors = [
-                IndexConnectorFmt.format(dim=dim.value)
-                for dim, index in field_indices
-                if not isinstance(index, SymbolExpr)
-            ]
-            # here `internals` refer to the names used as index in the tasklet code string:
-            # an index can be either a connector name (for dynamic/indirect indices)
-            # or a symbol value (for literal values and scalar arguments).
-            index_internals = ",".join(
-                str(index.value)
-                if isinstance(index, SymbolExpr)
-                else IndexConnectorFmt.format(dim=dim.value)
-                for dim, index in field_indices
-            )
-            deref_node = self._add_tasklet(
-                "runtime_deref",
-                {"field"} | set(index_connectors),
-                {"val"},
-                code=f"val = field[{index_internals}]",
-            )
-            # add new termination point for the field parameter
-            self._add_input_data_edge(
-                arg_expr.field,
-                sbs.Range.from_array(field_desc),
-                deref_node,
-                "field",
-            )
+        # we use a tasklet to dereference an iterator when one or more indices are the result of some computation,
+        # either indirection through connectivity table or dynamic cartesian offset.
+        assert all(dim in arg_expr.indices for dim in arg_expr.dimensions)
+        assert len(field_desc.shape) == len(arg_expr.dimensions)
+        field_indices = [(dim, arg_expr.indices[dim]) for dim in arg_expr.dimensions]
+        index_connectors = [
+            IndexConnectorFmt.format(dim=dim.value)
+            for dim, index in field_indices
+            if not isinstance(index, SymbolExpr)
+        ]
+        # here `internals` refer to the names used as index in the tasklet code string:
+        # an index can be either a connector name (for dynamic/indirect indices)
+        # or a symbol value (for literal values and scalar arguments).
+        index_internals = ",".join(
+            str(index.value)
+            if isinstance(index, SymbolExpr)
+            else IndexConnectorFmt.format(dim=dim.value)
+            for dim, index in field_indices
+        )
+        deref_node = self._add_tasklet(
+            "runtime_deref",
+            {"field"} | set(index_connectors),
+            {"val"},
+            code=f"val = field[{index_internals}]",
+        )
+        # add new termination point for the field parameter
+        self._add_input_data_edge(
+            arg_expr.field,
+            sbs.Range.from_array(field_desc),
+            deref_node,
+            "field",
+        )
 
-            for dim, index_expr in field_indices:
-                # add termination points for the dynamic iterator indices
-                deref_connector = IndexConnectorFmt.format(dim=dim.value)
-                if isinstance(index_expr, MemletExpr):
-                    self._add_input_data_edge(
-                        index_expr.dc_node,
-                        index_expr.subset,
-                        deref_node,
-                        deref_connector,
-                    )
+        for dim, index_expr in field_indices:
+            # add termination points for the dynamic iterator indices
+            deref_connector = IndexConnectorFmt.format(dim=dim.value)
+            if isinstance(index_expr, MemletExpr):
+                self._add_input_data_edge(
+                    index_expr.dc_node,
+                    index_expr.subset,
+                    deref_node,
+                    deref_connector,
+                )
 
-                elif isinstance(index_expr, ValueExpr):
-                    self._add_edge(
-                        index_expr.dc_node,
-                        None,
-                        deref_node,
-                        deref_connector,
-                        dace.Memlet(data=index_expr.dc_node.data, subset="0"),
-                    )
-                else:
-                    assert isinstance(index_expr, SymbolExpr)
+            elif isinstance(index_expr, ValueExpr):
+                self._add_edge(
+                    index_expr.dc_node,
+                    None,
+                    deref_node,
+                    deref_connector,
+                    dace.Memlet(data=index_expr.dc_node.data, subset="0"),
+                )
+            else:
+                assert isinstance(index_expr, SymbolExpr)
 
-            return self._construct_tasklet_result(
-                field_desc.dtype, deref_node, "val", arg_expr.local_offset
-            )
+        return self._construct_tasklet_result(field_desc.dtype, deref_node, "val")
 
     def _visit_neighbors(self, node: gtir.FunCall) -> ValueExpr:
-        assert len(node.args) == 2
         assert isinstance(node.type, itir_ts.ListType)
+        assert len(node.args) == 2
 
         assert isinstance(node.args[0], gtir.OffsetLiteral)
         offset = node.args[0].value
@@ -543,8 +556,9 @@ class LambdaToDataflow(eve.NodeVisitor):
         # to the view nodes. The simplify pass will remove the redundant access nodes.
         field_slice = self._construct_local_view(
             MemletExpr(
-                it.field,
-                sbs.Range.from_string(
+                dc_node=it.field,
+                gt_dtype=node.type,
+                subset=sbs.Range.from_string(
                     ",".join(
                         it.indices[dim].value  # type: ignore[union-attr]
                         if dim != offset_provider.neighbor_axis
@@ -556,8 +570,11 @@ class LambdaToDataflow(eve.NodeVisitor):
         )
         connectivity_slice = self._construct_local_view(
             MemletExpr(
-                self.state.add_access(connectivity),
-                sbs.Range.from_string(f"{origin_index.value}, 0:{offset_provider.max_neighbors}"),
+                dc_node=self.state.add_access(connectivity),
+                gt_dtype=node.type,
+                subset=sbs.Range.from_string(
+                    f"{origin_index.value}, 0:{offset_provider.max_neighbors}"
+                ),
             )
         )
 
@@ -565,8 +582,8 @@ class LambdaToDataflow(eve.NodeVisitor):
             (offset_provider.max_neighbors,), field_desc.dtype
         )
         neighbors_node = self.state.add_access(neighbors_temp)
-
-        neighbor_idx = dace_gtir_utils.get_map_variable(offset)
+        offset_type = gtx_common.Dimension(offset, gtx_common.DimensionKind.LOCAL)
+        neighbor_idx = dace_gtir_utils.get_map_variable(offset_type)
 
         index_connector = "__index"
         output_connector = "__val"
@@ -604,7 +621,9 @@ class LambdaToDataflow(eve.NodeVisitor):
             external_edges=True,
         )
 
-        return ValueExpr(neighbors_node, node.type, offset)
+        return ValueExpr(
+            dc_node=neighbors_node, gt_dtype=itir_ts.ListType(node.type.element_type, offset_type)
+        )
 
     def _visit_map(self, node: gtir.FunCall) -> ValueExpr:
         """
@@ -629,8 +648,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         assert isinstance(node.type.element_type, ts.ScalarType)
         dc_dtype = dace_utils.as_dace_type(node.type.element_type)
 
-        input_args = [self.visit(arg) for arg in node.args]
-        input_connectors = [f"__arg{i}" for i in range(len(input_args))]
+        input_connectors = [f"__arg{i}" for i in range(len(node.args))]
         output_connector = "__out"
 
         # Here we build the body of the tasklet
@@ -638,27 +656,37 @@ class LambdaToDataflow(eve.NodeVisitor):
         fun_python_code = gtir_python_codegen.get_source(fun_node)
         tasklet_expression = f"{output_connector} = {fun_python_code}"
 
-        input_local_offsets = [
-            input_arg.local_offset for input_arg in input_args if input_arg.local_offset is not None
-        ]
-        if len(input_local_offsets) == 0:
+        input_args = [self.visit(arg) for arg in node.args]
+        input_connectivities: dict[gtx_common.Dimension, gtx_common.Connectivity] = {}
+        for input_arg in input_args:
+            assert isinstance(input_arg.gt_dtype, itir_ts.ListType)
+            assert input_arg.gt_dtype.offset_type is not None
+            offset_type = input_arg.gt_dtype.offset_type
+            if offset_type == _CONST_DIM:
+                # this input argument is the result of `make_const_list`
+                continue
+            offset_provider = self.subgraph_builder.get_offset_provider(offset_type.value)
+            assert isinstance(offset_provider, gtx_common.Connectivity)
+            input_connectivities[offset_type] = offset_provider
+
+        if len(input_connectivities) == 0:
             raise ValueError(f"Missing information on local dimension for map node {node}.")
 
         # GT4Py guarantees that all connectivities used to generate lists of neighbors
         # have the same length, that is the same value of 'max_neighbors'.
-        local_connectivities = dace_utils.filter_connectivities(
-            {
-                offset: self.subgraph_builder.get_offset_provider(offset)
-                for offset in input_local_offsets
-            }
-        )
-        if len(set(table.max_neighbors for table in local_connectivities.values())) != 1:
-            raise ValueError(
-                "Unexpected arguments to map expression with different local dimensions."
+        if (
+            len(
+                set(
+                    (conn.has_skip_values, conn.max_neighbors)
+                    for conn in input_connectivities.values()
+                )
             )
-        local_offset, offset_provider = next(iter(local_connectivities.items()))
+            != 1
+        ):
+            raise ValueError("Unexpected arguments to map expression with different neighborhood.")
+        offset_type, offset_provider = next(iter(input_connectivities.items()))
         local_size = offset_provider.max_neighbors
-        map_index = dace_gtir_utils.get_map_variable(local_offset)
+        map_index = dace_gtir_utils.get_map_variable(offset_type)
 
         # The dataflow we build in this class has some loose connections on input edges.
         # These edges are described as set of nodes, that will have to be connected to
@@ -668,47 +696,31 @@ class LambdaToDataflow(eve.NodeVisitor):
         # than representing map-to-map edges (which require memlets with 2 pass-nodes).
         input_memlets = {}
         input_nodes = {}
-        skip_value_connectivities: dict[str, gtx_common.Connectivity] = {}
-        for conn, input_expr in zip(input_connectors, input_args):
-            input_node = self._construct_local_view(input_expr).dc_node
+        for conn, input_arg in zip(input_connectors, input_args):
+            input_node = self._construct_local_view(input_arg).dc_node
             input_desc = input_node.desc(self.sdfg)
             # we assume that there is a single local dimension
             if len(input_desc.shape) != 1:
                 raise ValueError(f"More than one local dimension in map expression {node}.")
             input_size = input_desc.shape[0]
             if input_size == 1:
+                assert input_arg.gt_dtype.offset_type == _CONST_DIM
                 input_memlets[conn] = dace.Memlet(data=input_node.data, subset="0")
-            elif input_size != local_size:
+            elif input_size == local_size:
+                input_memlets[conn] = dace.Memlet(data=input_node.data, subset=map_index)
+            else:
                 raise ValueError(
                     f"Argument to map node with local size {input_size}, expected {local_size}."
                 )
-            else:
-                assert input_expr.local_offset
-                input_memlets[conn] = dace.Memlet(data=input_node.data, subset=map_index)
-
             input_nodes[input_node.data] = input_node
 
         result, _ = self.sdfg.add_temp_transient((local_size,), dc_dtype)
         result_node = self.state.add_access(result)
 
-        skip_value_connectivities = {
-            offset: offset_provider
-            for offset, offset_provider in local_connectivities.items()
-            if offset_provider.has_skip_values
-        }
-
-        if len(skip_value_connectivities) == 0:
-            result_offset = local_offset
-        else:
-            # In case one or more of input expressions contain skip values, we use
+        if offset_provider.has_skip_values:
+            # In case the `map_` input expressions contain skip values, we use
             # the connectivity-based offset provider as mask for map computation.
-            # Therefore, the result of map computation will also contain skip values.
-            # GT4Py guarantees that the skip values are placed in the same positions
-            # for all input expressions.
-
-            result_offset, offset_provider = next(iter(skip_value_connectivities.items()))
-
-            connectivity = dace_utils.connectivity_identifier(result_offset)
+            connectivity = dace_utils.connectivity_identifier(offset_type.value)
             connectivity_desc = self.sdfg.arrays[connectivity]
             connectivity_desc.transient = False
 
@@ -716,8 +728,13 @@ class LambdaToDataflow(eve.NodeVisitor):
 
             connectivity_slice = self._construct_local_view(
                 MemletExpr(
-                    self.state.add_access(connectivity),
-                    sbs.Range.from_string(f"{origin_map_index}, 0:{offset_provider.max_neighbors}"),
+                    dc_node=self.state.add_access(connectivity),
+                    gt_dtype=itir_ts.ListType(
+                        element_type=node.type.element_type, offset_type=offset_type
+                    ),
+                    subset=sbs.Range.from_string(
+                        f"{origin_map_index}, 0:{offset_provider.max_neighbors}"
+                    ),
                 )
             )
 
@@ -749,7 +766,10 @@ class LambdaToDataflow(eve.NodeVisitor):
             external_edges=True,
         )
 
-        return ValueExpr(result_node, dc_dtype, result_offset)
+        return ValueExpr(
+            dc_node=result_node,
+            gt_dtype=itir_ts.ListType(node.type.element_type, offset_type),
+        )
 
     def _make_reduce_with_skip_values(
         self,
@@ -774,8 +794,12 @@ class LambdaToDataflow(eve.NodeVisitor):
         """
         origin_map_index = dace_gtir_utils.get_map_variable(offset_provider.origin_axis)
 
-        assert input_expr.local_offset is not None
-        connectivity = dace_utils.connectivity_identifier(input_expr.local_offset)
+        assert (
+            isinstance(input_expr.gt_dtype, itir_ts.ListType)
+            and input_expr.gt_dtype.offset_type is not None
+        )
+        offset_type = input_expr.gt_dtype.offset_type
+        connectivity = dace_utils.connectivity_identifier(offset_type.value)
         connectivity_node = self.state.add_access(connectivity)
         connectivity_desc = connectivity_node.desc(self.sdfg)
         connectivity_desc.transient = False
@@ -881,8 +905,12 @@ class LambdaToDataflow(eve.NodeVisitor):
 
         input_expr = self.visit(node.args[0])
         assert isinstance(input_expr, (MemletExpr, ValueExpr))
-        assert input_expr.local_offset is not None
-        offset_provider = self.subgraph_builder.get_offset_provider(input_expr.local_offset)
+        assert (
+            isinstance(input_expr.gt_dtype, itir_ts.ListType)
+            and input_expr.gt_dtype.offset_type is not None
+        )
+        offset_type = input_expr.gt_dtype.offset_type
+        offset_provider = self.subgraph_builder.get_offset_provider(offset_type.value)
         assert isinstance(offset_provider, gtx_common.Connectivity)
 
         if offset_provider.has_skip_values:
@@ -998,9 +1026,13 @@ class LambdaToDataflow(eve.NodeVisitor):
 
         # a new iterator with a shifted index along one dimension
         return IteratorExpr(
-            it.field,
-            it.dimensions,
-            {dim: (new_index if dim == offset_dim else index) for dim, index in it.indices.items()},
+            field=it.field,
+            gt_dtype=it.gt_dtype,
+            dimensions=it.dimensions,
+            indices={
+                dim: (new_index if dim == offset_dim else index)
+                for dim, index in it.indices.items()
+            },
         )
 
     def _make_dynamic_neighbor_offset(
@@ -1068,8 +1100,9 @@ class LambdaToDataflow(eve.NodeVisitor):
         if isinstance(offset_expr, SymbolExpr):
             # use memlet to retrieve the neighbor index
             shifted_indices[neighbor_dim] = MemletExpr(
-                offset_table_node,
-                sbs.Indices([origin_index.value, offset_expr.value]),
+                dc_node=offset_table_node,
+                gt_dtype=it.gt_dtype,
+                subset=sbs.Indices([origin_index.value, offset_expr.value]),
             )
         else:
             # dynamic offset: we cannot use a memlet to retrieve the offset value, use a tasklet node
@@ -1077,7 +1110,9 @@ class LambdaToDataflow(eve.NodeVisitor):
                 offset_expr, offset_table_node, origin_index
             )
 
-        return IteratorExpr(it.field, it.dimensions, shifted_indices)
+        return IteratorExpr(
+            field=it.field, gt_dtype=it.gt_dtype, dimensions=it.dimensions, indices=shifted_indices
+        )
 
     def _visit_shift(self, node: gtir.FunCall) -> IteratorExpr:
         # convert builtin-index type to dace type
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py
index da940e88..ad8f490f 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py
@@ -344,9 +344,7 @@ class GTIRToSDFG(eve.NodeVisitor, SDFGBuilder):
                 head_state.add_nedge(
                     field.dc_node, temp_node, sdfg.make_array_memlet(field.dc_node.data)
                 )
-                return gtir_builtin_translators.FieldopData(
-                    temp_node, field.gt_dtype, field.local_offset
-                )
+                return gtir_builtin_translators.FieldopData(temp_node, field.gt_type)
 
         temp_result = gtx_utils.tree_map(make_temps)(result)
         return list(gtx_utils.flatten_nested_tuple((temp_result,)))
@@ -489,9 +487,9 @@ class GTIRToSDFG(eve.NodeVisitor, SDFGBuilder):
             target_desc = sdfg.arrays[target.dc_node.data]
             assert not target_desc.transient
 
-            if isinstance(target.gt_dtype, ts.FieldType):
+            if isinstance(target.gt_type, ts.FieldType):
                 subset = ",".join(
-                    f"{domain[dim][0]}:{domain[dim][1]}" for dim in target.gt_dtype.dims
+                    f"{domain[dim][0]}:{domain[dim][1]}" for dim in target.gt_type.dims
                 )
             else:
                 assert len(domain) == 0
@@ -582,7 +580,7 @@ class GTIRToSDFG(eve.NodeVisitor, SDFGBuilder):
             sym: self.global_symbols[sym]
             for sym in symbol_ref_utils.collect_symbol_refs(node.expr, self.global_symbols.keys())
         } | {
-            pname: dace_gtir_utils.get_tuple_type(arg) if isinstance(arg, tuple) else arg.gt_dtype
+            pname: dace_gtir_utils.get_tuple_type(arg) if isinstance(arg, tuple) else arg.gt_type
             for pname, arg in lambda_args_mapping
         }
 
@@ -742,9 +740,7 @@ class GTIRToSDFG(eve.NodeVisitor, SDFGBuilder):
                 head_state.add_edge(
                     nsdfg_node, connector, outer_node, None, sdfg.make_array_memlet(outer)
                 )
-                outer_data = gtir_builtin_translators.FieldopData(
-                    outer_node, inner_data.gt_dtype, inner_data.local_offset
-                )
+                outer_data = gtir_builtin_translators.FieldopData(outer_node, inner_data.gt_type)
             elif inner_data.dc_node.data in lambda_arg_nodes:
                 # This if branch and the next one handle the non-transient result nodes.
                 # Non-transient nodes are just input nodes that are immediately returned
@@ -753,9 +749,7 @@ class GTIRToSDFG(eve.NodeVisitor, SDFGBuilder):
                 outer_data = lambda_arg_nodes[inner_data.dc_node.data]
             else:
                 outer_node = head_state.add_access(inner_data.dc_node.data)
-                outer_data = gtir_builtin_translators.FieldopData(
-                    outer_node, inner_data.gt_dtype, inner_data.local_offset
-                )
+                outer_data = gtir_builtin_translators.FieldopData(outer_node, inner_data.gt_type)
             # Isolated access node will make validation fail.
             # Isolated access nodes can be found in the join-state of an if-expression
             # or in lambda expressions that just construct tuples from input arguments.
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py b/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py
index baae8a6c..caec6cd8 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py
@@ -19,15 +19,10 @@ from gt4py.next.iterator import ir as gtir
 from gt4py.next.type_system import type_specifications as ts
 
 
-def get_map_variable(dim: gtx_common.Dimension | str) -> str:
+def get_map_variable(dim: gtx_common.Dimension) -> str:
     """
     Format map variable name based on the naming convention for application-specific SDFG transformations.
     """
-    if not isinstance(dim, gtx_common.Dimension):
-        if len(dim) != 0:
-            dim = gtx_common.Dimension(dim, gtx_common.DimensionKind.LOCAL)
-        else:
-            raise ValueError("Dimension name cannot be empty.")
     suffix = "dim" if dim.kind == gtx_common.DimensionKind.LOCAL else ""
     return f"i_{dim.value}_gtx_{dim.kind}{suffix}"
 
@@ -68,7 +63,7 @@ def get_tuple_type(data: tuple[Any, ...]) -> ts.TupleType:
     Compute the `ts.TupleType` corresponding to the structure of a tuple of data nodes.
     """
     return ts.TupleType(
-        types=[get_tuple_type(d) if isinstance(d, tuple) else d.gt_dtype for d in data]
+        types=[get_tuple_type(d) if isinstance(d, tuple) else d.gt_type for d in data]
     )
 
 


Test patch:
diff --git a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/test_gtir_to_sdfg.py b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/test_gtir_to_sdfg.py
index cc72adae..a94157ec 100644
--- a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/test_gtir_to_sdfg.py
+++ b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/test_gtir_to_sdfg.py
@@ -1352,7 +1352,9 @@ def test_gtir_reduce_with_skip_values():
     e = np.random.rand(SKIP_VALUE_MESH.num_edges)
     v_ref = [
         functools.reduce(
-            lambda x, y: x + y, [e[i] if i != -1 else 0.0 for i in v2e_neighbors], init_value
+            lambda x, y: x + y,
+            [e[i] if i != gtx_common._DEFAULT_SKIP_VALUE else 0.0 for i in v2e_neighbors],
+            init_value,
         )
         for v2e_neighbors in connectivity_V2E.table
     ]
@@ -1394,120 +1396,74 @@ def test_gtir_reduce_dot_product():
     init_value = np.random.rand()
     vertex_domain = im.domain(gtx_common.GridType.UNSTRUCTURED, ranges={Vertex: (0, "nvertices")})
 
-    connectivity_V2E = SIMPLE_MESH_OFFSET_PROVIDER["V2E"]
+    connectivity_V2E = SKIP_VALUE_MESH_OFFSET_PROVIDER["V2E"]
     assert isinstance(connectivity_V2E, gtx_common.NeighborTable)
 
-    # create mesh with skip values
-    connectivity_V2E_skip = copy.deepcopy(connectivity_V2E)
-    connectivity_V2E_skip.has_skip_values = True
-    connectivity_V2E_skip.table = np.asarray(
-        [
-            [x if i != skip_idx else gtx_common._DEFAULT_SKIP_VALUE for i, x in enumerate(row)]
-            for skip_idx, row in zip(
-                np.random.randint(0, connectivity_V2E.max_neighbors, size=SIMPLE_MESH.num_vertices),
-                connectivity_V2E.table,
-                strict=True,
-            )
-        ],
-        dtype=connectivity_V2E.table.dtype,
-    )
-    # safety check that the connectivity table actually contains skip values
-    assert len(np.where(connectivity_V2E.table == gtx_common._DEFAULT_SKIP_VALUE)) != 0
-
-    offset_provider = SIMPLE_MESH_OFFSET_PROVIDER | {
-        "V2E_skip": connectivity_V2E_skip,
-    }
-
-    V2E_SKIP_SYMBOLS = dict(
-        __connectivity_V2E_skip_size_0=SIMPLE_MESH.num_vertices,
-        __connectivity_V2E_skip_size_1=connectivity_V2E_skip.max_neighbors,
-        __connectivity_V2E_skip_stride_0=connectivity_V2E_skip.max_neighbors,
-        __connectivity_V2E_skip_stride_1=1,
-    )
-
-    e = np.random.rand(SIMPLE_MESH.num_edges)
-    v = np.empty(SIMPLE_MESH.num_vertices, dtype=e.dtype)
+    v2e_field = np.random.rand(SKIP_VALUE_MESH.num_vertices, connectivity_V2E.max_neighbors)
+    e = np.random.rand(SKIP_VALUE_MESH.num_edges)
+    v = np.empty(SKIP_VALUE_MESH.num_vertices, dtype=e.dtype)
     v_ref = [
         functools.reduce(
             lambda x, y: x + y,
             map(
                 lambda x: 0.0 if x[1] == gtx_common._DEFAULT_SKIP_VALUE else x[0],
-                zip((e[v2e_neighbors] * e[v2e_skip_neighbors]) + 1.0, v2e_skip_neighbors),
+                zip((e[v2e_neighbors] * v2e_values) + 1.0, v2e_neighbors),
             ),
             init_value,
         )
-        for v2e_neighbors, v2e_skip_neighbors in zip(
-            connectivity_V2E.table, connectivity_V2E_skip.table
-        )
+        for v2e_neighbors, v2e_values in zip(connectivity_V2E.table, v2e_field)
     ]
 
-    stencil_inlined = im.call(
-        im.call("as_fieldop")(
-            im.lambda_("it")(
-                im.call(im.call("reduce")("plus", im.literal_from_value(init_value)))(
-                    im.map_("plus")(
-                        im.map_("multiplies")(
-                            im.neighbors("V2E", "it"),
-                            im.neighbors("V2E_skip", "it"),
+    testee = gtir.Program(
+        id=f"reduce_dot_product",
+        function_definitions=[],
+        params=[
+            gtir.Sym(id="v2e_field", type=V2E_FTYPE),
+            gtir.Sym(id="edges", type=EFTYPE),
+            gtir.Sym(id="vertices", type=VFTYPE),
+            gtir.Sym(id="nvertices", type=SIZE_TYPE),
+        ],
+        declarations=[],
+        body=[
+            gtir.SetAt(
+                expr=im.call(
+                    im.call("as_fieldop")(
+                        im.lambda_("it")(
+                            im.call(im.call("reduce")("plus", im.literal_from_value(init_value)))(
+                                im.deref("it")
+                            )
                         ),
-                        im.call("make_const_list")(1.0),
+                        vertex_domain,
                     )
-                )
-            ),
-            vertex_domain,
-        )
-    )("edges")
-
-    stencil_fieldview = im.call(
-        im.call("as_fieldop")(
-            im.lambda_("it")(
-                im.call(im.call("reduce")("plus", im.literal_from_value(init_value)))(
-                    im.deref("it")
-                )
-            ),
-            vertex_domain,
-        )
-    )(
-        im.op_as_fieldop(im.map_("plus"), vertex_domain)(
-            im.op_as_fieldop(im.map_("multiplies"), vertex_domain)(
-                im.as_fieldop_neighbors("V2E", "edges", vertex_domain),
-                im.as_fieldop_neighbors("V2E_skip", "edges", vertex_domain),
-            ),
-            im.op_as_fieldop("make_const_list", vertex_domain)(1.0),
-        )
+                )(
+                    im.op_as_fieldop(im.map_("plus"), vertex_domain)(
+                        im.op_as_fieldop(im.map_("multiplies"), vertex_domain)(
+                            im.as_fieldop_neighbors("V2E", "edges", vertex_domain),
+                            "v2e_field",
+                        ),
+                        im.op_as_fieldop("make_const_list", vertex_domain)(1.0),
+                    )
+                ),
+                domain=vertex_domain,
+                target=gtir.SymRef(id="vertices"),
+            )
+        ],
     )
 
-    for i, stencil in enumerate([stencil_inlined, stencil_fieldview]):
-        testee = gtir.Program(
-            id=f"reduce_dot_product_{i}",
-            function_definitions=[],
-            params=[
-                gtir.Sym(id="edges", type=EFTYPE),
-                gtir.Sym(id="vertices", type=VFTYPE),
-                gtir.Sym(id="nvertices", type=SIZE_TYPE),
-            ],
-            declarations=[],
-            body=[
-                gtir.SetAt(
-                    expr=stencil,
-                    domain=vertex_domain,
-                    target=gtir.SymRef(id="vertices"),
-                )
-            ],
-        )
-
-        sdfg = dace_backend.build_sdfg_from_gtir(testee, offset_provider)
+    sdfg = dace_backend.build_sdfg_from_gtir(testee, SKIP_VALUE_MESH_OFFSET_PROVIDER)
 
-        sdfg(
-            e,
-            v,
-            connectivity_V2E=connectivity_V2E.table,
-            connectivity_V2E_skip=connectivity_V2E_skip.table,
-            **FSYMBOLS,
-            **make_mesh_symbols(SIMPLE_MESH),
-            **V2E_SKIP_SYMBOLS,
-        )
-        assert np.allclose(v, v_ref)
+    sdfg(
+        v2e_field,
+        e,
+        v,
+        connectivity_V2E=connectivity_V2E.table,
+        **make_mesh_symbols(SKIP_VALUE_MESH),
+        __v2e_field_size_0=SKIP_VALUE_MESH.num_vertices,
+        __v2e_field_size_1=connectivity_V2E.max_neighbors,
+        __v2e_field_stride_0=connectivity_V2E.max_neighbors,
+        __v2e_field_stride_1=1,
+    )
+    assert np.allclose(v, v_ref)
 
 
 def test_gtir_reduce_with_cond_neighbors():
@@ -1518,6 +1474,7 @@ def test_gtir_reduce_with_cond_neighbors():
         function_definitions=[],
         params=[
             gtir.Sym(id="pred", type=ts.ScalarType(ts.ScalarKind.BOOL)),
+            gtir.Sym(id="v2e_field", type=V2E_FTYPE),
             gtir.Sym(id="edges", type=EFTYPE),
             gtir.Sym(id="vertices", type=VFTYPE),
             gtir.Sym(id="nvertices", type=SIZE_TYPE),
@@ -1535,7 +1492,7 @@ def test_gtir_reduce_with_cond_neighbors():
                 )(
                     im.if_(
                         "pred",
-                        im.as_fieldop_neighbors("V2E_FULL", "edges", vertex_domain),
+                        "v2e_field",
                         im.as_fieldop_neighbors("V2E", "edges", vertex_domain),
                     )
                 ),
@@ -1545,49 +1502,45 @@ def test_gtir_reduce_with_cond_neighbors():
         ],
     )
 
-    connectivity_V2E_simple = SIMPLE_MESH_OFFSET_PROVIDER["V2E"]
-    assert isinstance(connectivity_V2E_simple, gtx_common.NeighborTable)
-    connectivity_V2E_skip_values = copy.deepcopy(SKIP_VALUE_MESH_OFFSET_PROVIDER["V2E"])
-    assert isinstance(connectivity_V2E_skip_values, gtx_common.NeighborTable)
-    assert SKIP_VALUE_MESH.num_vertices <= SIMPLE_MESH.num_vertices
-    connectivity_V2E_skip_values.table = np.concatenate(
-        (
-            connectivity_V2E_skip_values.table[:, 0 : connectivity_V2E_simple.max_neighbors],
-            connectivity_V2E_simple.table[SKIP_VALUE_MESH.num_vertices :, :],
-        ),
-        axis=0,
-    )
-    connectivity_V2E_skip_values.max_neighbors = connectivity_V2E_simple.max_neighbors
+    connectivity_V2E = SKIP_VALUE_MESH_OFFSET_PROVIDER["V2E"]
+    assert isinstance(connectivity_V2E, gtx_common.NeighborTable)
 
-    e = np.random.rand(SIMPLE_MESH.num_edges)
+    v2e_field = np.random.rand(SKIP_VALUE_MESH.num_vertices, connectivity_V2E.max_neighbors)
+    e = np.random.rand(SKIP_VALUE_MESH.num_edges)
 
-    for use_full in [False, True]:
-        sdfg = dace_backend.build_sdfg_from_gtir(
-            testee,
-            SIMPLE_MESH_OFFSET_PROVIDER | {"V2E_FULL": connectivity_V2E_skip_values},
-        )
+    for use_sparse in [False, True]:
+        sdfg = dace_backend.build_sdfg_from_gtir(testee, SKIP_VALUE_MESH_OFFSET_PROVIDER)
 
-        v = np.empty(SIMPLE_MESH.num_vertices, dtype=e.dtype)
+        v = np.empty(SKIP_VALUE_MESH.num_vertices, dtype=e.dtype)
         v_ref = [
             functools.reduce(
-                lambda x, y: x + y, [e[i] if i != -1 else 0.0 for i in v2e_neighbors], init_value
+                lambda x, y: x + y,
+                [
+                    v if i != gtx_common._DEFAULT_SKIP_VALUE else 0.0
+                    for i, v in zip(v2e_neighbors, v2e_values, strict=True)
+                ],
+                init_value,
             )
-            for v2e_neighbors in (
-                connectivity_V2E_simple.table if use_full else connectivity_V2E_skip_values.table
+            if use_sparse
+            else functools.reduce(
+                lambda x, y: x + y,
+                [e[i] if i != gtx_common._DEFAULT_SKIP_VALUE else 0.0 for i in v2e_neighbors],
+                init_value,
             )
+            for v2e_neighbors, v2e_values in zip(connectivity_V2E.table, v2e_field, strict=True)
         ]
         sdfg(
-            np.bool_(use_full),
+            np.bool_(use_sparse),
+            v2e_field,
             e,
             v,
-            connectivity_V2E=connectivity_V2E_skip_values.table,
-            connectivity_V2E_FULL=connectivity_V2E_simple.table,
+            connectivity_V2E=connectivity_V2E.table,
             **FSYMBOLS,
-            **make_mesh_symbols(SIMPLE_MESH),
-            __connectivity_V2E_FULL_size_0=SIMPLE_MESH.num_edges,
-            __connectivity_V2E_FULL_size_1=connectivity_V2E_skip_values.max_neighbors,
-            __connectivity_V2E_FULL_stride_0=connectivity_V2E_skip_values.max_neighbors,
-            __connectivity_V2E_FULL_stride_1=1,
+            **make_mesh_symbols(SKIP_VALUE_MESH),
+            __v2e_field_size_0=SKIP_VALUE_MESH.num_vertices,
+            __v2e_field_size_1=connectivity_V2E.max_neighbors,
+            __v2e_field_stride_0=connectivity_V2E.max_neighbors,
+            __v2e_field_stride_1=1,
         )
         assert np.allclose(v, v_ref)