Repository: GridTools/gt4py
Pull request #1243 (2023-04-26 09:54:38)

fix[next]: improved tuple collapse
Extend TupleCollapse pass to also collapse `tuple_get(N, make_tuple(e_0, e_1, ..., e_N, e_N_plus_1))` -> `e_N`.


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9d58e2de..031b5e1e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -130,7 +130,14 @@ repos:
       tests/cartesian_tests/integration_tests/multi_feature_tests/utils.py |
       tests/cartesian_tests/integration_tests/feature_tests/test_call_interface.py |
       tests/cartesian_tests/unit_tests/frontend_tests/test_gtscript_frontend.py |
-      tests/next_tests/.*
+      tests/next_tests/unit_tests/.* |
+      tests/next_tests/integration_tests/multi_feature_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/ffront_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/iterator_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/otf_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/math_builtin_test_data.py |
+      tests/next_tests/past_common_fixtures.py |
+      tests/next_tests/toy_connectivity.py |
       )$
 
 - repo: https://github.com/pre-commit/mirrors-mypy
@@ -187,5 +194,15 @@ repos:
       build/.* |
       ci/.* |
       docs/.* |
+      tests/conftest.py |
+      tests/cartesian_tests/.* |
+      tests/next_tests/unit_tests/.* |
+      tests/next_tests/integration_tests/multi_feature_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/ffront_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/iterator_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/otf_tests/.* |
+      tests/next_tests/integration_tests/feature_tests/math_builtin_test_data.py |
+      tests/next_tests/past_common_fixtures.py |
+      tests/next_tests/toy_connectivity.py |
       tests/.*
       )$
diff --git a/CODING_GUIDELINES.md b/CODING_GUIDELINES.md
index 60dd7c65..a5b766a8 100644
--- a/CODING_GUIDELINES.md
+++ b/CODING_GUIDELINES.md
@@ -132,6 +132,8 @@ Each test suite should follow the following structure:
 
 Temporarily it may be allowed to split unit tests for a module into multiple `test_<module>_<feature>.py` test modules. This should be taken as a reason to think about splitting the source module though.
 
+Temporarily, tests for testing utilities can be placed next to the module containing them, with the name `test_util_<module>.py`. This should be taken as a hint that the tested utils should be moved into the library.
+
 TODO: add missing test conventions.
 
 <!--
diff --git a/pyproject.toml b/pyproject.toml
index 98734e39..117e62ee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -224,7 +224,22 @@ skip_glob = ['*.venv/**', '_local/**']
 # -- mypy  --
 [tool.mypy]
 disallow_incomplete_defs = true
-exclude = ['^setup\.py$', '^build/.*py$', '^ci/*.py$', '^tests/*.py$']
+exclude = [
+  '^setup\.py$',
+  '^build/.*py$',
+  '^ci/*.py$',
+  '^tests/cartesian_tests/*.py$',
+  '^tests/storage_tests/*.py$',
+  '^tests/eve_tests/*.py$',
+  '^tests/next_tests/unit_tests/*.py$',
+  '^tests/next_tests/integration_tests/multi_feature_tests/*.py$',
+  '^tests/next_tests/integration_tests/feature_tests/ffront_tests/*.py$',
+  '^tests/next_tests/integration_tests/feature_tests/iterator_tests/*.py$',
+  '^tests/next_tests/integration_tests/feature_tests/otf_tests/*.py$',
+  '^tests/next_tests/integration_tests/feature_tests/math_builtin_test_data.py$',
+  '^tests/next_tests/past_common_fixtures.py$',
+  '^tests/next_tests/toy_connectivity.py$'
+]
 ignore_missing_imports = true
 install_types = true
 namespace_packages = false
diff --git a/src/gt4py/next/common.py b/src/gt4py/next/common.py
index 056330ac..5ece3a23 100644
--- a/src/gt4py/next/common.py
+++ b/src/gt4py/next/common.py
@@ -37,7 +37,7 @@ from gt4py.eve.type_definitions import StrEnum
 
 DimT = TypeVar("DimT", bound="Dimension")
 DimsT = TypeVar("DimsT", bound=Sequence["Dimension"])
-DT = TypeVar("DT", bound="DType")
+DT = TypeVar("DT", bound="Scalar")
 
 Scalar: TypeAlias = SupportsInt | SupportsFloat | np.int32 | np.int64 | np.float32 | np.float64
 
diff --git a/src/gt4py/next/ffront/ast_passes/single_static_assign.py b/src/gt4py/next/ffront/ast_passes/single_static_assign.py
index ede74d6f..4181d7f4 100644
--- a/src/gt4py/next/ffront/ast_passes/single_static_assign.py
+++ b/src/gt4py/next/ffront/ast_passes/single_static_assign.py
@@ -11,12 +11,82 @@
 # distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
 #
 # SPDX-License-Identifier: GPL-3.0-or-later
+from __future__ import annotations
 
 import ast
+import collections.abc
+import dataclasses
+import typing
 
 from gt4py.next.ffront.fbuiltins import TYPE_BUILTIN_NAMES
 
 
+ASTNodeT = typing.TypeVar("ASTNodeT", bound=ast.AST)
+
+_UNIQUE_NAME_SEPARATOR = "ᐞ"
+
+
+# TODO(tehrengruber): use a global mechanism to get uniqueness across multiple passes (not only
+#  the ssa pass).
+def unique_name(name: str, num_assignments: int) -> str:
+    """Given the name of a variable return a unique name after the given amount of assignments."""
+    if num_assignments >= 0:
+        return f"{name}{_UNIQUE_NAME_SEPARATOR}{num_assignments}"
+    assert num_assignments == -1
+    return name
+
+
+def _make_assign(target: str, source: str, location_node: ast.AST) -> ast.Assign:
+    result = ast.Assign(
+        targets=[ast.Name(ctx=ast.Store(), id=target)], value=ast.Name(ctx=ast.Load(), id=source)
+    )
+    for node in ast.walk(result):
+        ast.copy_location(node, location_node)
+    return result
+
+
+def _is_guaranteed_to_return(node: ast.stmt | list[ast.stmt]) -> bool:
+    if isinstance(node, list):
+        return any(_is_guaranteed_to_return(child) for child in node)
+    if isinstance(node, ast.Return):
+        return True
+    if isinstance(node, ast.If):
+        return _is_guaranteed_to_return(node.body) and _is_guaranteed_to_return(node.orelse)
+    return False
+
+
+@dataclasses.dataclass
+class _AssignmentTracker:
+    """Helper class to keep track of the number of assignments to a variable."""
+
+    #: mapping from a variable name to the number of times it is assigned to. `-1` signifies it to
+    #:  be defined only, but not assigned to.
+    _counts: dict[str, int] = dataclasses.field(default_factory=dict)
+
+    def define(self, name: str) -> None:
+        if name in self.names():
+            raise ValueError(f"Variable {name} is already defined.")
+        # -1 signifies a
+        self._counts[name] = -1
+
+    def assign(self, name: str) -> None:
+        self._counts[name] = self.count(name) + 1
+
+    def count(self, name: str) -> int:
+        return self._counts.get(name, -1)
+
+    def names(self) -> collections.abc.Set:
+        return self._counts.keys()
+
+    def copy(self) -> _AssignmentTracker:
+        return _AssignmentTracker({**self._counts})
+
+
+def _merge_assignment_tracker(a: _AssignmentTracker, b: _AssignmentTracker) -> _AssignmentTracker:
+    return _AssignmentTracker({k: max(a.count(k), b.count(k)) for k in (a.names() & b.names())})
+
+
+@dataclasses.dataclass
 class SingleStaticAssignPass(ast.NodeTransformer):
     """
     Rename variables in assignments to avoid overwriting.
@@ -43,50 +113,106 @@ class SingleStaticAssignPass(ast.NodeTransformer):
     ...     )
     ... ))
     def foo():
-        a__0 = 1
-        a__1 = 2 + a__0
-        a__2 = 3 + a__1
-        return a__2
+        aᐞ0 = 1
+        aᐞ1 = 2 + aᐞ0
+        aᐞ2 = 3 + aᐞ1
+        return aᐞ2
 
-    Note that each variable name is assigned only once and never updated / overwritten.
+    Note that each variable name is assigned only once (per branch) and never updated / overwritten.
 
     Note also that after parsing, running the pass and unparsing we get invalid but
     readable python code. This is ok because this pass is not intended for
     python-to-python translation.
+
+    WARNING: This pass is not intended as a general-purpose SSA transformation.
+    The pass does not support any general Python AST. Known limitations include:
+        * Nested functions aren't supported
+        * While loops aren't supported
     """
 
-    class RhsRenamer(ast.NodeTransformer):
-        """
-        Rename right hand side names.
+    assignment_tracker: _AssignmentTracker = dataclasses.field(default_factory=_AssignmentTracker)
 
-        Only read from parent visitor state, should not modify.
-        """
+    @classmethod
+    def apply(cls, node: ASTNodeT) -> ASTNodeT:
+        return cls().visit(node)
+
+    def _rename(self, node: ASTNodeT) -> ASTNodeT:
+        for child_node in ast.walk(node):
+            if isinstance(child_node, ast.Name):
+                child_node.id = unique_name(
+                    child_node.id, self.assignment_tracker.count(child_node.id)
+                )
+        return node
+
+    def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
+        # For practical purposes, this is sufficient, but really not general at all.
+        # However, the algorithm was never intended to be general.
+
+        prev_assignment_tracker = self.assignment_tracker.copy()
+
+        for arg in node.args.args:
+            self.assignment_tracker.define(arg.arg)
+
+        node.body = [self.visit(stmt) for stmt in node.body]
+
+        self.assignment_tracker = prev_assignment_tracker
+        return node
 
-        @classmethod
-        def apply(cls, name_counter, separator, node):
-            return cls(name_counter, separator).visit(node)
+    def visit_If(self, node: ast.If) -> ast.If:
+        prev_assignment_tracker = self.assignment_tracker
 
-        def __init__(self, name_counter, separator):
-            super().__init__()
-            self.name_counter: dict[str, int] = name_counter
-            self.separator: str = separator
+        node.test = self._rename(node.test)
 
-        def visit_Name(self, node: ast.Name) -> ast.Name:
-            if node.id in self.name_counter:
-                node.id = f"{node.id}{self.separator}{self.name_counter[node.id]}"
+        self.assignment_tracker = prev_assignment_tracker.copy()
+        node.body = [self.visit(el) for el in node.body]
+        body_assignment_tracker = self.assignment_tracker
+        body_returns = _is_guaranteed_to_return(node.body)
+
+        self.assignment_tracker = prev_assignment_tracker.copy()
+        node.orelse = [self.visit(el) for el in node.orelse]
+        orelse_assignment_tracker = self.assignment_tracker
+        orelse_returns = _is_guaranteed_to_return(node.orelse)
+
+        if body_returns and not orelse_returns:
+            self.assignment_tracker = orelse_assignment_tracker
             return node
 
-    @classmethod
-    def apply(cls, node: ast.AST) -> ast.AST:
-        return cls().visit(node)
+        if orelse_returns and not body_returns:
+            self.assignment_tracker = body_assignment_tracker
+            return node
 
-    def __init__(self, separator="__"):
-        super().__init__()
-        self.name_counter: dict[str, int] = {}
-        self.separator: str = separator
+        if body_returns and orelse_returns:
+            self.assignment_tracker = _AssignmentTracker()
+            return node
+
+        assert not body_returns and not orelse_returns
+
+        self.assignment_tracker = _merge_assignment_tracker(
+            body_assignment_tracker, orelse_assignment_tracker
+        )
+
+        # ensure both branches conclude with the same unique names
+        for name in self.assignment_tracker.names():
+            assignment_count = self.assignment_tracker.count(name)
+            body_assignment_count = body_assignment_tracker.count(name)
+            orelse_assignment_count = orelse_assignment_tracker.count(name)
+
+            if body_assignment_count != assignment_count:
+                new_assign = _make_assign(
+                    unique_name(name, assignment_count),
+                    unique_name(name, body_assignment_count),
+                    node,
+                )
+                node.body.append(new_assign)
+            elif orelse_assignment_count != assignment_count:
+                new_assign = _make_assign(
+                    unique_name(name, assignment_count),
+                    unique_name(name, orelse_assignment_count),
+                    node,
+                )
+                node.orelse.append(new_assign)
 
-    def _rename(self, node):
-        return self.RhsRenamer.apply(self.name_counter, self.separator, node)
+        return node
 
     def visit_Assign(self, node: ast.Assign) -> ast.Assign:
         # first update rhs names to reference the latest version
@@ -104,18 +230,18 @@ class SingleStaticAssignPass(ast.NodeTransformer):
             node.value = self._rename(node.value)
             node.target = self.visit(node.target)
         elif isinstance(node.target, ast.Name):
-            target_id = node.target.id
+            # An annotated assignment without a value always applies to the next assignment. Ensure
+            # the two have the same name by keeping the assignment tracker as is (after this
+            # statement).
+            prev_assignment_tracker = self.assignment_tracker.copy()
             node.target = self.visit(node.target)
-            self.name_counter[target_id] -= 1
+            self.assignment_tracker = prev_assignment_tracker
         return node
 
     def visit_Name(self, node: ast.Name) -> ast.Name:
         if node.id in TYPE_BUILTIN_NAMES:
             return node
-        elif node.id in self.name_counter:
-            self.name_counter[node.id] += 1
-            node.id = f"{node.id}{self.separator}{self.name_counter[node.id]}"
-        else:
-            self.name_counter[node.id] = 0
-            node.id = f"{node.id}{self.separator}0"
+
+        self.assignment_tracker.assign(node.id)
+        node.id = unique_name(node.id, self.assignment_tracker.count(node.id))
         return node
diff --git a/src/gt4py/next/ffront/decorator.py b/src/gt4py/next/ffront/decorator.py
index b0cc585a..25d194e1 100644
--- a/src/gt4py/next/ffront/decorator.py
+++ b/src/gt4py/next/ffront/decorator.py
@@ -238,7 +238,7 @@ class Program:
                 f"The following closure variables are undefined: {', '.join(undefined_symbols)}"
             )
 
-    def with_backend(self, backend: ppi.ProgramExecutor) -> "Program":
+    def with_backend(self, backend: ppi.ProgramExecutor) -> Program:
         return Program(
             past_node=self.past_node,
             closure_vars=self.closure_vars,
@@ -246,6 +246,15 @@ class Program:
             definition=self.definition,  # type: ignore[arg-type]  # mypy wrongly deduces definition as method here
         )
 
+    def with_grid_type(self, grid_type: GridType) -> Program:
+        return Program(
+            past_node=self.past_node,
+            closure_vars=self.closure_vars,
+            backend=self.backend,
+            definition=self.definition,
+            grid_type=grid_type,
+        )
+
     @functools.cached_property
     def _all_closure_vars(self) -> dict[str, Any]:
         return _get_closure_vars_recursively(self.closure_vars)
