Repository: GridTools/gt4py
Pull request #1462 (2024-02-19 16:57:40)

feat[cartesian]: Setting extra compile args and DaCe block size via env variables
## Description

This PR adds the ability to read extra compile arguments and the DaCe block size from the environment. When the `dace:gpu` backend is used, the block size is also included in the stencil fingerprint. These features are quite useful for the PMAP model.

## Requirements

- [ ] All fixes and/or new features come with corresponding tests.
- [ ] Important design decisions have been documented in the approriate ADR inside the [docs/development/ADRs/](docs/development/ADRs/Index.md) folder.

If this PR contains code authored by new contributors please make sure:

- [ ] All the authors are covered by a valid contributor assignment agreement provided to ETH Zurich and signed by the employer if needed.
- [ ] The PR contains an updated version of the `AUTHORS.md` file adding the names of all the new contributors.



Patch:
diff --git a/docs/development/tools/requirements.md b/docs/development/tools/requirements.md
index 3c76e9b8..010f3174 100644
--- a/docs/development/tools/requirements.md
+++ b/docs/development/tools/requirements.md
@@ -5,7 +5,7 @@ The specification of required third-party packages is scattered and partially du
 The following files in this repository contain information about required third-party packages:
 
 - `pyproject.toml`: GT4Py [package configuration](https://peps.python.org/pep-0621/) used by the build backend (`setuptools`). Install dependencies are specified in the _project.dependencies_ and _project.optional-dependencies_ tables.
-- `requirements-dev.in`: [requirements file](https://pip.pypa.io/en/stable/reference/requirements-file-format/) used by **pip**. It contains a list of packages required for the development of GT4Py.
+- `requirements-dev.in`: [requirements file](https://pip.pypa.io/en/stable/reference/requirements-file-format/) used by **pip**. It contains a list of packages required only for the development of GT4Py.
 - `requirements-dev.txt`: requirements file used by **pip**. It contains a completely frozen list of all packages required for installing and developing GT4Py. It is used by **pip** and **tox** to initialize the standard development and testing environments. It is automatically generated automatically from `requirements-dev.in` by **pip-compile**, when running the **tox** environment to update requirements.
 - `constraints.txt`: [constraints file](https://pip.pypa.io/en/stable/user_guide/#constraints-files) used by **pip** and **tox** to initialize a subset of the standard development environment making sure that if other packages are installed, transitive dependencies are taken from the frozen package list. It is generated automatically from `requirements-dev.in` using **pip-compile**.
 - `min-requirements-test.txt`: requirements file used by **pip**. It contains the minimum list of requirements to run GT4Py tests with the oldest compatible versions of all dependencies. It is generated automatically from `pyproject.toml` using **cog**.
@@ -16,7 +16,7 @@ The expected workflow to update GT4Py requirements is as follows:
 
 1. For changes in the GT4Py package dependencies, update the relevant table in `pyproject.toml`. When adding new tables to the _project.optional-dependencies_ section, make sure to add the new table as a dependency of the `all-` extra tables when possible.
 
-2. For changes in the development tools, update the `requirements-dev.in` file.
+2. For changes in the development tools, update the `requirements-dev.in` file. Note that required project packages already appearing in `pyproject.toml` should not be duplicated here.
 
 3. Run the **tox** _requirements-base_ environment to update all files automatically with **pip-compile** and **cog**. Note that **pip-compile** will most likely update the versions of some unrelated tools if new versions are available in PyPI.
 
diff --git a/requirements-dev.in b/requirements-dev.in
index 3c0a3389..57981360 100644
--- a/requirements-dev.in
+++ b/requirements-dev.in
@@ -9,7 +9,6 @@ clang-format>=9.0
 cogapp>=3.3
 coverage[toml]>=5.0
 darglint>=1.6
-factory-boy>=3.1
 flake8>=5.0.4
 flake8-bugbear>=20.11.1
 flake8-builtins>=1.5.3
diff --git a/src/gt4py/cartesian/backend/dace_backend.py b/src/gt4py/cartesian/backend/dace_backend.py
index b02c765a..0bfdec79 100644
--- a/src/gt4py/cartesian/backend/dace_backend.py
+++ b/src/gt4py/cartesian/backend/dace_backend.py
@@ -544,9 +544,14 @@ class DaCeComputationCodegen:
     def apply(cls, stencil_ir: gtir.Stencil, builder: "StencilBuilder", sdfg: dace.SDFG):
         self = cls()
         with dace.config.temporary_config():
+            # To prevent conflict with 3rd party usage of DaCe config always make sure that any
+            #  changes be under the temporary_config manager
             if gt_config.GT4PY_USE_HIP:
                 dace.config.Config.set("compiler", "cuda", "backend", value="hip")
             dace.config.Config.set("compiler", "cuda", "max_concurrent_streams", value=-1)
+            dace.config.Config.set(
+                "compiler", "cuda", "default_block_size", value=gt_config.DACE_DEFAULT_BLOCK_SIZE
+            )
             dace.config.Config.set("compiler", "cpu", "openmp_sections", value=False)
             code_objects = sdfg.generate_code()
         is_gpu = "CUDA" in {co.title for co in code_objects}
diff --git a/src/gt4py/cartesian/caching.py b/src/gt4py/cartesian/caching.py
index 421a37b2..4d716a6c 100644
--- a/src/gt4py/cartesian/caching.py
+++ b/src/gt4py/cartesian/caching.py
@@ -312,6 +312,8 @@ class JITCachingStrategy(CachingStrategy):
             "api_annotations": f"[{', '.join(self._extract_api_annotations())}]",
             **self._externals,
         }
+        if self.builder.backend.name == "dace:gpu":
+            fingerprint["default_block_size"] = gt_config.DACE_DEFAULT_BLOCK_SIZE
 
         # typeignore because attrclass StencilID has generated constructor
         return StencilID(  # type: ignore
diff --git a/src/gt4py/cartesian/config.py b/src/gt4py/cartesian/config.py
index 23bf36de..ad031b80 100644
--- a/src/gt4py/cartesian/config.py
+++ b/src/gt4py/cartesian/config.py
@@ -14,7 +14,7 @@
 
 import multiprocessing
 import os
-from typing import Any, Dict, Optional
+from typing import Any, Dict, List, Optional
 
 import gridtools_cpp
 
@@ -49,6 +49,10 @@ GT_INCLUDE_PATH: str = os.path.abspath(gridtools_cpp.get_include_dir())
 GT_CPP_TEMPLATE_DEPTH: int = 1024
 
 # Settings dict
+GT4PY_EXTRA_COMPILE_ARGS: str = os.environ.get("GT4PY_EXTRA_COMPILE_ARGS", "")
+extra_compile_args: List[str] = (
+    list(GT4PY_EXTRA_COMPILE_ARGS.split(" ")) if GT4PY_EXTRA_COMPILE_ARGS else []
+)
 build_settings: Dict[str, Any] = {
     "boost_include_path": os.path.join(BOOST_ROOT, "include"),
     "cuda_bin_path": os.path.join(CUDA_ROOT, "bin"),
@@ -57,10 +61,7 @@ build_settings: Dict[str, Any] = {
     "gt_include_path": os.environ.get("GT_INCLUDE_PATH", GT_INCLUDE_PATH),
     "openmp_cppflags": os.environ.get("OPENMP_CPPFLAGS", "-fopenmp").split(),
     "openmp_ldflags": os.environ.get("OPENMP_LDFLAGS", "-fopenmp").split(),
-    "extra_compile_args": {
-        "cxx": [],
-        "cuda": [],
-    },
+    "extra_compile_args": {"cxx": extra_compile_args, "cuda": extra_compile_args},
     "extra_link_args": [],
     "parallel_jobs": multiprocessing.cpu_count(),
     "cpp_template_depth": os.environ.get("GT_CPP_TEMPLATE_DEPTH", GT_CPP_TEMPLATE_DEPTH),
@@ -83,3 +84,5 @@ cache_settings: Dict[str, Any] = {
 code_settings: Dict[str, Any] = {"root_package_name": "_GT_"}
 
 os.environ.setdefault("DACE_CONFIG", os.path.join(os.path.abspath("."), ".dace.conf"))
+
+DACE_DEFAULT_BLOCK_SIZE: str = os.environ.get("DACE_DEFAULT_BLOCK_SIZE", "64,8,1")
diff --git a/src/gt4py/next/common.py b/src/gt4py/next/common.py
index fdf515d2..e55e13a3 100644
--- a/src/gt4py/next/common.py
+++ b/src/gt4py/next/common.py
@@ -75,6 +75,9 @@ class Dimension:
     def __str__(self):
         return f"{self.value}[{self.kind}]"
 
+    def __call__(self, val: int) -> NamedIndex:
+        return self, val
+
 
 class Infinity(enum.Enum):
     """Describes an unbounded `UnitRange`."""
@@ -272,7 +275,10 @@ NamedIndex: TypeAlias = tuple[Dimension, IntIndex]  # TODO: convert to NamedTupl
 NamedRange: TypeAlias = tuple[Dimension, UnitRange]  # TODO: convert to NamedTuple
 FiniteNamedRange: TypeAlias = tuple[Dimension, FiniteUnitRange]  # TODO: convert to NamedTuple
 RelativeIndexElement: TypeAlias = IntIndex | slice | types.EllipsisType
-AbsoluteIndexElement: TypeAlias = NamedIndex | NamedRange
+NamedSlice: TypeAlias = (
+    slice  # once slice is generic we should do: slice[NamedIndex, NamedIndex, Literal[1]], see https://peps.python.org/pep-0696/
+)
+AbsoluteIndexElement: TypeAlias = NamedIndex | NamedRange | NamedSlice
 AnyIndexElement: TypeAlias = RelativeIndexElement | AbsoluteIndexElement
 AbsoluteIndexSequence: TypeAlias = Sequence[NamedRange | NamedIndex]
 RelativeIndexSequence: TypeAlias = tuple[
@@ -307,6 +313,10 @@ def is_named_index(v: AnyIndexSpec) -> TypeGuard[NamedRange]:
     )
 
 
+def is_named_slice(obj: AnyIndexSpec) -> TypeGuard[NamedRange]:
+    return isinstance(obj, slice) and (is_named_index(obj.start) and is_named_index(obj.stop))
+
+
 def is_any_index_element(v: AnyIndexSpec) -> TypeGuard[AnyIndexElement]:
     return (
         is_int_index(v)
@@ -732,7 +742,7 @@ class ConnectivityKind(enum.Flag):
 
 
 @extended_runtime_checkable
-# type: ignore[misc] # DimT should be covariant, but break in another place
+# type: ignore[misc] # DimT should be covariant, but breaks in another place
 class ConnectivityField(Field[DimsT, core_defs.IntegralScalar], Protocol[DimsT, DimT]):
     @property
     @abc.abstractmethod
@@ -749,6 +759,10 @@ class ConnectivityField(Field[DimsT, core_defs.IntegralScalar], Protocol[DimsT,
     @abc.abstractmethod
     def inverse_image(self, image_range: UnitRange | NamedRange) -> Sequence[NamedRange]: ...
 
+    @property
+    @abc.abstractmethod
+    def skip_value(self) -> Optional[core_defs.IntegralScalar]: ...
+
     # Operators
     def __abs__(self) -> Never:
         raise TypeError("'ConnectivityField' does not support this operation.")
@@ -840,6 +854,7 @@ def _connectivity(
     *,
     domain: Optional[DomainLike] = None,
     dtype: Optional[core_defs.DType] = None,
+    skip_value: Optional[core_defs.IntegralScalar] = None,
 ) -> ConnectivityField:
     raise NotImplementedError
 
@@ -918,6 +933,10 @@ class CartesianConnectivity(ConnectivityField[DimsT, DimT]):
     def codomain(self) -> DimT:
         return self.dimension
 
+    @property
+    def skip_value(self) -> None:
+        return None
+
     @functools.cached_property
     def kind(self) -> ConnectivityKind:
         return ConnectivityKind(0)
@@ -1083,4 +1102,4 @@ class FieldBuiltinFuncRegistry:
 #: Numeric value used to represent missing values in connectivities.
 #: Equivalent to the `_FillValue` attribute in the UGRID Conventions
 #: (see: http://ugrid-conventions.github.io/ugrid-conventions/).
-SKIP_VALUE: Final[int] = -1
+_DEFAULT_SKIP_VALUE: Final[int] = -1
diff --git a/src/gt4py/next/config.py b/src/gt4py/next/config.py
index 74bf56d6..682d5254 100644
--- a/src/gt4py/next/config.py
+++ b/src/gt4py/next/config.py
@@ -26,22 +26,34 @@ class BuildCacheLifetime(enum.Enum):
 
 
 class CMakeBuildType(enum.Enum):
-    def _generate_next_value_(name, start, count, last_values):
-        return "".join(part.capitalize() for part in name.split("_"))
+    """
+    CMake build types enum.
 
-    DEBUG = enum.auto()
-    RELEASE = enum.auto()
-    REL_WITH_DEB_INFO = enum.auto()
-    MIN_SIZE_REL = enum.auto()
+    Member values have to be valid CMake syntax.
+    """
 
+    DEBUG = "Debug"
+    RELEASE = "Release"
+    REL_WITH_DEB_INFO = "RelWithDebInfo"
+    MIN_SIZE_REL = "MinSizeRel"
 
-def env_flag_to_bool(flag_value: str) -> bool:
-    """Like in gt4py.cartesian, env vars for flags should be set to '0' or '1'."""
+
+def env_flag_to_bool(name: str, default: bool) -> bool:
+    """Recognize true or false signaling string values."""
+    flag_value = None
+    if name in os.environ:
+        flag_value = os.environ[name].lower()
     match flag_value:
-        case "0" | "1":
-            return bool(int(flag_value))
+        case None:
+            return default
+        case "0" | "false" | "off":
+            return False
+        case "1" | "true" | "on":
+            return True
         case _:
-            raise ValueError("GT4Py flag environment variables must have value '0' or '1'.")
+            raise ValueError(
+                "Invalid GT4Py environment flag value: use '0 | false | off' or '1 | true | on'."
+            )
 
 
 _PREFIX: Final[str] = "GT4PY"
@@ -49,11 +61,18 @@ _PREFIX: Final[str] = "GT4PY"
 #: Master debug flag
 #: Changes defaults for all the other options to be as helpful for debugging as possible.
 #: Does not override values set in environment variables.
-DEBUG: Final[bool] = env_flag_to_bool(os.environ.get(f"{_PREFIX}_DEBUG", "0"))
+DEBUG: Final[bool] = env_flag_to_bool(f"{_PREFIX}_DEBUG", default=False)
+
+
+#: Verbose flag for DSL compilation errors
+VERBOSE_EXCEPTIONS: bool = env_flag_to_bool(
+    f"{_PREFIX}_VERBOSE_EXCEPTIONS", default=True if DEBUG else False
+)
+
 
 #: Where generated code projects should be persisted.
 #: Only active if BUILD_CACHE_LIFETIME is set to PERSISTENT
-BUILD_CACHE_DIR: Final[pathlib.Path] = (
+BUILD_CACHE_DIR: pathlib.Path = (
     pathlib.Path(os.environ.get(f"{_PREFIX}_BUILD_CACHE_DIR", tempfile.gettempdir()))
     / "gt4py_cache"
 )
@@ -62,14 +81,12 @@ BUILD_CACHE_DIR: Final[pathlib.Path] = (
 #: Whether generated code projects should be kept around between runs.
 #: - SESSION: generated code projects get destroyed when the interpreter shuts down
 #: - PERSISTENT: generated code projects are written to BUILD_CACHE_DIR and persist between runs
-BUILD_CACHE_LIFETIME: Final[BuildCacheLifetime] = getattr(
-    BuildCacheLifetime,
-    os.environ.get(f"{_PREFIX}_BUILD_CACHE_LIFETIME", "persistent" if DEBUG else "session").upper(),
-)
+BUILD_CACHE_LIFETIME: BuildCacheLifetime = BuildCacheLifetime[
+    os.environ.get(f"{_PREFIX}_BUILD_CACHE_LIFETIME", "persistent" if DEBUG else "session").upper()
+]
 
 #: Build type to be used when CMake is used to compile generated code.
 #: Might have no effect when CMake is not used as part of the toolchain.
-CMAKE_BUILD_TYPE: Final[CMakeBuildType] = getattr(
-    CMakeBuildType,
-    os.environ.get(f"{_PREFIX}_CMAKE_BUILD_TYPE", "debug" if DEBUG else "release").upper(),
-)
+CMAKE_BUILD_TYPE: CMakeBuildType = CMakeBuildType[
+    os.environ.get(f"{_PREFIX}_CMAKE_BUILD_TYPE", "debug" if DEBUG else "release").upper()
+]
diff --git a/src/gt4py/next/constructors.py b/src/gt4py/next/constructors.py
index 8b41bf7c..f8e7b9bf 100644
--- a/src/gt4py/next/constructors.py
+++ b/src/gt4py/next/constructors.py
@@ -308,6 +308,7 @@ def as_connectivity(
     *,
     allocator: Optional[next_allocators.FieldBufferAllocatorProtocol] = None,
     device: Optional[core_defs.Device] = None,
+    skip_value: Optional[core_defs.IntegralScalar] = None,
     # copy=False, TODO
 ) -> common.ConnectivityField:
     """
@@ -330,6 +331,9 @@ def as_connectivity(
     Raises:
         ValueError: If the domain or codomain is invalid, or if the shape of the data does not match the domain shape.
     """
+    assert (
+        skip_value is None or skip_value == common._DEFAULT_SKIP_VALUE
+    )  # TODO(havogt): not yet configurable
     if isinstance(domain, Sequence) and all(isinstance(dim, common.Dimension) for dim in domain):
         domain = cast(Sequence[common.Dimension], domain)
         if len(domain) != data.ndim:
@@ -359,7 +363,7 @@ def as_connectivity(
     # TODO(havogt): consider adding MutableNDArrayObject
     buffer.ndarray[...] = storage_utils.asarray(data)  # type: ignore[index]
     connectivity_field = common._connectivity(
-        buffer.ndarray, codomain=codomain, domain=actual_domain
+        buffer.ndarray, codomain=codomain, domain=actual_domain, skip_value=skip_value
     )
     assert isinstance(connectivity_field, nd_array_field.NdArrayConnectivityField)
 
diff --git a/src/gt4py/next/embedded/common.py b/src/gt4py/next/embedded/common.py
index 94efe4d6..f9201da2 100644
--- a/src/gt4py/next/embedded/common.py
+++ b/src/gt4py/next/embedded/common.py
@@ -146,3 +146,32 @@ def _find_index_of_dim(
         if dim == d:
             return i
     return None
+
+
+def canonicalize_any_index_sequence(
+    index: common.AnyIndexSpec,
+) -> common.AnyIndexSpec:
+    # TODO: instead of canonicalizing to `NamedRange`, we should canonicalize to `NamedSlice`
+    new_index: common.AnyIndexSpec = (index,) if isinstance(index, slice) else index
+    if isinstance(new_index, tuple) and all(isinstance(i, slice) for i in new_index):
+        new_index = tuple([_named_slice_to_named_range(i) for i in new_index])  # type: ignore[arg-type, assignment] # all i's are slices as per if statement
+    return new_index
+
+
+def _named_slice_to_named_range(
+    idx: common.NamedSlice,
+) -> common.NamedRange | common.NamedSlice:
+    assert hasattr(idx, "start") and hasattr(idx, "stop")
+    if common.is_named_slice(idx):
+        idx_start_0, idx_start_1, idx_stop_0, idx_stop_1 = idx.start[0], idx.start[1], idx.stop[0], idx.stop[1]  # type: ignore[attr-defined]
+        if idx_start_0 != idx_stop_0:
+            raise IndexError(
+                f"Dimensions slicing mismatch between '{idx_start_0.value}' and '{idx_stop_0.value}'."
+            )
+        assert isinstance(idx_start_1, int) and isinstance(idx_stop_1, int)
+        return (idx_start_0, common.UnitRange(idx_start_1, idx_stop_1))
+    if common.is_named_index(idx.start) and idx.stop is None:
+        raise IndexError(f"Upper bound needs to be specified for {idx}.")
+    if common.is_named_index(idx.stop) and idx.start is None:
+        raise IndexError(f"Lower bound needs to be specified for {idx}.")
+    return idx
diff --git a/src/gt4py/next/embedded/nd_array_field.py b/src/gt4py/next/embedded/nd_array_field.py
index c39408ba..38aab09d 100644
--- a/src/gt4py/next/embedded/nd_array_field.py
+++ b/src/gt4py/next/embedded/nd_array_field.py
@@ -26,8 +26,9 @@ from numpy import typing as npt
 from gt4py._core import definitions as core_defs
 from gt4py.eve.extended_typing import Any, Never, Optional, ParamSpec, TypeAlias, TypeVar
 from gt4py.next import common
-from gt4py.next.embedded import common as embedded_common
+from gt4py.next.embedded import common as embedded_common, context as embedded_context
 from gt4py.next.ffront import fbuiltins
+from gt4py.next.iterator import embedded as itir_embedded
 
 
 try:
@@ -170,9 +171,12 @@ class NdArrayField(
         if not common.is_connectivity_field(connectivity):
             assert isinstance(connectivity, fbuiltins.FieldOffset)
             connectivity = connectivity.as_connectivity_field()
-
         assert common.is_connectivity_field(connectivity)
 
+        # Current implementation relies on skip_value == -1:
+        # if we assume the indexed array has at least one element, we wrap around without out of bounds
+        assert connectivity.skip_value is None or connectivity.skip_value == -1
+
         # Compute the new domain
         dim = connectivity.codomain
         dim_idx = self.domain.dim_index(dim)
@@ -297,6 +301,7 @@ class NdArrayField(
     def _slice(
         self, index: common.AnyIndexSpec
     ) -> tuple[common.Domain, common.RelativeIndexSequence]:
+        index = embedded_common.canonicalize_any_index_sequence(index)
         new_domain = embedded_common.sub_domain(self.domain, index)
 
         index_sequence = common.as_any_index_sequence(index)
@@ -315,6 +320,7 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
     NdArrayField[common.DimsT, core_defs.IntegralScalar],
 ):
     _codomain: common.DimT
+    _skip_value: Optional[core_defs.IntegralScalar]
 
     @functools.cached_property
     def _cache(self) -> dict:
@@ -329,6 +335,10 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
     def codomain(self) -> common.DimT:
         return self._codomain
 
+    @property
+    def skip_value(self) -> Optional[core_defs.IntegralScalar]:
+        return self._skip_value
+
     @functools.cached_property
     def kind(self) -> common.ConnectivityKind:
         kind = common.ConnectivityKind.MODIFY_STRUCTURE
@@ -349,6 +359,7 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
         *,
         domain: common.DomainLike,
         dtype: Optional[core_defs.DTypeLike] = None,
+        skip_value: Optional[core_defs.IntegralScalar] = None,
     ) -> NdArrayConnectivityField:
         domain = common.domain(domain)
         xp = cls.array_ns
@@ -367,7 +378,12 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
 
         assert isinstance(codomain, common.Dimension)
 
-        return cls(domain, array, codomain)
+        return cls(
+            domain,
+            array,
+            codomain,
+            _skip_value=skip_value,
+        )
 
     def inverse_image(
         self, image_range: common.UnitRange | common.NamedRange
@@ -390,47 +406,16 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
             assert isinstance(image_range, common.UnitRange)
 
             assert common.UnitRange.is_finite(image_range)
-            restricted_mask = (self._ndarray >= image_range.start) & (
-                self._ndarray < image_range.stop
-            )
-            # indices of non-zero elements in each dimension
-            nnz: tuple[core_defs.NDArrayObject, ...] = xp.nonzero(restricted_mask)
-
-            new_dims = []
-            non_contiguous_dims = []
-
-            for i, dim_nnz_indices in enumerate(nnz):
-                # Check if the indices are contiguous
-                first_data_index = dim_nnz_indices[0]
-                assert isinstance(first_data_index, core_defs.INTEGRAL_TYPES)
-                last_data_index = dim_nnz_indices[-1]
-                assert isinstance(last_data_index, core_defs.INTEGRAL_TYPES)
-                indices, counts = xp.unique(dim_nnz_indices, return_counts=True)
-                dim_range = self._domain[i]
-
-                if len(xp.unique(counts)) == 1 and (
-                    len(indices) == last_data_index - first_data_index + 1
-                ):
-                    idx_offset = dim_range[1].start
-                    start = idx_offset + first_data_index
-                    assert common.is_int_index(start)
-                    stop = idx_offset + last_data_index + 1
-                    assert common.is_int_index(stop)
-                    new_dims.append(
-                        common.named_range(
-                            (
-                                dim_range[0],
-                                (start, stop),
-                            )
-                        )
-                    )
-                else:
-                    non_contiguous_dims.append(dim_range[0])
 
-            if non_contiguous_dims:
-                raise ValueError(
-                    f"Restriction generates non-contiguous dimensions '{non_contiguous_dims}'."
-                )
+            relative_ranges = _hypercube(self._ndarray, image_range, xp, self.skip_value)
+
+            if relative_ranges is None:
+                raise ValueError("Restriction generates non-contiguous dimensions.")
+
+            new_dims = [
+                common.named_range((d, rr + ar.start))
+                for d, ar, rr in zip(self.domain.dims, self.domain.ranges, relative_ranges)
+            ]
 
             self._cache[cache_key] = new_dims
 
@@ -444,7 +429,7 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
             xp = cls.array_ns
             new_domain, buffer_slice = self._slice(index)
             new_buffer = xp.asarray(self.ndarray[buffer_slice])
-            restricted_connectivity = cls(new_domain, new_buffer, self.codomain)
+            restricted_connectivity = cls(new_domain, new_buffer, self.codomain, self.skip_value)
             self._cache[cache_key] = restricted_connectivity
 
         return restricted_connectivity
@@ -452,6 +437,41 @@ class NdArrayConnectivityField(  # type: ignore[misc] # for __ne__, __eq__
     __getitem__ = restrict
 
 
+def _hypercube(
+    index_array: core_defs.NDArrayObject,
+    image_range: common.UnitRange,
+    xp: ModuleType,
+    skip_value: Optional[core_defs.IntegralScalar] = None,
+) -> Optional[list[common.UnitRange]]:
+    """
+    Return the hypercube that contains all indices in `index_array` that are within `image_range`, or `None` if no such hypercube exists.
+
+    If `skip_value` is given, the selected values are ignored. It returns the smallest hypercube.
+    A bigger hypercube could be constructed by adding lines that contain only `skip_value`s.
+    Example:
+    index_array =  0  1 -1
+                   3  4 -1
+                  -1 -1 -1
+    skip_value = -1
+    would currently select the 2x2 range [0,2], [0,2], but could also select the 3x3 range [0,3], [0,3].
+    """
+    select_mask = (index_array >= image_range.start) & (index_array < image_range.stop)
+
+    nnz: tuple[core_defs.NDArrayObject, ...] = xp.nonzero(select_mask)
+
+    slices = tuple(
+        slice(xp.min(dim_nnz_indices), xp.max(dim_nnz_indices) + 1) for dim_nnz_indices in nnz
+    )
+    hcube = select_mask[tuple(slices)]
+    if skip_value is not None:
+        ignore_mask = index_array == skip_value
+        hcube |= ignore_mask[tuple(slices)]
+    if not xp.all(hcube):
+        return None
+
+    return [common.UnitRange(s.start, s.stop) for s in slices]
+
+
 # -- Specialized implementations for builtin operations on array fields --
 
 NdArrayField.register_builtin_func(
@@ -483,21 +503,49 @@ NdArrayField.register_builtin_func(
 NdArrayField.register_builtin_func(fbuiltins.where, _make_builtin("where", "where"))
 
 
-def _make_reduction(builtin_name: str, array_builtin_name: str) -> Callable[
+def _make_reduction(
+    builtin_name: str, array_builtin_name: str, initial_value_op: Callable
+) -> Callable[
     ...,
     NdArrayField[common.DimsT, core_defs.ScalarT],
 ]:
     def _builtin_op(
         field: NdArrayField[common.DimsT, core_defs.ScalarT], axis: common.Dimension
     ) -> NdArrayField[common.DimsT, core_defs.ScalarT]:
+        xp = field.array_ns
+
         if not axis.kind == common.DimensionKind.LOCAL:
             raise ValueError("Can only reduce local dimensions.")
         if axis not in field.domain.dims:
             raise ValueError(f"Field can not be reduced as it doesn't have dimension '{axis}'.")
+        if len([d for d in field.domain.dims if d.kind is common.DimensionKind.LOCAL]) > 1:
+            raise NotImplementedError(
+                "Reducing a field with more than one local dimension is not supported."
+            )
         reduce_dim_index = field.domain.dims.index(axis)
+        current_offset_provider = embedded_context.offset_provider.get(None)
+        assert current_offset_provider is not None
+        offset_definition = current_offset_provider[
+            axis.value
+        ]  # assumes offset and local dimension have same name
+        assert isinstance(offset_definition, itir_embedded.NeighborTableOffsetProvider)
         new_domain = common.Domain(*[nr for nr in field.domain if nr[0] != axis])
+
+        broadcast_slice = tuple(
+            slice(None) if d in [axis, offset_definition.origin_axis] else xp.newaxis
+            for d in field.domain.dims
+        )
+        masked_array = xp.where(
+            xp.asarray(offset_definition.table[broadcast_slice]) != common._DEFAULT_SKIP_VALUE,
+            field.ndarray,
+            initial_value_op(field),
+        )
+
         return field.__class__.from_array(
-            getattr(field.array_ns, array_builtin_name)(field.ndarray, axis=reduce_dim_index),
+            getattr(xp, array_builtin_name)(
+                masked_array,
+                axis=reduce_dim_index,
+            ),
             domain=new_domain,
         )
 
@@ -505,9 +553,15 @@ def _make_reduction(builtin_name: str, array_builtin_name: str) -> Callable[
     return _builtin_op
 
 
-NdArrayField.register_builtin_func(fbuiltins.neighbor_sum, _make_reduction("neighbor_sum", "sum"))
-NdArrayField.register_builtin_func(fbuiltins.max_over, _make_reduction("max_over", "max"))
-NdArrayField.register_builtin_func(fbuiltins.min_over, _make_reduction("min_over", "min"))
+NdArrayField.register_builtin_func(
+    fbuiltins.neighbor_sum, _make_reduction("neighbor_sum", "sum", lambda x: x.dtype.scalar_type(0))
+)
+NdArrayField.register_builtin_func(
+    fbuiltins.max_over, _make_reduction("max_over", "max", lambda x: x.array_ns.min(x._ndarray))
+)
+NdArrayField.register_builtin_func(
+    fbuiltins.min_over, _make_reduction("min_over", "min", lambda x: x.array_ns.max(x._ndarray))
+)
 
 
 # -- Concrete array implementations --
diff --git a/src/gt4py/next/errors/__init__.py b/src/gt4py/next/errors/__init__.py
index dd48d6f0..c9653329 100644
--- a/src/gt4py/next/errors/__init__.py
+++ b/src/gt4py/next/errors/__init__.py
@@ -17,7 +17,6 @@
 from . import (  # noqa: module needs to be loaded for pretty printing of uncaught exceptions.
     excepthook,
 )
-from .excepthook import set_verbose_exceptions
 from .exceptions import (
     DSLError,
     InvalidParameterAnnotationError,
@@ -37,5 +36,4 @@ __all__ = [
     "MissingArgumentError",
     "UndefinedSymbolError",
     "UnsupportedPythonFeatureError",
-    "set_verbose_exceptions",
 ]
diff --git a/src/gt4py/next/errors/excepthook.py b/src/gt4py/next/errors/excepthook.py
index f1dd18e1..673eaca7 100644
--- a/src/gt4py/next/errors/excepthook.py
+++ b/src/gt4py/next/errors/excepthook.py
@@ -20,39 +20,12 @@ in an inconvenient way. The previously set excepthook is used to print all
 other errors.
 """
 
-import os
 import sys
-import warnings
 from typing import Callable
 
-from . import exceptions, formatting
-
-
-def _get_verbose_exceptions_envvar() -> bool:
-    """Detect if the user enabled verbose exceptions in the environment variables."""
-    env_var_name = "GT4PY_VERBOSE_EXCEPTIONS"
-    if env_var_name in os.environ:
-        false_values = ["0", "false", "off"]
-        true_values = ["1", "true", "on"]
-        value = os.environ[env_var_name].lower()
-        if value in false_values:
-            return False
-        elif value in true_values:
-            return True
-        else:
-            values = ", ".join([*false_values, *true_values])
-            msg = f"the 'GT4PY_VERBOSE_EXCEPTIONS' environment variable must be one of {values} (case insensitive)"
-            warnings.warn(msg)
-    return False
-
+from gt4py.next import config
 
-_verbose_exceptions: bool = _get_verbose_exceptions_envvar()
-
-
-def set_verbose_exceptions(enabled: bool = False) -> None:
-    """Programmatically set whether to use verbose printing for uncaught errors."""
-    global _verbose_exceptions
-    _verbose_exceptions = enabled
+from . import exceptions, formatting
 
 
 def _format_uncaught_error(err: exceptions.DSLError, verbose_exceptions: bool) -> list[str]:
@@ -77,7 +50,7 @@ def compilation_error_hook(fallback: Callable, type_: type, value: BaseException
     also printed.
     """
     if isinstance(value, exceptions.DSLError):
-        exc_strs = _format_uncaught_error(value, _verbose_exceptions)
+        exc_strs = _format_uncaught_error(value, config.VERBOSE_EXCEPTIONS)
         print("".join(exc_strs), file=sys.stderr)
     else:
         fallback(type_, value, tb)
diff --git a/src/gt4py/next/ffront/decorator.py b/src/gt4py/next/ffront/decorator.py
index a556d0ea..0cf1611b 100644
--- a/src/gt4py/next/ffront/decorator.py
+++ b/src/gt4py/next/ffront/decorator.py
@@ -221,6 +221,10 @@ class Program:
                 f"The following closure variables are undefined: {', '.join(undefined_symbols)}."
             )
 
+    @property
+    def __name__(self) -> str:
+        return self.definition.__name__
+
     @functools.cached_property
     def __gt_allocator__(
         self,
@@ -603,6 +607,10 @@ class FieldOperator(GTCallable, Generic[OperatorNodeT]):
             operator_attributes=operator_attributes,
         )
 
+    @property
+    def __name__(self) -> str:
+        return self.definition.__name__
+
     def __gt_type__(self) -> ts.CallableType:
         type_ = self.foast_node.type
         assert isinstance(type_, ts.CallableType)
diff --git a/src/gt4py/next/ffront/fbuiltins.py b/src/gt4py/next/ffront/fbuiltins.py
index 493493f6..34251a36 100644
--- a/src/gt4py/next/ffront/fbuiltins.py
+++ b/src/gt4py/next/ffront/fbuiltins.py
@@ -360,12 +360,14 @@ class FieldOffset(runtime.Offset):
             if common.is_connectivity_field(offset_definition):
                 connectivity = offset_definition
             elif isinstance(offset_definition, gtx.NeighborTableOffsetProvider):
-                assert not offset_definition.has_skip_values
                 connectivity = gtx.as_connectivity(
                     domain=self.target,
                     codomain=self.source,
                     data=offset_definition.table,
                     dtype=offset_definition.index_type,
+                    skip_value=(
+                        common._DEFAULT_SKIP_VALUE if offset_definition.has_skip_values else None
+                    ),
                 )
             else:
                 raise NotImplementedError()
diff --git a/src/gt4py/next/ffront/past_to_itir.py b/src/gt4py/next/ffront/past_to_itir.py
index 8be93096..e092538c 100644
--- a/src/gt4py/next/ffront/past_to_itir.py
+++ b/src/gt4py/next/ffront/past_to_itir.py
@@ -231,7 +231,6 @@ class ProgramLowering(
         node_domain: Optional[past.Expr],
         slices: Optional[list[past.Slice]] = None,
     ) -> itir.FunCall:
-        domain_args = []
 
         assert isinstance(out_field.type, ts.TypeSpec)
         out_field_types = type_info.primitive_constituents(out_field.type).to_list()
@@ -246,6 +245,8 @@ class ProgramLowering(
                 " caught in type deduction already."
             )
 
+        domain_args = []
+        domain_args_kind = []
         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))
@@ -271,11 +272,17 @@ class ProgramLowering(
                     args=[itir.AxisLiteral(value=dim.value), lower, upper],
                 )
             )
+            domain_args_kind.append(dim.kind)
 
         if self.grid_type == GridType.CARTESIAN:
             domain_builtin = "cartesian_domain"
         elif self.grid_type == GridType.UNSTRUCTURED:
             domain_builtin = "unstructured_domain"
+            # for no good reason, the domain arguments for unstructured need to be in order (horizontal, vertical)
+            if domain_args_kind[0] == DimensionKind.VERTICAL:
+                assert len(domain_args) == 2
+                assert domain_args_kind[1] == DimensionKind.HORIZONTAL
+                domain_args[0], domain_args[1] = domain_args[1], domain_args[0]
         else:
             raise AssertionError()
 
diff --git a/src/gt4py/next/iterator/atlas_utils.py b/src/gt4py/next/iterator/atlas_utils.py
index 500c9253..a23a4b51 100644
--- a/src/gt4py/next/iterator/atlas_utils.py
+++ b/src/gt4py/next/iterator/atlas_utils.py
@@ -17,6 +17,8 @@ try:
 except ImportError:
     IrregularConnectivity = None
 
+from gt4py.next import common
+
 
 # TODO(tehrengruber): make this a proper Connectivity instead of faking a numpy array
 class AtlasTable:
@@ -29,7 +31,7 @@ class AtlasTable:
             if neigh_index < self.atlas_connectivity.cols(primary_index):
                 return self.atlas_connectivity[primary_index, neigh_index]
             else:
-                return None
+                return common._DEFAULT_SKIP_VALUE
         else:
             if neigh_index < 2:
                 return self.atlas_connectivity[primary_index, neigh_index]
@@ -53,3 +55,12 @@ class AtlasTable:
                 if v is not None:
                     maximum = max(maximum, v)
         return maximum
+
+    def asnumpy(self):
+        import numpy as np
+
+        res = np.empty(self.shape, dtype=self.dtype)
+        for i in range(self.shape[0]):
+            for j in range(self.shape[1]):
+                res[i, j] = self[i, j]
+        return res
diff --git a/src/gt4py/next/iterator/embedded.py b/src/gt4py/next/iterator/embedded.py
index a45b81a7..80fc5392 100644
--- a/src/gt4py/next/iterator/embedded.py
+++ b/src/gt4py/next/iterator/embedded.py
@@ -532,7 +532,7 @@ def execute_shift(
                 assert common.is_int_index(cur_index)
                 if offset_implementation.mapped_index(cur_index, index) in [
                     None,
-                    common.SKIP_VALUE,
+                    common._DEFAULT_SKIP_VALUE,
                 ]:
                     return None
 
@@ -559,7 +559,7 @@ def execute_shift(
         assert common.is_int_index(cur_index)
         if offset_implementation.mapped_index(cur_index, index) in [
             None,
-            common.SKIP_VALUE,
+            common._DEFAULT_SKIP_VALUE,
         ]:
             return None
         else:
diff --git a/src/gt4py/next/iterator/tracing.py b/src/gt4py/next/iterator/tracing.py
index 05ebd023..1c9887cd 100644
--- a/src/gt4py/next/iterator/tracing.py
+++ b/src/gt4py/next/iterator/tracing.py
@@ -147,6 +147,8 @@ for builtin_name in builtins.BUILTINS:
 def make_node(o):
     if isinstance(o, Node):
         return o
+    if isinstance(o, common.Dimension):
+        return AxisLiteral(value=o.value)
     if callable(o):
         if o.__name__ == "<lambda>":
             return lambdadef(o)
@@ -156,8 +158,6 @@ def make_node(o):
         return OffsetLiteral(value=o.value)
     if isinstance(o, core_defs.Scalar):
         return im.literal_from_value(o)
-    if isinstance(o, common.Dimension):
-        return AxisLiteral(value=o.value)
     if isinstance(o, tuple):
         return _f("make_tuple", *(make_node(arg) for arg in o))
     if o is None:
diff --git a/src/gt4py/next/iterator/transforms/pass_manager.py b/src/gt4py/next/iterator/transforms/pass_manager.py
index cd8ebb55..e8f836dd 100644
--- a/src/gt4py/next/iterator/transforms/pass_manager.py
+++ b/src/gt4py/next/iterator/transforms/pass_manager.py
@@ -38,7 +38,7 @@ from gt4py.next.iterator.transforms.unroll_reduce import UnrollReduce
 @enum.unique
 class LiftMode(enum.Enum):
     FORCE_INLINE = enum.auto()
-    FORCE_TEMPORARIES = enum.auto()
+    USE_TEMPORARIES = enum.auto()
     SIMPLE_HEURISTIC = enum.auto()
 
 
@@ -47,7 +47,7 @@ def _inline_lifts(ir, lift_mode):
         return InlineLifts().visit(ir)
     elif lift_mode == LiftMode.SIMPLE_HEURISTIC:
         return InlineLifts(simple_inline_heuristic.is_eligible_for_inlining).visit(ir)
-    elif lift_mode == LiftMode.FORCE_TEMPORARIES:
+    elif lift_mode == LiftMode.USE_TEMPORARIES:
         return InlineLifts(
             flags=InlineLifts.Flag.INLINE_TRIVIAL_DEREF_LIFT
             | InlineLifts.Flag.INLINE_DEREF_LIFT  # some tuple exprs found in FVM don't work yet.
diff --git a/src/gt4py/next/otf/compilation/cache.py b/src/gt4py/next/otf/compilation/cache.py
index ee5ec650..810952d0 100644
--- a/src/gt4py/next/otf/compilation/cache.py
+++ b/src/gt4py/next/otf/compilation/cache.py
@@ -27,7 +27,6 @@ from gt4py.next.otf.binding import interface
 _session_cache_dir = tempfile.TemporaryDirectory(prefix="gt4py_session_")
 
 _session_cache_dir_path = pathlib.Path(_session_cache_dir.name)
-_persistent_cache_dir_path = config.BUILD_CACHE_DIR
 
 
 def _serialize_param(parameter: interface.Parameter) -> str:
@@ -72,7 +71,7 @@ def get_cache_folder(
         case config.BuildCacheLifetime.SESSION:
             base_path = _session_cache_dir_path
         case config.BuildCacheLifetime.PERSISTENT:
-            base_path = _persistent_cache_dir_path
+            base_path = config.BUILD_CACHE_DIR
         case _:
             raise ValueError("Unsupported caching lifetime.")
 
diff --git a/src/gt4py/next/program_processors/runners/dace_iterator/__init__.py b/src/gt4py/next/program_processors/runners/dace_iterator/__init__.py
index aba56561..1263cff5 100644
--- a/src/gt4py/next/program_processors/runners/dace_iterator/__init__.py
+++ b/src/gt4py/next/program_processors/runners/dace_iterator/__init__.py
@@ -22,7 +22,6 @@ import numpy as np
 from dace.codegen.compiled_sdfg import CompiledSDFG
 from dace.sdfg import utils as sdutils
 from dace.transformation.auto import auto_optimize as autoopt
-from dace.transformation.interstate import RefineNestedAccess
 
 import gt4py.next.allocators as next_allocators
 import gt4py.next.iterator.ir as itir
@@ -53,8 +52,19 @@ def get_sorted_dim_ranges(domain: common.Domain) -> Sequence[common.FiniteUnitRa
 _build_type = "Release"
 
 
-def convert_arg(arg: Any):
+def convert_arg(arg: Any, sdfg_param: str):
     if common.is_field(arg):
+        # field domain offsets are not supported
+        non_zero_offsets = [
+            (dim, dim_range)
+            for dim, dim_range in zip(arg.domain.dims, arg.domain.ranges)
+            if dim_range.start != 0
+        ]
+        if non_zero_offsets:
+            dim, dim_range = non_zero_offsets[0]
+            raise RuntimeError(
+                f"Field '{sdfg_param}' passed as array slice with offset {dim_range.start} on dimension {dim.value}."
+            )
         sorted_dims = get_sorted_dims(arg.domain.dims)
         ndim = len(sorted_dims)
         dim_indices = [dim_index for dim_index, _ in sorted_dims]
@@ -99,7 +109,7 @@ def preprocess_program(
 
 def get_args(sdfg: dace.SDFG, args: Sequence[Any]) -> dict[str, Any]:
     sdfg_params: Sequence[str] = sdfg.arg_names
-    return {sdfg_param: convert_arg(arg) for sdfg_param, arg in zip(sdfg_params, args)}
+    return {sdfg_param: convert_arg(arg, sdfg_param) for sdfg_param, arg in zip(sdfg_params, args)}
 
 
 def _ensure_is_on_device(
@@ -140,27 +150,6 @@ def get_shape_args(
     return shape_args
 
 
-def get_offset_args(sdfg: dace.SDFG, args: Sequence[Any]) -> Mapping[str, int]:
-    sdfg_arrays: Mapping[str, dace.data.Array] = sdfg.arrays
-    sdfg_params: Sequence[str] = sdfg.arg_names
-    field_args = {param: arg for param, arg in zip(sdfg_params, args) if common.is_field(arg)}
-
-    # assume that arrays for connectivity tables do not use offset
-    assert all(
-        drange.start == 0
-        for sdfg_param, arg in field_args.items()
-        if sdfg_param.startswith("__connectivity")
-        for drange in arg.domain.ranges
-    )
-
-    return {
-        str(sym): -drange.start
-        for sdfg_param, arg in field_args.items()
-        if not sdfg_param.startswith("__connectivity")
-        for sym, drange in zip(sdfg_arrays[sdfg_param].offset, get_sorted_dim_ranges(arg.domain))
-    }
-
-
 def get_stride_args(
     arrays: Mapping[str, dace.data.Array], args: Mapping[str, Any]
 ) -> Mapping[str, int]:
@@ -243,7 +232,6 @@ def get_sdfg_args(sdfg: dace.SDFG, *args, check_args: bool = False, **kwargs) ->
     dace_conn_shapes = get_shape_args(sdfg.arrays, dace_conn_args)
     dace_strides = get_stride_args(sdfg.arrays, dace_field_args)
     dace_conn_strides = get_stride_args(sdfg.arrays, dace_conn_args)
-    dace_offsets = get_offset_args(sdfg, args)
     all_args = {
         **dace_args,
         **dace_conn_args,
@@ -251,7 +239,6 @@ def get_sdfg_args(sdfg: dace.SDFG, *args, check_args: bool = False, **kwargs) ->
         **dace_conn_shapes,
         **dace_strides,
         **dace_conn_strides,
-        **dace_offsets,
     }
 
     if check_args:
@@ -308,9 +295,6 @@ def build_sdfg_from_itir(
     sdfg = sdfg_genenerator.visit(program)
     if sdfg is None:
         raise RuntimeError(f"Visit failed for program {program.id}.")
-    elif tmps:
-        # This pass is needed to avoid transformation errors in SDFG inlining, because temporaries are using offsets
-        sdfg.apply_transformations_repeated(RefineNestedAccess)
 
     for nested_sdfg in sdfg.all_sdfgs_recursive():
         if not nested_sdfg.debuginfo:
diff --git a/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_sdfg.py b/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_sdfg.py
index 05a10444..ed71522d 100644
--- a/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_sdfg.py
+++ b/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_sdfg.py
@@ -11,6 +11,7 @@
 # distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
 #
 # SPDX-License-Identifier: GPL-3.0-or-later
+import warnings
 from typing import Any, Mapping, Optional, Sequence, cast
 
 import dace
@@ -104,7 +105,7 @@ def _make_array_shape_and_strides(
     dims: Sequence[Dimension],
     neighbor_tables: Mapping[str, NeighborTable],
     sort_dims: bool,
-) -> tuple[list[dace.symbol], list[dace.symbol], list[dace.symbol]]:
+) -> tuple[list[dace.symbol], list[dace.symbol]]:
     """
     Parse field dimensions and allocate symbols for array shape and strides.
 
@@ -127,9 +128,8 @@ def _make_array_shape_and_strides(
         )
         for i, dim in sorted_dims
     ]
-    offset = [dace.symbol(unique_name(f"{name}_offset{i}"), dtype) for i, _ in sorted_dims]
     strides = [dace.symbol(unique_name(f"{name}_stride{i}"), dtype) for i, _ in sorted_dims]
-    return shape, offset, strides
+    return shape, strides
 
 
 def _check_no_lifts(node: itir.StencilClosure):
@@ -177,11 +177,10 @@ class ItirToSDFG(eve.NodeVisitor):
         name: str,
         type_: ts.TypeSpec,
         neighbor_tables: Mapping[str, NeighborTable],
-        has_offset: bool = True,
         sort_dimensions: bool = True,
     ):
         if isinstance(type_, ts.FieldType):
-            shape, offset, strides = _make_array_shape_and_strides(
+            shape, strides = _make_array_shape_and_strides(
                 name, type_.dims, neighbor_tables, sort_dimensions
             )
             dtype = as_dace_type(type_.dtype)
@@ -189,7 +188,6 @@ class ItirToSDFG(eve.NodeVisitor):
                 name,
                 shape=shape,
                 strides=strides,
-                offset=(offset if has_offset else None),
                 dtype=dtype,
             )
 
@@ -249,22 +247,27 @@ class ItirToSDFG(eve.NodeVisitor):
             # Another option, in the future, is to use symbolic strides and apply auto-tuning or some heuristics
             # to assign optimal stride values.
             tmp_shape, _ = new_array_symbols(tmp_name, len(dims))
-            tmp_offset = [
-                dace.symbol(unique_name(f"{tmp_name}_offset{i}")) for i in range(len(dims))
-            ]
             _, tmp_array = program_sdfg.add_array(
-                tmp_name, tmp_shape, as_dace_type(type_.dtype), offset=tmp_offset, transient=True
+                tmp_name, tmp_shape, as_dace_type(type_.dtype), transient=True
             )
 
             # Loop through all dimensions to visit the symbolic expressions for array shape and offset.
             # These expressions are later mapped to interstate symbols.
-            for (_, (begin, end)), offset_sym, shape_sym in zip(
+            for (_, (begin, end)), shape_sym in zip(
                 tmp_domain,
-                tmp_array.offset,
                 tmp_array.shape,
             ):
-                tmp_symbols[str(offset_sym)] = f"0 - {begin.value}"
-                tmp_symbols[str(shape_sym)] = f"{end.value} - {begin.value}"
+                """
+                The temporary field has a dimension range defined by `begin` and `end` values.
+                Therefore, the actual size is given by the difference `end.value - begin.value`.
+                Instead of allocating the actual size, we allocate space to enable indexing from 0
+                because we want to avoid using dace array offsets (which will be deprecated soon).
+                The result should still be valid, but the stencil will be using only a subset
+                of the array.
+                """
+                if not (isinstance(begin, SymbolExpr) and begin.value == "0"):
+                    warnings.warn(f"Domain start offset for temporary {tmp_name} is ignored.")
+                tmp_symbols[str(shape_sym)] = end.value
 
         return tmp_symbols
 
@@ -319,7 +322,6 @@ class ItirToSDFG(eve.NodeVisitor):
                 connectivity_identifier(offset),
                 type_,
                 neighbor_tables,
-                has_offset=False,
                 sort_dimensions=False,
             )
 
@@ -663,7 +665,6 @@ class ItirToSDFG(eve.NodeVisitor):
             output_name,
             shape=(array_table[node.output.id].shape[scan_dim_index],),
             strides=(array_table[node.output.id].strides[scan_dim_index],),
-            offset=(array_table[node.output.id].offset[scan_dim_index],),
             dtype=array_table[node.output.id].dtype,
         )
 
@@ -770,7 +771,7 @@ class ItirToSDFG(eve.NodeVisitor):
 
     def _visit_domain(
         self, node: itir.FunCall, context: Context
-    ) -> tuple[tuple[str, tuple[ValueExpr, ValueExpr]], ...]:
+    ) -> tuple[tuple[str, tuple[SymbolExpr | ValueExpr, SymbolExpr | ValueExpr]], ...]:
         assert isinstance(node.fun, itir.SymRef)
         assert node.fun.id == "cartesian_domain" or node.fun.id == "unstructured_domain"
 
diff --git a/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_tasklet.py b/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_tasklet.py
index 3a33ee1e..6ab371bf 100644
--- a/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_tasklet.py
+++ b/src/gt4py/next/program_processors/runners/dace_iterator/itir_to_tasklet.py
@@ -11,6 +11,7 @@
 # distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
 #
 # SPDX-License-Identifier: GPL-3.0-or-later
+import copy
 import dataclasses
 import itertools
 from collections.abc import Sequence
@@ -22,7 +23,7 @@ import numpy as np
 import gt4py.eve.codegen
 from gt4py import eve
 from gt4py.next import Dimension, StridedNeighborOffsetProvider, type_inference as next_typing
-from gt4py.next.common import SKIP_VALUE as neighbor_skip_value
+from gt4py.next.common import _DEFAULT_SKIP_VALUE as neighbor_skip_value
 from gt4py.next.iterator import ir as itir, type_inference as itir_typing
 from gt4py.next.iterator.embedded import NeighborTableOffsetProvider
 from gt4py.next.iterator.ir import FunCall, Lambda
@@ -566,37 +567,120 @@ def builtin_can_deref(
 def builtin_if(
     transformer: "PythonTaskletCodegen", node: itir.Expr, node_args: list[itir.Expr]
 ) -> list[ValueExpr]:
-    di = dace_debuginfo(node, transformer.context.body.debuginfo)
-    args = transformer.visit(node_args)
-    assert len(args) == 3
-    if_node = args[0][0] if isinstance(args[0], list) else args[0]
-
-    # the argument could be a list of elements on each branch representing the result of `make_tuple`
-    # however, the normal case is to find one value expression
-    assert len(args[1]) == len(args[2])
-    if_expr_args = [
-        (a[0] if isinstance(a, list) else a, b[0] if isinstance(b, list) else b)
-        for a, b in zip(args[1], args[2])
-    ]
-
-    # in case of tuple arguments, generate one if-tasklet for each element of the output tuple
-    if_expr_values = []
-    for a, b in if_expr_args:
-        assert a.dtype == b.dtype
-        expr_args = [
-            (arg, f"{arg.value.data}_v")
-            for arg in (if_node, a, b)
-            if not isinstance(arg, SymbolExpr)
-        ]
-        internals = [
-            arg.value if isinstance(arg, SymbolExpr) else f"{arg.value.data}_v"
-            for arg in (if_node, a, b)
-        ]
-        expr = "({1} if {0} else {2})".format(*internals)
-        if_expr = transformer.add_expr_tasklet(expr_args, expr, a.dtype, "if", dace_debuginfo=di)
-        if_expr_values.append(if_expr[0])
+    assert len(node_args) == 3
+    sdfg = transformer.context.body
+    current_state = transformer.context.state
+    is_start_state = sdfg.start_block == current_state
+
+    # build an empty state to join true and false branches
+    join_state = sdfg.add_state_before(current_state, "join")
+
+    def build_if_state(arg, state):
+        symbol_map = copy.deepcopy(transformer.context.symbol_map)
+        node_context = Context(sdfg, state, symbol_map)
+        node_taskgen = PythonTaskletCodegen(
+            transformer.offset_provider, node_context, transformer.node_types
+        )
+        return node_taskgen.visit(arg)
+
+    # represent the if-statement condition as a tasklet inside an `if_statement` state preceding `join` state
+    stmt_state = sdfg.add_state_before(join_state, "if_statement", is_start_state)
+    stmt_node = build_if_state(node_args[0], stmt_state)[0]
+    assert isinstance(stmt_node, ValueExpr)
+    assert stmt_node.dtype == dace.dtypes.bool
+    assert sdfg.arrays[stmt_node.value.data].shape == (1,)
+
+    # visit true and false branches (here called `tbr` and `fbr`) as separate states, following `if_statement` state
+    tbr_state = sdfg.add_state("true_branch")
+    sdfg.add_edge(
+        stmt_state, tbr_state, dace.InterstateEdge(condition=f"{stmt_node.value.data} == True")
+    )
+    sdfg.add_edge(tbr_state, join_state, dace.InterstateEdge())
+    tbr_values = build_if_state(node_args[1], tbr_state)
+    #
+    fbr_state = sdfg.add_state("false_branch")
+    sdfg.add_edge(
+        stmt_state, fbr_state, dace.InterstateEdge(condition=f"{stmt_node.value.data} == False")
+    )
+    sdfg.add_edge(fbr_state, join_state, dace.InterstateEdge())
+    fbr_values = build_if_state(node_args[2], fbr_state)
+
+    assert isinstance(stmt_node, ValueExpr)
+    assert stmt_node.dtype == dace.dtypes.bool
+    # make the result of the if-statement evaluation available inside current state
+    ctx_stmt_node = ValueExpr(current_state.add_access(stmt_node.value.data), stmt_node.dtype)
+
+    # we distinguish between select if-statements, where both true and false branches are symbolic expressions,
+    # and therefore do not require exclusive branch execution, and regular if-statements where at least one branch
+    # is a value expression, which has to be evaluated at runtime with conditional state transition
+    result_values = []
+    assert len(tbr_values) == len(fbr_values)
+    for tbr_value, fbr_value in zip(tbr_values, fbr_values):
+        assert isinstance(tbr_value, (SymbolExpr, ValueExpr))
+        assert isinstance(fbr_value, (SymbolExpr, ValueExpr))
+        assert tbr_value.dtype == fbr_value.dtype
+
+        if all(isinstance(x, SymbolExpr) for x in (tbr_value, fbr_value)):
+            # both branches return symbolic expressions, therefore the if-node can be translated
+            # to a select-tasklet inside current state
+            # TODO: use select-memlet when it becomes available in dace
+            code = f"{tbr_value.value} if _cond else {fbr_value.value}"
+            if_expr = transformer.add_expr_tasklet(
+                [(ctx_stmt_node, "_cond")], code, tbr_value.dtype, "if_select"
+            )[0]
+            result_values.append(if_expr)
+        else:
+            # at least one of the two branches contains a value expression, which should be evaluated
+            # only if the corresponding true/false condition is satisfied
+            desc = sdfg.arrays[
+                tbr_value.value.data if isinstance(tbr_value, ValueExpr) else fbr_value.value.data
+            ]
+            var = unique_var_name()
+            if isinstance(desc, dace.data.Scalar):
+                sdfg.add_scalar(var, desc.dtype, transient=True)
+            else:
+                sdfg.add_array(var, desc.shape, desc.dtype, transient=True)
+
+            # write result to transient data container and access it in the original state
+            for state, expr in [(tbr_state, tbr_value), (fbr_state, fbr_value)]:
+                val_node = state.add_access(var)
+                if isinstance(expr, ValueExpr):
+                    state.add_nedge(
+                        expr.value, val_node, dace.Memlet.from_array(expr.value.data, desc)
+                    )
+                else:
+                    assert desc.shape == (1,)
+                    state.add_edge(
+                        state.add_tasklet("write_symbol", {}, {"_out"}, f"_out = {expr.value}"),
+                        "_out",
+                        val_node,
+                        None,
+                        dace.Memlet(var, "0"),
+                    )
+            result_values.append(ValueExpr(current_state.add_access(var), desc.dtype))
+
+    if tbr_state.is_empty() and fbr_state.is_empty():
+        # if all branches are symbolic expressions, the true/false and join states can be removed
+        # as well as the conditional state transition
+        sdfg.remove_nodes_from([join_state, tbr_state, fbr_state])
+        sdfg.add_edge(stmt_state, current_state, dace.InterstateEdge())
+    elif tbr_state.is_empty():
+        # use direct edge from if-statement to join state for true branch
+        tbr_condition = sdfg.edges_between(stmt_state, tbr_state)[0].condition
+        sdfg.edges_between(stmt_state, join_state)[0].contition = tbr_condition
+        sdfg.remove_node(tbr_state)
+    elif fbr_state.is_empty():
+        # use direct edge from if-statement to join state for false branch
+        fbr_condition = sdfg.edges_between(stmt_state, fbr_state)[0].condition
+        sdfg.edges_between(stmt_state, join_state)[0].contition = fbr_condition
+        sdfg.remove_node(fbr_state)
+    else:
+        # remove direct edge from if-statement to join state
+        sdfg.remove_edge(sdfg.edges_between(stmt_state, join_state)[0])
+        # the if-statement condition is not used in current state
+        current_state.remove_node(ctx_stmt_node.value)
 
-    return if_expr_values
+    return result_values
 
 
 def builtin_list_get(
diff --git a/src/gt4py/next/program_processors/runners/gtfn.py b/src/gt4py/next/program_processors/runners/gtfn.py
index 04af4a52..4a65f6d0 100644
--- a/src/gt4py/next/program_processors/runners/gtfn.py
+++ b/src/gt4py/next/program_processors/runners/gtfn.py
@@ -23,7 +23,8 @@ import gt4py._core.definitions as core_defs
 import gt4py.next.allocators as next_allocators
 from gt4py.eve.utils import content_hash
 from gt4py.next import common, config
-from gt4py.next.iterator.transforms import LiftMode, global_tmps
+from gt4py.next.iterator import transforms
+from gt4py.next.iterator.transforms import global_tmps
 from gt4py.next.otf import recipes, stages, workflow
 from gt4py.next.otf.binding import nanobind
 from gt4py.next.otf.compilation import compiler
@@ -150,6 +151,7 @@ class GTFNBackendFactory(factory.Factory):
     class Params:
         name_device = "cpu"
         name_cached = ""
+        name_temps = ""
         name_postfix = ""
         gpu = factory.Trait(
             allocator=next_allocators.StandardGPUFieldBufferAllocator(),
@@ -165,13 +167,18 @@ class GTFNBackendFactory(factory.Factory):
             ),
             name_cached="_cached",
         )
+        use_temporaries = factory.Trait(
+            otf_workflow__translation__lift_mode=transforms.LiftMode.USE_TEMPORARIES,
+            otf_workflow__translation__temporary_extraction_heuristics=global_tmps.SimpleTemporaryExtractionHeuristics,
+            name_temps="_with_temporaries",
+        )
         device_type = core_defs.DeviceType.CPU
         hash_function = compilation_hash
         otf_workflow = factory.SubFactory(
             GTFNCompileWorkflowFactory, device_type=factory.SelfAttribute("..device_type")
         )
         name = factory.LazyAttribute(
-            lambda o: f"run_gtfn_{o.name_device}{o.name_cached}{o.name_postfix}"
+            lambda o: f"run_gtfn_{o.name_device}{o.name_temps}{o.name_cached}{o.name_postfix}"
         )
 
     executor = factory.LazyAttribute(
@@ -189,11 +196,7 @@ run_gtfn_imperative = GTFNBackendFactory(
 
 run_gtfn_cached = GTFNBackendFactory(cached=True)
 
-run_gtfn_with_temporaries = GTFNBackendFactory(
-    name_postfix="_with_temporaries",
-    otf_workflow__translation__lift_mode=LiftMode.FORCE_TEMPORARIES,
-    otf_workflow__translation__temporary_extraction_heuristics=global_tmps.SimpleTemporaryExtractionHeuristics,
-)
+run_gtfn_with_temporaries = GTFNBackendFactory(use_temporaries=True)
 
 run_gtfn_gpu = GTFNBackendFactory(gpu=True)
 


Test patch:
diff --git a/min-extra-requirements-test.txt b/min-extra-requirements-test.txt
index 6579db0f..37d48499 100644
--- a/min-extra-requirements-test.txt
+++ b/min-extra-requirements-test.txt
@@ -64,7 +64,6 @@ dace==0.15.1
 darglint==1.6
 deepdiff==5.6.0
 devtools==0.6
-factory-boy==3.1
 factory-boy==3.3.0
 flake8-bugbear==20.11.1
 flake8-builtins==1.5.3
diff --git a/min-requirements-test.txt b/min-requirements-test.txt
index 6a05dbb9..c67125df 100644
--- a/min-requirements-test.txt
+++ b/min-requirements-test.txt
@@ -60,7 +60,6 @@ cytoolz==0.12.1
 darglint==1.6
 deepdiff==5.6.0
 devtools==0.6
-factory-boy==3.1
 factory-boy==3.3.0
 flake8-bugbear==20.11.1
 flake8-builtins==1.5.3
diff --git a/tests/next_tests/definitions.py b/tests/next_tests/definitions.py
index 56b220e0..a8ba72f3 100644
--- a/tests/next_tests/definitions.py
+++ b/tests/next_tests/definitions.py
@@ -170,7 +170,6 @@ EMBEDDED_SKIP_LIST = [
         XFAIL,
         UNSUPPORTED_MESSAGE,
     ),  # we can't extract the field type from scan args
-    (USES_MESH_WITH_SKIP_VALUES, XFAIL, UNSUPPORTED_MESSAGE),
 ]
 GTFN_SKIP_TEST_LIST = COMMON_SKIP_TEST_LIST + [
     # floordiv not yet supported, see https://github.com/GridTools/gt4py/issues/1136
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/ffront_test_utils.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/ffront_test_utils.py
index d8c46960..aca601d7 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/ffront_test_utils.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/ffront_test_utils.py
@@ -239,12 +239,12 @@ def skip_value_mesh() -> MeshDescriptor:
 
     v2e_arr = np.array(
         [
-            [1, 8, 7, 0, -1],
-            [2, 8, 1, -1, -1],
-            [3, 9, 8, 2, -1],
-            [4, 10, 3, -1, -1],
-            [5, 11, 4, -1, -1],
-            [0, 6, 4, -1, -1],
+            [1, 8, 7, 0, common._DEFAULT_SKIP_VALUE],
+            [2, 8, 1, common._DEFAULT_SKIP_VALUE, common._DEFAULT_SKIP_VALUE],
+            [3, 9, 8, 2, common._DEFAULT_SKIP_VALUE],
+            [4, 10, 3, common._DEFAULT_SKIP_VALUE, common._DEFAULT_SKIP_VALUE],
+            [5, 11, 4, common._DEFAULT_SKIP_VALUE, common._DEFAULT_SKIP_VALUE],
+            [0, 6, 4, common._DEFAULT_SKIP_VALUE, common._DEFAULT_SKIP_VALUE],
             [6, 7, 9, 10, 11],
         ],
         dtype=gtx.IndexType,
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py
index e499f83f..5ded38ab 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_execution.py
@@ -530,7 +530,7 @@ def test_nested_reduction(unstructured_case):
                 initial=0,
             )[unstructured_case.offset_provider["V2E"].table],
             axis=1,
-            where=unstructured_case.offset_provider["V2E"].table != common.SKIP_VALUE,
+            where=unstructured_case.offset_provider["V2E"].table != common._DEFAULT_SKIP_VALUE,
         ),
         comparison=lambda a, tmp_2: np.all(a == tmp_2),
     )
@@ -595,7 +595,7 @@ def test_tuple_with_local_field_in_reduction_shifted(unstructured_case):
             e[v2e.table] + np.tile(v, (v2e.max_neighbors, 1)).T,
             axis=1,
             initial=0,
-            where=v2e.table != common.SKIP_VALUE,
+            where=v2e.table != common._DEFAULT_SKIP_VALUE,
         )[unstructured_case.offset_provider["E2V"].table[:, 0]],
     )
 
@@ -735,7 +735,7 @@ def test_ternary_builtin_neighbor_sum(unstructured_case):
                 b[v2e_table],
                 axis=1,
                 initial=0,
-                where=v2e_table != common.SKIP_VALUE,
+                where=v2e_table != common._DEFAULT_SKIP_VALUE,
             )
         ),
     )
@@ -1079,9 +1079,9 @@ def test_where_k_offset(cartesian_case):
     )()
     out = cases.allocate(cartesian_case, fieldop_where_k_offset, "inp")()
 
-    ref = np.where(k_index.asnumpy() > 0, np.roll(inp.asnumpy(), 1, axis=1), 2)
+    ref = np.where(k_index.asnumpy() > 0, np.roll(inp.asnumpy(), 1, axis=1), out.asnumpy())
 
-    cases.verify(cartesian_case, prog, inp, k_index, out=out[:, 1:], ref=ref[:, 1:])
+    cases.verify(cartesian_case, prog, inp, k_index, out=out, ref=ref)
 
 
 def test_undefined_symbols(cartesian_case):
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_external_local_field.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_external_local_field.py
index 569d7b56..16d40384 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_external_local_field.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_external_local_field.py
@@ -54,7 +54,7 @@ def test_external_local_field(unstructured_case):
             v2e_table,
             axis=1,
             initial=0,
-            where=v2e_table != common.SKIP_VALUE,
+            where=v2e_table != common._DEFAULT_SKIP_VALUE,
         ),
     )
 
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_gt4py_builtins.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_gt4py_builtins.py
index 05824fa7..583246f5 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_gt4py_builtins.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_gt4py_builtins.py
@@ -63,7 +63,7 @@ def test_maxover_execution_(unstructured_case, strategy):
         inp.asnumpy()[v2e_table],
         axis=1,
         initial=np.min(inp.asnumpy()),
-        where=v2e_table != common.SKIP_VALUE,
+        where=v2e_table != common._DEFAULT_SKIP_VALUE,
     )
     cases.verify(unstructured_case, testee, inp, ref=ref, out=out)
 
@@ -83,31 +83,65 @@ def test_minover_execution(unstructured_case):
             edge_f[v2e_table],
             axis=1,
             initial=np.max(edge_f),
-            where=v2e_table != common.SKIP_VALUE,
+            where=v2e_table != common._DEFAULT_SKIP_VALUE,
         ),
     )
 
 
-@pytest.mark.uses_unstructured_shift
-def test_reduction_execution(unstructured_case):
-    @gtx.field_operator
-    def reduction(edge_f: cases.EField) -> cases.VField:
-        return neighbor_sum(edge_f(V2E), axis=V2EDim)
+@gtx.field_operator
+def reduction_e_field(edge_f: cases.EField) -> cases.VField:
+    return neighbor_sum(edge_f(V2E), axis=V2EDim)
 
-    @gtx.program
-    def fencil(edge_f: cases.EField, out: cases.VField):
-        reduction(edge_f, out=out)
 
+@gtx.field_operator
+def reduction_ek_field(
+    edge_f: common.Field[[Edge, KDim], np.int32]
+) -> common.Field[[Vertex, KDim], np.int32]:
+    return neighbor_sum(edge_f(V2E), axis=V2EDim)
+
+
+@gtx.field_operator
+def reduction_ke_field(
+    edge_f: common.Field[[KDim, Edge], np.int32]
+) -> common.Field[[KDim, Vertex], np.int32]:
+    return neighbor_sum(edge_f(V2E), axis=V2EDim)
+
+
+@pytest.mark.uses_unstructured_shift
+@pytest.mark.parametrize(
+    "fop", [reduction_e_field, reduction_ek_field, reduction_ke_field], ids=lambda fop: fop.__name__
+)
+def test_neighbor_sum(unstructured_case, fop):
     v2e_table = unstructured_case.offset_provider["V2E"].table
-    cases.verify_with_default_data(
+
+    edge_f = cases.allocate(unstructured_case, fop, "edge_f")()
+
+    local_dim_idx = edge_f.domain.dims.index(Edge) + 1
+    adv_indexing = tuple(
+        slice(None) if dim is not Edge else v2e_table for dim in edge_f.domain.dims
+    )
+
+    broadcast_slice = []
+    for dim in edge_f.domain.dims:
+        if dim is Edge:
+            broadcast_slice.append(slice(None))
+            broadcast_slice.append(slice(None))
+        else:
+            broadcast_slice.append(None)
+
+    broadcasted_table = v2e_table[tuple(broadcast_slice)]
+    ref = np.sum(
+        edge_f.asnumpy()[adv_indexing],
+        axis=local_dim_idx,
+        initial=0,
+        where=broadcasted_table != common._DEFAULT_SKIP_VALUE,
+    )
+    cases.verify(
         unstructured_case,
-        fencil,
-        ref=lambda edge_f: np.sum(
-            edge_f[v2e_table],
-            axis=1,
-            initial=0,
-            where=v2e_table != common.SKIP_VALUE,
-        ),
+        fop,
+        edge_f,
+        out=cases.allocate(unstructured_case, fop, cases.RETURN)(),
+        ref=ref,
     )
 
 
@@ -143,7 +177,7 @@ def test_reduction_execution_with_offset(unstructured_case):
             field.asnumpy()[:, 1][v2e_table],
             axis=1,
             initial=0,
-            where=v2e_table != common.SKIP_VALUE,
+            where=v2e_table != common._DEFAULT_SKIP_VALUE,
         ).reshape(out.shape),
         offset_provider=unstructured_case.offset_provider | {"Koff": KDim},
     )
@@ -171,7 +205,7 @@ def test_reduction_expression_in_call(unstructured_case):
             -edge_f[v2e_table] ** 2 * 2,
             axis=1,
             initial=0,
-            where=v2e_table != common.SKIP_VALUE,
+            where=v2e_table != common._DEFAULT_SKIP_VALUE,
         ),
     )
 
@@ -190,7 +224,7 @@ def test_reduction_with_common_expression(unstructured_case):
             flux[v2e_table] * 2,
             axis=1,
             initial=0,
-            where=v2e_table != common.SKIP_VALUE,
+            where=v2e_table != common._DEFAULT_SKIP_VALUE,
         ),
     )
 
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_temporaries_with_sizes.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_temporaries_with_sizes.py
index 13d8f771..a264843f 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_temporaries_with_sizes.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_temporaries_with_sizes.py
@@ -111,7 +111,7 @@ def test_verification(testee, run_gtfn_with_temporaries_and_symbolic_sizes, mesh
 def test_temporary_symbols(testee, mesh_descriptor):
     itir_with_tmp = apply_common_transforms(
         testee.itir,
-        lift_mode=LiftMode.FORCE_TEMPORARIES,
+        lift_mode=LiftMode.USE_TEMPORARIES,
         offset_provider=mesh_descriptor.offset_provider,
     )
 
diff --git a/tests/next_tests/integration_tests/multi_feature_tests/ffront_tests/test_ffront_fvm_nabla.py b/tests/next_tests/integration_tests/multi_feature_tests/ffront_tests/test_ffront_fvm_nabla.py
new file mode 100644
index 00000000..aeed607b
--- /dev/null
+++ b/tests/next_tests/integration_tests/multi_feature_tests/ffront_tests/test_ffront_fvm_nabla.py
@@ -0,0 +1,124 @@
+# 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 typing import Tuple
+
+import numpy as np
+import pytest
+
+
+pytest.importorskip("atlas4py")  # isort: skip
+
+from gt4py import next as gtx
+from gt4py.next import allocators, neighbor_sum
+from gt4py.next.iterator import atlas_utils
+from gt4py.next.program_processors import processor_interface as ppi
+
+from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import (
+    exec_alloc_descriptor,
+)
+from next_tests.integration_tests.multi_feature_tests.fvm_nabla_setup import (
+    assert_close,
+    nabla_setup,
+)
+
+
+Vertex = gtx.Dimension("Vertex")
+Edge = gtx.Dimension("Edge")
+V2EDim = gtx.Dimension("V2E", kind=gtx.DimensionKind.LOCAL)
+E2VDim = gtx.Dimension("E2V", kind=gtx.DimensionKind.LOCAL)
+
+V2E = gtx.FieldOffset("V2E", source=Edge, target=(Vertex, V2EDim))
+E2V = gtx.FieldOffset("E2V", source=Vertex, target=(Edge, E2VDim))
+
+
+@gtx.field_operator
+def compute_zavgS(
+    pp: gtx.Field[[Vertex], float], S_M: gtx.Field[[Edge], float]
+) -> gtx.Field[[Edge], float]:
+    zavg = 0.5 * (pp(E2V[0]) + pp(E2V[1]))
+    return S_M * zavg
+
+
+@gtx.field_operator
+def compute_pnabla(
+    pp: gtx.Field[[Vertex], float],
+    S_M: gtx.Field[[Edge], float],
+    sign: gtx.Field[[Vertex, V2EDim], float],
+    vol: gtx.Field[[Vertex], float],
+) -> gtx.Field[[Vertex], float]:
+    zavgS = compute_zavgS(pp, S_M)
+    pnabla_M = neighbor_sum(zavgS(V2E) * sign, axis=V2EDim)
+    return pnabla_M / vol
+
+
+@gtx.field_operator
+def pnabla(
+    pp: gtx.Field[[Vertex], float],
+    S_M: Tuple[gtx.Field[[Edge], float], gtx.Field[[Edge], float]],
+    sign: gtx.Field[[Vertex, V2EDim], float],
+    vol: gtx.Field[[Vertex], float],
+) -> Tuple[gtx.Field[[Vertex], float], gtx.Field[[Vertex], float]]:
+    return compute_pnabla(pp, S_M[0], sign, vol), compute_pnabla(pp, S_M[1], sign, vol)
+
+
+def test_ffront_compute_zavgS(exec_alloc_descriptor):
+    executor, allocator = exec_alloc_descriptor.executor, exec_alloc_descriptor.allocator
+
+    setup = nabla_setup()
+
+    pp = gtx.as_field([Vertex], setup.input_field, allocator=allocator)
+    S_M = tuple(map(gtx.as_field.partial([Edge], allocator=allocator), setup.S_fields))
+
+    zavgS = gtx.zeros({Edge: setup.edges_size}, allocator=allocator)
+
+    e2v = gtx.NeighborTableOffsetProvider(
+        atlas_utils.AtlasTable(setup.edges2node_connectivity).asnumpy(), Edge, Vertex, 2, False
+    )
+
+    compute_zavgS.with_backend(executor)(pp, S_M[0], out=zavgS, offset_provider={"E2V": e2v})
+
+    assert_close(-199755464.25741270, np.min(zavgS.asnumpy()))
+    assert_close(388241977.58389181, np.max(zavgS.asnumpy()))
+
+
+def test_ffront_nabla(exec_alloc_descriptor):
+    executor, allocator = exec_alloc_descriptor.executor, exec_alloc_descriptor.allocator
+
+    setup = nabla_setup()
+
+    sign = gtx.as_field([Vertex, V2EDim], setup.sign_field, allocator=allocator)
+    pp = gtx.as_field([Vertex], setup.input_field, allocator=allocator)
+    S_M = tuple(map(gtx.as_field.partial([Edge], allocator=allocator), setup.S_fields))
+    vol = gtx.as_field([Vertex], setup.vol_field, allocator=allocator)
+
+    pnabla_MXX = gtx.zeros({Vertex: setup.nodes_size}, allocator=allocator)
+    pnabla_MYY = gtx.zeros({Vertex: setup.nodes_size}, allocator=allocator)
+
+    e2v = gtx.NeighborTableOffsetProvider(
+        atlas_utils.AtlasTable(setup.edges2node_connectivity).asnumpy(), Edge, Vertex, 2, False
+    )
+    v2e = gtx.NeighborTableOffsetProvider(
+        atlas_utils.AtlasTable(setup.nodes2edge_connectivity).asnumpy(), Vertex, Edge, 7
+    )
+
+    pnabla.with_backend(executor)(
+        pp, S_M, sign, vol, out=(pnabla_MXX, pnabla_MYY), offset_provider={"E2V": e2v, "V2E": v2e}
+    )
+
+    # TODO this check is not sensitive enough, need to implement a proper numpy reference!
+    assert_close(-3.5455427772566003e-003, np.min(pnabla_MXX.asnumpy()))
+    assert_close(3.5455427772565435e-003, np.max(pnabla_MXX.asnumpy()))
+    assert_close(-3.3540113705465301e-003, np.min(pnabla_MYY.asnumpy()))
+    assert_close(3.3540113705465301e-003, np.max(pnabla_MYY.asnumpy()))
diff --git a/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/fvm_nabla_setup.py b/tests/next_tests/integration_tests/multi_feature_tests/fvm_nabla_setup.py
similarity index 100%
rename from tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/fvm_nabla_setup.py
rename to tests/next_tests/integration_tests/multi_feature_tests/fvm_nabla_setup.py
diff --git a/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_fvm_nabla.py b/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_fvm_nabla.py
index e1d959ab..9ee364c0 100644
--- a/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_fvm_nabla.py
+++ b/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_fvm_nabla.py
@@ -38,7 +38,7 @@ from gt4py.next.iterator.builtins import (
 from gt4py.next.iterator.runtime import closure, fendef, fundef, offset
 from gt4py.next.iterator.transforms.pass_manager import LiftMode
 
-from next_tests.integration_tests.multi_feature_tests.iterator_tests.fvm_nabla_setup import (
+from next_tests.integration_tests.multi_feature_tests.fvm_nabla_setup import (
     assert_close,
     nabla_setup,
 )
diff --git a/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_vertical_advection.py b/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_vertical_advection.py
index f2a6505a..b28c98ab 100644
--- a/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_vertical_advection.py
+++ b/tests/next_tests/integration_tests/multi_feature_tests/iterator_tests/test_vertical_advection.py
@@ -127,10 +127,7 @@ def test_tridiag(fencil, tridiag_reference, program_processor, lift_mode):
         and lift_mode == LiftMode.FORCE_INLINE
     ):
         pytest.skip("gtfn does only support lifted scans when using temporaries")
-    if (
-        program_processor == gtfn.run_gtfn_with_temporaries
-        or lift_mode == LiftMode.FORCE_TEMPORARIES
-    ):
+    if program_processor == gtfn.run_gtfn_with_temporaries or lift_mode == LiftMode.USE_TEMPORARIES:
         pytest.xfail("tuple_get on columns not supported.")
     a, b, c, d, x = tridiag_reference
     shape = a.shape
diff --git a/tests/next_tests/unit_tests/conftest.py b/tests/next_tests/unit_tests/conftest.py
index d3f9bdb7..17418a9c 100644
--- a/tests/next_tests/unit_tests/conftest.py
+++ b/tests/next_tests/unit_tests/conftest.py
@@ -38,7 +38,7 @@ import next_tests
 @pytest.fixture(
     params=[
         transforms.LiftMode.FORCE_INLINE,
-        transforms.LiftMode.FORCE_TEMPORARIES,
+        transforms.LiftMode.USE_TEMPORARIES,
         transforms.LiftMode.SIMPLE_HEURISTIC,
     ],
     ids=lambda p: f"lift_mode={p.name}",
diff --git a/tests/next_tests/unit_tests/embedded_tests/test_common.py b/tests/next_tests/unit_tests/embedded_tests/test_common.py
index de511fda..91f15ee9 100644
--- a/tests/next_tests/unit_tests/embedded_tests/test_common.py
+++ b/tests/next_tests/unit_tests/embedded_tests/test_common.py
@@ -19,7 +19,12 @@ import pytest
 from gt4py.next import common
 from gt4py.next.common import UnitRange
 from gt4py.next.embedded import exceptions as embedded_exceptions
-from gt4py.next.embedded.common import _slice_range, iterate_domain, sub_domain
+from gt4py.next.embedded.common import (
+    _slice_range,
+    canonicalize_any_index_sequence,
+    iterate_domain,
+    sub_domain,
+)
 
 
 @pytest.mark.parametrize(
@@ -147,3 +152,31 @@ def test_iterate_domain():
     testee = list(iterate_domain(domain))
 
     assert testee == ref
+
+
+@pytest.mark.parametrize(
+    "slices, expected",
+    [
+        [slice(I(3), I(4)), ((I, common.UnitRange(3, 4)),)],
+        [
+            (slice(J(3), J(6)), slice(I(3), I(5))),
+            ((J, common.UnitRange(3, 6)), (I, common.UnitRange(3, 5))),
+        ],
+        [slice(I(1), J(7)), IndexError],
+        [
+            slice(I(1), None),
+            IndexError,
+        ],
+        [
+            slice(None, K(8)),
+            IndexError,
+        ],
+    ],
+)
+def test_slicing(slices, expected):
+    if expected is IndexError:
+        with pytest.raises(IndexError):
+            canonicalize_any_index_sequence(slices)
+    else:
+        testee = canonicalize_any_index_sequence(slices)
+        assert testee == expected
diff --git a/tests/next_tests/unit_tests/embedded_tests/test_nd_array_field.py b/tests/next_tests/unit_tests/embedded_tests/test_nd_array_field.py
index 79830a75..49f74a56 100644
--- a/tests/next_tests/unit_tests/embedded_tests/test_nd_array_field.py
+++ b/tests/next_tests/unit_tests/embedded_tests/test_nd_array_field.py
@@ -463,6 +463,49 @@ def test_absolute_indexing(domain_slice, expected_dimensions, expected_shape):
     assert indexed_field.domain.dims == expected_dimensions
 
 
+def test_absolute_indexing_dim_sliced():
+    domain = common.Domain(
+        dims=(IDim, JDim, KDim), ranges=(UnitRange(5, 10), UnitRange(5, 15), UnitRange(10, 25))
+    )
+    field = common._field(np.ones((5, 10, 15)), domain=domain)
+    indexed_field_1 = field[JDim(8) : JDim(10), IDim(5) : IDim(9)]
+    expected = field[(IDim, UnitRange(5, 9)), (JDim, UnitRange(8, 10))]
+
+    assert common.is_field(indexed_field_1)
+    assert indexed_field_1 == expected
+
+
+def test_absolute_indexing_dim_sliced_single_slice():
+    domain = common.Domain(
+        dims=(IDim, JDim, KDim), ranges=(UnitRange(5, 10), UnitRange(5, 15), UnitRange(10, 25))
+    )
+    field = common._field(np.ones((5, 10, 15)), domain=domain)
+    indexed_field_1 = field[KDim(11)]
+    indexed_field_2 = field[(KDim, 11)]
+
+    assert common.is_field(indexed_field_1)
+    assert indexed_field_1 == indexed_field_2
+
+
+def test_absolute_indexing_wrong_dim_sliced():
+    domain = common.Domain(
+        dims=(IDim, JDim, KDim), ranges=(UnitRange(5, 10), UnitRange(5, 15), UnitRange(10, 25))
+    )
+    field = common._field(np.ones((5, 10, 15)), domain=domain)
+
+    with pytest.raises(IndexError, match="Dimensions slicing mismatch between 'JDim' and 'IDim'."):
+        field[JDim(8) : IDim(10)]
+
+
+def test_absolute_indexing_empty_dim_sliced():
+    domain = common.Domain(
+        dims=(IDim, JDim, KDim), ranges=(UnitRange(5, 10), UnitRange(5, 15), UnitRange(10, 25))
+    )
+    field = common._field(np.ones((5, 10, 15)), domain=domain)
+    with pytest.raises(IndexError, match="Lower bound needs to be specified"):
+        field[: IDim(10)]
+
+
 def test_absolute_indexing_value_return():
     domain = common.Domain(dims=(IDim, JDim), ranges=(UnitRange(10, 20), UnitRange(5, 15)))
     field = common._field(np.reshape(np.arange(100, dtype=np.int32), (10, 10)), domain=domain)
@@ -669,60 +712,60 @@ def test_connectivity_field_inverse_image():
 
 def test_connectivity_field_inverse_image_2d_domain():
     V = Dimension("V")
-    E = Dimension("E")
-    E2V = Dimension("E2V")
+    C = Dimension("C")
+    C2V = Dimension("C2V")
 
     V_START, V_STOP = 0, 3
-    E_START, E_STOP = 0, 3
-    E2V_START, E2V_STOP = 0, 3
+    C_START, C_STOP = 0, 3
+    C2V_START, C2V_STOP = 0, 3
 
-    e2v_conn = common._connectivity(
+    c2v_conn = common._connectivity(
         np.asarray([[0, 0, 2], [1, 1, 2], [2, 2, 2]]),
         domain=common.domain(
             [
-                common.named_range((E, (E_START, E_STOP))),
-                common.named_range((E2V, (E2V_START, E2V_STOP))),
+                common.named_range((C, (C_START, C_STOP))),
+                common.named_range((C2V, (C2V_START, C2V_STOP))),
             ]
         ),
         codomain=V,
     )
 
-    # e2c_conn:
-    #  ---E2V----
+    # c2v_conn:
+    #  ---C2V----
     #  |[[0 0 2]
-    #  E [1 1 2]
+    #  C [1 1 2]
     #  | [2 2 2]]
 
     # Test contiguous and non-contiguous ranges.
-    # For the 'e2c_conn' defined above, the only valid range including 2
+    # For the 'c2v_conn' defined above, the only valid range including 2
     # is [0, 3). Otherwise, the inverse image would be non-contiguous.
     image_range = UnitRange(V_START, V_STOP)
-    result = e2v_conn.inverse_image(image_range)
+    result = c2v_conn.inverse_image(image_range)
 
     assert len(result) == 2
-    assert result[0] == (E, UnitRange(E_START, E_STOP))
-    assert result[1] == (E2V, UnitRange(E2V_START, E2V_STOP))
+    assert result[0] == (C, UnitRange(C_START, C_STOP))
+    assert result[1] == (C2V, UnitRange(C2V_START, C2V_STOP))
 
-    result = e2v_conn.inverse_image(UnitRange(0, 2))
+    result = c2v_conn.inverse_image(UnitRange(0, 2))
     assert len(result) == 2
-    assert result[0] == (E, UnitRange(0, 2))
-    assert result[1] == (E2V, UnitRange(0, 2))
+    assert result[0] == (C, UnitRange(0, 2))
+    assert result[1] == (C2V, UnitRange(0, 2))
 
-    result = e2v_conn.inverse_image(UnitRange(0, 1))
+    result = c2v_conn.inverse_image(UnitRange(0, 1))
     assert len(result) == 2
-    assert result[0] == (E, UnitRange(0, 1))
-    assert result[1] == (E2V, UnitRange(0, 2))
+    assert result[0] == (C, UnitRange(0, 1))
+    assert result[1] == (C2V, UnitRange(0, 2))
 
-    result = e2v_conn.inverse_image(UnitRange(1, 2))
+    result = c2v_conn.inverse_image(UnitRange(1, 2))
     assert len(result) == 2
-    assert result[0] == (E, UnitRange(1, 2))
-    assert result[1] == (E2V, UnitRange(0, 2))
+    assert result[0] == (C, UnitRange(1, 2))
+    assert result[1] == (C2V, UnitRange(0, 2))
 
     with pytest.raises(ValueError, match="generates non-contiguous dimensions"):
-        result = e2v_conn.inverse_image(UnitRange(1, 3))
+        result = c2v_conn.inverse_image(UnitRange(1, 3))
 
     with pytest.raises(ValueError, match="generates non-contiguous dimensions"):
-        result = e2v_conn.inverse_image(UnitRange(2, 3))
+        result = c2v_conn.inverse_image(UnitRange(2, 3))
 
 
 def test_connectivity_field_inverse_image_non_contiguous():
@@ -746,3 +789,85 @@ def test_connectivity_field_inverse_image_non_contiguous():
 
     with pytest.raises(ValueError, match="generates non-contiguous dimensions"):
         e2v_conn.inverse_image(UnitRange(V_START, V_STOP))
+
+
+def test_connectivity_field_inverse_image_2d_domain_skip_values():
+    V = Dimension("V")
+    C = Dimension("C")
+    C2V = Dimension("C2V")
+
+    V_START, V_STOP = 0, 3
+    C_START, C_STOP = 0, 4
+    C2V_START, C2V_STOP = 0, 4
+
+    c2v_conn = common._connectivity(
+        np.asarray([[-1, 0, 2, -1], [1, 1, 2, 2], [2, 2, -1, -1], [-1, 2, -1, -1]]),
+        domain=common.domain(
+            [
+                common.named_range((C, (C_START, C_STOP))),
+                common.named_range((C2V, (C2V_START, C2V_STOP))),
+            ]
+        ),
+        codomain=V,
+        skip_value=-1,
+    )
+
+    # c2v_conn:
+    #  ---C2V---------
+    #  |[[-1  0  2 -1]
+    #  C [ 1  1  2  2]
+    #  | [ 2  2 -1 -1]
+    #  | [-1  2 -1 -1]]
+
+    image_range = UnitRange(V_START, V_STOP)
+    result = c2v_conn.inverse_image(image_range)
+
+    assert len(result) == 2
+    assert result[0] == (C, UnitRange(C_START, C_STOP))
+    assert result[1] == (C2V, UnitRange(C2V_START, C2V_STOP))
+
+    result = c2v_conn.inverse_image(UnitRange(0, 2))
+    assert len(result) == 2
+    assert result[0] == (C, UnitRange(0, 2))
+    assert result[1] == (C2V, UnitRange(0, 2))
+
+    result = c2v_conn.inverse_image(UnitRange(0, 1))
+    assert len(result) == 2
+    assert result[0] == (C, UnitRange(0, 1))
+    assert result[1] == (C2V, UnitRange(1, 2))
+
+    result = c2v_conn.inverse_image(UnitRange(1, 2))
+    assert len(result) == 2
+    assert result[0] == (C, UnitRange(1, 2))
+    assert result[1] == (C2V, UnitRange(0, 2))
+
+    with pytest.raises(ValueError, match="generates non-contiguous dimensions"):
+        result = c2v_conn.inverse_image(UnitRange(1, 3))
+
+    with pytest.raises(ValueError, match="generates non-contiguous dimensions"):
+        result = c2v_conn.inverse_image(UnitRange(2, 3))
+
+
+@pytest.mark.parametrize(
+    "index_array, expected",
+    [
+        ([0, 0, 1], [(0, 2)]),
+        ([0, 1, 0], None),
+        ([0, -1, 0], [(0, 3)]),
+        ([[1, 1, 1], [1, 0, 0]], [(1, 2), (1, 3)]),
+        (
+            [[1, 0, -1], [1, 0, 0]],
+            [(0, 2), (1, 3)],
+        ),
+    ],
+)
+def test_hypercube(index_array, expected):
+    index_array = np.asarray(index_array)
+    image_range = common.UnitRange(0, 1)
+    skip_value = -1
+
+    expected = [common.unit_range(e) for e in expected] if expected is not None else None
+
+    result = nd_array_field._hypercube(index_array, image_range, np, skip_value)
+
+    assert result == expected
diff --git a/tests/next_tests/unit_tests/errors_tests/test_excepthook.py b/tests/next_tests/unit_tests/errors_tests/test_excepthook.py
index 526844d7..d9f29b99 100644
--- a/tests/next_tests/unit_tests/errors_tests/test_excepthook.py
+++ b/tests/next_tests/unit_tests/errors_tests/test_excepthook.py
@@ -12,11 +12,8 @@
 #
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-import os
-
 from gt4py import eve
 from gt4py.next import errors
-from gt4py.next.errors import excepthook
 
 
 def test_format_uncaught_error():
@@ -35,23 +32,3 @@ def test_format_uncaught_error():
         assert str_usermode.find("Traceback") < 0
         assert str_usermode.find("cause") < 0
         assert str_usermode.find("ValueError") < 0
-
-
-def test_get_verbose_exceptions():
-    env_var_name = "GT4PY_VERBOSE_EXCEPTIONS"
-
-    # Make sure to save and restore the environment variable, we don't want to
-    # affect other tests running in the same process.
-    saved = os.environ.get(env_var_name, None)
-    try:
-        os.environ[env_var_name] = "False"
-        assert excepthook._get_verbose_exceptions_envvar() is False
-        os.environ[env_var_name] = "True"
-        assert excepthook._get_verbose_exceptions_envvar() is True
-        os.environ[env_var_name] = "invalid value"  # Should emit a warning too
-        assert excepthook._get_verbose_exceptions_envvar() is False
-        del os.environ[env_var_name]
-        assert excepthook._get_verbose_exceptions_envvar() is False
-    finally:
-        if saved is not None:
-            os.environ[env_var_name] = saved
diff --git a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/test_gtfn.py b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/test_gtfn.py
index 897ddcfc..755cddcf 100644
--- a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/test_gtfn.py
+++ b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/test_gtfn.py
@@ -26,11 +26,13 @@ other variables are computed at import time based on them.
 
 import gt4py._core.definitions as core_defs
 from gt4py.next import allocators, config
+from gt4py.next.iterator import transforms
+from gt4py.next.iterator.transforms import global_tmps
 from gt4py.next.otf import workflow
 from gt4py.next.program_processors.runners import gtfn
 
 
-def test_backend_factory_set_device():
+def test_backend_factory_trait_device():
     cpu_version = gtfn.GTFNBackendFactory(gpu=False, cached=False)
     gpu_version = gtfn.GTFNBackendFactory(gpu=True, cached=False)
 
@@ -51,12 +53,29 @@ def test_backend_factory_set_device():
     assert allocators.is_field_allocator_for(gpu_version.allocator, core_defs.DeviceType.CUDA)
 
 
-def test_backend_factory_set_cached():
+def test_backend_factory_trait_cached():
     cached_version = gtfn.GTFNBackendFactory(gpu=False, cached=True)
     assert isinstance(cached_version.executor.otf_workflow, workflow.CachedStep)
     assert cached_version.executor.__name__ == "run_gtfn_cpu_cached"
 
 
+def test_backend_factory_trait_temporaries():
+    inline_version = gtfn.GTFNBackendFactory(cached=False)
+    temps_version = gtfn.GTFNBackendFactory(cached=False, use_temporaries=True)
+
+    assert inline_version.executor.otf_workflow.translation.lift_mode is None
+    assert (
+        temps_version.executor.otf_workflow.translation.lift_mode
+        is transforms.LiftMode.USE_TEMPORARIES
+    )
+
+    assert inline_version.executor.otf_workflow.translation.temporary_extraction_heuristics is None
+    assert (
+        temps_version.executor.otf_workflow.translation.temporary_extraction_heuristics
+        is global_tmps.SimpleTemporaryExtractionHeuristics
+    )
+
+
 def test_backend_factory_build_cache_config(monkeypatch):
     monkeypatch.setattr(config, "BUILD_CACHE_LIFETIME", config.BuildCacheLifetime.SESSION)
     session_version = gtfn.GTFNBackendFactory()
diff --git a/tests/next_tests/unit_tests/test_config.py b/tests/next_tests/unit_tests/test_config.py
new file mode 100644
index 00000000..0b906207
--- /dev/null
+++ b/tests/next_tests/unit_tests/test_config.py
@@ -0,0 +1,52 @@
+# 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
+
+import os
+
+import pytest
+
+from gt4py.next import config
+
+
+@pytest.fixture
+def env_var():
+    """Just in case another test will ever use that environment variable."""
+    env_var_name = "GT4PY_TEST_ENV_VAR"
+    saved = os.environ.get(env_var_name, None)
+    yield env_var_name
+    if saved is not None:
+        os.environ[env_var_name] = saved
+
+
+@pytest.mark.parametrize("value", ["False", "false", "0", "off"])
+def test_env_flag_to_bool_false(env_var, value):
+    os.environ[env_var] = value
+    assert config.env_flag_to_bool(env_var, default=True) is False
+
+
+@pytest.mark.parametrize("value", ["True", "true", "1", "on"])
+def test_env_flag_to_bool_true(env_var, value):
+    os.environ[env_var] = value
+    assert config.env_flag_to_bool(env_var, default=False) is True
+
+
+def test_env_flag_to_bool_invalid(env_var):
+    os.environ[env_var] = "invalid value"
+    with pytest.raises(ValueError):
+        config.env_flag_to_bool(env_var, default=False)
+
+
+def test_env_flag_to_bool_unset(env_var):
+    del os.environ[env_var]
+    assert config.env_flag_to_bool(env_var, default=False) is False