@@ -460,6 +469,7 @@ class FieldOperator(GTCallable, Generic[OperatorNodeT]):
         cls,
         definition: types.FunctionType,
         backend: Optional[ppi.ProgramExecutor] = None,
+        grid_type: Optional[GridType] = None,
         *,
         operator_node_cls: type[OperatorNodeT] = foast.FieldOperator,
         operator_attributes: Optional[dict[str, Any]] = None,
@@ -485,8 +495,9 @@ class FieldOperator(GTCallable, Generic[OperatorNodeT]):
         return cls(
             foast_node=foast_node,
             closure_vars=closure_vars,
-            backend=backend,
             definition=definition,
+            backend=backend,
+            grid_type=grid_type,
         )
 
     def __gt_type__(self) -> ts.CallableType:
@@ -628,11 +639,7 @@ def field_operator(
     ...
 
 
-def field_operator(
-    definition=None,
-    *,
-    backend=None,
-):
+def field_operator(definition=None, *, backend=None, grid_type=None):
     """
     Generate an implementation of the field operator from a Python function object.
 
@@ -650,7 +657,7 @@ def field_operator(
     """
 
     def field_operator_inner(definition: types.FunctionType) -> FieldOperator[foast.FieldOperator]:
-        return FieldOperator.from_function(definition, backend)
+        return FieldOperator.from_function(definition, backend, grid_type)
 
     return field_operator_inner if definition is None else field_operator_inner(definition)
 
diff --git a/src/gt4py/next/iterator/embedded.py b/src/gt4py/next/iterator/embedded.py
index e3dd18a8..c5befa58 100644
--- a/src/gt4py/next/iterator/embedded.py
+++ b/src/gt4py/next/iterator/embedded.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import abc
+import contextvars as cvars
 import copy
 import dataclasses
 import itertools
@@ -182,9 +183,10 @@ class MutableLocatedField(LocatedField, Protocol):
         ...
 
 
-# TODO see https://github.com/GridTools/gt4py/pull/1120
-_column_range: Optional[range] = None
-_offset_provider: Optional[OffsetProvider] = None
+#: Column range used in column mode (`column_axis != None`) in the current closure execution context.
+column_range_cvar: cvars.ContextVar[range] = cvars.ContextVar("column_range")
+#: Offset provider dict in the current closure execution context.
+offset_provider_cvar: cvars.ContextVar[OffsetProvider] = cvars.ContextVar("offset_provider")
 
 
 class Column(np.lib.mixins.NDArrayOperatorsMixin):
@@ -197,7 +199,8 @@ class Column(np.lib.mixins.NDArrayOperatorsMixin):
     def __init__(self, kstart: int, data: np.ndarray | Scalar) -> None:
         self.kstart = kstart
         assert isinstance(data, (np.ndarray, Scalar))  # type: ignore # mypy bug
-        self.data = data if isinstance(data, np.ndarray) else np.full(len(_column_range), data)  # type: ignore[arg-type]
+        column_range = column_range_cvar.get()
+        self.data = data if isinstance(data, np.ndarray) else np.full(len(column_range), data)
 
     def __getitem__(self, i: int) -> Any:
         result = self.data[i - self.kstart]
@@ -720,22 +723,23 @@ def _make_tuple(
     *,
     column_axis: Optional[Tag] = None,
 ) -> Column | npt.DTypeLike | tuple[tuple | Column | npt.DTypeLike, ...]:
+    column_range = column_range_cvar.get()
     if isinstance(field_or_tuple, tuple):
         if column_axis is not None:
-            assert _column_range
+            assert column_range
             # construct a Column of tuples
             column_axis_idx = _axis_idx(_get_axes(field_or_tuple), column_axis)
             if column_axis_idx is None:
                 column_axis_idx = -1  # field doesn't have the column index, e.g. ContantField
             first = tuple(
-                _make_tuple(f, _single_vertical_idx(indices, column_axis_idx, _column_range.start))
+                _make_tuple(f, _single_vertical_idx(indices, column_axis_idx, column_range.start))
                 for f in field_or_tuple
             )
             col = Column(
-                _column_range.start, np.zeros(len(_column_range), dtype=_column_dtype(first))
+                column_range.start, np.zeros(len(column_range), dtype=_column_dtype(first))
             )
             col[0] = first
-            for i in _column_range[1:]:
+            for i in column_range[1:]:
                 col[i] = tuple(
                     _make_tuple(f, _single_vertical_idx(indices, column_axis_idx, i))
                     for f in field_or_tuple
@@ -747,8 +751,8 @@ def _make_tuple(
         data = field_or_tuple.field_getitem(indices)
         if column_axis is not None:
             # wraps a vertical slice of an input field into a `Column`
-            assert _column_range is not None
-            return Column(_column_range.start, data)
+            assert column_range is not None
+            return Column(column_range.start, data)
         else:
             return data
 
@@ -768,10 +772,11 @@ class MDIterator:
 
     def shift(self, *offsets: OffsetPart) -> MDIterator:
         complete_offsets = group_offsets(*offsets)
-        assert _offset_provider is not None
+        offset_provider = offset_provider_cvar.get()
+        assert offset_provider is not None
         return MDIterator(
             self.field,
-            shift_position(self.pos, *complete_offsets, offset_provider=_offset_provider),
+            shift_position(self.pos, *complete_offsets, offset_provider=offset_provider),
             column_axis=self.column_axis,
         )
 
@@ -792,13 +797,14 @@ class MDIterator:
             if not all(axis.value in shifted_pos.keys() for axis in axes if axis is not None):
                 raise IndexError("Iterator position doesn't point to valid location for its field.")
         slice_column = dict[Tag, range]()
+        column_range = column_range_cvar.get()
         if self.column_axis is not None:
-            assert _column_range is not None
+            assert column_range is not None
             k_pos = shifted_pos.pop(self.column_axis)
             assert isinstance(k_pos, int)
             # the following range describes a range in the field
             # (negative values are relative to the origin, not relative to the size)
-            slice_column[self.column_axis] = range(k_pos, k_pos + len(_column_range))
+            slice_column[self.column_axis] = range(k_pos, k_pos + len(column_range))
 
         assert _is_concrete_position(shifted_pos)
         ordered_indices = get_ordered_indices(
@@ -834,9 +840,10 @@ def make_in_iterator(
         init = [None] * sparse_dimensions.count(sparse_dim)
         new_pos[sparse_dim] = init  # type: ignore[assignment] # looks like mypy is confused
     if column_axis is not None:
+        column_range = column_range_cvar.get()
         # if we deal with column stencil the column position is just an offset by which the whole column needs to be shifted
-        assert _column_range is not None
-        new_pos[column_axis] = _column_range.start
+        assert column_range is not None
+        new_pos[column_axis] = column_range.start
     it = MDIterator(
         inp,
         new_pos,
@@ -1083,8 +1090,9 @@ class _ConstList(Generic[DT]):
 def neighbors(offset: runtime.Offset, it: ItIterator) -> _List:
     offset_str = offset.value if isinstance(offset, runtime.Offset) else offset
     assert isinstance(offset_str, str)
-    assert _offset_provider is not None
-    connectivity = _offset_provider[offset_str]
+    offset_provider = offset_provider_cvar.get()
+    assert offset_provider is not None
+    connectivity = offset_provider[offset_str]
     assert isinstance(connectivity, common.Connectivity)
     return _List(
         shifted.deref()
@@ -1141,8 +1149,9 @@ class SparseListIterator:
     offsets: Sequence[OffsetPart] = dataclasses.field(default_factory=list, kw_only=True)
 
     def deref(self) -> Any:
-        assert _offset_provider is not None
-        connectivity = _offset_provider[self.list_offset]
+        offset_provider = offset_provider_cvar.get()
+        assert offset_provider is not None
+        connectivity = offset_provider[self.list_offset]
         assert isinstance(connectivity, common.Connectivity)
         return _List(
             shifted.deref()
@@ -1290,13 +1299,14 @@ def _column_dtype(elem: Any) -> np.dtype:
 @builtins.scan.register(EMBEDDED)
 def scan(scan_pass, is_forward: bool, init):
     def impl(*iters: ItIterator):
-        if _column_range is None:
+        column_range = column_range_cvar.get()
+        if column_range is None:
             raise RuntimeError("Column range is not defined, cannot scan.")
 
-        column_range = _column_range if is_forward else reversed(_column_range)
+        sorted_column_range = column_range if is_forward else reversed(column_range)
         state = init
-        col = Column(_column_range.start, np.zeros(len(_column_range), dtype=_column_dtype(init)))
-        for i in column_range:
+        col = Column(column_range.start, np.zeros(len(column_range), dtype=_column_dtype(init)))
+        for i in sorted_column_range:
             state = scan_pass(state, *map(shifted_scan_arg(i), iters))
             col[i] = state
 
@@ -1321,8 +1331,7 @@ def fendef_embedded(fun: Callable[..., None], *args: Any, **kwargs: Any):
     if "offset_provider" not in kwargs:
         raise RuntimeError("offset_provider not provided")
 
-    global _offset_provider
-    _offset_provider = kwargs["offset_provider"]
+    offset_provider = kwargs["offset_provider"]
 
     @runtime.closure.register(EMBEDDED)
     def closure(
@@ -1336,42 +1345,48 @@ def fendef_embedded(fun: Callable[..., None], *args: Any, **kwargs: Any):
         if not (is_located_field(out) or can_be_tuple_field(out)):
             raise TypeError("Out needs to be a located field.")
 
-        global _column_range
+        column_range = None
         column: Optional[ColumnDescriptor] = None
         if kwargs.get("column_axis") and kwargs["column_axis"].value in domain:
             column_axis = kwargs["column_axis"]
             column = ColumnDescriptor(column_axis.value, domain[column_axis.value])
             del domain[column_axis.value]
 
-            _column_range = column.col_range
+            column_range = column.col_range
 
         out = as_tuple_field(out) if can_be_tuple_field(out) else out
 
-        for pos in _domain_iterator(domain):
-            promoted_ins = [promote_scalars(inp) for inp in ins]
-            ins_iters = list(
-                make_in_iterator(
-                    inp,
-                    pos,
-                    column_axis=column.axis if column else None,
+        def _closure_runner():
+            # Set context variables before executing the closure
+            column_range_cvar.set(column_range)
+            offset_provider_cvar.set(offset_provider)
+
+            for pos in _domain_iterator(domain):
+                promoted_ins = [promote_scalars(inp) for inp in ins]
+                ins_iters = list(
+                    make_in_iterator(
+                        inp,
+                        pos,
+                        column_axis=column.axis if column else None,
+                    )
+                    for inp in promoted_ins
                 )
-                for inp in promoted_ins
-            )
-            res = sten(*ins_iters)
+                res = sten(*ins_iters)
 
-            if column is None:
-                assert _is_concrete_position(pos)
-                ordered_indices = get_ordered_indices(out.axes, pos)
-                out.field_setitem(ordered_indices, res)
-            else:
-                col_pos = pos.copy()
-                for k in column.col_range:
-                    col_pos[column.axis] = k
-                    assert _is_concrete_position(col_pos)
-                    ordered_indices = get_ordered_indices(out.axes, col_pos)
-                    out.field_setitem(ordered_indices, res[k])
-
-        _column_range = None
+                if column is None:
+                    assert _is_concrete_position(pos)
+                    ordered_indices = get_ordered_indices(out.axes, pos)
+                    out.field_setitem(ordered_indices, res)
+                else:
+                    col_pos = pos.copy()
+                    for k in column.col_range:
+                        col_pos[column.axis] = k
+                        assert _is_concrete_position(col_pos)
+                        ordered_indices = get_ordered_indices(out.axes, col_pos)
+                        out.field_setitem(ordered_indices, res[k])
+
+        ctx = cvars.copy_context()
+        ctx.run(_closure_runner)
 
     fun(*args)
 
diff --git a/src/gt4py/next/iterator/ir.py b/src/gt4py/next/iterator/ir.py
index 1a909927..54b06ca8 100644
--- a/src/gt4py/next/iterator/ir.py
+++ b/src/gt4py/next/iterator/ir.py
@@ -68,6 +68,11 @@ class Literal(Expr):
     value: str
     type: str  # noqa: A003
 
+    @datamodels.validator("type")
+    def _type_validator(self: datamodels.DataModelTP, attribute: datamodels.Attribute, value):
+        if value not in TYPEBUILTINS:
+            raise ValueError(f"{value} is not a valid builtin type.")
+
 
 class NoneLiteral(Expr):
     _none_literal: int = 0
diff --git a/src/gt4py/next/iterator/ir_makers.py b/src/gt4py/next/iterator/ir_makers.py
index c7dfe9d0..8fec9aaa 100644
--- a/src/gt4py/next/iterator/ir_makers.py
+++ b/src/gt4py/next/iterator/ir_makers.py
@@ -218,9 +218,9 @@ def make_tuple(*args):
     return call("make_tuple")(*args)
 
 
-def tuple_get(tuple_expr, index):
-    """Create a tuple_get FunCall, shorthand for ``call("tuple_get")(tuple_expr, index)``."""
-    return call("tuple_get")(tuple_expr, index)
+def tuple_get(index, tuple_expr):
+    """Create a tuple_get FunCall, shorthand for ``call("tuple_get")(index, tuple_expr)``."""
+    return call("tuple_get")(index, tuple_expr)
 
 
 def if_(cond, true_val, false_val):
diff --git a/src/gt4py/next/iterator/transforms/collapse_tuple.py b/src/gt4py/next/iterator/transforms/collapse_tuple.py
index df75ff11..efe0941a 100644
--- a/src/gt4py/next/iterator/transforms/collapse_tuple.py
+++ b/src/gt4py/next/iterator/transforms/collapse_tuple.py
@@ -29,25 +29,47 @@ def _get_tuple_size(node: ir.Node) -> int:
 
 @dataclass(frozen=True)
 class CollapseTuple(eve.NodeTranslator):
-    """Transform `make_tuple(tuple_get(0, t), tuple_get(1, t), ..., tuple_get(N-1,t))` -> t."""
+    """
+    Simplifies `make_tuple`, `tuple_get` calls.
+
+      - `make_tuple(tuple_get(0, t), tuple_get(1, t), ..., tuple_get(N-1,t))` -> `t`
+      - `tuple_get(i, make_tuple(e_0, e_1, ..., e_i, ..., e_N))` -> `e_i`
+    """
 
     ignore_tuple_size: bool
+    collapse_make_tuple_tuple_get: bool
+    collapse_tuple_get_make_tuple: bool
 
     @classmethod
-    def apply(cls, node: ir.Node, ignore_tuple_size: bool = False) -> ir.Node:
+    def apply(
+        cls,
+        node: ir.Node,
+        *,
+        ignore_tuple_size: bool = False,
+        # the following options are mostly for allowing separate testing of the modes
+        collapse_make_tuple_tuple_get: bool = True,
+        collapse_tuple_get_make_tuple: bool = True,
+    ) -> ir.Node:
         """
-        Transform `make_tuple(tuple_get(0, t), tuple_get(1, t), ..., tuple_get(N-1,t))` -> t.
+        Simplifies `make_tuple`, `tuple_get` calls.
 
         If `ignore_tuple_size`, apply the transformation even if length of the inner tuple
         is greater than the length of the outer tuple.
         """
-        return cls(ignore_tuple_size).visit(node)
+        return cls(
+            ignore_tuple_size, collapse_make_tuple_tuple_get, collapse_tuple_get_make_tuple
+        ).visit(node)
 
     def visit_FunCall(self, node: ir.FunCall, **kwargs) -> ir.Node:
-        if node.fun == ir.SymRef(id="make_tuple") and all(
-            isinstance(arg, ir.FunCall) and arg.fun == ir.SymRef(id="tuple_get")
-            for arg in node.args
+        if (
+            self.collapse_make_tuple_tuple_get
+            and node.fun == ir.SymRef(id="make_tuple")
+            and all(
+                isinstance(arg, ir.FunCall) and arg.fun == ir.SymRef(id="tuple_get")
+                for arg in node.args
+            )
         ):
+            # `make_tuple(tuple_get(0, t), tuple_get(1, t), ..., tuple_get(N-1,t))` -> `t`
             assert isinstance(node.args[0], ir.FunCall)
             first_expr = node.args[0].args[1]
 
@@ -55,9 +77,24 @@ class CollapseTuple(eve.NodeTranslator):
                 assert isinstance(v, ir.FunCall)
                 assert isinstance(v.args[0], ir.Literal)
                 if not (int(v.args[0].value) == i and v.args[1] == first_expr):
+                    # tuple argument differs, just continue with the rest of the tree
                     return self.generic_visit(node)
 
             if self.ignore_tuple_size or _get_tuple_size(first_expr) == len(node.args):
                 return first_expr
-
+        if (
+            self.collapse_tuple_get_make_tuple
+            and node.fun == ir.SymRef(id="tuple_get")
+            and isinstance(node.args[1], ir.FunCall)
+            and node.args[1].fun == ir.SymRef(id="make_tuple")
+            and isinstance(node.args[0], ir.Literal)
+        ):
+            # `tuple_get(i, make_tuple(e_0, e_1, ..., e_i, ..., e_N))` -> `e_i`
+            assert node.args[0].type in ir.INTEGER_BUILTINS
+            make_tuple_call = node.args[1]
+            idx = int(node.args[0].value)
+            assert idx < len(
+                make_tuple_call.args
+            ), f"Index {idx} is out of bounds for tuple of size {len(make_tuple_call.args)}"
+            return node.args[1].args[idx]
         return self.generic_visit(node)
diff --git a/src/gt4py/next/iterator/transforms/cse.py b/src/gt4py/next/iterator/transforms/cse.py
index fde6f902..a030d61b 100644
--- a/src/gt4py/next/iterator/transforms/cse.py
+++ b/src/gt4py/next/iterator/transforms/cse.py
@@ -66,6 +66,16 @@ def _is_collectable_expr(node: ir.Node):
     return False
 
 
+def _is_if_can_deref(node: ir.Node):
+    # `if_(can_deref(...), ..., ...)`
+    return (
+        isinstance(node, ir.FunCall)
+        and node.fun == ir.SymRef(id="if_")
+        and isinstance(node.args[0], ir.FunCall)
+        and node.args[0].fun == ir.SymRef(id="can_deref")
+    )
+
+
 @dataclasses.dataclass
 class CollectSubexpressions(VisitorWithSymbolTableTrait, NodeVisitor):
     subexprs: dict[ir.Node, list[tuple[int, set[int]]]] = dataclasses.field(
@@ -75,12 +85,19 @@ class CollectSubexpressions(VisitorWithSymbolTableTrait, NodeVisitor):
     @classmethod
     def apply(cls, node: ir.Node):
         obj = cls()
-        obj.visit(node, used_symbol_ids=set(), collected_child_node_ids=set())
+        obj.visit(
+            node, used_symbol_ids=set(), collected_child_node_ids=set(), allow_collection=True
+        )
         # return subexpression in pre-order of the tree, i.e. the nodes closer to the root come
         # first, and skip the root node itself
         return {k: v for k, v in reversed(obj.subexprs.items()) if k is not node}
 
     def visit(self, node, **kwargs):
+        # TODO(tehrengruber): improve this case as we might miss subexpression that could be eliminated
+        # disable collection (for all child nodes) if node matches `if_(can_deref(...), ..., ...)`
+        if _is_if_can_deref(node):
+            kwargs["allow_collection"] = False
+
         if not isinstance(node, SymbolTableTrait) and not _is_collectable_expr(node):
             return super().visit(node, **kwargs)
 
@@ -101,7 +118,7 @@ class CollectSubexpressions(VisitorWithSymbolTableTrait, NodeVisitor):
 
         # if no symbols are used that are defined in the root node, i.e. the node given to `apply`,
         # we collect the subexpression
-        if not used_symbol_ids and _is_collectable_expr(node):
+        if not used_symbol_ids and _is_collectable_expr(node) and kwargs["allow_collection"]:
             self.subexprs.setdefault(node, []).append((id(node), collected_child_node_ids))
 
             # propagate to parent that we have collected its child


Test patch:
diff --git a/tests/next_tests/integration_tests/feature_tests/cases.py b/tests/next_tests/integration_tests/feature_tests/cases.py
new file mode 100644
index 00000000..61afc376
--- /dev/null
+++ b/tests/next_tests/integration_tests/feature_tests/cases.py
@@ -0,0 +1,564 @@
+# 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 __future__ import annotations
+
+import dataclasses
+import functools
+import inspect
+import types
+import typing
+from typing import Any, Callable, Literal, Optional, Protocol, TypeAlias
+
+import numpy as np
+import pytest
+
+from gt4py.eve import extended_typing as xtyping
+from gt4py.eve.extended_typing import Self
+from gt4py.next import common
+from gt4py.next.ffront import decorator, fbuiltins
+from gt4py.next.iterator import embedded, ir as itir
+from gt4py.next.program_processors import processor_interface as ppi
+from gt4py.next.type_system import type_specifications as ts, type_translation
+
+from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import (  # noqa: F401 #  fixture and aliases
+    Edge,
+    IDim,
+    Ioff,
+    JDim,
+    Joff,
+    KDim,
+    Koff,
+    Vertex,
+    fieldview_backend,
+    reduction_setup,
+)
+
+
+# mypy does not accept [IDim, ...] as a type
+IField: TypeAlias = common.Field[[IDim], np.int64]  # type: ignore [valid-type]
+IJKField: TypeAlias = common.Field[[IDim, JDim, KDim], np.int64]  # type: ignore [valid-type]
+IJKFloatField: TypeAlias = common.Field[[IDim, JDim, KDim], np.float64]  # type: ignore [valid-type]
+VField: TypeAlias = common.Field[[Vertex], np.int64]  # type: ignore [valid-type]
+EField: TypeAlias = common.Field[[Edge], np.int64]  # type: ignore [valid-type]
+
+# TODO(ricoh): unify the following with the `ffront_test_utils.reduction_setup`
+#   fixture if `ffront_test_utils.reduction_setup` is not completely superseded
+#   by `unstructured_case`.
+V2EDim = common.Dimension("V2E", kind=common.DimensionKind.LOCAL)
+E2VDim = common.Dimension("E2V", kind=common.DimensionKind.LOCAL)
+V2E = fbuiltins.FieldOffset("V2E", source=Edge, target=(Vertex, V2EDim))
+E2V = fbuiltins.FieldOffset("E2V", source=Vertex, target=(Edge, E2VDim))
+
+ScalarValue: TypeAlias = np.int32 | np.int64 | np.float32 | np.float64 | np.generic
+FieldValue: TypeAlias = common.Field | embedded.LocatedFieldImpl
+FieldViewArg: TypeAlias = FieldValue | ScalarValue | tuple["FieldViewArg", ...]
+FieldViewInout: TypeAlias = FieldValue | tuple["FieldViewInout", ...]
+ReferenceValue: TypeAlias = (
+    common.Field | np.typing.NDArray[ScalarValue] | tuple["ReferenceValue", ...]
+)
+OffsetProvider: TypeAlias = dict[str, common.Connectivity | common.Dimension]
+
+
+#: To allocate the return value of a field operator, we must pass
+#: something that is not an argument name. Currently this is  the
+#: literal string "return" (because it is read from annotations).
+#: This could change if implemented differently. RETURN acts as a
+#: proxy to avoid propagating the change to client code.
+RETURN = "return"
+
+
+class DataInitializer(Protocol):
+    @property
+    def scalar_value(self) -> ScalarValue:
+        ...
+
+    def scalar(self, dtype: np.typing.DTypeLike) -> ScalarValue:
+        # some unlikely numpy dtypes are picky about arguments
+        return np.dtype(dtype).type(self.scalar_value)  # type: ignore [arg-type]
+
+    def field(
+        self,
+        backend: ppi.ProgramProcessor,
+        sizes: dict[common.Dimension, int],
+        dtype: np.typing.DTypeLike,
+    ) -> FieldValue:
+        ...
+
+    def from_case(
+        self: Self,
+        case: Case,
+        fieldview_prog: decorator.FieldOperator | decorator.Program,
+        arg_name: str,
+    ) -> Self:
+        return self
+
+
+@dataclasses.dataclass
+class ConstInitializer(DataInitializer):
+    """Initialize with a given value across the coordinate space."""
+
+    value: ScalarValue
+
+    @property
+    def scalar_value(self) -> ScalarValue:
+        return self.value
+
+    def field(
+        self,
+        backend: ppi.ProgramProcessor,
+        sizes: dict[common.Dimension, int],
+        dtype: np.typing.DTypeLike,
+    ) -> FieldValue:
+        return embedded.np_as_located_field(*sizes.keys())(
+            np.full(tuple(sizes.values()), self.value, dtype=dtype)
+        )
+
+
+@dataclasses.dataclass(init=False)
+class ZeroInitializer(ConstInitializer):
+    """Initialize with zeros."""
+
+    def __init__(self):
+        self.value = 0
+
+
+@dataclasses.dataclass
+class UniqueInitializer(DataInitializer):
+    """
+    Initialize with a unique value in each coordinate point.
+
+    Data initialized with the same instance will also have unique values across
+    data containers.
+    """
+
+    start: int = 0
+
+    @property
+    def scalar_value(self) -> ScalarValue:
+        start = self.start
+        self.start += 1
+        return np.int64(start)
+
+    def field(
+        self,
+        backend: ppi.ProgramProcessor,
+        sizes: dict[common.Dimension, int],
+        dtype: np.typing.DTypeLike,
+    ) -> FieldValue:
+        start = self.start
+        svals = tuple(sizes.values())
+        n_data = int(np.prod(svals))
+        self.start += n_data
+        return embedded.np_as_located_field(*sizes.keys())(
+            np.arange(start, start + n_data, dtype=dtype).reshape(svals)
+        )
+
+    def from_case(
+        self: Self,
+        case: Case,
+        fieldview_prog: decorator.FieldOperator | decorator.Program,
+        arg_name: str,
+    ) -> Self:
+        param_types = get_param_types(fieldview_prog)
+        param_sizes = [
+            get_param_size(param_type, sizes=case.default_sizes)
+            for param_type in param_types.values()
+        ]
+        param_index = list(param_types.keys()).index(arg_name)
+        return self.__class__(start=self.start + sum(param_sizes[:param_index]))
+
+
+@dataclasses.dataclass(frozen=True)
+class Builder:
+    partial: functools.partial
+
+    def __call__(self, *args: Any, **kwargs: Any) -> Any:
+        return self.partial(*args, **kwargs)
+
+    def __getattr__(self, name: str) -> Any:
+        raise AttributeError(f"No setter for argument {name}.")
+
+
+@typing.overload
+def make_builder(*args: Callable) -> Callable[..., Builder]:
+    ...
+
+
+@typing.overload
+def make_builder(
+    *args: Literal[None], **kwargs: dict[str, Any]
+) -> Callable[[Callable], Callable[..., Builder]]:
+    ...
+
+
+@typing.overload
+def make_builder(
+    *args: Optional[Callable], **kwargs: dict[str, Any]
+) -> Callable[[Callable], Callable[..., Builder]] | Callable[..., Builder]:
+    ...
+
+
+# TODO(ricoh): Think about improving the type hints using `typing.ParamSpec`.
+def make_builder(
+    *args: Optional[Callable], **kwargs: dict[str, Any]
+) -> Callable[[Callable], Callable[..., Builder]] | Callable[..., Builder]:
+    """Create a fluid interface for a function with many arguments."""
+
+    def make_builder_inner(func: Callable) -> Callable[..., Builder]:
+        def make_setter(argname: str) -> Callable[[Builder, Any], Builder]:
+            def setter(self: Builder, arg: Any) -> Builder:
+                return self.__class__(
+                    partial=functools.partial(
+                        self.partial.func,
+                        *self.partial.args,
+                        **(self.partial.keywords | {argname: arg}),
+                    )
+                )
+
+            setter.__name__ = argname
+            return setter
+
+        def make_flag_setter(
+            flag_name: str, flag_kwargs: dict[str, Any]
+        ) -> Callable[[Builder], Builder]:
+            def setter(self: Builder) -> Builder:
+                return self.__class__(
+                    partial=functools.partial(
+                        self.partial.func,
+                        *self.partial.args,
+                        **(self.partial.keywords | flag_kwargs),
+                    )
+                )
+
+            setter.__name__ = flag_name
+            return setter
+
+        argspec = inspect.getfullargspec(func)
+
+        @dataclasses.dataclass(frozen=True)
+        class NewBuilder(Builder):
+            ...
+
+        for argname in argspec.args + argspec.kwonlyargs:
+            setattr(NewBuilder, argname, make_setter(argname))
+
+        for flag, flag_kwargs in kwargs.items():
+            setattr(NewBuilder, flag, make_flag_setter(flag, flag_kwargs))
+
+        func_snake_words = func.__name__.split("_")
+        func_camel_name = "".join(word.capitalize() for word in func_snake_words)
+
+        NewBuilder.__name__ = f"{func_camel_name}Builder"
+
+        return lambda *args, **kwargs: NewBuilder(functools.partial(func, *args, **kwargs))
+
+    if 0 < len(args) <= 1 and args[0] is not None:
+        return make_builder_inner(args[0])
+    if len(args) > 1:
+        raise ValueError(f"make_builder takes only one positional argument, {len(args)} received!")
+    return make_builder_inner
+
+
+@make_builder(zeros={"strategy": ZeroInitializer()}, unique={"strategy": UniqueInitializer()})
+def allocate(
+    case: Case,
+    fieldview_prog: decorator.FieldOperator | decorator.Program,
+    name: str,
+    *,
+    sizes: Optional[dict[common.Dimension, int]] = None,
+    strategy: Optional[DataInitializer] = None,
+    dtype: Optional[np.typing.DTypeLike] = None,
+    extend: Optional[dict[common.Dimension, tuple[int, int]]] = None,
+) -> FieldViewArg:
+    """
+    Allocate a parameter or return value from a fieldview code object.
+
+    Args:
+        case: The test case.
+        fieldview_prog: The field operator or program to be verified.
+        name: The name of the input argument to allocate, or ``RETURN``
+            for the return value of a field operator.
+    Keyword Args:
+        sizes: Override for the test case dimension sizes.
+            Use with caution.
+        strategy: How to initialize the data.
+        dtype: Override for the dtype in the argument's type hint.
+        extend: Lower and upper size extension per dimension.
+            Useful for shifted fields, which must start off bigger
+            than the output field in the shifted dimension.
+    """
+    sizes = extend_sizes(case.default_sizes | (sizes or {}), extend)
+    arg_type = get_param_types(fieldview_prog)[name]
+    if strategy is None:
+        if name in ["out", RETURN]:
+            strategy = ZeroInitializer()
+        else:
+            strategy = UniqueInitializer()
+    return _allocate_from_type(
+        case=case,
+        arg_type=arg_type,
+        sizes=sizes,
+        dtype=dtype,
+        strategy=strategy.from_case(case=case, fieldview_prog=fieldview_prog, arg_name=name),
+    )
+
+
+def run(
+    case: Case,
+    fieldview_prog: decorator.FieldOperator | decorator.Program,
+    *args: FieldViewArg,
+    **kwargs: Any,
+) -> None:
+    """Run fieldview code in the context of a given test case."""
+    if kwargs.get("offset_provider", None) is None:
+        kwargs["offset_provider"] = case.offset_provider
+    fieldview_prog.with_grid_type(case.grid_type).with_backend(case.backend)(*args, **kwargs)
+
+
+def verify(
+    case: Case,
+    fieldview_prog: decorator.FieldOperator | decorator.Program,
+    *args: FieldViewArg,
+    ref: ReferenceValue,
+    out: Optional[FieldViewInout] = None,
+    inout: Optional[FieldViewInout] = None,
+    offset_provider: Optional[OffsetProvider] = None,
+    comparison: Callable[[Any, Any], bool] = np.allclose,
+) -> None:
+    """
+    Check the result of executing a fieldview program or operator against ref.
+
+    Args:
+        case: The test case.
+        fieldview_prog: The field operator or program to be verified.
+        *args: positional input arguments to the fieldview code.
+    Keyword Args:
+        ref: A field or array which will be compared to the results of the
+            fieldview code.
+        out: If given will be passed to the fieldview code as ``out=`` keyword
+            argument. This will hold the results and be used to compare
+            to ``ref``.
+        inout: If ``out`` is not passed it is assumed that the fieldview code
+            does not take an ``out`` keyword argument, so some of the other
+            arguments must be in/out parameters. Pass the according field
+            or tuple of fields here and they will be compared to ``ref`` under
+            the assumption that the fieldview code stores it's results in
+            them.
+        offset_provied: An override for the test case's offset_provider.
+            Use with care!
+        comparison: A comparison function, which will be called as
+            ``comparison(ref, <out | inout>)`` and should return a boolean.
+
+    One of ``out`` or ``inout`` must be passed. If ``out`` is passed it will be
+    used as an argument to the fieldview program and compared against ``ref``.
+    Else, ``inout`` will not be passed and compared to ``ref``.
+    """
+    if out:
+        run(
+            case,
+            fieldview_prog,
+            *args,
+            out=out,
+            offset_provider=offset_provider,
+        )
+    else:
+        run(case, fieldview_prog, *args, offset_provider=offset_provider)
+
+    out_comp = out or inout
+    out_comp_str = str(out_comp)
+    assert out_comp is not None
+    if hasattr(out_comp, "array"):
+        out_comp_str = str(out_comp.array())
+    assert comparison(ref, out_comp), (
+        f"Verification failed:\n"
+        f"\tcomparison={comparison.__name__}(ref, out)\n"
+        f"\tref = {ref}\n\tout = {out_comp_str}"
+    )
+
+
+def verify_with_default_data(
+    case: Case,
+    fieldop: decorator.FieldOperator,
+    ref: Callable,
+    comparison: Callable[[Any, Any], bool] = np.allclose,
+) -> None:
+    """
+    Check the fieldview code against a reference calculation.
+
+    This is a convenience function to reduce boiler plate
+    and is not meant to hide logic for complex cases. The
+    ``verify`` function allows more fine grained control for such tests.
+
+    Args:
+        case: The test case.
+        fieldview_prog: The field operator or program to be verified.
+        ref: A callable which will be called with all the input arguments
+            of the fieldview code, after applying ``.array()`` on the fields.
+        comparison: A comparison function, which will be called as
+            ``comparison(ref, <out | inout>)`` and should return a boolean.
+    """
+    inps, kwfields = get_default_data(case, fieldop)
+    ref_args = tuple(i.array() if hasattr(i, "array") else i for i in inps)
+    verify(
+        case,
+        fieldop,
+        *inps,
+        **kwfields,
+        ref=ref(*ref_args),
+        offset_provider=case.offset_provider,
+        comparison=comparison,
+    )
+
+
+@pytest.fixture
+def no_default_backend():
+    """Temporarily switch off default backend for feature tests."""
+    backup_backend = decorator.DEFAULT_BACKEND
+    decorator.DEFAULT_BACKEND = no_backend
+    yield
+    decorator.DEFAULT_BACKEND = backup_backend
+
+
+@pytest.fixture
+def cartesian_case(no_default_backend, fieldview_backend):  # noqa: F811 # fixtures
+    yield Case(
+        fieldview_backend,
+        offset_provider={"Ioff": IDim, "Joff": JDim, "Koff": KDim},
+        default_sizes={IDim: 10, JDim: 10, KDim: 10},
+        grid_type=common.GridType.CARTESIAN,
+    )
+
+
+@pytest.fixture
+def unstructured_case(
+    no_default_backend, reduction_setup, fieldview_backend  # noqa: F811 # fixtures
+):
+    yield Case(
+        fieldview_backend,
+        offset_provider=reduction_setup.offset_provider,
+        default_sizes={
+            Vertex: reduction_setup.num_vertices,
+            Edge: reduction_setup.num_edges,
+        },
+        grid_type=common.GridType.UNSTRUCTURED,
+    )
+
+
+def _allocate_from_type(
+    case: Case,
+    arg_type: ts.TypeSpec,
+    sizes: dict[common.Dimension, int],
+    strategy: DataInitializer,
+    dtype: Optional[np.typing.DTypeLike] = None,
+    tuple_start: Optional[int] = None,
+) -> FieldViewArg:
+    """Allocate data based on the type or a (nested) tuple thereof."""
+    match arg_type:
+        case ts.FieldType(dims=dims, dtype=arg_dtype):
+            return strategy.field(
+                backend=case.backend,
+                sizes={dim: sizes[dim] for dim in dims},
+                dtype=dtype or arg_dtype.kind.name.lower(),
+            )
+        case ts.ScalarType(kind=kind):
+            return strategy.scalar(dtype=dtype or kind.name.lower())
+        case ts.TupleType(types=types):
+            return tuple(
+                (
+                    _allocate_from_type(
+                        case=case, arg_type=t, sizes=sizes, dtype=dtype, strategy=strategy
+                    )
+                    for t in types
+                )
+            )
+        case _:
+            raise TypeError(
+                f"Can not allocate for type {arg_type} with initializer {strategy or 'default'}"
+            )
+
+
+def get_param_types(
+    fieldview_prog: decorator.FieldOperator | decorator.Program,
+) -> dict[str, ts.TypeSpec]:
+    if fieldview_prog.definition is None:
+        raise ValueError(
+            f"test cases do not support {type(fieldview_prog)} with empty .definition attribute (as you would get from .as_program())!"
+        )
+    annotations = xtyping.get_type_hints(fieldview_prog.definition)
+    return {
+        name: type_translation.from_type_hint(type_hint) for name, type_hint in annotations.items()
+    }
+
+
+def get_param_size(param_type: ts.TypeSpec, sizes: dict[common.Dimension, int]) -> int:
+    match param_type:
+        case ts.FieldType(dims=dims):
+            return int(np.prod([sizes[dim] for dim in sizes if dim in dims]))
+        case ts.ScalarType(shape=shape):
+            return int(np.prod(shape)) if shape else 1
+        case ts.TupleType(types):
+            return sum([get_param_size(t, sizes=sizes) for t in types])
+        case _:
+            raise TypeError(f"Can not get size for parameter of type {param_type}")
+
+
+def extend_sizes(
+    sizes: dict[common.Dimension, int],
+    extend: Optional[dict[common.Dimension, tuple[int, int]]] = None,
+) -> dict[common.Dimension, int]:
+    """Calculate the sizes per dimension given a set of extensions."""
+    sizes = sizes.copy()
+    if extend:
+        for dim, (lower, upper) in extend.items():
+            sizes[dim] += upper - lower
+    return sizes
+
+
+def get_default_data(
+    case: Case,
+    fieldview_prog: decorator.FieldOperator | decorator.Program,
+) -> tuple[tuple[common.Field | ScalarValue | tuple, ...], dict[str, common.Field]]:
+    """
+    Allocate default data for a fieldview code object given a test case.
+
+    Meant to reduce boiler plate for simple cases, everything else
+    should rely on ``allocate()``.
+    """
+    param_types = get_param_types(fieldview_prog)
+    kwfields: dict[str, Any] = {}
+    if RETURN in param_types:
+        if not isinstance(param_types[RETURN], types.NoneType):
+            kwfields = {"out": allocate(case, fieldview_prog, RETURN).zeros()()}
+        param_types.pop(RETURN)
+    if "out" in param_types:
+        kwfields = {"out": allocate(case, fieldview_prog, "out").zeros()()}
+        param_types.pop("out")
+    inps = tuple(allocate(case, fieldview_prog, name)() for name in param_types)
+    return inps, kwfields
+
+
+def no_backend(program: itir.FencilDefinition, *args: Any, **kwargs: Any) -> None:
+    """Temporary default backend to not accidentally test the wrong backend."""
+    raise ValueError("No backend selected! Backend selection is mandatory in tests.")
+
+
+@dataclasses.dataclass
+class Case:
+    """Parametrizable components for single feature integration tests."""
+
+    backend: ppi.ProgramProcessor
+    offset_provider: dict[str, common.Connectivity | common.Dimension]
+    default_sizes: dict[common.Dimension, int]
+    grid_type: common.GridType
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 6fb9d880..d85da941 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
@@ -38,75 +38,72 @@ from gt4py.next.ffront.foast_passes.type_deduction import FieldOperatorTypeDeduc
 from gt4py.next.iterator.embedded import index_field, np_as_located_field
 from gt4py.next.program_processors.runners import gtfn_cpu
 
-from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import *
+from next_tests.integration_tests.feature_tests import cases
+from next_tests.integration_tests.feature_tests.cases import (
+    E2V,
+    V2E,
+    E2VDim,
+    Edge,
+    IDim,
+    Ioff,
+    JDim,
+    Joff,
+    KDim,
+    Koff,
+    V2EDim,
+    Vertex,
+    cartesian_case,
+    no_default_backend,
+    unstructured_case,
+)
+from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import (
+    Cell,
+    fieldview_backend,
+    reduction_setup,
+    size,
+)
 
 
-def test_copy(fieldview_backend):
-    a_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
-    b_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
-
-    @field_operator(backend=fieldview_backend)
-    def copy(inp: Field[[IDim], float64]) -> Field[[IDim], float64]:
-        field_tuple = (inp, inp)
+def test_copy(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: cases.IJKField) -> cases.IJKField:
+        field_tuple = (a, a)
         field_0 = field_tuple[0]
         field_1 = field_tuple[1]
         return field_0
 
-    copy(a_I_float, out=b_I_float, offset_provider={})
-
-    assert np.allclose(a_I_float, b_I_float)
-
-
-def test_multicopy(fieldview_backend):
-    inp0 = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
-    inp1 = np_as_located_field(IDim)(np.random.randn(size).astype("float32"))
-    out0 = np_as_located_field(IDim)(np.zeros((size), dtype=float64))
-    out1 = np_as_located_field(IDim)(np.zeros((size), dtype=float32))
-
-    @field_operator(backend=fieldview_backend)
-    def multicopy(
-        inp1: Field[[IDim], float64], inp2: Field[[IDim], float32]
-    ) -> tuple[Field[[IDim], float64], Field[[IDim], float32]]:
-        return inp1, inp2
-
-    multicopy(inp0, inp1, out=(out0, out1), offset_provider={})
+    cases.verify_with_default_data(cartesian_case, testee, ref=lambda a: a)
 
-    assert np.allclose(inp0, out0)
-    assert np.allclose(inp1, out1)
 
+def test_multicopy(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: cases.IJKField, b: cases.IJKField) -> tuple[cases.IJKField, cases.IJKField]:
+        return a, b
 
-def test_cartesian_shift(fieldview_backend):
-    a = np_as_located_field(IDim)(np.arange(size + 1, dtype=np.float64))
-    out_I_float = np_as_located_field(IDim)(np.zeros((size,), dtype=float64))
-
-    @field_operator(backend=fieldview_backend)
-    def shift_by_one(inp: Field[[IDim], float64]) -> Field[[IDim], float64]:
-        return inp(Ioff[1])
-
-    @program(backend=fieldview_backend)
-    def fencil(inp: Field[[IDim], float64], out: Field[[IDim], float64]) -> None:
-        shift_by_one(inp, out=out)
-
-    fencil(a, out_I_float, offset_provider={"Ioff": IDim})
-
-    assert np.allclose(out_I_float.array(), np.arange(1, 11))
+    cases.verify_with_default_data(cartesian_case, testee, ref=lambda a, b: (a, b))
 
 
-def test_unstructured_shift(reduction_setup, fieldview_backend):
-    E2V = reduction_setup.E2V
+def test_cartesian_shift(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: cases.IJKField) -> cases.IJKField:
+        return a(Ioff[1])
 
-    a = np_as_located_field(Vertex)(np.arange(0, reduction_setup.num_vertices, dtype=np.float64))
-    b = np_as_located_field(Edge)(np.zeros(reduction_setup.num_edges))
+    a = cases.allocate(cartesian_case, testee, "a").extend({IDim: (0, 1)})()
+    out = cases.allocate(cartesian_case, testee, cases.RETURN)()
 
-    @field_operator(backend=fieldview_backend)
-    def shift_unstructured(inp: Field[[Vertex], float64]) -> Field[[Edge], float64]:
-        return inp(E2V[0])
+    cases.verify(cartesian_case, testee, a, out=out, ref=a[1:])
 
-    shift_unstructured(a, out=b, offset_provider={"E2V": reduction_setup.offset_provider["E2V"]})
 
-    ref = np.asarray(a)[reduction_setup.offset_provider["E2V"].table[slice(0, None), 0]]
+def test_unstructured_shift(unstructured_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: cases.VField) -> cases.EField:
+        return a(E2V[0])
 
-    assert np.allclose(b, ref)
+    cases.verify_with_default_data(
+        unstructured_case,
+        testee,
+        ref=lambda a: a[unstructured_case.offset_provider["E2V"].table[:, 0]],
+    )
 
 
 def test_composed_unstructured_shift(reduction_setup, fieldview_backend):
@@ -149,223 +146,187 @@ def test_composed_unstructured_shift(reduction_setup, fieldview_backend):
         assert np.allclose(b, ref)
 
 
-def test_fold_shifts(fieldview_backend):
+def test_fold_shifts(cartesian_case):  # noqa: F811 # fixtures
     """Shifting the result of an addition should work."""
-    a = np_as_located_field(IDim)(np.arange(size + 1, dtype=np.float64))
-    b = np_as_located_field(IDim)(np.ones((size + 2)) * 2)
-    out_I_float = np_as_located_field(IDim)(np.zeros((size,), dtype=float64))
 
-    @field_operator(backend=fieldview_backend)
-    def auto_lift(
-        inp1: Field[[IDim], float64], inp2: Field[[IDim], float64]
-    ) -> Field[[IDim], float64]:
-        tmp = inp1 + inp2(Ioff[1])
+    @field_operator
+    def testee(a: cases.IJKField, b: cases.IJKField) -> cases.IJKField:
+        tmp = a + b(Ioff[1])
         return tmp(Ioff[1])
 
-    @program(backend=fieldview_backend)
-    def fencil(
-        inp1: Field[[IDim], float64], inp2: Field[[IDim], float64], out: Field[[IDim], float64]
-    ) -> None:
-        auto_lift(inp1, inp2, out=out)
-
-    fencil(a, b, out_I_float, offset_provider={"Ioff": IDim})
-
-    assert np.allclose(a[1:] + b[2:], out_I_float)
+    a = cases.allocate(cartesian_case, testee, "a").extend({cases.IDim: (0, 1)})()
+    b = cases.allocate(cartesian_case, testee, "b").extend({cases.IDim: (0, 2)})()
+    out = cases.allocate(cartesian_case, testee, cases.RETURN)()
 
+    cases.verify(cartesian_case, testee, a, b, out=out, ref=a[1:] + b[2:])
 
-def test_tuples(fieldview_backend):
-    a_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
-    b_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
-    out_I_float = np_as_located_field(IDim)(np.zeros((size,), dtype=float64))
 
-    @field_operator(backend=fieldview_backend)
-    def tuples(
-        inp1: Field[[IDim], float64], inp2: Field[[IDim], float64]
-    ) -> Field[[IDim], float64]:
-        inps = inp1, inp2
+def test_tuples(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: cases.IJKFloatField, b: cases.IJKFloatField) -> cases.IJKFloatField:
+        inps = a, b
         scalars = 1.3, float64(5.0), float64("3.4")
         return (inps[0] * scalars[0] + inps[1] * scalars[1]) * scalars[2]
 
-    @program(backend=fieldview_backend)
-    def fencil(
-        inp1: Field[[IDim], float64], inp2: Field[[IDim], float64], out: Field[[IDim], float64]
-    ) -> None:
-        tuples(inp1, inp2, out=out)
-
-    fencil(a_I_float, b_I_float, out_I_float, offset_provider={})
-
-    assert np.allclose((a_I_float.array() * 1.3 + b_I_float.array() * 5.0) * 3.4, out_I_float)
+    cases.verify_with_default_data(
+        cartesian_case, testee, ref=lambda a, b: (a * 1.3 + b * 5.0) * 3.4
+    )
 
 
-def test_scalar_arg(fieldview_backend):
+def test_scalar_arg(unstructured_case):  # noqa: F811 # fixtures
     """Test scalar argument being turned into 0-dim field."""
-    inp = 5.0
-    out = np_as_located_field(Vertex)(np.zeros([size]))
-
-    @field_operator(backend=fieldview_backend)
-    def scalar_arg(scalar_inp: float64) -> Field[[Vertex], float64]:
-        return broadcast(scalar_inp + 1.0, (Vertex,))
-
-    scalar_arg(inp, out=out, offset_provider={})
-
-    ref = np.full([size], 6.0)
-    assert np.allclose(ref, out.array())
-
-
-def test_nested_scalar_arg(fieldview_backend):
-    inp = 5.0
-    out = np_as_located_field(Vertex)(np.zeros([size]))
 
-    @field_operator(backend=fieldview_backend)
-    def scalar_arg_inner(scalar_inp: float64) -> Field[[Vertex], float64]:
-        return broadcast(scalar_inp + 1.0, (Vertex,))
-
-    @field_operator(backend=fieldview_backend)
-    def scalar_arg(scalar_inp: float64) -> Field[[Vertex], float64]:
-        return scalar_arg_inner(scalar_inp + 1.0)
-
-    scalar_arg(inp, out=out, offset_provider={})
+    @field_operator
+    def testee(a: int) -> cases.VField:
+        return broadcast(a + 1, (Vertex,))
+
+    cases.verify_with_default_data(
+        unstructured_case,
+        testee,
+        ref=lambda a: np.full(
+            [unstructured_case.default_sizes[Vertex]],
+            a + 1,
+            dtype=int,
+        ),
+        comparison=lambda a, b: np.all(a == b),
+    )
 
-    ref = np.full([size], 7.0)
-    assert np.allclose(ref, out.array())
 
+def test_nested_scalar_arg(unstructured_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee_inner(a: int) -> cases.VField:
+        return broadcast(a + 1, (Vertex,))
 
-def test_scalar_arg_with_field(fieldview_backend):
-    if fieldview_backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
-        pytest.xfail("IndexFields and ConstantFields are not supported yet.")
+    @field_operator
+    def testee(a: int) -> cases.VField:
+        return testee_inner(a + 1)
 
-    inp = index_field(Edge, dtype=float64)
-    factor = 3.0
-    out = np_as_located_field(Edge)(np.zeros((size,), dtype=np.float64))
+    cases.verify_with_default_data(
+        unstructured_case,
+        testee,
+        ref=lambda a: np.full([unstructured_case.default_sizes[Vertex]], a + 2, dtype=int),
+    )
 
-    @field_operator(backend=fieldview_backend)
-    def scalar_and_field_args(
-        inp: Field[[Edge], float64], factor: float64
-    ) -> Field[[Edge], float64]:
-        tmp = factor * inp
-        return tmp(EdgeOffset[1])
 
-    @program(backend=fieldview_backend)
-    def fencil(out: Field[[Edge], float64], inp: Field[[Edge], float64], factor: float64) -> None:
-        scalar_and_field_args(inp, factor, out=out)
+def test_scalar_arg_with_field(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: cases.IJKField, b: int) -> cases.IJKField:
+        tmp = b * a
+        return tmp(Ioff[1])
 
-    fencil(out, inp, factor, offset_provider={"EdgeOffset": Edge})
+    a = cases.allocate(cartesian_case, testee, "a").extend({IDim: (0, 1)})()
+    b = cases.allocate(cartesian_case, testee, "b")()
+    out = cases.allocate(cartesian_case, testee, cases.RETURN)()
+    ref = a.array()[1:] * b
 
-    ref = np.arange(1, size + 1) * factor
-    assert np.allclose(ref, out.array())
+    cases.verify(cartesian_case, testee, a, b, out=out, ref=ref)
 
 
-def test_scalar_in_domain_spec_and_fo_call(fieldview_backend):
-    if fieldview_backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
+def test_scalar_in_domain_spec_and_fo_call(cartesian_case):  # noqa: F811 # fixtures
+    if cartesian_case.backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
         pytest.xfail(
             "Scalar arguments not supported to be used in both domain specification "
             "and as an argument to a field operator."
         )
 
-    size = 10
-    out = np_as_located_field(Vertex)(np.zeros(10, dtype=int))
-
-    @field_operator(backend=fieldview_backend)
-    def foo(size: int) -> Field[[Vertex], int]:
-        return broadcast(size, (Vertex,))
-
-    @program(backend=fieldview_backend)
-    def bar(size: int, out: Field[[Vertex], int]):
-        foo(size, out=out, domain={Vertex: (0, size)})
+    @field_operator
+    def testee_op(size: int) -> cases.IField:
+        return broadcast(size, (IDim,))
 
-    bar(size, out, offset_provider={})
+    @program
+    def testee(size: int, out: cases.IField):
+        testee_op(size, out=out, domain={IDim: (0, size)})
 
-    assert (out.array() == size).all()
+    size = cartesian_case.default_sizes[IDim]
+    out = cases.allocate(cartesian_case, testee, "out").zeros()()
 
+    cases.verify(
+        cartesian_case, testee, size, out=out, ref=np.full_like(out.array(), size, dtype=int)
+    )
 
-def test_scalar_scan(fieldview_backend):
-    size = 10
-    KDim = Dimension("K", kind=DimensionKind.VERTICAL)
-    qc = np_as_located_field(IDim, KDim)(np.zeros((size, size)))
-    scalar = 1.0
-    expected = np.full((size, size), np.arange(start=1, stop=11, step=1).astype(float64))
 
+def test_scalar_scan(cartesian_case):  # noqa: F811 # fixtures
     @scan_operator(axis=KDim, forward=True, init=(0.0))
-    def _scan_scalar(carry: float, qc_in: float, scalar: float) -> float:
-        qc = qc_in + carry + scalar
+    def testee_scan(state: float, qc_in: float, scalar: float) -> float:
+        qc = qc_in + state + scalar
         return qc
 
-    @program(backend=fieldview_backend)
-    def scan_scalar(qc: Field[[IDim, KDim], float], scalar: float):
-        _scan_scalar(qc, scalar, out=qc)
+    @program
+    def testee(qc: Field[[IDim, KDim], float], scalar: float):
+        testee_scan(qc, scalar, out=qc)
 
-    scan_scalar(qc, scalar, offset_provider={})
-    assert np.allclose(np.asarray(qc), expected)
+    qc = cases.allocate(cartesian_case, testee, "qc").zeros()()
+    scalar = 1.0
+    ksize = cartesian_case.default_sizes[KDim]
+    expected = np.full((ksize, ksize), np.arange(start=1, stop=11, step=1).astype(float64))
 
+    cases.verify(cartesian_case, testee, qc, scalar, inout=qc, ref=expected)
 
-def test_tuple_scalar_scan(fieldview_backend):
-    if fieldview_backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
-        pytest.xfail("Scalar tuple arguments are not supported in gtfn yet.")
 
-    size = 10
-    KDim = Dimension("K", kind=DimensionKind.VERTICAL)
-    qc = np_as_located_field(IDim, KDim)(np.zeros((size, size)))
-    tuple_scalar = (1.0, (1.0, 0.0))
-    expected = np.full((size, size), np.arange(start=1, stop=11, step=1).astype(float64))
+def test_tuple_scalar_scan(cartesian_case):  # noqa: F811 # fixtures
+    if cartesian_case.backend in [gtfn_cpu.run_gtfn, gtfn_cpu.run_gtfn_imperative]:
+        pytest.xfail("Scalar tuple arguments are not supported in gtfn yet.")
 
     @scan_operator(axis=KDim, forward=True, init=0.0)
-    def _scan_tuple_scalar(
+    def testee_scan(
         state: float, qc_in: float, tuple_scalar: tuple[float, tuple[float, float]]
     ) -> float:
         return (qc_in + state + tuple_scalar[1][0] + tuple_scalar[1][1]) / tuple_scalar[0]
 
-    @field_operator(backend=fieldview_backend)
-    def scan_tuple_scalar(
+    @field_operator
+    def testee_op(
         qc: Field[[IDim, KDim], float], tuple_scalar: tuple[float, tuple[float, float]]
     ) -> Field[[IDim, KDim], float]:
-        return _scan_tuple_scalar(qc, tuple_scalar)
-
-    scan_tuple_scalar(qc, tuple_scalar, out=qc, offset_provider={})
-    assert np.allclose(np.asarray(qc), expected)
-
-
-def test_astype_int(fieldview_backend):
-    size = 10
-    b_float_64 = np_as_located_field(IDim)(np.full((size,), fill_value=1.5, dtype=np.float64))
-    c_int64 = np_as_located_field(IDim)(np.ones((size,), dtype=np.int64))
-    out_int_64 = np_as_located_field(IDim)(np.zeros((size,), dtype=np.int64))
-
-    @field_operator(backend=fieldview_backend)
-    def astype_fieldop_int(b: Field[[IDim], float64]) -> Field[[IDim], int64]:
-        d = astype(b, int64)
-        return d
-
-    astype_fieldop_int(b_float_64, out=out_int_64, offset_provider={})
-    assert np.allclose(c_int64.array(), out_int_64)
+        return testee_scan(qc, tuple_scalar)
 
+    qc = cases.allocate(cartesian_case, testee_op, "qc").zeros()()
+    tuple_scalar = (1.0, (1.0, 0.0))
+    ksize = cartesian_case.default_sizes[KDim]
+    expected = np.full((ksize, ksize), np.arange(start=1, stop=11, step=1).astype(float64))
 
-def test_astype_bool(fieldview_backend):
-    b_float_64 = np_as_located_field(IDim)(np.full((size,), fill_value=0.5, dtype=np.float64))
-    c_bool = np_as_located_field(IDim)(np.ones((size,), dtype=bool))
-    out_bool = np_as_located_field(IDim)(np.zeros((size,), dtype=bool))
+    cases.verify(cartesian_case, testee_op, qc, tuple_scalar, out=qc, ref=expected)
 
-    @field_operator(backend=fieldview_backend)
-    def astype_fieldop_bool(b: Field[[IDim], float64]) -> Field[[IDim], bool]:
-        d = astype(b, bool)
-        return d
 
-    astype_fieldop_bool(b_float_64, out=out_bool, offset_provider={})
-    assert np.allclose(c_bool, out_bool)
+def test_astype_int(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: Field[[IDim], float64]) -> Field[[IDim], int64]:
+        b = astype(a, int64)
+        return b
+
+    cases.verify_with_default_data(
+        cartesian_case,
+        testee,
+        ref=lambda a: a.astype(int),
+        comparison=lambda a, b: np.all(a == b),
+    )
 
 
-def test_astype_float(fieldview_backend):
-    c_float64 = np_as_located_field(IDim)(
-        np.full((size,), fill_value=np.float64("5e300"), dtype=np.float64)
+def test_astype_bool(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: Field[[IDim], float64]) -> Field[[IDim], bool]:
+        b = astype(a, bool)
+        return b
+
+    cases.verify_with_default_data(
+        cartesian_case,
+        testee,
+        ref=lambda a: a.astype(bool),
+        comparison=lambda a, b: np.all(a == b),
     )
-    out_int_32 = np_as_located_field(IDim)(np.zeros((size,), dtype=np.float32))
 
-    @field_operator(backend=fieldview_backend)
-    def astype_fieldop_float(b: Field[[IDim], float64]) -> Field[[IDim], np.float32]:
-        d = astype(b, float32)
-        return d
 
-    astype_fieldop_float(c_float64, out=out_int_32, offset_provider={})
-    assert np.all(out_int_32.array() == np.inf)
+def test_astype_float(cartesian_case):  # noqa: F811 # fixtures
+    @field_operator
+    def testee(a: Field[[IDim], float64]) -> Field[[IDim], np.float32]:
+        b = astype(a, float32)
+        return b
+
+    cases.verify_with_default_data(
+        cartesian_case,
+        testee,
+        ref=lambda a: a.astype(np.float32),
+        comparison=lambda a, b: np.all(a == b),
+    )
 
 
 def test_offset_field(fieldview_backend):
@@ -499,7 +460,6 @@ def test_tuple_return_2(reduction_setup, fieldview_backend):
     assert np.allclose(ref, rs.out)
 
 
-@pytest.mark.xfail
 def test_tuple_with_local_field_in_reduction_shifted(reduction_setup, fieldview_backend):
     rs = reduction_setup
     V2EDim = rs.V2EDim
@@ -519,12 +479,7 @@ def test_tuple_with_local_field_in_reduction_shifted(reduction_setup, fieldview_
         edge_field: Field[[Edge], float64], vertex_field: Field[[Vertex], float64]
     ) -> Field[[Edge], float64]:
         tup = edge_field(V2E), vertex_field
-        # the shift inside the reduction fails as tup is a tuple of iterators
-        #  (as it contains a local field) which can not be shifted
         red = neighbor_sum(tup[0] + vertex_field, axis=V2EDim)
-        # even if the above is fixed we need to be careful with a subsequent
-        #  shift as the lifted lambda will contain tup as an argument which -
-        #  again - can not be shifted.
         return red(E2V[0])
 
     reduce_tuple_element(a, b, out=out, offset_provider=rs.offset_provider)
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_foast_pretty_printer.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_foast_pretty_printer.py
index e9f22383..e13410e4 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_foast_pretty_printer.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_foast_pretty_printer.py
@@ -18,6 +18,7 @@ import textwrap
 import pytest
 
 from gt4py.next.common import Dimension, DimensionKind, Field
+from gt4py.next.ffront.ast_passes import single_static_assign as ssa
 from gt4py.next.ffront.decorator import field_operator, scan_operator
 from gt4py.next.ffront.fbuiltins import int64
 from gt4py.next.ffront.foast_pretty_printer import pretty_format
@@ -80,10 +81,10 @@ def test_scanop():
         return inp
 
     expected = textwrap.dedent(
-        """
+        f"""
         @scan_operator(axis=Dimension(value="KDim", kind=DimensionKind.VERTICAL), forward=False, init=1.0)
         def scan(inp: int64) -> int64:
-          foo__0 = inp
+          {ssa.unique_name("foo", 0)} = inp
           return inp
         """
     ).strip()
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 d01a1bef..87152819 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
@@ -153,6 +153,20 @@ def test_reduction_expression(reduction_setup, fieldview_backend):
     assert np.allclose(ref, rs.out.array())
 
 
+def test_reduction_with_common_expression(reduction_setup, fieldview_backend):
+    rs = reduction_setup
+    V2EDim, V2E = rs.V2EDim, rs.V2E
+
+    @field_operator(backend=fieldview_backend)
+    def testee(flux: Field[[Edge], int64]) -> Field[[Vertex], int64]:
+        return neighbor_sum(flux(V2E) + flux(V2E), axis=V2EDim)
+
+    testee(rs.inp, out=rs.out, offset_provider=rs.offset_provider)
+
+    ref = np.sum(rs.v2e_table * 2, axis=1)
+    assert np.allclose(ref, rs.out.array())
+
+
 def test_conditional_nested_tuple(fieldview_backend):
     a_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
     b_I_float = np_as_located_field(IDim)(np.random.randn(size).astype("float64"))
diff --git a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_type_deduction.py b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_type_deduction.py
index 110d8937..894c496a 100644
--- a/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_type_deduction.py
+++ b/tests/next_tests/integration_tests/feature_tests/ffront_tests/test_type_deduction.py
@@ -18,6 +18,7 @@ import pytest
 
 import gt4py.next.ffront.type_specifications
 from gt4py.next.common import DimensionKind, GTTypeError
+from gt4py.next.ffront.ast_passes import single_static_assign as ssa
 from gt4py.next.ffront.experimental import as_offset
 from gt4py.next.ffront.fbuiltins import (
     Dimension,
@@ -302,11 +303,11 @@ def test_unpack_assign():
 
     parsed = FieldOperatorParser.apply_to_function(unpack_explicit_tuple)
 
-    assert parsed.body.annex.symtable["tmp_a__0"].type == ts.FieldType(
+    assert parsed.body.annex.symtable[ssa.unique_name("tmp_a", 0)].type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
-    assert parsed.body.annex.symtable["tmp_b__0"].type == ts.FieldType(
+    assert parsed.body.annex.symtable[ssa.unique_name("tmp_b", 0)].type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
@@ -364,7 +365,7 @@ def test_assign_tuple():
 
     parsed = FieldOperatorParser.apply_to_function(temp_tuple)
 
-    assert parsed.body.annex.symtable["tmp__0"].type == ts.TupleType(
+    assert parsed.body.annex.symtable[ssa.unique_name("tmp", 0)].type == ts.TupleType(
         types=[
             ts.FieldType(
                 dims=[TDim],
diff --git a/tests/next_tests/integration_tests/feature_tests/test_util_cases.py b/tests/next_tests/integration_tests/feature_tests/test_util_cases.py
new file mode 100644
index 00000000..3b5cf1a5
--- /dev/null
+++ b/tests/next_tests/integration_tests/feature_tests/test_util_cases.py
@@ -0,0 +1,103 @@
+# 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 numpy as np
+import pytest
+
+from gt4py.next import common
+from gt4py.next.ffront.decorator import field_operator
+from gt4py.next.program_processors.runners import roundtrip
+
+from next_tests.integration_tests.feature_tests import cases
+from next_tests.integration_tests.feature_tests.cases import (  # noqa: F401 # fixtures
+    cartesian_case,
+    fieldview_backend,
+    no_default_backend,
+)
+
+
+@field_operator
+def addition(a: cases.IJKField, b: cases.IJKField) -> cases.IJKField:
+    return a + b
+
+
+@field_operator
+def mixed_args(
+    a: cases.IJKField, b: np.float64, c: cases.IJKField
+) -> tuple[cases.IJKField, tuple[cases.IJKField, cases.IJKField]]:
+    return (a, (c, a))
+
+
+def test_allocate_default_unique(cartesian_case):  # noqa: F811 # fixtures
+    a = cases.allocate(cartesian_case, mixed_args, "a")()
+
+    assert np.min(a.array()) == 0
+    assert np.max(a.array()) == np.prod(tuple(cartesian_case.default_sizes.values())) - 1
+
+    b = cases.allocate(cartesian_case, mixed_args, "b")()
+
+    assert b == np.max(a.array()) + 1
+
+    c = cases.allocate(cartesian_case, mixed_args, "c")()
+
+    assert np.min(c.array()) == b + 1
+    assert np.max(c.array()) == np.prod(tuple(cartesian_case.default_sizes.values())) * 2
+
+
+def test_allocate_return_default_zeros(cartesian_case):  # noqa: F811 # fixtures
+    a, (b, c) = cases.allocate(cartesian_case, mixed_args, cases.RETURN)()
+
+    assert np.all(a.array() == 0)
+    assert np.all(a.array() == b.array())
+    assert np.all(b.array() == c.array())
+
+
+def test_allocate_const(cartesian_case):  # noqa: F811 # fixtures
+    a = cases.allocate(cartesian_case, mixed_args, "a").strategy(cases.ConstInitializer(42))()
+    assert np.all(a.array() == 42)
+
+    b = cases.allocate(cartesian_case, mixed_args, "b").strategy(cases.ConstInitializer(42))()
+    assert b == 42.0
+
+
+@pytest.mark.parametrize("fieldview_backend", [roundtrip.executor])
+def test_verify_fails_with_wrong_reference(cartesian_case):  # noqa: F811 # fixtures
+    a = cases.allocate(cartesian_case, addition, "a")()
+    b = cases.allocate(cartesian_case, addition, "b")()
+    out = cases.allocate(cartesian_case, addition, cases.RETURN)()
+    wrong_ref = a
+
+    with pytest.raises(AssertionError):
+        cases.verify(cartesian_case, addition, a, b, out=out, ref=wrong_ref)
+
+
+@pytest.mark.parametrize("fieldview_backend", [roundtrip.executor])
+def test_verify_fails_with_wrong_type(cartesian_case):  # noqa: F811 # fixtures
+    a = cases.allocate(cartesian_case, addition, "a").dtype(np.float32)()
+    b = cases.allocate(cartesian_case, addition, "b")()
+    out = cases.allocate(cartesian_case, addition, cases.RETURN)()
+
+    with pytest.raises(common.GTTypeError):
+        cases.verify(cartesian_case, addition, a, b, out=out, ref=a.array() + b.array())
+
+
+@pytest.mark.parametrize("fieldview_backend", [roundtrip.executor])
+def test_verify_with_default_data_fails_with_wrong_reference(
+    cartesian_case,  # noqa: F811 # fixtures
+):
+    def wrong_ref(a, b):
+        return a - b
+
+    with pytest.raises(AssertionError):
+        cases.verify_with_default_data(cartesian_case, addition, ref=wrong_ref)
diff --git a/tests/next_tests/unit_tests/ffront_tests/ast_passes_tests/test_single_static_assign.py b/tests/next_tests/unit_tests/ffront_tests/ast_passes_tests/test_single_static_assign.py
index 735d6694..052f272d 100644
--- a/tests/next_tests/unit_tests/ffront_tests/ast_passes_tests/test_single_static_assign.py
+++ b/tests/next_tests/unit_tests/ffront_tests/ast_passes_tests/test_single_static_assign.py
@@ -18,7 +18,10 @@
 import ast
 import textwrap
 
-from gt4py.next.ffront.ast_passes.single_static_assign import SingleStaticAssignPass
+from gt4py.next.ffront.ast_passes.single_static_assign import (
+    _UNIQUE_NAME_SEPARATOR as SEP,
+    SingleStaticAssignPass,
+)
 
 
 def ssaify_string(code: str) -> ast.AST:
@@ -36,9 +39,9 @@ def test_sequence():
     )
 
     lines = ast.unparse(ssa_ast).split("\n")
-    assert lines[0] == "tmp__0 = 1"
-    assert lines[1] == "tmp__1 = 2"
-    assert lines[2] == "tmp__2 = 3"
+    assert lines[0] == f"tmp{SEP}0 = 1"
+    assert lines[1] == f"tmp{SEP}1 = 2"
+    assert lines[2] == f"tmp{SEP}2 = 3"
 
 
 def test_self_on_rhs():
@@ -50,8 +53,8 @@ def test_self_on_rhs():
         """
     )
     lines = ast.unparse(ssa_ast).split("\n")
-    assert lines[0] == "tmp__0 = 1"
-    assert lines[1] == "tmp__1 = tmp__0 + 1"
+    assert lines[0] == f"tmp{SEP}0 = 1"
+    assert lines[1] == f"tmp{SEP}1 = tmp{SEP}0 + 1"
 
 
 def test_multi_assign():
@@ -62,7 +65,7 @@ def test_multi_assign():
         """
     )
     lines = ast.unparse(ssa_ast).split("\n")
-    assert lines[0] == "a__0 = a__1 = b__0 = a__2 = b__1 = 1"
+    assert lines[0] == f"a{SEP}0 = a{SEP}1 = b{SEP}0 = a{SEP}2 = b{SEP}1 = 1"
 
 
 def test_external_name_values():
@@ -74,8 +77,8 @@ def test_external_name_values():
         """
     )
     lines = ast.unparse(ssa_ast).split("\n")
-    assert lines[0] == "a__0 = inp"
-    assert lines[1] == "a__1 = a__0 + inp"
+    assert lines[0] == f"a{SEP}0 = inp"
+    assert lines[1] == f"a{SEP}1 = a{SEP}0 + inp"
 
 
 def test_overwrite_external():
@@ -88,9 +91,9 @@ def test_overwrite_external():
         """
     )
     lines = ast.unparse(ssa_ast).split("\n")
-    assert lines[0] == "a__0 = inp"
-    assert lines[1] == "inp__0 = a__0 + inp"
-    assert lines[2] == "b__0 = inp__0"
+    assert lines[0] == f"a{SEP}0 = inp"
+    assert lines[1] == f"inp{SEP}0 = a{SEP}0 + inp"
+    assert lines[2] == f"b{SEP}0 = inp{SEP}0"
 
 
 def test_unpacking_swap():
@@ -103,9 +106,9 @@ def test_unpacking_swap():
         """
     )
     lines = ast.unparse(ssa_ast).split("\n")
-    assert lines[0] == "a__0 = 5"
-    assert lines[1] == "b__0 = 1"
-    assert lines[2] == "(b__1, a__1) = (a__0, b__0)"
+    assert lines[0] == f"a{SEP}0 = 5"
+    assert lines[1] == f"b{SEP}0 = 1"
+    assert lines[2] == f"(b{SEP}1, a{SEP}1) = (a{SEP}0, b{SEP}0)"
 
 
 def test_annotated_assign():
@@ -117,7 +120,7 @@ def test_annotated_assign():
             """
         )
     ).splitlines()
-    assert lines[0] == "a__0: int = 5"
+    assert lines[0] == f"a{SEP}0: int = 5"
 
 
 def test_empty_annotated_assign():
@@ -130,6 +133,669 @@ def test_empty_annotated_assign():
             """
         )
     ).splitlines()
-    assert lines[0] == "a__0 = 0"
-    assert lines[1] == "a__1: int"
-    assert lines[2] == "b__0 = a__0"
+    assert lines[0] == f"a{SEP}0 = 0"
+    assert lines[1] == f"a{SEP}1: int"
+    assert lines[2] == f"b{SEP}0 = a{SEP}0"
+
+
+def test_if():
+    lines = ast.unparse(
+        ssaify_string(
+            """
+            if True:
+                a = 1
+            else:
+                a = 2
+            a = 3
+            """
+        )
+    ).splitlines()
+
+    assert lines[1] == f"    a{SEP}0 = 1"
+    assert lines[3] == f"    a{SEP}0 = 2"
+    assert lines[4] == f"a{SEP}1 = 3"
+
+
+def test_if_variable_condition():
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            if b:
+                a = 1
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if b:
+            a{SEP}0 = 1
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_nested_if():
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            if True:
+                a = 1
+            else:
+                a = 2
+                if True:
+                    a = 3
+                a = 4
+            a = 5
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = 1
+            a{SEP}2 = a{SEP}0
+        else:
+            a{SEP}0 = 2
+            if True:
+                a{SEP}1 = 3
+            else:
+                a{SEP}1 = a{SEP}0
+            a{SEP}2 = 4
+        a{SEP}3 = 5
+    """
+    ).strip()
+
+    assert result == expected
+
+
+def test_nested_if_chain():
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            if True:
+                a = 1
+            else:
+                a = 2
+                if True:
+                    a = a+1
+                a = a+1
+            a = a+1
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = 1
+            a{SEP}2 = a{SEP}0
+        else:
+            a{SEP}0 = 2
+            if True:
+                a{SEP}1 = a{SEP}0 + 1
+            else:
+                a{SEP}1 = a{SEP}0
+            a{SEP}2 = a{SEP}1 + 1
+        a{SEP}3 = a{SEP}2 + 1
+    """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_branch_local():
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            if True:
+                a = 0
+                a = a + 1
+            else:
+                b = 1
+                b = b + 1
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = 0
+            a{SEP}1 = a{SEP}0 + 1
+        else:
+            b{SEP}0 = 1
+            b{SEP}1 = b{SEP}0 + 1
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_only_one_branch():
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            if True:
+                a = 0
+            b = a
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = 0
+        b{SEP}0 = a
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_only_one_branch_other():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            if True:
+                a = 0
+            else:
+                c = 1
+            b = c
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = 0
+        else:
+            c{SEP}0 = 1
+        b{SEP}0 = c
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_nested_all_branches_defined():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            if True:
+                if True:
+                    a = 0
+                    a = a + 1
+                    a = a + 1
+                    a = a + 1
+                    a = a + 1
+                else:
+                    a = 0
+                    a = a + 1
+                    a = a + 1
+            else:
+                a = 0
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            if True:
+                a{SEP}0 = 0
+                a{SEP}1 = a{SEP}0 + 1
+                a{SEP}2 = a{SEP}1 + 1
+                a{SEP}3 = a{SEP}2 + 1
+                a{SEP}4 = a{SEP}3 + 1
+            else:
+                a{SEP}0 = 0
+                a{SEP}1 = a{SEP}0 + 1
+                a{SEP}2 = a{SEP}1 + 1
+                a{SEP}4 = a{SEP}2
+        else:
+            a{SEP}0 = 0
+            a{SEP}4 = a{SEP}0
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_elif_all_branches_defined():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            if True:
+                a = 0
+                a = a + 1
+                a = a + 1
+                a = a + 1
+                a = a + 1
+            elif True:
+                a = 0
+                a = a + 1
+                a = a + 1
+            else:
+                a = 0
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = 0
+            a{SEP}1 = a{SEP}0 + 1
+            a{SEP}2 = a{SEP}1 + 1
+            a{SEP}3 = a{SEP}2 + 1
+            a{SEP}4 = a{SEP}3 + 1
+        else:
+            if True:
+                a{SEP}0 = 0
+                a{SEP}1 = a{SEP}0 + 1
+                a{SEP}2 = a{SEP}1 + 1
+            else:
+                a{SEP}0 = 0
+                a{SEP}2 = a{SEP}0
+            a{SEP}4 = a{SEP}2
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_nested_ifs_single_change():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            a = 0
+            if True:
+                b = 0
+            elif True:
+                if True:
+                    b = 1
+                else:
+                    if True:
+                        b = 2
+                        # because of this nested change, all branches need additions
+                        a = a + 1
+                    else:
+                        b = 3
+            else:
+                b = 4
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        a{SEP}0 = 0
+        if True:
+            b{SEP}0 = 0
+            a{SEP}1 = a{SEP}0
+        elif True:
+            if True:
+                b{SEP}0 = 1
+                a{SEP}1 = a{SEP}0
+            elif True:
+                b{SEP}0 = 2
+                a{SEP}1 = a{SEP}0 + 1
+            else:
+                b{SEP}0 = 3
+                a{SEP}1 = a{SEP}0
+        else:
+            b{SEP}0 = 4
+            a{SEP}1 = a{SEP}0
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_one_sided_inside_function():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f(a):
+                if True:
+                    a = a + 1
+                return a
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f(a):
+            if True:
+                a{SEP}0 = a + 1
+            else:
+                a{SEP}0 = a
+            return a{SEP}0
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_preservers_definite_assignment_analysis1():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f():
+                if True:
+                    a = 1
+                else:
+                    b = 0
+                return a, b
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f():
+            if True:
+                a{SEP}0 = 1
+            else:
+                b{SEP}0 = 0
+            return (a, b)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_preservers_definite_assignment_analysis2():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f():
+                if True:
+                    a = 1
+                else:
+                    a = 0
+                return a, b
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f():
+            if True:
+                a{SEP}0 = 1
+            else:
+                a{SEP}0 = 0
+            return (a{SEP}0, b)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_preservers_definite_assignment_analysis3():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f():
+                a = 0
+                if True:
+                    a = 1
+                return a, b
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f():
+            a{SEP}0 = 0
+            if True:
+                a{SEP}1 = 1
+            else:
+                a{SEP}1 = a{SEP}0
+            return (a{SEP}1, b)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_broken_collisions():
+    # Known bug of the current SSA implementation
+
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            a = a{SEP}0 + 1
+            return a, a{SEP}0
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        a{SEP}0 = a{SEP}0 + 1
+        return (a{SEP}0, a{SEP}0)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_collision_function_parameters():
+    # An earlier version couldn't handle this case correctly
+
+    result = ast.unparse(
+        ssaify_string(
+            f"""
+            def f(a, a{SEP}0):
+                return a{SEP}0
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f(a, a{SEP}0):
+            return a{SEP}0
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_broken_if():
+    # Known bug of the current SSA implementation
+
+    result = ast.unparse(
+        ssaify_string(
+            """
+            if True:
+                a = a + 1
+            return a
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        if True:
+            a{SEP}0 = a + 1
+        return a
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_annotated_assign():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            a: int # annotations always apply to the next assignment
+            a = a + 1
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        a{SEP}0: int
+        a{SEP}0 = a + 1
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_true_branch_returns():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f(a, b):
+                if True:
+                    a = a + 1
+                    d = 5
+                    return a
+                else:
+                    b = b + 1
+                    e = 6
+                return a, b, d, e
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f(a, b):
+            if True:
+                a{SEP}0 = a + 1
+                d{SEP}0 = 5
+                return a{SEP}0
+            else:
+                b{SEP}0 = b + 1
+                e{SEP}0 = 6
+            return (a, b{SEP}0, d, e{SEP}0)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_false_branch_returns():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f(a, b):
+                b = a + 3
+                if True:
+                    b = b + 1
+                    e = 6
+                else:
+                    a = a + 1
+                    d = 5
+                    return a
+                return a, b, d, e
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f(a, b):
+            b{SEP}0 = a + 3
+            if True:
+                b{SEP}1 = b{SEP}0 + 1
+                e{SEP}0 = 6
+            else:
+                a{SEP}0 = a + 1
+                d{SEP}0 = 5
+                return a{SEP}0
+            return (a, b{SEP}1, d, e{SEP}0)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_both_branches_return():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f(a, b):
+                if True:
+                    b = b + 1
+                    e = 6
+                    return e
+                else:
+                    a = a + 1
+                    d = 5
+                    return a
+                return a, b, d, e # this is dead-code
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+        def f(a, b):
+            if True:
+                b{SEP}0 = b + 1
+                e{SEP}0 = 6
+                return e{SEP}0
+            else:
+                a{SEP}0 = a + 1
+                d{SEP}0 = 5
+                return a{SEP}0
+            return (a, b, d, e)
+        """
+    ).strip()
+
+    assert result == expected
+
+
+def test_if_nested_returns():
+    result = ast.unparse(
+        ssaify_string(
+            """
+            def f(a, b):
+                if True:
+                    b = b + 1
+                    e = 6
+                    if e == b:
+                        return e
+                    else:
+                        return b
+                else:
+                    a = a + 1
+                    d = 5
+                return a, b, d, e
+            """
+        )
+    )
+
+    expected = textwrap.dedent(
+        f"""
+            def f(a, b):
+                if True:
+                    b{SEP}0 = b + 1
+                    e{SEP}0 = 6
+                    if e{SEP}0 == b{SEP}0:
+                        return e{SEP}0
+                    else:
+                        return b{SEP}0
+                else:
+                    a{SEP}0 = a + 1
+                    d{SEP}0 = 5
+                return (a{SEP}0, b, d{SEP}0, e)
+        """
+    ).strip()
+
+    assert result == expected
diff --git a/tests/next_tests/unit_tests/ffront_tests/test_foast_to_itir.py b/tests/next_tests/unit_tests/ffront_tests/test_foast_to_itir.py
index f0b437ab..6fdc02cc 100644
--- a/tests/next_tests/unit_tests/ffront_tests/test_foast_to_itir.py
+++ b/tests/next_tests/unit_tests/ffront_tests/test_foast_to_itir.py
@@ -25,6 +25,7 @@ import pytest
 
 from gt4py.next.common import DimensionKind, Field
 from gt4py.next.ffront import type_specifications as ts_ffront
+from gt4py.next.ffront.ast_passes import single_static_assign as ssa
 from gt4py.next.ffront.fbuiltins import (
     Dimension,
     FieldOffset,
@@ -146,11 +147,17 @@ def test_temp_assignment():
     parsed = FieldOperatorParser.apply_to_function(copy_field)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.let(itir.Sym(id="tmp__0", dtype=("float64", False), kind="Iterator"), "inp")(
-        im.let(itir.Sym(id="inp__0", dtype=("float64", False), kind="Iterator"), "tmp__0")(
-            im.let(itir.Sym(id="tmp2__0", dtype=("float64", False), kind="Iterator"), "inp__0")(
-                "tmp2__0"
-            )
+    reference = im.let(
+        itir.Sym(id=ssa.unique_name("tmp", 0), dtype=("float64", False), kind="Iterator"), "inp"
+    )(
+        im.let(
+            itir.Sym(id=ssa.unique_name("inp", 0), dtype=("float64", False), kind="Iterator"),
+            ssa.unique_name("tmp", 0),
+        )(
+            im.let(
+                itir.Sym(id=ssa.unique_name("tmp2", 0), dtype=("float64", False), kind="Iterator"),
+                ssa.unique_name("inp", 0),
+            )(ssa.unique_name("tmp2", 0))
         )
     )
 
@@ -167,17 +174,17 @@ def test_unary_ops():
     lowered = FieldOperatorLowering.apply(parsed)
 
     reference = im.let(
-        itir.Sym(id="tmp__0", dtype=("float64", False), kind="Iterator"),
+        itir.Sym(id=ssa.unique_name("tmp", 0), dtype=("float64", False), kind="Iterator"),
         im.promote_to_lifted_stencil("plus")(
             im.promote_to_const_iterator(im.literal("0", "float64")), "inp"
         ),
     )(
         im.let(
-            itir.Sym(id="tmp__1", dtype=("float64", False), kind="Iterator"),
+            itir.Sym(id=ssa.unique_name("tmp", 1), dtype=("float64", False), kind="Iterator"),
             im.promote_to_lifted_stencil("minus")(
-                im.promote_to_const_iterator(im.literal("0", "float64")), "tmp__0"
+                im.promote_to_const_iterator(im.literal("0", "float64")), ssa.unique_name("tmp", 0)
             ),
-        )("tmp__1")
+        )(ssa.unique_name("tmp", 1))
     )
 
     assert lowered.expr == reference
@@ -200,10 +207,14 @@ def test_unpacking():
     tuple_access_1 = im.promote_to_lifted_stencil(lambda x: im.tuple_get(1, x))("__tuple_tmp_0")
 
     reference = im.let("__tuple_tmp_0", tuple_expr)(
-        im.let(itir.Sym(id="tmp1__0", dtype=("float64", False), kind="Iterator"), tuple_access_0)(
+        im.let(
+            itir.Sym(id=ssa.unique_name("tmp1", 0), dtype=("float64", False), kind="Iterator"),
+            tuple_access_0,
+        )(
             im.let(
-                itir.Sym(id="tmp2__0", dtype=("float64", False), kind="Iterator"), tuple_access_1
-            )("tmp1__0")
+                itir.Sym(id=ssa.unique_name("tmp2", 0), dtype=("float64", False), kind="Iterator"),
+                tuple_access_1,
+            )(ssa.unique_name("tmp1", 0))
         )
     )
 
@@ -220,7 +231,7 @@ def test_annotated_assignment():
     parsed = FieldOperatorParser.apply_to_function(copy_field)
     lowered = FieldOperatorLowering.apply(parsed)
 
-    reference = im.let("tmp__0", "inp")("tmp__0")
+    reference = im.let(ssa.unique_name("tmp", 0), "inp")(ssa.unique_name("tmp", 0))
 
     assert lowered.expr == reference
 
@@ -258,7 +269,7 @@ def test_temp_tuple():
     lowered = FieldOperatorLowering.apply(parsed)
 
     tuple_expr = im.promote_to_lifted_stencil("make_tuple")("a", "b")
-    reference = im.let("tmp__0", tuple_expr)("tmp__0")
+    reference = im.let(ssa.unique_name("tmp", 0), tuple_expr)(ssa.unique_name("tmp", 0))
 
     assert lowered.expr == reference
 
@@ -310,12 +321,12 @@ def test_add_scalar_literals():
     lowered = FieldOperatorLowering.apply(parsed)
 
     reference = im.let(
-        "tmp__0",
+        ssa.unique_name("tmp", 0),
         im.promote_to_lifted_stencil("plus")(
             im.promote_to_const_iterator(im.literal("1", "int32")),
             im.promote_to_const_iterator(im.literal("1", "int32")),
         ),
-    )(im.promote_to_lifted_stencil("plus")("a", "tmp__0"))
+    )(im.promote_to_lifted_stencil("plus")("a", ssa.unique_name("tmp", 0)))
 
     assert lowered.expr == reference
 
@@ -495,11 +506,11 @@ def test_reduction_lowering_expr():
         im.promote_to_lifted_stencil("make_const_list")(
             im.promote_to_const_iterator(im.literal("1.1", "float64"))
         ),
-        im.promote_to_lifted_stencil(im.map_("plus"))("e1_nbh__0", "e2"),
+        im.promote_to_lifted_stencil(im.map_("plus"))(ssa.unique_name("e1_nbh", 0), "e2"),
     )
 
     reference = im.let(
-        itir.Sym(id="e1_nbh__0", dtype=("float64", True), kind="Iterator"),
+        itir.Sym(id=ssa.unique_name("e1_nbh", 0), dtype=("float64", True), kind="Iterator"),
         im.lifted_neighbors("V2E", "e1"),
     )(
         im.promote_to_lifted_stencil(
diff --git a/tests/next_tests/unit_tests/ffront_tests/test_func_to_foast.py b/tests/next_tests/unit_tests/ffront_tests/test_func_to_foast.py
index c444cfc8..5a892070 100644
--- a/tests/next_tests/unit_tests/ffront_tests/test_func_to_foast.py
+++ b/tests/next_tests/unit_tests/ffront_tests/test_func_to_foast.py
@@ -42,6 +42,7 @@ import pytest
 from gt4py.eve.pattern_matching import ObjectPattern as P
 from gt4py.next.common import Field, GTTypeError
 from gt4py.next.ffront import field_operator_ast as foast
+from gt4py.next.ffront.ast_passes import single_static_assign as ssa
 from gt4py.next.ffront.fbuiltins import (
     Dimension,
     astype,
@@ -172,7 +173,7 @@ def test_temp_assignment():
 
     parsed = FieldOperatorParser.apply_to_function(copy_field)
 
-    assert parsed.body.annex.symtable["tmp__0"].type == ts.FieldType(
+    assert parsed.body.annex.symtable[ssa.unique_name("tmp", 0)].type == ts.FieldType(
         dims=[TDim],
         dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64, shape=None),
     )
diff --git a/tests/next_tests/unit_tests/ffront_tests/test_past_to_itir.py b/tests/next_tests/unit_tests/ffront_tests/test_past_to_itir.py
index b543e98e..d8d46aed 100644
--- a/tests/next_tests/unit_tests/ffront_tests/test_past_to_itir.py
+++ b/tests/next_tests/unit_tests/ffront_tests/test_past_to_itir.py
@@ -156,14 +156,22 @@ def test_inout_prohibited(identity_def):
         GTTypeError,
         match=(r"Call to function with field as input and output not allowed."),
     ):
-        ProgramLowering.apply(ProgramParser.apply_to_function(inout_field_program))
+        ProgramLowering.apply(
+            ProgramParser.apply_to_function(inout_field_program),
+            function_definitions=[],
+            grid_type=GridType.CARTESIAN,
+        )
 
 
 def test_invalid_call_sig_program(invalid_call_sig_program_def):
     with pytest.raises(
         GTTypeError,
     ) as exc_info:
-        ProgramLowering.apply(ProgramParser.apply_to_function(invalid_call_sig_program_def))
+        ProgramLowering.apply(
+            ProgramParser.apply_to_function(invalid_call_sig_program_def),
+            function_definitions=[],
+            grid_type=GridType.CARTESIAN,
+        )
 
     assert exc_info.match("Invalid call to `identity`")
     # TODO(tehrengruber): re-enable again when call signature check doesn't return
diff --git a/tests/next_tests/unit_tests/iterator_tests/test_embedded_internals.py b/tests/next_tests/unit_tests/iterator_tests/test_embedded_internals.py
index 15b3d177..3a35570c 100644
--- a/tests/next_tests/unit_tests/iterator_tests/test_embedded_internals.py
+++ b/tests/next_tests/unit_tests/iterator_tests/test_embedded_internals.py
@@ -12,85 +12,138 @@
 #
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+import contextvars as cvars
+import threading
+from typing import Any, Callable, Optional
+
 import numpy as np
 import pytest
 
-from gt4py.next.iterator.embedded import Column
+from gt4py.next.iterator import embedded
+
+
+def _run_within_context(
+    func: Callable[[], Any],
+    *,
+    column_range: Optional[range] = None,
+    offset_provider: Optional[embedded.OffsetProvider] = None,
+) -> Any:
+    def wrapped_func():
+        embedded.column_range_cvar.set(column_range)
+        embedded.offset_provider_cvar.set(offset_provider)
+        func()
+
+    cvars.copy_context().run(wrapped_func)
 
 
 def test_column_ufunc():
-    a = Column(1, np.asarray(range(0, 3)))
-    b = Column(1, np.asarray(range(3, 6)))
+    def test_func():
+        a = embedded.Column(1, np.asarray(range(0, 3)))
+        b = embedded.Column(1, np.asarray(range(3, 6)))
+        res = a + b
+
+        assert isinstance(res, embedded.Column)
+        assert np.array_equal(res.data, a.data + b.data)
+        assert res.kstart == 1
+
+    _run_within_context(test_func)
+
+    def test_func(data_a: int, data_b: int):
+        a = embedded.Column(1, data_a)
+        b = embedded.Column(1, data_b)
+        res = a + b
 
-    res = a + b
-    assert isinstance(res, Column)
-    assert np.array_equal(res.data, a.data + b.data)
-    assert res.kstart == 1
+        assert isinstance(res, embedded.Column)
+        assert np.array_equal(res.data, a.data + b.data)
+        assert res.kstart == 1
+
+    # Setting an invalid column_range here shouldn't affect other contexts
+    embedded.column_range_cvar.set(range(2, 999))
+    _run_within_context(lambda: test_func(2, 3), column_range=range(0, 3))
 
 
 def test_column_ufunc_with_scalar():
-    a = Column(1, np.asarray(range(0, 3)))
-    res = 1.0 + a
-    assert isinstance(res, Column)
-    assert np.array_equal(res.data, a.data + 1.0)
-    assert res.kstart == 1
+    def test_func():
+        a = embedded.Column(1, np.asarray(range(0, 3)))
+        res = 1.0 + a
+        assert isinstance(res, embedded.Column)
+        assert np.array_equal(res.data, a.data + 1.0)
+        assert res.kstart == 1
 
 
 def test_column_ufunc_wrong_kstart():
-    a = Column(1, np.asarray(range(0, 3)))
-    wrong_kstart = Column(2, np.asarray(range(3, 6)))
+    def test_func():
+        a = embedded.Column(1, np.asarray(range(0, 3)))
+        wrong_kstart = embedded.Column(2, np.asarray(range(3, 6)))
+
+        with pytest.raises(ValueError):
+            a + wrong_kstart
 
-    with pytest.raises(ValueError):
-        a + wrong_kstart
+    _run_within_context(test_func)
 
 
 def test_column_ufunc_wrong_shape():
-    a = Column(1, np.asarray(range(0, 3)))
-    wrong_shape = Column(1, np.asarray([1, 2]))
+    def test_func():
+        a = embedded.Column(1, np.asarray(range(0, 3)))
+        wrong_shape = embedded.Column(1, np.asarray([1, 2]))
 
-    with pytest.raises(ValueError):
-        a + wrong_shape
+        with pytest.raises(ValueError):
+            a + wrong_shape
+
+    _run_within_context(test_func)
 
 
 def test_column_array_function():
-    cond = Column(1, np.asarray([True, False]))
-    a = Column(1, np.asarray([1, 1]))
-    b = Column(1, np.asarray([2, 2]))
+    def test_func():
+        cond = embedded.Column(1, np.asarray([True, False]))
+        a = embedded.Column(1, np.asarray([1, 1]))
+        b = embedded.Column(1, np.asarray([2, 2]))
+
+        res = np.where(cond, a, b)
+        ref = np.asarray([1, 2])
 
-    res = np.where(cond, a, b)
-    ref = np.asarray([1, 2])
+        assert isinstance(res, embedded.Column)
+        assert np.array_equal(res.data, ref)
+        assert res.kstart == 1
 
-    assert isinstance(res, Column)
-    assert np.array_equal(res.data, ref)
-    assert res.kstart == 1
+    _run_within_context(test_func)
 
 
 def test_column_array_function_with_scalar():
-    cond = Column(1, np.asarray([True, False]))
-    a = 1
-    b = Column(1, np.asarray([2, 2]))
+    def test_func():
+        cond = embedded.Column(1, np.asarray([True, False]))
+        a = 1
+        b = embedded.Column(1, np.asarray([2, 2]))
 
-    res = np.where(cond, a, b)
-    ref = np.asarray([1, 2])
+        res = np.where(cond, a, b)
+        ref = np.asarray([1, 2])
 
-    assert isinstance(res, Column)
-    assert np.array_equal(res.data, ref)
-    assert res.kstart == 1
+        assert isinstance(res, embedded.Column)
+        assert np.array_equal(res.data, ref)
+        assert res.kstart == 1
+
+    _run_within_context(test_func)
 
 
 def test_column_array_function_wrong_kstart():
-    cond = Column(1, np.asarray([True, False]))
-    wrong_kstart = Column(2, np.asarray([1, 1]))
-    b = Column(1, np.asarray([2, 2]))
+    def test_func():
+        cond = embedded.Column(1, np.asarray([True, False]))
+        wrong_kstart = embedded.Column(2, np.asarray([1, 1]))
+        b = embedded.Column(1, np.asarray([2, 2]))
+
+        with pytest.raises(ValueError):
+            np.where(cond, wrong_kstart, b)
 
-    with pytest.raises(ValueError):
-        np.where(cond, wrong_kstart, b)
+    _run_within_context(test_func)
 
 
 def test_column_array_function_wrong_shape():
-    cond = Column(1, np.asarray([True, False]))
-    wrong_shape = Column(2, np.asarray([1, 1, 1]))
-    b = Column(1, np.asarray([2, 2]))
+    def test_func():
+        cond = embedded.Column(1, np.asarray([True, False]))
+        wrong_shape = embedded.Column(2, np.asarray([1, 1, 1]))
+        b = embedded.Column(1, np.asarray([2, 2]))
+
+        with pytest.raises(ValueError):
+            np.where(cond, wrong_shape, b)
 
-    with pytest.raises(ValueError):
-        np.where(cond, wrong_shape, b)
+    _run_within_context(test_func)
diff --git a/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_collapse_tuple.py b/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_collapse_tuple.py
index 5860c0c6..736bf04d 100644
--- a/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_collapse_tuple.py
+++ b/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_collapse_tuple.py
@@ -14,47 +14,62 @@
 
 import pytest
 
-from gt4py.next.iterator import ir
+from gt4py.next.iterator import ir, ir_makers as im
 from gt4py.next.iterator.transforms.collapse_tuple import CollapseTuple
 
 
-def _tuple_get(i: int, t: ir.Expr):
-    return ir.FunCall(fun=ir.SymRef(id="tuple_get"), args=[ir.Literal(value=str(i), type="int"), t])
+def test_simple_make_tuple_tuple_get():
+    tuple_of_size_2 = im.make_tuple("first", "second")
+    testee = im.make_tuple(im.tuple_get(0, tuple_of_size_2), im.tuple_get(1, tuple_of_size_2))
 
+    actual = CollapseTuple.apply(testee, collapse_tuple_get_make_tuple=False)
 
-@pytest.fixture
-def tup_of_size_2():
-    return ir.FunCall(
-        fun=ir.SymRef(id="make_tuple"), args=[ir.SymRef(id="foo"), ir.SymRef(id="foo")]
-    )
+    expected = tuple_of_size_2
+    assert actual == expected
 
 
-def test_simple(tup_of_size_2):
-    testee = ir.FunCall(
-        fun=ir.SymRef(id="make_tuple"),
-        args=[_tuple_get(0, tup_of_size_2), _tuple_get(1, tup_of_size_2)],
+def test_nested_make_tuple_tuple_get():
+    tup_of_size2_from_lambda = im.call(im.lambda_()(im.make_tuple("first", "second")))()
+    testee = im.make_tuple(
+        im.tuple_get(0, tup_of_size2_from_lambda), im.tuple_get(1, tup_of_size2_from_lambda)
     )
-    expected = tup_of_size_2
-    actual = CollapseTuple.apply(testee)
-    assert actual == expected
 
+    actual = CollapseTuple.apply(testee, collapse_tuple_get_make_tuple=False)
 
-def test_incompatible_order(tup_of_size_2):
-    testee = ir.FunCall(
-        fun=ir.SymRef(id="make_tuple"),
-        args=[_tuple_get(1, tup_of_size_2), _tuple_get(0, tup_of_size_2)],
-    )
-    actual = CollapseTuple.apply(testee)
+    assert actual == tup_of_size2_from_lambda
+
+
+def test_different_tuples_make_tuple_tuple_get():
+    t0 = im.make_tuple("foo0", "bar0")
+    t1 = im.make_tuple("foo1", "bar1")
+    testee = im.make_tuple(im.tuple_get(0, t0), im.tuple_get(1, t1))
+
+    actual = CollapseTuple.apply(testee, collapse_tuple_get_make_tuple=False)
+
+    assert actual == testee  # did nothing
+
+
+def test_incompatible_order_make_tuple_tuple_get():
+    tuple_of_size_2 = im.make_tuple("first", "second")
+    testee = im.make_tuple(im.tuple_get(1, tuple_of_size_2), im.tuple_get(0, tuple_of_size_2))
+    actual = CollapseTuple.apply(testee, collapse_tuple_get_make_tuple=False)
     assert actual == testee  # did nothing
 
 
-def test_incompatible_size(tup_of_size_2):
-    testee = ir.FunCall(fun=ir.SymRef(id="make_tuple"), args=[_tuple_get(0, tup_of_size_2)])
-    actual = CollapseTuple.apply(testee)
+def test_incompatible_size_make_tuple_tuple_get():
+    testee = im.make_tuple(im.tuple_get(0, im.make_tuple("first", "second")))
+    actual = CollapseTuple.apply(testee, collapse_tuple_get_make_tuple=False)
     assert actual == testee  # did nothing
 
 
-def test_merged_with_smaller_outer_size(tup_of_size_2):
-    testee = ir.FunCall(fun=ir.SymRef(id="make_tuple"), args=[_tuple_get(0, tup_of_size_2)])
+def test_merged_with_smaller_outer_size_make_tuple_tuple_get():
+    testee = im.make_tuple(im.tuple_get(0, im.make_tuple("first", "second")))
     actual = CollapseTuple.apply(testee, ignore_tuple_size=True)
-    assert actual == tup_of_size_2
+    assert actual == im.make_tuple("first", "second")
+
+
+def test_simple_tuple_get_make_tuple():
+    expected = im.ref("bar")
+    testee = im.tuple_get(1, im.make_tuple("foo", expected))
+    actual = CollapseTuple.apply(testee, collapse_make_tuple_tuple_get=False)
+    assert expected == actual
diff --git a/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_cse.py b/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_cse.py
index afbd0c15..dbbbfd97 100644
--- a/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_cse.py
+++ b/tests/next_tests/unit_tests/iterator_tests/transforms_tests/test_cse.py
@@ -131,3 +131,25 @@ def test_lambda_redef_same_arg_scope():
     )
     actual = CSE().visit(testee)
     assert actual == expected
+
+
+def test_if_can_deref():
+    """
+    Test no subexpression is moved outside expressions of the form `if_(can_deref(...), ..., ...)`
+    """
+    # if can_deref(⟪Iₒ, 1ₒ⟫(it)) then ·⟪Iₒ, 1ₒ⟫(it) else ·⟪Iₒ, 1ₒ⟫(it) + ·⟪Iₒ, 1ₒ⟫(it)
+    testee = im.if_(
+        im.call("can_deref")(im.shift("I", 1)("it")),
+        im.deref(im.shift("I", 1)("it")),
+        # use something more involved where a subexpression can still be eliminated
+        im.plus(im.deref(im.shift("I", 1)("it")), im.deref(im.shift("I", 1)("it"))),
+    )
+    # if can_deref(⟪Iₒ, 1ₒ⟫(it)) then ·⟪Iₒ, 1ₒ⟫(it) else (λ(_cs_1) → _cs_1 + _cs_1)(·⟪Iₒ, 1ₒ⟫(it))
+    expected = im.if_(
+        im.call("can_deref")(im.shift("I", 1)("it")),
+        im.deref(im.shift("I", 1)("it")),
+        im.let("_cs_1", im.deref(im.shift("I", 1)("it")))(im.plus("_cs_1", "_cs_1")),
+    )
+
+    actual = CSE().visit(testee)
+    assert actual == expected