Repository: GridTools/gt4py
Pull request #1742 (2024-11-19 11:00:16)

feat[next]: SDFGConvertible Program for dace_fieldview backend
## Description

Add a `decrator.Program` subclass, which implements `SDFGConvertible` to `dace_fieldview` backend, analogous to the one in `dace_iterator`. Conditionally shadow `decorator.Program` with it and reactivate the orchestration tests by using `dace_fieldview` instead of `dace_iterator`. 

One caveat: The toolchain is not ready for pure `CompileTimeConnectivities` in all cases yet, so the `test_sdfgConvertible_connectivities` had to be adjusted for the moment.
## Requirements

- [x] All fixes and/or new features come with corresponding tests.
- [x] Important design decisions have been documented in the approriate ADR inside the [docs/development/ADRs/](docs/development/ADRs/Index.md) folder.

If this PR contains code authored by new contributors please make sure:

- [x] The PR contains an updated version of the `AUTHORS.md` file adding the names of all the new contributors.


edopao: If it can help, I got the tests to pass locally with this diff:
```
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/program.py b/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
index 803ae866..d65f4fac 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
@@ -176,7 +176,7 @@ class Program(decorator.Program, dace.frontend.python.common.SDFGConvertible):
                     conn = self.connectivities[name]
                     assert common.is_neighbor_table(conn)
                     self.connectivity_tables_data_descriptors[conn_id] = dace.data.Array(
-                        dtype=dace.int64 if conn.dtype == np.int64 else dace.int32,
+                        dtype=dace.dtypes.typeclass(conn.dtype.scalar_type),
                         shape=[
                             symbols[dace_utils.field_size_symbol_name(conn_id, 0)],
                             symbols[dace_utils.field_size_symbol_name(conn_id, 1)],
diff --git a/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py b/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
index 9bde9aca..80cd9eac 100644
--- a/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
+++ b/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
@@ -133,7 +133,7 @@ def test_sdfgConvertible_connectivities(unstructured_case):
         data=xp.asarray([[0, 1], [1, 2], [2, 0]]),
         allocator=allocator,
     )
-    connectivities = {"E2V": e2v.__gt_type__()}
+    connectivities = {"E2V": e2v}
     offset_provider = OffsetProvider_t.dtype._typeclass.as_ctypes()(E2V=e2v.data_ptr())
 
     SDFG = sdfg.to_sdfg(connectivities=connectivities)
```
DropD: > If it can help, I got the tests to pass locally with this diff:
> 
> ```
> diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/program.py b/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
> index 803ae866..d65f4fac 100644
> --- a/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
> +++ b/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
> @@ -176,7 +176,7 @@ class Program(decorator.Program, dace.frontend.python.common.SDFGConvertible):
>                      conn = self.connectivities[name]
>                      assert common.is_neighbor_table(conn)
>                      self.connectivity_tables_data_descriptors[conn_id] = dace.data.Array(
> -                        dtype=dace.int64 if conn.dtype == np.int64 else dace.int32,
> +                        dtype=dace.dtypes.typeclass(conn.dtype.scalar_type),
>                          shape=[
>                              symbols[dace_utils.field_size_symbol_name(conn_id, 0)],
>                              symbols[dace_utils.field_size_symbol_name(conn_id, 1)],
> diff --git a/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py b/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
> index 9bde9aca..80cd9eac 100644
> --- a/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
> +++ b/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
> @@ -133,7 +133,7 @@ def test_sdfgConvertible_connectivities(unstructured_case):
>          data=xp.asarray([[0, 1], [1, 2], [2, 0]]),
>          allocator=allocator,
>      )
> -    connectivities = {"E2V": e2v.__gt_type__()}
> +    connectivities = {"E2V": e2v}
>      offset_provider = OffsetProvider_t.dtype._typeclass.as_ctypes()(E2V=e2v.data_ptr())
>  
>      SDFG = sdfg.to_sdfg(connectivities=connectivities)
> ```

This is part of what I ended up doing but I wanted to honor the original purpose of the test, which was to compile the SDFG without knowledge of the connectivity tables and then call it with two different ones.

Patch:
diff --git a/.github/workflows/_disabled/gt4py-sphinx.yml b/.github/workflows/_disabled/gt4py-sphinx.yml
index d862ab73..cb3b2757 100644
--- a/.github/workflows/_disabled/gt4py-sphinx.yml
+++ b/.github/workflows/_disabled/gt4py-sphinx.yml
@@ -22,7 +22,7 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v1
       with:
-        python-version: 3.8
+        python-version: 3.10
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
diff --git a/.github/workflows/daily-ci.yml b/.github/workflows/daily-ci.yml
index 30ad0a6f..28512a18 100644
--- a/.github/workflows/daily-ci.yml
+++ b/.github/workflows/daily-ci.yml
@@ -15,7 +15,7 @@ jobs:
   daily-ci:
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         tox-module-factor: ["cartesian", "eve", "next", "storage"]
         os: ["ubuntu-latest"]
         requirements-file: ["requirements-dev.txt", "min-requirements-test.txt", "min-extra-requirements-test.txt"]
@@ -34,13 +34,6 @@ jobs:
       shell: bash
       run: |
         sudo apt install libboost-dev
-        wget https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.gz
-        echo 7bd7ddceec1a1dfdcbdb3e609b60d01739c38390a5f956385a12f3122049f0ca boost_1_76_0.tar.gz > boost_hash.txt
-        sha256sum -c boost_hash.txt
-        tar xzf boost_1_76_0.tar.gz
-        mkdir -p boost/include
-        mv boost_1_76_0/boost boost/include/
-        echo "BOOST_ROOT=${PWD}/boost" >> $GITHUB_ENV
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v5
       with:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e3831123..051781ea 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -50,7 +50,7 @@ repos:
   ## version = re.search('ruff==([0-9\.]*)', open("constraints.txt").read())[1]
   ## print(f"rev: v{version}")
   ##]]]
-  rev: v0.8.2
+  rev: v0.8.6
   ##[[[end]]]
   hooks:
     # Run the linter.
@@ -72,9 +72,9 @@ repos:
   ## version = re.search('mypy==([0-9\.]*)', open("constraints.txt").read())[1]
   ## print(f"#========= FROM constraints.txt: v{version} =========")
   ##]]]
-  #========= FROM constraints.txt: v1.13.0 =========
+  #========= FROM constraints.txt: v1.14.1 =========
   ##[[[end]]]
-  rev: v1.13.0  # MUST match version ^^^^ in constraints.txt (if the mirror is up-to-date)
+  rev: v1.14.1  # MUST match version ^^^^ in constraints.txt (if the mirror is up-to-date)
   hooks:
   - id: mypy
     additional_dependencies:  # versions from constraints.txt
@@ -90,31 +90,29 @@ repos:
     ## for pkg in packages:
     ##     print(f"- {pkg}==" + str(re.search(f'\n{pkg}==([0-9\.]*)', constraints)[1]))
     ##]]]
-    - astunparse==1.6.3
-    - attrs==24.2.0
-    - black==24.8.0
+    - attrs==24.3.0
+    - black==24.10.0
     - boltons==24.1.0
     - cached-property==2.0.1
-    - click==8.1.7
-    - cmake==3.31.1
-    - cytoolz==1.0.0
-    - deepdiff==8.0.1
+    - click==8.1.8
+    - cmake==3.31.2
+    - cytoolz==1.0.1
+    - deepdiff==8.1.1
     - devtools==0.12.2
     - diskcache==5.6.3
     - factory-boy==3.3.1
     - filelock==3.16.1
     - frozendict==2.4.6
     - gridtools-cpp==2.3.8
-    - importlib-resources==6.4.5
-    - jinja2==3.1.4
+    - jinja2==3.1.5
     - lark==1.2.2
     - mako==1.3.8
     - nanobind==2.4.0
-    - ninja==1.11.1.2
-    - numpy==1.24.4
+    - ninja==1.11.1.3
+    - numpy==1.26.4
     - packaging==24.2
     - pybind11==2.13.6
-    - setuptools==75.3.0
+    - setuptools==75.8.0
     - tabulate==0.9.0
     - typing-extensions==4.12.2
     - xxhash==3.0.0
diff --git a/README.md b/README.md
index b782e20f..07e0e1cd 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
 
 GT4Py is a Python library for generating high performance implementations of stencil kernels from a high-level definition using regular Python functions. GT4Py is part of the GridTools framework, a set of libraries and utilities to develop performance portable applications in the area of weather and climate modeling.
 
-**NOTE:** The `gt4py.next` subpackage contains a new version of GT4Py which is not compatible with the current _stable_ version defined in `gt4py.cartesian`. The new version is highly experimental, it only works with unstructured meshes and it requires `python >= 3.10`.
+**NOTE:** The `gt4py.next` subpackage contains a new version of GT4Py which is not compatible with the current _stable_ version defined in `gt4py.cartesian`. The new version is still experimental.
 
 ## 📃 Description
 
diff --git a/ci/cscs-ci.yml b/ci/cscs-ci.yml
index 7adb8845..c2a872c1 100644
--- a/ci/cscs-ci.yml
+++ b/ci/cscs-ci.yml
@@ -9,13 +9,6 @@ include:
   PYVERSION_PREFIX: py310
   PYVERSION: 3.10.9
 
-.py39: &py39
-  PYVERSION_PREFIX: py39
-  PYVERSION: 3.9.1
-
-.py38: &py38
-  PYVERSION_PREFIX: py38
-  PYVERSION: 3.8.5
 
 stages:
 - baseimage
@@ -78,20 +71,6 @@ build_py310_baseimage_aarch64:
   variables:
     <<: *py310
 
-build_py39_baseimage_x86_64:
-  extends: .build_baseimage_x86_64
-  variables:
-    <<: *py39
-build_py39_baseimage_aarch64:
-  extends: .build_baseimage_aarch64
-  variables:
-    <<: *py39
-
-build_py38_baseimage_x86_64:
-  extends: .build_baseimage_x86_64
-  variables:
-    <<: *py38
-
 
 .build_image:
   stage: image
@@ -128,23 +107,6 @@ build_py310_image_aarch64:
   variables:
     <<: *py310
 
-build_py39_image_x86_64:
-  extends: .build_image_x86_64
-  needs: [build_py39_baseimage_x86_64]
-  variables:
-    <<: *py39
-build_py39_image_aarch64:
-  extends: .build_image_aarch64
-  needs: [build_py39_baseimage_aarch64]
-  variables:
-    <<: *py39
-
-build_py38_image_x86_64:
-  extends: .build_image_x86_64
-  needs: [build_py38_baseimage_x86_64]
-  variables:
-    <<: *py38
-
 
 .test_helper:
   stage: test
@@ -210,20 +172,3 @@ test_py310_aarch64:
   needs: [build_py310_image_aarch64]
   variables:
     <<: *py310
-
-test_py39_x86_64:
-  extends: [.test_helper_x86_64]
-  needs: [build_py39_image_x86_64]
-  variables:
-    <<: *py39
-test_py39_aarch64:
-  extends: [.test_helper_aarch64]
-  needs: [build_py39_image_aarch64]
-  variables:
-    <<: *py39
-
-test_py38_x86_64:
-  extends: [.test_helper_x86_64]
-  needs: [build_py38_image_x86_64]
-  variables:
-    <<: *py38
diff --git a/constraints.txt b/constraints.txt
index fbdfb6e2..8b3e5e69 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -1,182 +1,178 @@
 #
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
 #    "tox run -e requirements-base"
 #
 aenum==3.1.15             # via dace
-alabaster==0.7.13         # via sphinx
+alabaster==1.0.0          # via sphinx
 annotated-types==0.7.0    # via pydantic
 asttokens==2.4.1          # via devtools, stack-data
-astunparse==1.6.3 ; python_version < "3.9"  # via dace, gt4py (pyproject.toml)
-attrs==24.2.0             # via gt4py (pyproject.toml), hypothesis, jsonschema, referencing
+astunparse==1.6.3         # via dace
+attrs==24.3.0             # via gt4py (pyproject.toml), hypothesis, jsonschema, referencing
 babel==2.16.0             # via sphinx
-backcall==0.2.0           # via ipython
-black==24.8.0             # via gt4py (pyproject.toml)
+black==24.10.0            # via gt4py (pyproject.toml)
 boltons==24.1.0           # via gt4py (pyproject.toml)
 bracex==2.5.post1         # via wcmatch
 build==1.2.2.post1        # via pip-tools
-bump-my-version==0.28.1   # via -r requirements-dev.in
+bump-my-version==0.29.0   # via -r requirements-dev.in
 cached-property==2.0.1    # via gt4py (pyproject.toml)
 cachetools==5.5.0         # via tox
-certifi==2024.8.30        # via requests
+certifi==2024.12.14       # via requests
 cfgv==3.4.0               # via pre-commit
 chardet==5.2.0            # via tox
-charset-normalizer==3.4.0  # via requests
-clang-format==19.1.4      # via -r requirements-dev.in, gt4py (pyproject.toml)
-click==8.1.7              # via black, bump-my-version, gt4py (pyproject.toml), pip-tools, rich-click
-cmake==3.31.1             # via gt4py (pyproject.toml)
+charset-normalizer==3.4.1  # via requests
+clang-format==19.1.6      # via -r requirements-dev.in, gt4py (pyproject.toml)
+click==8.1.8              # via black, bump-my-version, gt4py (pyproject.toml), pip-tools, rich-click
+cmake==3.31.2             # via gt4py (pyproject.toml)
 cogapp==3.4.1             # via -r requirements-dev.in
 colorama==0.4.6           # via tox
 comm==0.2.2               # via ipykernel
-contourpy==1.1.1          # via matplotlib
-coverage==7.6.1           # via -r requirements-dev.in, pytest-cov
+contourpy==1.3.1          # via matplotlib
+coverage==7.6.10          # via -r requirements-dev.in, pytest-cov
 cycler==0.12.1            # via matplotlib
-cytoolz==1.0.0            # via gt4py (pyproject.toml)
+cytoolz==1.0.1            # via gt4py (pyproject.toml)
 dace==1.0.0               # via gt4py (pyproject.toml)
 darglint==1.8.1           # via -r requirements-dev.in
-debugpy==1.8.9            # via ipykernel
+debugpy==1.8.11           # via ipykernel
 decorator==5.1.1          # via ipython
-deepdiff==8.0.1           # via gt4py (pyproject.toml)
+deepdiff==8.1.1           # via gt4py (pyproject.toml)
 devtools==0.12.2          # via gt4py (pyproject.toml)
 dill==0.3.9               # via dace
 diskcache==5.6.3          # via gt4py (pyproject.toml)
 distlib==0.3.9            # via virtualenv
-docutils==0.20.1          # via sphinx, sphinx-rtd-theme
-exceptiongroup==1.2.2     # via hypothesis, pytest
+docutils==0.21.2          # via sphinx, sphinx-rtd-theme
+exceptiongroup==1.2.2     # via hypothesis, ipython, pytest
 execnet==2.1.1            # via pytest-cache, pytest-xdist
 executing==2.1.0          # via devtools, stack-data
 factory-boy==3.3.1        # via gt4py (pyproject.toml), pytest-factoryboy
-faker==33.1.0             # via factory-boy
+faker==33.3.0             # via factory-boy
 fastjsonschema==2.21.1    # via nbformat
 filelock==3.16.1          # via gt4py (pyproject.toml), tox, virtualenv
-fonttools==4.55.2         # via matplotlib
+fonttools==4.55.3         # via matplotlib
 fparser==0.2.0            # via dace
 frozendict==2.4.6         # via gt4py (pyproject.toml)
-gitdb==4.0.11             # via gitpython
-gitpython==3.1.43         # via tach
+gitdb==4.0.12             # via gitpython
+gitpython==3.1.44         # via tach
 gridtools-cpp==2.3.8      # via gt4py (pyproject.toml)
-hypothesis==6.113.0       # via -r requirements-dev.in, gt4py (pyproject.toml)
-identify==2.6.1           # via pre-commit
+hypothesis==6.123.11      # via -r requirements-dev.in, gt4py (pyproject.toml)
+identify==2.6.5           # via pre-commit
 idna==3.10                # via requests
 imagesize==1.4.1          # via sphinx
-importlib-metadata==8.5.0  # via build, jupyter-client, sphinx
-importlib-resources==6.4.5 ; python_version < "3.9"  # via gt4py (pyproject.toml), jsonschema, jsonschema-specifications, matplotlib
 inflection==0.5.1         # via pytest-factoryboy
 iniconfig==2.0.0          # via pytest
 ipykernel==6.29.5         # via nbmake
-ipython==8.12.3           # via ipykernel
+ipython==8.31.0           # via ipykernel
+jax==0.4.38               # via gt4py (pyproject.toml)
+jaxlib==0.4.38            # via jax
 jedi==0.19.2              # via ipython
-jinja2==3.1.4             # via gt4py (pyproject.toml), sphinx
+jinja2==3.1.5             # via gt4py (pyproject.toml), sphinx
 jsonschema==4.23.0        # via nbformat
-jsonschema-specifications==2023.12.1  # via jsonschema
+jsonschema-specifications==2024.10.1  # via jsonschema
 jupyter-client==8.6.3     # via ipykernel, nbclient
-jupyter-core==5.7.2       # via ipykernel, jupyter-client, nbformat
-jupytext==1.16.4          # via -r requirements-dev.in
-kiwisolver==1.4.7         # via matplotlib
+jupyter-core==5.7.2       # via ipykernel, jupyter-client, nbclient, nbformat
+jupytext==1.16.6          # via -r requirements-dev.in
+kiwisolver==1.4.8         # via matplotlib
 lark==1.2.2               # via gt4py (pyproject.toml)
 mako==1.3.8               # via gt4py (pyproject.toml)
 markdown-it-py==3.0.0     # via jupytext, mdit-py-plugins, rich
-markupsafe==2.1.5         # via jinja2, mako
-matplotlib==3.7.5         # via -r requirements-dev.in
+markupsafe==3.0.2         # via jinja2, mako
+matplotlib==3.10.0        # via -r requirements-dev.in
 matplotlib-inline==0.1.7  # via ipykernel, ipython
 mdit-py-plugins==0.4.2    # via jupytext
 mdurl==0.1.2              # via markdown-it-py
+ml-dtypes==0.5.1          # via jax, jaxlib
 mpmath==1.3.0             # via sympy
-mypy==1.13.0              # via -r requirements-dev.in
+mypy==1.14.1              # via -r requirements-dev.in
 mypy-extensions==1.0.0    # via black, mypy
 nanobind==2.4.0           # via gt4py (pyproject.toml)
-nbclient==0.6.8           # via nbmake
+nbclient==0.10.2          # via nbmake
 nbformat==5.10.4          # via jupytext, nbclient, nbmake
-nbmake==1.5.4             # via -r requirements-dev.in
-nest-asyncio==1.6.0       # via ipykernel, nbclient
-networkx==3.1             # via dace, tach
-ninja==1.11.1.2           # via gt4py (pyproject.toml)
+nbmake==1.5.5             # via -r requirements-dev.in
+nest-asyncio==1.6.0       # via ipykernel
+networkx==3.4.2           # via dace, tach
+ninja==1.11.1.3           # via gt4py (pyproject.toml)
 nodeenv==1.9.1            # via pre-commit
-numpy==1.24.4             # via contourpy, dace, gt4py (pyproject.toml), matplotlib, scipy
-orderly-set==5.2.2        # via deepdiff
+numpy==1.26.4             # via contourpy, dace, gt4py (pyproject.toml), jax, jaxlib, matplotlib, ml-dtypes, scipy
+opt-einsum==3.4.0         # via jax
+orderly-set==5.2.3        # via deepdiff
 packaging==24.2           # via black, build, dace, gt4py (pyproject.toml), ipykernel, jupytext, matplotlib, pipdeptree, pyproject-api, pytest, pytest-factoryboy, setuptools-scm, sphinx, tox
 parso==0.8.4              # via jedi
 pathspec==0.12.1          # via black
 pexpect==4.9.0            # via ipython
-pickleshare==0.7.5        # via ipython
-pillow==10.4.0            # via matplotlib
+pillow==11.1.0            # via matplotlib
 pip-tools==7.4.1          # via -r requirements-dev.in
 pipdeptree==2.24.0        # via -r requirements-dev.in
-pkgutil-resolve-name==1.3.10  # via jsonschema
 platformdirs==4.3.6       # via black, jupyter-core, tox, virtualenv
 pluggy==1.5.0             # via pytest, tox
 ply==3.11                 # via dace
-pre-commit==3.5.0         # via -r requirements-dev.in
-prompt-toolkit==3.0.36    # via ipython, questionary, tach
-psutil==6.1.0             # via -r requirements-dev.in, ipykernel, pytest-xdist
+pre-commit==4.0.1         # via -r requirements-dev.in
+prompt-toolkit==3.0.48    # via ipython, questionary, tach
+psutil==6.1.1             # via -r requirements-dev.in, ipykernel, pytest-xdist
 ptyprocess==0.7.0         # via pexpect
 pure-eval==0.2.3          # via stack-data
 pybind11==2.13.6          # via gt4py (pyproject.toml)
-pydantic==2.10.3          # via bump-my-version, pydantic-settings
-pydantic-core==2.27.1     # via pydantic
-pydantic-settings==2.6.1  # via bump-my-version
-pydot==3.0.3              # via tach
-pygments==2.18.0          # via -r requirements-dev.in, devtools, ipython, nbmake, rich, sphinx
-pyparsing==3.1.4          # via matplotlib, pydot
+pydantic==2.10.4          # via bump-my-version, pydantic-settings
+pydantic-core==2.27.2     # via pydantic
+pydantic-settings==2.7.1  # via bump-my-version
+pydot==3.0.4              # via tach
+pygments==2.19.1          # via -r requirements-dev.in, devtools, ipython, nbmake, rich, sphinx
+pyparsing==3.2.1          # via matplotlib, pydot
 pyproject-api==1.8.0      # via tox
 pyproject-hooks==1.2.0    # via build, pip-tools
 pytest==8.3.4             # via -r requirements-dev.in, gt4py (pyproject.toml), nbmake, pytest-cache, pytest-cov, pytest-custom-exit-code, pytest-factoryboy, pytest-instafail, pytest-xdist
 pytest-cache==1.0         # via -r requirements-dev.in
-pytest-cov==5.0.0         # via -r requirements-dev.in
+pytest-cov==6.0.0         # via -r requirements-dev.in
 pytest-custom-exit-code==0.3.0  # via -r requirements-dev.in
 pytest-factoryboy==2.7.0  # via -r requirements-dev.in
 pytest-instafail==0.5.0   # via -r requirements-dev.in
 pytest-xdist==3.6.1       # via -r requirements-dev.in
 python-dateutil==2.9.0.post0  # via faker, jupyter-client, matplotlib
 python-dotenv==1.0.1      # via pydantic-settings
-pytz==2024.2              # via babel
 pyyaml==6.0.2             # via dace, jupytext, pre-commit, tach
 pyzmq==26.2.0             # via ipykernel, jupyter-client
-questionary==2.0.1        # via bump-my-version
+questionary==2.1.0        # via bump-my-version
 referencing==0.35.1       # via jsonschema, jsonschema-specifications
 requests==2.32.3          # via sphinx
 rich==13.9.4              # via bump-my-version, rich-click, tach
 rich-click==1.8.5         # via bump-my-version
-rpds-py==0.20.1           # via jsonschema, referencing
-ruff==0.8.2               # via -r requirements-dev.in
-scipy==1.10.1             # via gt4py (pyproject.toml)
+rpds-py==0.22.3           # via jsonschema, referencing
+ruff==0.8.6               # via -r requirements-dev.in
+scipy==1.15.0             # via gt4py (pyproject.toml), jax, jaxlib
 setuptools-scm==8.1.0     # via fparser
 six==1.17.0               # via asttokens, astunparse, python-dateutil
-smmap==5.0.1              # via gitdb
+smmap==5.0.2              # via gitdb
 snowballstemmer==2.2.0    # via sphinx
 sortedcontainers==2.4.0   # via hypothesis
-sphinx==7.1.2             # via -r requirements-dev.in, sphinx-rtd-theme, sphinxcontrib-jquery
+sphinx==8.1.3             # via -r requirements-dev.in, sphinx-rtd-theme, sphinxcontrib-jquery
 sphinx-rtd-theme==3.0.2   # via -r requirements-dev.in
-sphinxcontrib-applehelp==1.0.4  # via sphinx
-sphinxcontrib-devhelp==1.0.2  # via sphinx
-sphinxcontrib-htmlhelp==2.0.1  # via sphinx
+sphinxcontrib-applehelp==2.0.0  # via sphinx
+sphinxcontrib-devhelp==2.0.0  # via sphinx
+sphinxcontrib-htmlhelp==2.1.0  # via sphinx
 sphinxcontrib-jquery==4.1  # via sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1  # via sphinx
-sphinxcontrib-qthelp==1.0.3  # via sphinx
-sphinxcontrib-serializinghtml==1.1.5  # via sphinx
+sphinxcontrib-qthelp==2.0.0  # via sphinx
+sphinxcontrib-serializinghtml==2.0.0  # via sphinx
 stack-data==0.6.3         # via ipython
-stdlib-list==0.10.0       # via tach
 sympy==1.13.3             # via dace
 tabulate==0.9.0           # via gt4py (pyproject.toml)
-tach==0.16.5              # via -r requirements-dev.in
-tomli==2.2.1 ; python_version < "3.11"  # via -r requirements-dev.in, black, build, coverage, jupytext, mypy, pip-tools, pyproject-api, pytest, setuptools-scm, tach, tox
-tomli-w==1.0.0            # via tach
+tach==0.19.5              # via -r requirements-dev.in
+tomli==2.2.1 ; python_version < "3.11"  # via -r requirements-dev.in, black, build, coverage, jupytext, mypy, pip-tools, pyproject-api, pytest, setuptools-scm, sphinx, tach, tox
+tomli-w==1.1.0            # via tach
 tomlkit==0.13.2           # via bump-my-version
 toolz==1.0.0              # via cytoolz
 tornado==6.4.2            # via ipykernel, jupyter-client
 tox==4.23.2               # via -r requirements-dev.in
 traitlets==5.14.3         # via comm, ipykernel, ipython, jupyter-client, jupyter-core, matplotlib-inline, nbclient, nbformat
 types-tabulate==0.9.0.20241207  # via -r requirements-dev.in
-typing-extensions==4.12.2  # via annotated-types, black, faker, gt4py (pyproject.toml), ipython, mypy, pydantic, pydantic-core, pytest-factoryboy, rich, rich-click, setuptools-scm, tox
-urllib3==2.2.3            # via requests
-virtualenv==20.28.0       # via pre-commit, tox
+typing-extensions==4.12.2  # via black, faker, gt4py (pyproject.toml), ipython, mypy, pydantic, pydantic-core, pytest-factoryboy, rich, rich-click, tox
+urllib3==2.3.0            # via requests
+virtualenv==20.28.1       # via pre-commit, tox
 wcmatch==10.0             # via bump-my-version
 wcwidth==0.2.13           # via prompt-toolkit
 wheel==0.45.1             # via astunparse, pip-tools
 xxhash==3.0.0             # via gt4py (pyproject.toml)
-zipp==3.20.2              # via importlib-metadata, importlib-resources
 
 # The following packages are considered to be unsafe in a requirements file:
 pip==24.3.1               # via pip-tools, pipdeptree
-setuptools==75.3.0        # via gt4py (pyproject.toml), pip-tools, setuptools-scm
+setuptools==75.8.0        # via gt4py (pyproject.toml), pip-tools, setuptools-scm
diff --git a/pyproject.toml b/pyproject.toml
index d086363e..78735116 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,8 +14,6 @@ classifiers = [
   'License :: OSI Approved :: BSD License',
   'Operating System :: POSIX',
   'Programming Language :: Python',
-  'Programming Language :: Python :: 3.8',
-  'Programming Language :: Python :: 3.9',
   'Programming Language :: Python :: 3.10',
   'Programming Language :: Python :: 3.11',
   'Programming Language :: Python :: Implementation :: CPython',
@@ -24,7 +22,6 @@ classifiers = [
   'Topic :: Scientific/Engineering :: Physics'
 ]
 dependencies = [
-  "astunparse>=1.6.3;python_version<'3.9'",
   'attrs>=21.3',
   'black>=22.3',
   'boltons>=20.1',
@@ -39,7 +36,6 @@ dependencies = [
   'filelock>=3.16.1',
   'frozendict>=2.3',
   'gridtools-cpp>=2.3.8,==2.*',
-  "importlib-resources>=5.0;python_version<'3.9'",
   'jinja2>=3.0.0',
   'lark>=1.1.2',
   'mako>=1.1',
@@ -67,7 +63,7 @@ keywords = [
 license = {file = 'LICENSE.txt'}
 name = 'gt4py'
 readme = 'README.md'
-requires-python = '>=3.8'
+requires-python = '>=3.10'
 
 [project.optional-dependencies]
 # Bundles
@@ -80,9 +76,9 @@ cuda12 = ['cupy-cuda12x>=12.0']
 dace = ['dace>=1.0.0,<1.1.0']  # v1.x will contain breaking changes, see https://github.com/spcl/dace/milestone/4
 formatting = ['clang-format>=9.0']
 gpu = ['cupy>=12.0']
-jax-cpu = ['jax[cpu]>=0.4.18; python_version>="3.10"']
-jax-cuda11 = ['jax[cuda11_pip]>=0.4.18; python_version>="3.10"']
-jax-cuda12 = ['jax[cuda12_pip]>=0.4.18; python_version>="3.10"']
+jax-cpu = ['jax[cpu]>=0.4.18']
+jax-cuda11 = ['jax[cuda11_pip]>=0.4.18']
+jax-cuda12 = ['jax[cuda12_pip]>=0.4.18']
 performance = ['scipy>=1.9.2']
 rocm-43 = ['cupy-rocm-4-3']
 testing = ['hypothesis>=6.0.0', 'pytest>=7.0']
@@ -275,7 +271,7 @@ line-length = 100  # It should be the same as in `tool.black.line-length` above
 respect-gitignore = true
 show-fixes = true
 # show-source = true
-target-version = 'py38'
+target-version = 'py310'
 
 [tool.ruff.format]
 docstring-code-format = true
@@ -292,7 +288,8 @@ docstring-code-format = true
 # NPY: NumPy-specific rules
 # RUF: Ruff-specific rules
 ignore = [
-  'E501'  # [line-too-long]
+  'E501',  # [line-too-long]
+  'B905'  # [zip-without-explicit-strict]  # TODO(egparedes): Reevaluate this rule
 ]
 select = ['E', 'F', 'I', 'B', 'A', 'T10', 'ERA', 'NPY', 'RUF']
 typing-modules = ['gt4py.eve.extended_typing']
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 40554cef..463b1bc6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,181 +1,178 @@
 #
-# This file is autogenerated by pip-compile with Python 3.8
+# This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
 #    "tox run -e requirements-base"
 #
 aenum==3.1.15             # via -c constraints.txt, dace
-alabaster==0.7.13         # via -c constraints.txt, sphinx
+alabaster==1.0.0          # via -c constraints.txt, sphinx
 annotated-types==0.7.0    # via -c constraints.txt, pydantic
 asttokens==2.4.1          # via -c constraints.txt, devtools, stack-data
-astunparse==1.6.3 ; python_version < "3.9"  # via -c constraints.txt, dace, gt4py (pyproject.toml)
-attrs==24.2.0             # via -c constraints.txt, gt4py (pyproject.toml), hypothesis, jsonschema, referencing
+astunparse==1.6.3         # via -c constraints.txt, dace
+attrs==24.3.0             # via -c constraints.txt, gt4py (pyproject.toml), hypothesis, jsonschema, referencing
 babel==2.16.0             # via -c constraints.txt, sphinx
-backcall==0.2.0           # via -c constraints.txt, ipython
-black==24.8.0             # via -c constraints.txt, gt4py (pyproject.toml)
+black==24.10.0            # via -c constraints.txt, gt4py (pyproject.toml)
 boltons==24.1.0           # via -c constraints.txt, gt4py (pyproject.toml)
 bracex==2.5.post1         # via -c constraints.txt, wcmatch
 build==1.2.2.post1        # via -c constraints.txt, pip-tools
-bump-my-version==0.28.1   # via -c constraints.txt, -r requirements-dev.in
+bump-my-version==0.29.0   # via -c constraints.txt, -r requirements-dev.in
 cached-property==2.0.1    # via -c constraints.txt, gt4py (pyproject.toml)
 cachetools==5.5.0         # via -c constraints.txt, tox
-certifi==2024.8.30        # via -c constraints.txt, requests
+certifi==2024.12.14       # via -c constraints.txt, requests
 cfgv==3.4.0               # via -c constraints.txt, pre-commit
 chardet==5.2.0            # via -c constraints.txt, tox
-charset-normalizer==3.4.0  # via -c constraints.txt, requests
-clang-format==19.1.4      # via -c constraints.txt, -r requirements-dev.in, gt4py (pyproject.toml)
-click==8.1.7              # via -c constraints.txt, black, bump-my-version, gt4py (pyproject.toml), pip-tools, rich-click
-cmake==3.31.1             # via -c constraints.txt, gt4py (pyproject.toml)
+charset-normalizer==3.4.1  # via -c constraints.txt, requests
+clang-format==19.1.6      # via -c constraints.txt, -r requirements-dev.in, gt4py (pyproject.toml)
+click==8.1.8              # via -c constraints.txt, black, bump-my-version, gt4py (pyproject.toml), pip-tools, rich-click
+cmake==3.31.2             # via -c constraints.txt, gt4py (pyproject.toml)
 cogapp==3.4.1             # via -c constraints.txt, -r requirements-dev.in
 colorama==0.4.6           # via -c constraints.txt, tox
 comm==0.2.2               # via -c constraints.txt, ipykernel
-contourpy==1.1.1          # via -c constraints.txt, matplotlib
-coverage[toml]==7.6.1     # via -c constraints.txt, -r requirements-dev.in, pytest-cov
+contourpy==1.3.1          # via -c constraints.txt, matplotlib
+coverage[toml]==7.6.10    # via -c constraints.txt, -r requirements-dev.in, pytest-cov
 cycler==0.12.1            # via -c constraints.txt, matplotlib
-cytoolz==1.0.0            # via -c constraints.txt, gt4py (pyproject.toml)
+cytoolz==1.0.1            # via -c constraints.txt, gt4py (pyproject.toml)
 dace==1.0.0               # via -c constraints.txt, gt4py (pyproject.toml)
 darglint==1.8.1           # via -c constraints.txt, -r requirements-dev.in
-debugpy==1.8.9            # via -c constraints.txt, ipykernel
+debugpy==1.8.11           # via -c constraints.txt, ipykernel
 decorator==5.1.1          # via -c constraints.txt, ipython
-deepdiff==8.0.1           # via -c constraints.txt, gt4py (pyproject.toml)
+deepdiff==8.1.1           # via -c constraints.txt, gt4py (pyproject.toml)
 devtools==0.12.2          # via -c constraints.txt, gt4py (pyproject.toml)
 dill==0.3.9               # via -c constraints.txt, dace
 diskcache==5.6.3          # via -c constraints.txt, gt4py (pyproject.toml)
 distlib==0.3.9            # via -c constraints.txt, virtualenv
-docutils==0.20.1          # via -c constraints.txt, sphinx, sphinx-rtd-theme
-exceptiongroup==1.2.2     # via -c constraints.txt, hypothesis, pytest
+docutils==0.21.2          # via -c constraints.txt, sphinx, sphinx-rtd-theme
+exceptiongroup==1.2.2     # via -c constraints.txt, hypothesis, ipython, pytest
 execnet==2.1.1            # via -c constraints.txt, pytest-cache, pytest-xdist
 executing==2.1.0          # via -c constraints.txt, devtools, stack-data
 factory-boy==3.3.1        # via -c constraints.txt, gt4py (pyproject.toml), pytest-factoryboy
-faker==33.1.0             # via -c constraints.txt, factory-boy
+faker==33.3.0             # via -c constraints.txt, factory-boy
 fastjsonschema==2.21.1    # via -c constraints.txt, nbformat
 filelock==3.16.1          # via -c constraints.txt, gt4py (pyproject.toml), tox, virtualenv
-fonttools==4.55.2         # via -c constraints.txt, matplotlib
+fonttools==4.55.3         # via -c constraints.txt, matplotlib
 fparser==0.2.0            # via -c constraints.txt, dace
 frozendict==2.4.6         # via -c constraints.txt, gt4py (pyproject.toml)
-gitdb==4.0.11             # via -c constraints.txt, gitpython
-gitpython==3.1.43         # via -c constraints.txt, tach
+gitdb==4.0.12             # via -c constraints.txt, gitpython
+gitpython==3.1.44         # via -c constraints.txt, tach
 gridtools-cpp==2.3.8      # via -c constraints.txt, gt4py (pyproject.toml)
-hypothesis==6.113.0       # via -c constraints.txt, -r requirements-dev.in, gt4py (pyproject.toml)
-identify==2.6.1           # via -c constraints.txt, pre-commit
+hypothesis==6.123.11      # via -c constraints.txt, -r requirements-dev.in, gt4py (pyproject.toml)
+identify==2.6.5           # via -c constraints.txt, pre-commit
 idna==3.10                # via -c constraints.txt, requests
 imagesize==1.4.1          # via -c constraints.txt, sphinx
-importlib-metadata==8.5.0  # via -c constraints.txt, build, jupyter-client, sphinx
-importlib-resources==6.4.5 ; python_version < "3.9"  # via -c constraints.txt, gt4py (pyproject.toml), jsonschema, jsonschema-specifications, matplotlib
 inflection==0.5.1         # via -c constraints.txt, pytest-factoryboy
 iniconfig==2.0.0          # via -c constraints.txt, pytest
 ipykernel==6.29.5         # via -c constraints.txt, nbmake
-ipython==8.12.3           # via -c constraints.txt, ipykernel
+ipython==8.31.0           # via -c constraints.txt, ipykernel
+jax[cpu]==0.4.38          # via -c constraints.txt, gt4py (pyproject.toml)
+jaxlib==0.4.38            # via -c constraints.txt, jax
 jedi==0.19.2              # via -c constraints.txt, ipython
-jinja2==3.1.4             # via -c constraints.txt, gt4py (pyproject.toml), sphinx
+jinja2==3.1.5             # via -c constraints.txt, gt4py (pyproject.toml), sphinx
 jsonschema==4.23.0        # via -c constraints.txt, nbformat
-jsonschema-specifications==2023.12.1  # via -c constraints.txt, jsonschema
+jsonschema-specifications==2024.10.1  # via -c constraints.txt, jsonschema
 jupyter-client==8.6.3     # via -c constraints.txt, ipykernel, nbclient
-jupyter-core==5.7.2       # via -c constraints.txt, ipykernel, jupyter-client, nbformat
-jupytext==1.16.4          # via -c constraints.txt, -r requirements-dev.in
-kiwisolver==1.4.7         # via -c constraints.txt, matplotlib
+jupyter-core==5.7.2       # via -c constraints.txt, ipykernel, jupyter-client, nbclient, nbformat
+jupytext==1.16.6          # via -c constraints.txt, -r requirements-dev.in
+kiwisolver==1.4.8         # via -c constraints.txt, matplotlib
 lark==1.2.2               # via -c constraints.txt, gt4py (pyproject.toml)
 mako==1.3.8               # via -c constraints.txt, gt4py (pyproject.toml)
 markdown-it-py==3.0.0     # via -c constraints.txt, jupytext, mdit-py-plugins, rich
-markupsafe==2.1.5         # via -c constraints.txt, jinja2, mako
-matplotlib==3.7.5         # via -c constraints.txt, -r requirements-dev.in
+markupsafe==3.0.2         # via -c constraints.txt, jinja2, mako
+matplotlib==3.10.0        # via -c constraints.txt, -r requirements-dev.in
 matplotlib-inline==0.1.7  # via -c constraints.txt, ipykernel, ipython
 mdit-py-plugins==0.4.2    # via -c constraints.txt, jupytext
 mdurl==0.1.2              # via -c constraints.txt, markdown-it-py
+ml-dtypes==0.5.1          # via -c constraints.txt, jax, jaxlib
 mpmath==1.3.0             # via -c constraints.txt, sympy
-mypy==1.13.0              # via -c constraints.txt, -r requirements-dev.in
+mypy==1.14.1              # via -c constraints.txt, -r requirements-dev.in
 mypy-extensions==1.0.0    # via -c constraints.txt, black, mypy
 nanobind==2.4.0           # via -c constraints.txt, gt4py (pyproject.toml)
-nbclient==0.6.8           # via -c constraints.txt, nbmake
+nbclient==0.10.2          # via -c constraints.txt, nbmake
 nbformat==5.10.4          # via -c constraints.txt, jupytext, nbclient, nbmake
-nbmake==1.5.4             # via -c constraints.txt, -r requirements-dev.in
-nest-asyncio==1.6.0       # via -c constraints.txt, ipykernel, nbclient
-networkx==3.1             # via -c constraints.txt, dace, tach
-ninja==1.11.1.2           # via -c constraints.txt, gt4py (pyproject.toml)
+nbmake==1.5.5             # via -c constraints.txt, -r requirements-dev.in
+nest-asyncio==1.6.0       # via -c constraints.txt, ipykernel
+networkx==3.4.2           # via -c constraints.txt, dace, tach
+ninja==1.11.1.3           # via -c constraints.txt, gt4py (pyproject.toml)
 nodeenv==1.9.1            # via -c constraints.txt, pre-commit
-numpy==1.24.4             # via -c constraints.txt, contourpy, dace, gt4py (pyproject.toml), matplotlib
-orderly-set==5.2.2        # via -c constraints.txt, deepdiff
+numpy==1.26.4             # via -c constraints.txt, contourpy, dace, gt4py (pyproject.toml), jax, jaxlib, matplotlib, ml-dtypes, scipy
+opt-einsum==3.4.0         # via -c constraints.txt, jax
+orderly-set==5.2.3        # via -c constraints.txt, deepdiff
 packaging==24.2           # via -c constraints.txt, black, build, dace, gt4py (pyproject.toml), ipykernel, jupytext, matplotlib, pipdeptree, pyproject-api, pytest, pytest-factoryboy, setuptools-scm, sphinx, tox
 parso==0.8.4              # via -c constraints.txt, jedi
 pathspec==0.12.1          # via -c constraints.txt, black
 pexpect==4.9.0            # via -c constraints.txt, ipython
-pickleshare==0.7.5        # via -c constraints.txt, ipython
-pillow==10.4.0            # via -c constraints.txt, matplotlib
+pillow==11.1.0            # via -c constraints.txt, matplotlib
 pip-tools==7.4.1          # via -c constraints.txt, -r requirements-dev.in
 pipdeptree==2.24.0        # via -c constraints.txt, -r requirements-dev.in
-pkgutil-resolve-name==1.3.10  # via -c constraints.txt, jsonschema
 platformdirs==4.3.6       # via -c constraints.txt, black, jupyter-core, tox, virtualenv
 pluggy==1.5.0             # via -c constraints.txt, pytest, tox
 ply==3.11                 # via -c constraints.txt, dace
-pre-commit==3.5.0         # via -c constraints.txt, -r requirements-dev.in
-prompt-toolkit==3.0.36    # via -c constraints.txt, ipython, questionary, tach
-psutil==6.1.0             # via -c constraints.txt, -r requirements-dev.in, ipykernel, pytest-xdist
+pre-commit==4.0.1         # via -c constraints.txt, -r requirements-dev.in
+prompt-toolkit==3.0.48    # via -c constraints.txt, ipython, questionary, tach
+psutil==6.1.1             # via -c constraints.txt, -r requirements-dev.in, ipykernel, pytest-xdist
 ptyprocess==0.7.0         # via -c constraints.txt, pexpect
 pure-eval==0.2.3          # via -c constraints.txt, stack-data
 pybind11==2.13.6          # via -c constraints.txt, gt4py (pyproject.toml)
-pydantic==2.10.3          # via -c constraints.txt, bump-my-version, pydantic-settings
-pydantic-core==2.27.1     # via -c constraints.txt, pydantic
-pydantic-settings==2.6.1  # via -c constraints.txt, bump-my-version
-pydot==3.0.3              # via -c constraints.txt, tach
-pygments==2.18.0          # via -c constraints.txt, -r requirements-dev.in, devtools, ipython, nbmake, rich, sphinx
-pyparsing==3.1.4          # via -c constraints.txt, matplotlib, pydot
+pydantic==2.10.4          # via -c constraints.txt, bump-my-version, pydantic-settings
+pydantic-core==2.27.2     # via -c constraints.txt, pydantic
+pydantic-settings==2.7.1  # via -c constraints.txt, bump-my-version
+pydot==3.0.4              # via -c constraints.txt, tach
+pygments==2.19.1          # via -c constraints.txt, -r requirements-dev.in, devtools, ipython, nbmake, rich, sphinx
+pyparsing==3.2.1          # via -c constraints.txt, matplotlib, pydot
 pyproject-api==1.8.0      # via -c constraints.txt, tox
 pyproject-hooks==1.2.0    # via -c constraints.txt, build, pip-tools
 pytest==8.3.4             # via -c constraints.txt, -r requirements-dev.in, gt4py (pyproject.toml), nbmake, pytest-cache, pytest-cov, pytest-custom-exit-code, pytest-factoryboy, pytest-instafail, pytest-xdist
 pytest-cache==1.0         # via -c constraints.txt, -r requirements-dev.in
-pytest-cov==5.0.0         # via -c constraints.txt, -r requirements-dev.in
+pytest-cov==6.0.0         # via -c constraints.txt, -r requirements-dev.in
 pytest-custom-exit-code==0.3.0  # via -c constraints.txt, -r requirements-dev.in
 pytest-factoryboy==2.7.0  # via -c constraints.txt, -r requirements-dev.in
 pytest-instafail==0.5.0   # via -c constraints.txt, -r requirements-dev.in
 pytest-xdist[psutil]==3.6.1  # via -c constraints.txt, -r requirements-dev.in
 python-dateutil==2.9.0.post0  # via -c constraints.txt, faker, jupyter-client, matplotlib
 python-dotenv==1.0.1      # via -c constraints.txt, pydantic-settings
-pytz==2024.2              # via -c constraints.txt, babel
 pyyaml==6.0.2             # via -c constraints.txt, dace, jupytext, pre-commit, tach
 pyzmq==26.2.0             # via -c constraints.txt, ipykernel, jupyter-client
-questionary==2.0.1        # via -c constraints.txt, bump-my-version
+questionary==2.1.0        # via -c constraints.txt, bump-my-version
 referencing==0.35.1       # via -c constraints.txt, jsonschema, jsonschema-specifications
 requests==2.32.3          # via -c constraints.txt, sphinx
 rich==13.9.4              # via -c constraints.txt, bump-my-version, rich-click, tach
 rich-click==1.8.5         # via -c constraints.txt, bump-my-version
-rpds-py==0.20.1           # via -c constraints.txt, jsonschema, referencing
-ruff==0.8.2               # via -c constraints.txt, -r requirements-dev.in
+rpds-py==0.22.3           # via -c constraints.txt, jsonschema, referencing
+ruff==0.8.6               # via -c constraints.txt, -r requirements-dev.in
+scipy==1.15.0             # via -c constraints.txt, jax, jaxlib
 setuptools-scm==8.1.0     # via -c constraints.txt, fparser
 six==1.17.0               # via -c constraints.txt, asttokens, astunparse, python-dateutil
-smmap==5.0.1              # via -c constraints.txt, gitdb
+smmap==5.0.2              # via -c constraints.txt, gitdb
 snowballstemmer==2.2.0    # via -c constraints.txt, sphinx
 sortedcontainers==2.4.0   # via -c constraints.txt, hypothesis
-sphinx==7.1.2             # via -c constraints.txt, -r requirements-dev.in, sphinx-rtd-theme, sphinxcontrib-jquery
+sphinx==8.1.3             # via -c constraints.txt, -r requirements-dev.in, sphinx-rtd-theme, sphinxcontrib-jquery
 sphinx-rtd-theme==3.0.2   # via -c constraints.txt, -r requirements-dev.in
-sphinxcontrib-applehelp==1.0.4  # via -c constraints.txt, sphinx
-sphinxcontrib-devhelp==1.0.2  # via -c constraints.txt, sphinx
-sphinxcontrib-htmlhelp==2.0.1  # via -c constraints.txt, sphinx
+sphinxcontrib-applehelp==2.0.0  # via -c constraints.txt, sphinx
+sphinxcontrib-devhelp==2.0.0  # via -c constraints.txt, sphinx
+sphinxcontrib-htmlhelp==2.1.0  # via -c constraints.txt, sphinx
 sphinxcontrib-jquery==4.1  # via -c constraints.txt, sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1  # via -c constraints.txt, sphinx
-sphinxcontrib-qthelp==1.0.3  # via -c constraints.txt, sphinx
-sphinxcontrib-serializinghtml==1.1.5  # via -c constraints.txt, sphinx
+sphinxcontrib-qthelp==2.0.0  # via -c constraints.txt, sphinx
+sphinxcontrib-serializinghtml==2.0.0  # via -c constraints.txt, sphinx
 stack-data==0.6.3         # via -c constraints.txt, ipython
-stdlib-list==0.10.0       # via -c constraints.txt, tach
 sympy==1.13.3             # via -c constraints.txt, dace
 tabulate==0.9.0           # via -c constraints.txt, gt4py (pyproject.toml)
-tach==0.16.5              # via -c constraints.txt, -r requirements-dev.in
-tomli==2.2.1 ; python_version < "3.11"  # via -c constraints.txt, -r requirements-dev.in, black, build, coverage, jupytext, mypy, pip-tools, pyproject-api, pytest, setuptools-scm, tach, tox
-tomli-w==1.0.0            # via -c constraints.txt, tach
+tach==0.19.5              # via -c constraints.txt, -r requirements-dev.in
+tomli==2.2.1 ; python_version < "3.11"  # via -c constraints.txt, -r requirements-dev.in, black, build, coverage, jupytext, mypy, pip-tools, pyproject-api, pytest, setuptools-scm, sphinx, tach, tox
+tomli-w==1.1.0            # via -c constraints.txt, tach
 tomlkit==0.13.2           # via -c constraints.txt, bump-my-version
 toolz==1.0.0              # via -c constraints.txt, cytoolz
 tornado==6.4.2            # via -c constraints.txt, ipykernel, jupyter-client
 tox==4.23.2               # via -c constraints.txt, -r requirements-dev.in
 traitlets==5.14.3         # via -c constraints.txt, comm, ipykernel, ipython, jupyter-client, jupyter-core, matplotlib-inline, nbclient, nbformat
 types-tabulate==0.9.0.20241207  # via -c constraints.txt, -r requirements-dev.in
-typing-extensions==4.12.2  # via -c constraints.txt, annotated-types, black, faker, gt4py (pyproject.toml), ipython, mypy, pydantic, pydantic-core, pytest-factoryboy, rich, rich-click, setuptools-scm, tox
-urllib3==2.2.3            # via -c constraints.txt, requests
-virtualenv==20.28.0       # via -c constraints.txt, pre-commit, tox
+typing-extensions==4.12.2  # via -c constraints.txt, black, faker, gt4py (pyproject.toml), ipython, mypy, pydantic, pydantic-core, pytest-factoryboy, rich, rich-click, tox
+urllib3==2.3.0            # via -c constraints.txt, requests
+virtualenv==20.28.1       # via -c constraints.txt, pre-commit, tox
 wcmatch==10.0             # via -c constraints.txt, bump-my-version
 wcwidth==0.2.13           # via -c constraints.txt, prompt-toolkit
 wheel==0.45.1             # via -c constraints.txt, astunparse, pip-tools
 xxhash==3.0.0             # via -c constraints.txt, gt4py (pyproject.toml)
-zipp==3.20.2              # via -c constraints.txt, importlib-metadata, importlib-resources
 
 # The following packages are considered to be unsafe in a requirements file:
 pip==24.3.1               # via -c constraints.txt, pip-tools, pipdeptree
-setuptools==75.3.0        # via -c constraints.txt, gt4py (pyproject.toml), pip-tools, setuptools-scm
+setuptools==75.8.0        # via -c constraints.txt, gt4py (pyproject.toml), pip-tools, setuptools-scm
diff --git a/src/gt4py/cartesian/frontend/gtscript_frontend.py b/src/gt4py/cartesian/frontend/gtscript_frontend.py
index f155ea62..4d8ac985 100644
--- a/src/gt4py/cartesian/frontend/gtscript_frontend.py
+++ b/src/gt4py/cartesian/frontend/gtscript_frontend.py
@@ -1451,21 +1451,6 @@ class IRMaker(ast.NodeVisitor):
                             message="Assignment to non-zero offsets in K is not available in PARALLEL. Choose FORWARD or BACKWARD.",
                             loc=nodes.Location.from_ast_node(t),
                         )
-                    if self.backend_name in ["gt:gpu", "dace:gpu"]:
-                        import cupy as cp
-
-                        if cp.cuda.runtime.runtimeGetVersion() < 12000:
-                            raise GTScriptSyntaxError(
-                                message=f"Assignment to non-zero offsets in K is not available in {self.backend_name} for CUDA<12. Please update CUDA.",
-                                loc=nodes.Location.from_ast_node(t),
-                            )
-
-                    if self.backend_name in ["gt:gpu"]:
-                        raise GTScriptSyntaxError(
-                            message=f"Assignment to non-zero offsets in K is not available in {self.backend_name} as an unsolved bug remains."
-                            "Please refer to https://github.com/GridTools/gt4py/issues/1754.",
-                            loc=nodes.Location.from_ast_node(t),
-                        )
 
             if not self._is_known(name):
                 if name in self.temp_decls:
diff --git a/src/gt4py/cartesian/gtc/common.py b/src/gt4py/cartesian/gtc/common.py
index dcb01db7..8c3c731c 100644
--- a/src/gt4py/cartesian/gtc/common.py
+++ b/src/gt4py/cartesian/gtc/common.py
@@ -229,8 +229,8 @@ class LevelMarker(eve.StrEnum):
 
 @enum.unique
 class ExprKind(eve.IntEnum):
-    SCALAR: ExprKind = typing.cast("ExprKind", enum.auto())
-    FIELD: ExprKind = typing.cast("ExprKind", enum.auto())
+    SCALAR = typing.cast("ExprKind", enum.auto())
+    FIELD = typing.cast("ExprKind", enum.auto())
 
 
 class LocNode(eve.Node):
diff --git a/src/gt4py/eve/datamodels/core.py b/src/gt4py/eve/datamodels/core.py
index 1b0e9951..31e63bdf 100644
--- a/src/gt4py/eve/datamodels/core.py
+++ b/src/gt4py/eve/datamodels/core.py
@@ -16,6 +16,7 @@ from __future__ import annotations
 import dataclasses
 import functools
 import sys
+import types
 import typing
 import warnings
 
@@ -1254,8 +1255,11 @@ def _make_concrete_with_cache(
     if not is_generic_datamodel_class(datamodel_cls):
         raise TypeError(f"'{datamodel_cls.__name__}' is not a generic model class.")
     for t in type_args:
+        _accepted_types: tuple[type, ...] = (type, type(None), xtyping.StdGenericAliasType)
+        if sys.version_info >= (3, 10):
+            _accepted_types = (*_accepted_types, types.UnionType)
         if not (
-            isinstance(t, (type, type(None), xtyping.StdGenericAliasType))
+            isinstance(t, _accepted_types)
             or (getattr(type(t), "__module__", None) in ("typing", "typing_extensions"))
         ):
             raise TypeError(
diff --git a/src/gt4py/eve/type_validation.py b/src/gt4py/eve/type_validation.py
index e1508322..695ab69d 100644
--- a/src/gt4py/eve/type_validation.py
+++ b/src/gt4py/eve/type_validation.py
@@ -14,6 +14,8 @@ import abc
 import collections.abc
 import dataclasses
 import functools
+import sys
+import types
 import typing
 
 from . import exceptions, extended_typing as xtyping, utils
@@ -193,6 +195,12 @@ class SimpleTypeValidatorFactory(TypeValidatorFactory):
             if type_annotation is None:
                 type_annotation = type(None)
 
+            if sys.version_info >= (3, 10):
+                if isinstance(
+                    type_annotation, types.UnionType
+                ):  # see https://github.com/python/cpython/issues/105499
+                    type_annotation = typing.Union[type_annotation.__args__]
+
             # Non-generic types
             if xtyping.is_actual_type(type_annotation):
                 assert not xtyping.get_args(type_annotation)
@@ -277,6 +285,7 @@ class SimpleTypeValidatorFactory(TypeValidatorFactory):
 
                 if issubclass(origin_type, (collections.abc.Sequence, collections.abc.Set)):
                     assert len(type_args) == 1
+                    make_recursive(type_args[0])
                     if (member_validator := make_recursive(type_args[0])) is None:
                         raise exceptions.EveValueError(
                             f"{type_args[0]} type annotation is not supported."
diff --git a/src/gt4py/eve/utils.py b/src/gt4py/eve/utils.py
index 2c66d392..96e41a7b 100644
--- a/src/gt4py/eve/utils.py
+++ b/src/gt4py/eve/utils.py
@@ -440,8 +440,8 @@ def content_hash(*args: Any, hash_algorithm: str | xtyping.HashlibAlgorithm | No
     return result
 
 
-ddiff = deepdiff.DeepDiff
-"""Shortcut for deepdiff.DeepDiff.
+ddiff = deepdiff.diff.DeepDiff
+"""Shortcut for deepdiff.diff.DeepDiff.
 
 Check https://zepworks.com/deepdiff/current/diff.html for more info.
 """
@@ -458,13 +458,13 @@ def dhash(obj: Any, **kwargs: Any) -> str:
 def pprint_ddiff(
     old: Any, new: Any, *, pprint_opts: Optional[Dict[str, Any]] = None, **kwargs: Any
 ) -> None:
-    """Pretty printing of deepdiff.DeepDiff objects.
+    """Pretty printing of deepdiff.diff.DeepDiff objects.
 
     Keyword Arguments:
         pprint_opts: kwargs dict with options for pprint.pprint.
     """
     pprint_opts = pprint_opts or {"indent": 2}
-    pprint.pprint(deepdiff.DeepDiff(old, new, **kwargs), **pprint_opts)
+    pprint.pprint(deepdiff.diff.DeepDiff(old, new, **kwargs), **pprint_opts)
 
 
 AnyWordsIterable = Union[str, Iterable[str]]
diff --git a/src/gt4py/next/ffront/decorator.py b/src/gt4py/next/ffront/decorator.py
index d1870950..d1631a46 100644
--- a/src/gt4py/next/ffront/decorator.py
+++ b/src/gt4py/next/ffront/decorator.py
@@ -80,7 +80,9 @@ class Program:
 
     definition_stage: ffront_stages.ProgramDefinition
     backend: Optional[next_backend.Backend]
-    connectivities: Optional[common.OffsetProviderType] = None
+    connectivities: Optional[common.OffsetProvider] = (
+        None  # TODO(ricoh): replace with common.OffsetProviderType once the temporary pass doesn't require the runtime information
+    )
 
     @classmethod
     def from_function(
@@ -304,7 +306,7 @@ class FrozenProgram:
 
 
 try:
-    from gt4py.next.program_processors.runners.dace_iterator import Program
+    from gt4py.next.program_processors.runners.dace_fieldview.program import Program
 except ImportError:
     pass
 
diff --git a/src/gt4py/next/ffront/field_operator_ast.py b/src/gt4py/next/ffront/field_operator_ast.py
index 4693fed1..4f547aae 100644
--- a/src/gt4py/next/ffront/field_operator_ast.py
+++ b/src/gt4py/next/ffront/field_operator_ast.py
@@ -180,7 +180,7 @@ class IfStmt(Stmt):
     @datamodels.root_validator
     @classmethod
     def _collect_common_symbols(cls: type[IfStmt], instance: IfStmt) -> None:
-        common_symbol_names = (
+        common_symbol_names = sorted(  # sort is required to get stable results across runs
             instance.true_branch.annex.symtable.keys() & instance.false_branch.annex.symtable.keys()
         )
         instance.annex.propagated_symbols = {
diff --git a/src/gt4py/next/ffront/foast_passes/type_deduction.py b/src/gt4py/next/ffront/foast_passes/type_deduction.py
index d334487a..6b40cbb7 100644
--- a/src/gt4py/next/ffront/foast_passes/type_deduction.py
+++ b/src/gt4py/next/ffront/foast_passes/type_deduction.py
@@ -6,7 +6,7 @@
 # Please, refer to the LICENSE file in the root directory.
 # SPDX-License-Identifier: BSD-3-Clause
 
-from typing import Any, Optional, TypeVar, cast
+from typing import Any, Optional, TypeAlias, TypeVar, cast
 
 import gt4py.next.ffront.field_operator_ast as foast
 from gt4py.eve import NodeTranslator, NodeVisitor, traits
@@ -48,7 +48,7 @@ def with_altered_scalar_kind(
     if isinstance(type_spec, ts.FieldType):
         return ts.FieldType(
             dims=type_spec.dims,
-            dtype=ts.ScalarType(kind=new_scalar_kind, shape=type_spec.dtype.shape),
+            dtype=with_altered_scalar_kind(type_spec.dtype, new_scalar_kind),
         )
     elif isinstance(type_spec, ts.ScalarType):
         return ts.ScalarType(kind=new_scalar_kind, shape=type_spec.shape)
@@ -68,13 +68,18 @@ def construct_tuple_type(
     >>> mask_type = ts.FieldType(
     ...     dims=[Dimension(value="I")], dtype=ts.ScalarType(kind=ts.ScalarKind.BOOL)
     ... )
-    >>> true_branch_types = [ts.ScalarType(kind=ts.ScalarKind), ts.ScalarType(kind=ts.ScalarKind)]
+    >>> true_branch_types = [
+    ...     ts.ScalarType(kind=ts.ScalarKind.FLOAT64),
+    ...     ts.ScalarType(kind=ts.ScalarKind.FLOAT64),
+    ... ]
     >>> false_branch_types = [
-    ...     ts.FieldType(dims=[Dimension(value="I")], dtype=ts.ScalarType(kind=ts.ScalarKind)),
-    ...     ts.ScalarType(kind=ts.ScalarKind),
+    ...     ts.FieldType(
+    ...         dims=[Dimension(value="I")], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
+    ...     ),
+    ...     ts.ScalarType(kind=ts.ScalarKind.FLOAT64),
     ... ]
     >>> print(construct_tuple_type(true_branch_types, false_branch_types, mask_type))
-    [FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<enum 'ScalarKind'>, shape=None)), FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<enum 'ScalarKind'>, shape=None))]
+    [FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<ScalarKind.FLOAT64: 1064>, shape=None)), FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<ScalarKind.FLOAT64: 1064>, shape=None))]
     """
     element_types_new = true_branch_types
     for i, element in enumerate(true_branch_types):
@@ -105,8 +110,8 @@ def promote_to_mask_type(
     >>> I, J = (Dimension(value=dim) for dim in ["I", "J"])
     >>> bool_type = ts.ScalarType(kind=ts.ScalarKind.BOOL)
     >>> dtype = ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
-    >>> promote_to_mask_type(ts.FieldType(dims=[I, J], dtype=bool_type), ts.ScalarType(kind=dtype))
-    FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>), Dimension(value='J', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=ScalarType(kind=<ScalarKind.FLOAT64: 1064>, shape=None), shape=None))
+    >>> promote_to_mask_type(ts.FieldType(dims=[I, J], dtype=bool_type), dtype)
+    FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>), Dimension(value='J', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<ScalarKind.FLOAT64: 1064>, shape=None))
     >>> promote_to_mask_type(
     ...     ts.FieldType(dims=[I, J], dtype=bool_type), ts.FieldType(dims=[I], dtype=dtype)
     ... )
@@ -360,7 +365,7 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
     def visit_TupleTargetAssign(
         self, node: foast.TupleTargetAssign, **kwargs: Any
     ) -> foast.TupleTargetAssign:
-        TargetType = list[foast.Starred | foast.Symbol]
+        TargetType: TypeAlias = list[foast.Starred | foast.Symbol]
         values = self.visit(node.value, **kwargs)
 
         if isinstance(values.type, ts.TupleType):
@@ -374,7 +379,7 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
                 )
 
             new_targets: TargetType = []
-            new_type: ts.TupleType | ts.DataType
+            new_type: ts.DataType
             for i, index in enumerate(indices):
                 old_target = targets[i]
 
@@ -391,7 +396,8 @@ class FieldOperatorTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTransla
                         location=old_target.location,
                     )
                 else:
-                    new_type = values.type.types[index]
+                    new_type = values.type.types[index]  # type: ignore[assignment] # see check in next line
+                    assert isinstance(new_type, ts.DataType)
                     new_target = self.visit(
                         old_target, refine_type=new_type, location=old_target.location, **kwargs
                     )
diff --git a/src/gt4py/next/ffront/foast_to_gtir.py b/src/gt4py/next/ffront/foast_to_gtir.py
index 3c65695a..4519b4e5 100644
--- a/src/gt4py/next/ffront/foast_to_gtir.py
+++ b/src/gt4py/next/ffront/foast_to_gtir.py
@@ -236,6 +236,7 @@ class FieldOperatorLowering(eve.PreserveLocationVisitor, eve.NodeTranslator):
     def visit_UnaryOp(self, node: foast.UnaryOp, **kwargs: Any) -> itir.Expr:
         # TODO(tehrengruber): extend iterator ir to support unary operators
         dtype = type_info.extract_dtype(node.type)
+        assert isinstance(dtype, ts.ScalarType)
         if node.op in [dialect_ast_enums.UnaryOperator.NOT, dialect_ast_enums.UnaryOperator.INVERT]:
             if dtype.kind != ts.ScalarKind.BOOL:
                 raise NotImplementedError(f"'{node.op}' is only supported on 'bool' arguments.")
@@ -417,12 +418,14 @@ class FieldOperatorLowering(eve.PreserveLocationVisitor, eve.NodeTranslator):
 
     def _visit_max_over(self, node: foast.Call, **kwargs: Any) -> itir.Expr:
         dtype = type_info.extract_dtype(node.type)
+        assert isinstance(dtype, ts.ScalarType)
         min_value, _ = type_info.arithmetic_bounds(dtype)
         init_expr = self._make_literal(str(min_value), dtype)
         return self._make_reduction_expr(node, "maximum", init_expr, **kwargs)
 
     def _visit_min_over(self, node: foast.Call, **kwargs: Any) -> itir.Expr:
         dtype = type_info.extract_dtype(node.type)
+        assert isinstance(dtype, ts.ScalarType)
         _, max_value = type_info.arithmetic_bounds(dtype)
         init_expr = self._make_literal(str(max_value), dtype)
         return self._make_reduction_expr(node, "minimum", init_expr, **kwargs)
diff --git a/src/gt4py/next/ffront/past_passes/type_deduction.py b/src/gt4py/next/ffront/past_passes/type_deduction.py
index 92f73272..93552735 100644
--- a/src/gt4py/next/ffront/past_passes/type_deduction.py
+++ b/src/gt4py/next/ffront/past_passes/type_deduction.py
@@ -104,6 +104,15 @@ class ProgramTypeDeduction(traits.VisitorWithSymbolTableTrait, NodeTranslator):
             location=node.location,
         )
 
+    def visit_Slice(self, node: past.Slice, **kwargs: Any) -> past.Slice:
+        return past.Slice(
+            lower=self.visit(node.lower, **kwargs),
+            upper=self.visit(node.upper, **kwargs),
+            step=self.visit(node.step, **kwargs),
+            type=ts.DeferredType(constraint=None),
+            location=node.location,
+        )
+
     def visit_Subscript(self, node: past.Subscript, **kwargs: Any) -> past.Subscript:
         value = self.visit(node.value, **kwargs)
         return past.Subscript(
diff --git a/src/gt4py/next/ffront/past_process_args.py b/src/gt4py/next/ffront/past_process_args.py
index 7958b7a8..1add6687 100644
--- a/src/gt4py/next/ffront/past_process_args.py
+++ b/src/gt4py/next/ffront/past_process_args.py
@@ -109,6 +109,7 @@ def _field_constituents_shape_and_dims(
     match arg_type:
         case ts.TupleType():
             for el, el_type in zip(arg, arg_type.types):
+                assert isinstance(el_type, ts.DataType)
                 yield from _field_constituents_shape_and_dims(el, el_type)
         case ts.FieldType():
             dims = type_info.extract_dims(arg_type)
diff --git a/src/gt4py/next/ffront/type_info.py b/src/gt4py/next/ffront/type_info.py
index 8160a2c4..83ecf928 100644
--- a/src/gt4py/next/ffront/type_info.py
+++ b/src/gt4py/next/ffront/type_info.py
@@ -169,7 +169,9 @@ def _scan_param_promotion(param: ts.TypeSpec, arg: ts.TypeSpec) -> ts.FieldType
     --------
     >>> _scan_param_promotion(
     ...     ts.ScalarType(kind=ts.ScalarKind.INT64),
-    ...     ts.FieldType(dims=[common.Dimension("I")], dtype=ts.ScalarKind.FLOAT64),
+    ...     ts.FieldType(
+    ...         dims=[common.Dimension("I")], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
+    ...     ),
     ... )
     FieldType(dims=[Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)], dtype=ScalarType(kind=<ScalarKind.INT64: 64>, shape=None))
     """
@@ -252,8 +254,8 @@ def function_signature_incompatibilities_scanop(
     # build a function type to leverage the already existing signature checking capabilities
     function_type = ts.FunctionType(
         pos_only_args=[],
-        pos_or_kw_args=promoted_params,  # type: ignore[arg-type] # dict is invariant, but we don't care here.
-        kw_only_args=promoted_kwparams,  # type: ignore[arg-type] # same as above
+        pos_or_kw_args=promoted_params,
+        kw_only_args=promoted_kwparams,
         returns=ts.DeferredType(constraint=None),
     )
 
diff --git a/src/gt4py/next/ffront/type_specifications.py b/src/gt4py/next/ffront/type_specifications.py
index e4f6c826..b76a1162 100644
--- a/src/gt4py/next/ffront/type_specifications.py
+++ b/src/gt4py/next/ffront/type_specifications.py
@@ -6,23 +6,19 @@
 # Please, refer to the LICENSE file in the root directory.
 # SPDX-License-Identifier: BSD-3-Clause
 
-from dataclasses import dataclass
 
 import gt4py.next.type_system.type_specifications as ts
-from gt4py.next import common as func_common
+from gt4py.next import common
 
 
-@dataclass(frozen=True)
 class ProgramType(ts.TypeSpec, ts.CallableType):
     definition: ts.FunctionType
 
 
-@dataclass(frozen=True)
 class FieldOperatorType(ts.TypeSpec, ts.CallableType):
     definition: ts.FunctionType
 
 
-@dataclass(frozen=True)
 class ScanOperatorType(ts.TypeSpec, ts.CallableType):
-    axis: func_common.Dimension
+    axis: common.Dimension
     definition: ts.FunctionType
diff --git a/src/gt4py/next/iterator/embedded.py b/src/gt4py/next/iterator/embedded.py
index 13c64e26..5949d294 100644
--- a/src/gt4py/next/iterator/embedded.py
+++ b/src/gt4py/next/iterator/embedded.py
@@ -54,7 +54,6 @@ from gt4py.next.embedded import (
 )
 from gt4py.next.ffront import fbuiltins
 from gt4py.next.iterator import builtins, runtime
-from gt4py.next.iterator.type_system import type_specifications as itir_ts
 from gt4py.next.otf import arguments
 from gt4py.next.type_system import type_specifications as ts, type_translation
 
@@ -1460,7 +1459,7 @@ class _List(Generic[DT]):
     def __getitem__(self, i: int):
         return self.values[i]
 
-    def __gt_type__(self) -> itir_ts.ListType:
+    def __gt_type__(self) -> ts.ListType:
         offset_tag = self.offset.value
         assert isinstance(offset_tag, str)
         element_type = type_translation.from_value(self.values[0])
@@ -1470,7 +1469,7 @@ class _List(Generic[DT]):
         connectivity = offset_provider[offset_tag]
         assert common.is_neighbor_connectivity(connectivity)
         local_dim = connectivity.__gt_type__().neighbor_dim
-        return itir_ts.ListType(element_type=element_type, offset_type=local_dim)
+        return ts.ListType(element_type=element_type, offset_type=local_dim)
 
 
 @dataclasses.dataclass(frozen=True)
@@ -1480,10 +1479,10 @@ class _ConstList(Generic[DT]):
     def __getitem__(self, _):
         return self.value
 
-    def __gt_type__(self) -> itir_ts.ListType:
+    def __gt_type__(self) -> ts.ListType:
         element_type = type_translation.from_value(self.value)
         assert isinstance(element_type, ts.DataType)
-        return itir_ts.ListType(
+        return ts.ListType(
             element_type=element_type,
             offset_type=_CONST_DIM,
         )
@@ -1801,7 +1800,7 @@ def _fieldspec_list_to_value(
     domain: common.Domain, type_: ts.TypeSpec
 ) -> tuple[common.Domain, ts.TypeSpec]:
     """Translate the list element type into the domain."""
-    if isinstance(type_, itir_ts.ListType):
+    if isinstance(type_, ts.ListType):
         if type_.offset_type == _CONST_DIM:
             return domain.insert(
                 len(domain), common.named_range((_CONST_DIM, 1))
diff --git a/src/gt4py/next/iterator/transforms/extractors.py b/src/gt4py/next/iterator/transforms/extractors.py
new file mode 100644
index 00000000..04c2b091
--- /dev/null
+++ b/src/gt4py/next/iterator/transforms/extractors.py
@@ -0,0 +1,72 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2024, ETH Zurich
+# All rights reserved.
+#
+# Please, refer to the LICENSE file in the root directory.
+# SPDX-License-Identifier: BSD-3-Clause
+
+from gt4py import eve
+from gt4py.next.iterator import ir as itir
+from gt4py.next.type_system import type_specifications as ts
+
+
+class SymbolNameSetExtractor(eve.NodeVisitor):
+    """Extract a set of symbol names"""
+
+    def visit_Literal(self, node: itir.Literal) -> set[str]:
+        return set()
+
+    def generic_visitor(self, node: itir.Node) -> set[str]:
+        input_fields: set[str] = set()
+        for child in eve.trees.iter_children_values(node):
+            input_fields |= self.visit(child)
+        return input_fields
+
+    def visit_Node(self, node: itir.Node) -> set[str]:
+        return set()
+
+    def visit_Program(self, node: itir.Program) -> set[str]:
+        names = set()
+        for stmt in node.body:
+            names |= self.visit(stmt)
+        return names
+
+    def visit_IfStmt(self, node: itir.IfStmt) -> set[str]:
+        names = set()
+        for stmt in node.true_branch + node.false_branch:
+            names |= self.visit(stmt)
+        return names
+
+    def visit_Temporary(self, node: itir.Temporary) -> set[str]:
+        return set()
+
+    def visit_SymRef(self, node: itir.SymRef) -> set[str]:
+        return {str(node.id)}
+
+    @classmethod
+    def only_fields(cls, program: itir.Program) -> set[str]:
+        field_param_names = [
+            str(param.id) for param in program.params if isinstance(param.type, ts.FieldType)
+        ]
+        return {name for name in cls().visit(program) if name in field_param_names}
+
+
+class InputNamesExtractor(SymbolNameSetExtractor):
+    """Extract the set of symbol names passed into field operators within a program."""
+
+    def visit_SetAt(self, node: itir.SetAt) -> set[str]:
+        return self.visit(node.expr)
+
+    def visit_FunCall(self, node: itir.FunCall) -> set[str]:
+        input_fields = set()
+        for arg in node.args:
+            input_fields |= self.visit(arg)
+        return input_fields
+
+
+class OutputNamesExtractor(SymbolNameSetExtractor):
+    """Extract the set of symbol names written to within a program"""
+
+    def visit_SetAt(self, node: itir.SetAt) -> set[str]:
+        return self.visit(node.target)
diff --git a/src/gt4py/next/iterator/transforms/fuse_as_fieldop.py b/src/gt4py/next/iterator/transforms/fuse_as_fieldop.py
index b7087472..cc42896f 100644
--- a/src/gt4py/next/iterator/transforms/fuse_as_fieldop.py
+++ b/src/gt4py/next/iterator/transforms/fuse_as_fieldop.py
@@ -20,10 +20,7 @@ from gt4py.next.iterator.transforms import (
     inline_lifts,
     trace_shifts,
 )
-from gt4py.next.iterator.type_system import (
-    inference as type_inference,
-    type_specifications as it_ts,
-)
+from gt4py.next.iterator.type_system import inference as type_inference
 from gt4py.next.type_system import type_info, type_specifications as ts
 
 
@@ -140,7 +137,7 @@ def fuse_as_fieldop(
             if arg.type and not isinstance(arg.type, ts.DeferredType):
                 assert isinstance(arg.type, ts.TypeSpec)
                 dtype = type_info.apply_to_primitive_constituents(type_info.extract_dtype, arg.type)
-                assert not isinstance(dtype, it_ts.ListType)
+                assert not isinstance(dtype, ts.ListType)
             new_param: str
             if isinstance(
                 arg, itir.SymRef
@@ -246,7 +243,7 @@ class FuseAsFieldOp(eve.NodeTranslator):
                             )
                             or cpm.is_call_to(arg, "if_")
                         )
-                        and (isinstance(dtype, it_ts.ListType) or len(arg_shifts) <= 1)
+                        and (isinstance(dtype, ts.ListType) or len(arg_shifts) <= 1)
                     )
                 )
 
diff --git a/src/gt4py/next/iterator/transforms/global_tmps.py b/src/gt4py/next/iterator/transforms/global_tmps.py
index 334fb330..ac7fcb8f 100644
--- a/src/gt4py/next/iterator/transforms/global_tmps.py
+++ b/src/gt4py/next/iterator/transforms/global_tmps.py
@@ -98,12 +98,12 @@ def _transform_by_pattern(
                 tmp_expr.type,
                 tuple_constructor=lambda *elements: tuple(elements),
             )
-            tmp_dtypes: ts.ScalarType | tuple[ts.ScalarType | tuple, ...] = (
-                type_info.apply_to_primitive_constituents(
-                    type_info.extract_dtype,
-                    tmp_expr.type,
-                    tuple_constructor=lambda *elements: tuple(elements),
-                )
+            tmp_dtypes: (
+                ts.ScalarType | ts.ListType | tuple[ts.ScalarType | ts.ListType | tuple, ...]
+            ) = type_info.apply_to_primitive_constituents(
+                type_info.extract_dtype,
+                tmp_expr.type,
+                tuple_constructor=lambda *elements: tuple(elements),
             )
 
             # allocate temporary for all tuple elements
diff --git a/src/gt4py/next/iterator/type_system/inference.py b/src/gt4py/next/iterator/type_system/inference.py
index 1b980783..1da59546 100644
--- a/src/gt4py/next/iterator/type_system/inference.py
+++ b/src/gt4py/next/iterator/type_system/inference.py
@@ -275,8 +275,8 @@ def _get_dimensions_from_types(types) -> dict[str, common.Dimension]:
         if isinstance(obj, common.Dimension):
             yield obj
         elif isinstance(obj, ts.TypeSpec):
-            for field in dataclasses.fields(obj.__class__):
-                yield from _get_dimensions(getattr(obj, field.name))
+            for field in obj.__datamodel_fields__.keys():
+                yield from _get_dimensions(getattr(obj, field))
         elif isinstance(obj, collections.abc.Mapping):
             for el in obj.values():
                 yield from _get_dimensions(el)
@@ -479,7 +479,7 @@ class ITIRTypeInference(eve.NodeTranslator):
         assert domain.dims != "unknown"
         assert node.dtype
         return type_info.apply_to_primitive_constituents(
-            lambda dtype: ts.FieldType(dims=domain.dims, dtype=dtype),  # type: ignore[arg-type]  # ensured by domain.dims != "unknown" above
+            lambda dtype: ts.FieldType(dims=domain.dims, dtype=dtype),
             node.dtype,
         )
 
diff --git a/src/gt4py/next/iterator/type_system/type_specifications.py b/src/gt4py/next/iterator/type_system/type_specifications.py
index eef8c75d..7825bf1c 100644
--- a/src/gt4py/next/iterator/type_system/type_specifications.py
+++ b/src/gt4py/next/iterator/type_system/type_specifications.py
@@ -6,43 +6,29 @@
 # Please, refer to the LICENSE file in the root directory.
 # SPDX-License-Identifier: BSD-3-Clause
 
-import dataclasses
-from typing import Literal, Optional
+from typing import Literal
 
 from gt4py.next import common
 from gt4py.next.type_system import type_specifications as ts
 
 
-@dataclasses.dataclass(frozen=True)
 class NamedRangeType(ts.TypeSpec):
     dim: common.Dimension
 
 
-@dataclasses.dataclass(frozen=True)
 class DomainType(ts.DataType):
     dims: list[common.Dimension] | Literal["unknown"]
 
 
-@dataclasses.dataclass(frozen=True)
 class OffsetLiteralType(ts.TypeSpec):
     value: ts.ScalarType | common.Dimension
 
 
-@dataclasses.dataclass(frozen=True)
-class ListType(ts.DataType):
-    element_type: ts.DataType
-    # TODO(havogt): the `offset_type` is not yet used in type_inference,
-    # it is meant to describe the neighborhood (via the local dimension)
-    offset_type: Optional[common.Dimension] = None
-
-
-@dataclasses.dataclass(frozen=True)
 class IteratorType(ts.DataType, ts.CallableType):
     position_dims: list[common.Dimension] | Literal["unknown"]
     defined_dims: list[common.Dimension]
     element_type: ts.DataType
 
 
-@dataclasses.dataclass(frozen=True)
 class ProgramType(ts.TypeSpec):
     params: dict[str, ts.DataType]
diff --git a/src/gt4py/next/iterator/type_system/type_synthesizer.py b/src/gt4py/next/iterator/type_system/type_synthesizer.py
index 5be9ed74..22a04ec0 100644
--- a/src/gt4py/next/iterator/type_system/type_synthesizer.py
+++ b/src/gt4py/next/iterator/type_system/type_synthesizer.py
@@ -155,18 +155,18 @@ def if_(pred: ts.ScalarType, true_branch: ts.DataType, false_branch: ts.DataType
 
 
 @_register_builtin_type_synthesizer
-def make_const_list(scalar: ts.ScalarType) -> it_ts.ListType:
+def make_const_list(scalar: ts.ScalarType) -> ts.ListType:
     assert isinstance(scalar, ts.ScalarType)
-    return it_ts.ListType(element_type=scalar)
+    return ts.ListType(element_type=scalar)
 
 
 @_register_builtin_type_synthesizer
-def list_get(index: ts.ScalarType | it_ts.OffsetLiteralType, list_: it_ts.ListType) -> ts.DataType:
+def list_get(index: ts.ScalarType | it_ts.OffsetLiteralType, list_: ts.ListType) -> ts.DataType:
     if isinstance(index, it_ts.OffsetLiteralType):
         assert isinstance(index.value, ts.ScalarType)
         index = index.value
     assert isinstance(index, ts.ScalarType) and type_info.is_integral(index)
-    assert isinstance(list_, it_ts.ListType)
+    assert isinstance(list_, ts.ListType)
     return list_.element_type
 
 
@@ -198,14 +198,14 @@ def index(arg: ts.DimensionType) -> ts.FieldType:
 
 
 @_register_builtin_type_synthesizer
-def neighbors(offset_literal: it_ts.OffsetLiteralType, it: it_ts.IteratorType) -> it_ts.ListType:
+def neighbors(offset_literal: it_ts.OffsetLiteralType, it: it_ts.IteratorType) -> ts.ListType:
     assert (
         isinstance(offset_literal, it_ts.OffsetLiteralType)
         and isinstance(offset_literal.value, common.Dimension)
         and offset_literal.value.kind == common.DimensionKind.LOCAL
     )
     assert isinstance(it, it_ts.IteratorType)
-    return it_ts.ListType(element_type=it.element_type)
+    return ts.ListType(element_type=it.element_type)
 
 
 @_register_builtin_type_synthesizer
@@ -270,7 +270,7 @@ def _convert_as_fieldop_input_to_iterator(
         else:
             defined_dims.append(dim)
     if is_nb_field:
-        element_type = it_ts.ListType(element_type=element_type)
+        element_type = ts.ListType(element_type=element_type)
 
     return it_ts.IteratorType(
         position_dims=domain.dims, defined_dims=defined_dims, element_type=element_type
@@ -342,14 +342,14 @@ def scan(
 def map_(op: TypeSynthesizer) -> TypeSynthesizer:
     @TypeSynthesizer
     def applied_map(
-        *args: it_ts.ListType, offset_provider_type: common.OffsetProviderType
-    ) -> it_ts.ListType:
+        *args: ts.ListType, offset_provider_type: common.OffsetProviderType
+    ) -> ts.ListType:
         assert len(args) > 0
-        assert all(isinstance(arg, it_ts.ListType) for arg in args)
+        assert all(isinstance(arg, ts.ListType) for arg in args)
         arg_el_types = [arg.element_type for arg in args]
         el_type = op(*arg_el_types, offset_provider_type=offset_provider_type)
         assert isinstance(el_type, ts.DataType)
-        return it_ts.ListType(element_type=el_type)
+        return ts.ListType(element_type=el_type)
 
     return applied_map
 
@@ -357,8 +357,8 @@ def map_(op: TypeSynthesizer) -> TypeSynthesizer:
 @_register_builtin_type_synthesizer
 def reduce(op: TypeSynthesizer, init: ts.TypeSpec) -> TypeSynthesizer:
     @TypeSynthesizer
-    def applied_reduce(*args: it_ts.ListType, offset_provider_type: common.OffsetProviderType):
-        assert all(isinstance(arg, it_ts.ListType) for arg in args)
+    def applied_reduce(*args: ts.ListType, offset_provider_type: common.OffsetProviderType):
+        assert all(isinstance(arg, ts.ListType) for arg in args)
         return op(
             init, *(arg.element_type for arg in args), offset_provider_type=offset_provider_type
         )
diff --git a/src/gt4py/next/otf/binding/nanobind.py b/src/gt4py/next/otf/binding/nanobind.py
index 24913a13..edd56fad 100644
--- a/src/gt4py/next/otf/binding/nanobind.py
+++ b/src/gt4py/next/otf/binding/nanobind.py
@@ -86,6 +86,7 @@ def _type_string(type_: ts.TypeSpec) -> str:
         return f"std::tuple<{','.join(_type_string(t) for t in type_.types)}>"
     elif isinstance(type_, ts.FieldType):
         ndims = len(type_.dims)
+        assert isinstance(type_.dtype, ts.ScalarType)
         dtype = cpp_interface.render_scalar_type(type_.dtype)
         shape = f"nanobind::shape<{', '.join(['gridtools::nanobind::dynamic_size'] * ndims)}>"
         buffer_t = f"nanobind::ndarray<{dtype}, {shape}>"
diff --git a/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py b/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py
index d5b34fd5..f7bb1805 100644
--- a/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py
+++ b/src/gt4py/next/program_processors/codegens/gtfn/itir_to_gtfn_ir.py
@@ -701,7 +701,7 @@ class GTFN_lowering(eve.NodeTranslator, eve.VisitorWithSymbolTableTrait):
         def dtype_to_cpp(x: ts.DataType) -> str:
             if isinstance(x, ts.TupleType):
                 assert all(isinstance(i, ts.ScalarType) for i in x.types)
-                return "::gridtools::tuple<" + ", ".join(dtype_to_cpp(i) for i in x.types) + ">"
+                return "::gridtools::tuple<" + ", ".join(dtype_to_cpp(i) for i in x.types) + ">"  # type: ignore[arg-type] # ensured by assert
             assert isinstance(x, ts.ScalarType)
             res = pytype_to_cpptype(x)
             assert isinstance(res, str)
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py
index cffbd74c..354a9692 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_builtin_translators.py
@@ -23,7 +23,6 @@ from gt4py.next.iterator.ir_utils import (
     domain_utils,
     ir_makers as im,
 )
-from gt4py.next.iterator.type_system import type_specifications as itir_ts
 from gt4py.next.program_processors.runners.dace_common import utility as dace_utils
 from gt4py.next.program_processors.runners.dace_fieldview import (
     gtir_dataflow,
@@ -119,7 +118,7 @@ class FieldopData:
                 )
 
             elif len(local_dims) == 1:
-                field_dtype = itir_ts.ListType(
+                field_dtype = ts.ListType(
                     element_type=self.gt_type.dtype, offset_type=local_dims[0]
                 )
                 field_domain = [
@@ -267,10 +266,11 @@ def _create_field_operator(
     if isinstance(output_edge.result.gt_dtype, ts.ScalarType):
         assert output_edge.result.gt_dtype == node_type.dtype
         assert isinstance(dataflow_output_desc, dace.data.Scalar)
+        assert isinstance(node_type.dtype, ts.ScalarType)
         assert dataflow_output_desc.dtype == dace_utils.as_dace_type(node_type.dtype)
         field_dtype = output_edge.result.gt_dtype
     else:
-        assert isinstance(node_type.dtype, itir_ts.ListType)
+        assert isinstance(node_type.dtype, ts.ListType)
         assert output_edge.result.gt_dtype.element_type == node_type.dtype.element_type
         assert isinstance(dataflow_output_desc, dace.data.Array)
         assert isinstance(output_edge.result.gt_dtype.element_type, ts.ScalarType)
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py
index a3653fb5..03761438 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_dataflow.py
@@ -31,7 +31,6 @@ from gt4py import eve
 from gt4py.next import common as gtx_common
 from gt4py.next.iterator import ir as gtir
 from gt4py.next.iterator.ir_utils import common_pattern_matcher as cpm, ir_makers as im
-from gt4py.next.iterator.type_system import type_specifications as itir_ts
 from gt4py.next.program_processors.runners.dace_common import utility as dace_utils
 from gt4py.next.program_processors.runners.dace_fieldview import (
     gtir_python_codegen,
@@ -64,7 +63,7 @@ class ValueExpr:
     """
 
     dc_node: dace.nodes.AccessNode
-    gt_dtype: itir_ts.ListType | ts.ScalarType
+    gt_dtype: ts.ListType | ts.ScalarType
 
 
 @dataclasses.dataclass(frozen=True)
@@ -79,7 +78,7 @@ class MemletExpr:
     """
 
     dc_node: dace.nodes.AccessNode
-    gt_dtype: itir_ts.ListType | ts.ScalarType
+    gt_dtype: ts.ListType | ts.ScalarType
     subset: dace_subsets.Range
 
 
@@ -112,7 +111,7 @@ class IteratorExpr:
     """
 
     field: dace.nodes.AccessNode
-    gt_dtype: itir_ts.ListType | ts.ScalarType
+    gt_dtype: ts.ListType | ts.ScalarType
     field_domain: list[tuple[gtx_common.Dimension, dace.symbolic.SymExpr]]
     indices: dict[gtx_common.Dimension, DataExpr]
 
@@ -121,7 +120,7 @@ class IteratorExpr:
             raise ValueError(f"Cannot deref iterator {self}.")
 
         field_desc = self.field.desc(sdfg)
-        if isinstance(self.gt_dtype, itir_ts.ListType):
+        if isinstance(self.gt_dtype, ts.ListType):
             assert len(field_desc.shape) == len(self.field_domain) + 1
             assert self.gt_dtype.offset_type is not None
             field_domain = [*self.field_domain, (self.gt_dtype.offset_type, 0)]
@@ -444,7 +443,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         return ValueExpr(
             dc_node=temp_node,
             gt_dtype=(
-                itir_ts.ListType(element_type=data_type, offset_type=_CONST_DIM)
+                ts.ListType(element_type=data_type, offset_type=_CONST_DIM)
                 if use_array
                 else data_type
             ),
@@ -547,7 +546,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         return self._construct_tasklet_result(field_desc.dtype, deref_node, "val")
 
     def _visit_neighbors(self, node: gtir.FunCall) -> ValueExpr:
-        assert isinstance(node.type, itir_ts.ListType)
+        assert isinstance(node.type, ts.ListType)
         assert len(node.args) == 2
 
         assert isinstance(node.args[0], gtir.OffsetLiteral)
@@ -650,7 +649,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         )
 
         return ValueExpr(
-            dc_node=neighbors_node, gt_dtype=itir_ts.ListType(node.type.element_type, offset_type)
+            dc_node=neighbors_node, gt_dtype=ts.ListType(node.type.element_type, offset_type)
         )
 
     def _visit_map(self, node: gtir.FunCall) -> ValueExpr:
@@ -669,7 +668,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         In above example, the result would be an array with size V2E.max_neighbors,
         containing the V2E neighbor values incremented by 1.0.
         """
-        assert isinstance(node.type, itir_ts.ListType)
+        assert isinstance(node.type, ts.ListType)
         assert isinstance(node.fun, gtir.FunCall)
         assert len(node.fun.args) == 1  # the operation to be mapped on the arguments
 
@@ -689,7 +688,7 @@ class LambdaToDataflow(eve.NodeVisitor):
             gtx_common.Dimension, gtx_common.NeighborConnectivityType
         ] = {}
         for input_arg in input_args:
-            assert isinstance(input_arg.gt_dtype, itir_ts.ListType)
+            assert isinstance(input_arg.gt_dtype, ts.ListType)
             assert input_arg.gt_dtype.offset_type is not None
             offset_type = input_arg.gt_dtype.offset_type
             if offset_type == _CONST_DIM:
@@ -759,7 +758,7 @@ class LambdaToDataflow(eve.NodeVisitor):
             connectivity_slice = self._construct_local_view(
                 MemletExpr(
                     dc_node=self.state.add_access(connectivity),
-                    gt_dtype=itir_ts.ListType(
+                    gt_dtype=ts.ListType(
                         element_type=node.type.element_type, offset_type=offset_type
                     ),
                     subset=dace_subsets.Range.from_string(
@@ -798,7 +797,7 @@ class LambdaToDataflow(eve.NodeVisitor):
 
         return ValueExpr(
             dc_node=result_node,
-            gt_dtype=itir_ts.ListType(node.type.element_type, offset_type),
+            gt_dtype=ts.ListType(node.type.element_type, offset_type),
         )
 
     def _make_reduce_with_skip_values(
@@ -825,7 +824,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         origin_map_index = dace_gtir_utils.get_map_variable(offset_provider_type.source_dim)
 
         assert (
-            isinstance(input_expr.gt_dtype, itir_ts.ListType)
+            isinstance(input_expr.gt_dtype, ts.ListType)
             and input_expr.gt_dtype.offset_type is not None
         )
         offset_type = input_expr.gt_dtype.offset_type
@@ -938,7 +937,7 @@ class LambdaToDataflow(eve.NodeVisitor):
         input_expr = self.visit(node.args[0])
         assert isinstance(input_expr, (MemletExpr, ValueExpr))
         assert (
-            isinstance(input_expr.gt_dtype, itir_ts.ListType)
+            isinstance(input_expr.gt_dtype, ts.ListType)
             and input_expr.gt_dtype.offset_type is not None
         )
         offset_type = input_expr.gt_dtype.offset_type
@@ -1232,7 +1231,7 @@ class LambdaToDataflow(eve.NodeVisitor):
                     connector,
                 )
 
-        if isinstance(node.type, itir_ts.ListType):
+        if isinstance(node.type, ts.ListType):
             # The only builtin function (so far) handled here that returns a list
             # is 'make_const_list'. There are other builtin functions (map_, neighbors)
             # that return a list but they are handled in specialized visit methods.
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py
index 9bd40f75..10895ce6 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/gtir_sdfg.py
@@ -290,6 +290,7 @@ class GTIRToSDFG(eve.NodeVisitor, SDFGBuilder):
                 # represent zero-dimensional fields as scalar arguments
                 return self._add_storage(sdfg, symbolic_arguments, name, gt_type.dtype, transient)
             # handle default case: field with one or more dimensions
+            assert isinstance(gt_type.dtype, ts.ScalarType)
             dc_dtype = dace_utils.as_dace_type(gt_type.dtype)
             if tuple_name is None:
                 # Use symbolic shape, which allows to invoke the program with fields of different size;
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/program.py b/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
new file mode 100644
index 00000000..7f809152
--- /dev/null
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/program.py
@@ -0,0 +1,248 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2024, ETH Zurich
+# All rights reserved.
+#
+# Please, refer to the LICENSE file in the root directory.
+# SPDX-License-Identifier: BSD-3-Clause
+
+import collections
+import dataclasses
+import itertools
+import typing
+from typing import Any, ClassVar, Optional, Sequence
+
+import dace
+import numpy as np
+
+from gt4py.next import backend as next_backend, common
+from gt4py.next.ffront import decorator
+from gt4py.next.iterator import ir as itir, transforms as itir_transforms
+from gt4py.next.iterator.transforms import extractors as extractors
+from gt4py.next.otf import arguments, recipes, toolchain
+from gt4py.next.program_processors.runners.dace_common import utility as dace_utils
+from gt4py.next.type_system import type_specifications as ts
+
+
+@dataclasses.dataclass(frozen=True)
+class Program(decorator.Program, dace.frontend.python.common.SDFGConvertible):
+    """Extension of GT4Py Program implementing the SDFGConvertible interface via GTIR."""
+
+    sdfg_closure_cache: dict[str, Any] = dataclasses.field(default_factory=dict)
+    # Being a ClassVar ensures that in an SDFG with multiple nested GT4Py Programs,
+    # there is no name mangling of the connectivity tables used across the nested SDFGs
+    # since they share the same memory address.
+    connectivity_tables_data_descriptors: ClassVar[
+        dict[str, dace.data.Array]
+    ] = {}  # symbolically defined
+
+    def __sdfg__(self, *args: Any, **kwargs: Any) -> dace.sdfg.sdfg.SDFG:
+        if (self.backend is None) or "dace" not in self.backend.name.lower():
+            raise ValueError("The SDFG can be generated only for the DaCe backend.")
+
+        offset_provider: common.OffsetProvider = {
+            **(self.connectivities or {}),
+            **self._implicit_offset_provider,
+        }
+        column_axis = kwargs.get("column_axis", None)
+
+        # TODO(ricoh): connectivity tables required here for now.
+        gtir_stage = typing.cast(next_backend.Transforms, self.backend.transforms).past_to_itir(
+            toolchain.CompilableProgram(
+                data=self.past_stage,
+                args=arguments.CompileTimeArgs(
+                    args=tuple(p.type for p in self.past_stage.past_node.params),
+                    kwargs={},
+                    column_axis=column_axis,
+                    offset_provider=offset_provider,
+                ),
+            )
+        )
+        program = gtir_stage.data
+        program = itir_transforms.apply_fieldview_transforms(  # run the transforms separately because they require the runtime info
+            program, offset_provider=offset_provider
+        )
+        object.__setattr__(
+            gtir_stage,
+            "data",
+            program,
+        )
+        object.__setattr__(
+            gtir_stage.args, "offset_provider", gtir_stage.args.offset_provider_type
+        )  # TODO(ricoh): currently this is circumventing the frozenness of CompileTimeArgs
+        # in order to isolate DaCe from the runtime tables in connectivities.offset_provider.
+        # These are needed at the time of writing for mandatory GTIR passes.
+        # Remove this as soon as Program does not expect connectivity tables anymore.
+
+        _crosscheck_dace_parsing(
+            dace_parsed_args=[*args, *kwargs.values()],
+            gt4py_program_args=[p.type for p in program.params],
+        )
+
+        compile_workflow = typing.cast(
+            recipes.OTFCompileWorkflow,
+            self.backend.executor
+            if not hasattr(self.backend.executor, "step")
+            else self.backend.executor.step,
+        )  # We know which backend we are using, but we don't know if the compile workflow is cached.
+        # TODO(ricoh): switch 'itir_transforms_off=True' because we ran them separately previously
+        # and so we can ensure the SDFG does not know any runtime info it shouldn't know. Remove with
+        # the other parts of the workaround when possible.
+        sdfg = dace.SDFG.from_json(
+            compile_workflow.translation.replace(itir_transforms_off=True)(gtir_stage).source_code
+        )
+
+        self.sdfg_closure_cache["arrays"] = sdfg.arrays
+
+        # Halo exchange related metadata, i.e. gt4py_program_input_fields, gt4py_program_output_fields,
+        # offset_providers_per_input_field. Add them as dynamic attributes to the SDFG
+        field_params = {
+            str(param.id): param for param in program.params if isinstance(param.type, ts.FieldType)
+        }
+
+        def single_horizontal_dim_per_field(
+            fields: typing.Iterable[itir.Sym],
+        ) -> typing.Iterator[tuple[str, common.Dimension]]:
+            for field in fields:
+                assert isinstance(field.type, ts.FieldType)
+                horizontal_dims = [
+                    dim for dim in field.type.dims if dim.kind is common.DimensionKind.HORIZONTAL
+                ]
+                # do nothing for fields with multiple horizontal dimensions
+                # or without horizontal dimensions
+                # this is only meant for use with unstructured grids
+                if len(horizontal_dims) == 1:
+                    yield str(field.id), horizontal_dims[0]
+
+        input_fields = (
+            field_params[name] for name in extractors.InputNamesExtractor.only_fields(program)
+        )
+        sdfg.gt4py_program_input_fields = dict(single_horizontal_dim_per_field(input_fields))
+
+        output_fields = (
+            field_params[name] for name in extractors.OutputNamesExtractor.only_fields(program)
+        )
+        sdfg.gt4py_program_output_fields = dict(single_horizontal_dim_per_field(output_fields))
+
+        # TODO (ricoh): bring back sdfg.offset_providers_per_input_field.
+        #               A starting point would be to use the "trace_shifts" pass on GTIR
+        #               and associate the extracted shifts with each input field.
+        #               Analogous to the version in `runners.dace_iterator.__init__`, which
+        #               was removed when merging #1742.
+
+        return sdfg
+
+    def __sdfg_closure__(self, reevaluate: Optional[dict[str, str]] = None) -> dict[str, Any]:
+        """
+        Return the closure arrays of the SDFG represented by this object
+        as a mapping between array name and the corresponding value.
+
+        The connectivity tables are defined symbolically, i.e. table sizes & strides are DaCe symbols.
+        The need to define the connectivity tables in the `__sdfg_closure__` arises from the fact that
+        the offset providers are not part of GT4Py Program's arguments.
+        Keep in mind, that `__sdfg_closure__` is called after `__sdfg__` method.
+        """
+        closure_dict: dict[str, Any] = {}
+
+        if self.connectivities:
+            symbols = {}
+            with_table = [
+                name for name, conn in self.connectivities.items() if common.is_neighbor_table(conn)
+            ]
+            in_arrays_with_id = [
+                (name, conn_id)
+                for name in with_table
+                if (conn_id := dace_utils.connectivity_identifier(name))
+                in self.sdfg_closure_cache["arrays"]
+            ]
+            in_arrays = (name for name, _ in in_arrays_with_id)
+            name_axis = list(itertools.product(in_arrays, [0, 1]))
+
+            def size_symbol_name(name: str, axis: int) -> str:
+                return dace_utils.field_size_symbol_name(
+                    dace_utils.connectivity_identifier(name), axis
+                )
+
+            connectivity_tables_size_symbols = {
+                (sname := size_symbol_name(name, axis)): dace.symbol(sname)
+                for name, axis in name_axis
+            }
+
+            def stride_symbol_name(name: str, axis: int) -> str:
+                return dace_utils.field_stride_symbol_name(
+                    dace_utils.connectivity_identifier(name), axis
+                )
+
+            connectivity_table_stride_symbols = {
+                (sname := stride_symbol_name(name, axis)): dace.symbol(sname)
+                for name, axis in name_axis
+            }
+
+            symbols = connectivity_tables_size_symbols | connectivity_table_stride_symbols
+
+            # Define the storage location (e.g. CPU, GPU) of the connectivity tables
+            if "storage" not in self.connectivity_tables_data_descriptors:
+                for _, conn_id in in_arrays_with_id:
+                    self.connectivity_tables_data_descriptors["storage"] = self.sdfg_closure_cache[
+                        "arrays"
+                    ][conn_id].storage
+                    break
+
+            # Build the closure dictionary
+            for name, conn_id in in_arrays_with_id:
+                if conn_id not in self.connectivity_tables_data_descriptors:
+                    conn = self.connectivities[name]
+                    assert common.is_neighbor_table(conn)
+                    self.connectivity_tables_data_descriptors[conn_id] = dace.data.Array(
+                        dtype=dace.dtypes.dtype_to_typeclass(conn.dtype.dtype.type),
+                        shape=[
+                            symbols[dace_utils.field_size_symbol_name(conn_id, 0)],
+                            symbols[dace_utils.field_size_symbol_name(conn_id, 1)],
+                        ],
+                        strides=[
+                            symbols[dace_utils.field_stride_symbol_name(conn_id, 0)],
+                            symbols[dace_utils.field_stride_symbol_name(conn_id, 1)],
+                        ],
+                        storage=Program.connectivity_tables_data_descriptors["storage"],
+                    )
+                closure_dict[conn_id] = self.connectivity_tables_data_descriptors[conn_id]
+
+        return closure_dict
+
+    def __sdfg_signature__(self) -> tuple[Sequence[str], Sequence[str]]:
+        return [p.id for p in self.past_stage.past_node.params], []
+
+
+def _crosscheck_dace_parsing(dace_parsed_args: list[Any], gt4py_program_args: list[Any]) -> None:
+    for dace_parsed_arg, gt4py_program_arg in zip(
+        dace_parsed_args,
+        gt4py_program_args,
+        strict=False,  # dace does not see implicit size args
+    ):
+        match dace_parsed_arg:
+            case dace.data.Scalar():
+                assert dace_parsed_arg.dtype == dace_utils.as_dace_type(gt4py_program_arg)
+            case bool() | np.bool_():
+                assert isinstance(gt4py_program_arg, ts.ScalarType)
+                assert gt4py_program_arg.kind == ts.ScalarKind.BOOL
+            case int() | np.integer():
+                assert isinstance(gt4py_program_arg, ts.ScalarType)
+                assert gt4py_program_arg.kind in [ts.ScalarKind.INT32, ts.ScalarKind.INT64]
+            case float() | np.floating():
+                assert isinstance(gt4py_program_arg, ts.ScalarType)
+                assert gt4py_program_arg.kind in [ts.ScalarKind.FLOAT32, ts.ScalarKind.FLOAT64]
+            case str() | np.str_():
+                assert isinstance(gt4py_program_arg, ts.ScalarType)
+                assert gt4py_program_arg.kind == ts.ScalarKind.STRING
+            case dace.data.Array():
+                assert isinstance(gt4py_program_arg, ts.FieldType)
+                assert isinstance(gt4py_program_arg.dtype, ts.ScalarType)
+                assert len(dace_parsed_arg.shape) == len(gt4py_program_arg.dims)
+                assert dace_parsed_arg.dtype == dace_utils.as_dace_type(gt4py_program_arg.dtype)
+            case dace.data.Structure() | dict() | collections.OrderedDict():
+                # offset provider
+                pass
+            case _:
+                raise ValueError(
+                    f"Unresolved case for {dace_parsed_arg} (==, !=) {gt4py_program_arg}"
+                )
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py
index 4f3efb19..0902bd66 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/__init__.py
@@ -35,7 +35,13 @@ from .simplify import (
     gt_simplify,
     gt_substitute_compiletime_symbols,
 )
-from .strides import gt_change_transient_strides
+from .strides import (
+    gt_change_transient_strides,
+    gt_map_strides_to_dst_nested_sdfg,
+    gt_map_strides_to_src_nested_sdfg,
+    gt_propagate_strides_from_access_node,
+    gt_propagate_strides_of,
+)
 from .util import gt_find_constant_arguments, gt_make_transients_persistent
 
 
@@ -59,6 +65,10 @@ __all__ = [
     "gt_gpu_transformation",
     "gt_inline_nested_sdfg",
     "gt_make_transients_persistent",
+    "gt_map_strides_to_dst_nested_sdfg",
+    "gt_map_strides_to_src_nested_sdfg",
+    "gt_propagate_strides_from_access_node",
+    "gt_propagate_strides_of",
     "gt_reduce_distributed_buffering",
     "gt_set_gpu_blocksize",
     "gt_set_iteration_order",
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py
index 2cd30201..7b14144e 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/gpu_utils.py
@@ -95,7 +95,7 @@ def gt_gpu_transformation(
 
     if try_removing_trivial_maps:
         # In DaCe a Tasklet, outside of a Map, can not write into an _array_ that is on
-        #  GPU. `sdfg.appyl_gpu_transformations()` will wrap such Tasklets in a Map. So
+        #  GPU. `sdfg.apply_gpu_transformations()` will wrap such Tasklets in a Map. So
         #  we might end up with lots of these trivial Maps, each requiring a separate
         #  kernel launch. To prevent this we will combine these trivial maps, if
         #  possible, with their downstream maps.
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py
index 6b7bd1b6..4339a761 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/simplify.py
@@ -950,7 +950,7 @@ class GT4PyMapBufferElimination(dace_transformation.SingleStateTransformation):
 
     def apply(
         self,
-        graph: dace.SDFGState | dace.SDFG,
+        graph: dace.SDFGState,
         sdfg: dace.SDFG,
     ) -> None:
         # Removal
@@ -971,6 +971,9 @@ class GT4PyMapBufferElimination(dace_transformation.SingleStateTransformation):
             tmp_out_subset = dace_subsets.Range.from_array(tmp_desc)
         assert glob_in_subset is not None
 
+        # Recursively visit the nested SDFGs for mapping of strides from inner to outer array
+        gtx_transformations.gt_map_strides_to_src_nested_sdfg(sdfg, graph, map_to_tmp_edge, glob_ac)
+
         # We now remove the `tmp` node, and create a new connection between
         #  the global node and the map exit.
         new_map_to_glob_edge = graph.add_edge(
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py
index 4e254f28..980b2a8f 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/transformations/strides.py
@@ -6,14 +6,30 @@
 # Please, refer to the LICENSE file in the root directory.
 # SPDX-License-Identifier: BSD-3-Clause
 
+from typing import Optional, TypeAlias
+
 import dace
 from dace import data as dace_data
+from dace.sdfg import nodes as dace_nodes
 
 from gt4py.next.program_processors.runners.dace_fieldview import (
     transformations as gtx_transformations,
 )
 
 
+PropagatedStrideRecord: TypeAlias = tuple[str, dace_nodes.NestedSDFG]
+"""Record of a stride that has been propagated into a NestedSDFG.
+
+The type combines the NestedSDFG into which the strides were already propagated
+and the data within that NestedSDFG to which we have propagated the strides,
+which is the connector name on the NestedSDFG.
+We need the NestedSDFG because we have to know what was already processed,
+however, we also need the inner array name because of aliasing, i.e. a data
+descriptor on the outside could be mapped to multiple data descriptors
+inside the NestedSDFG.
+"""
+
+
 def gt_change_transient_strides(
     sdfg: dace.SDFG,
     gpu: bool,
@@ -24,6 +40,11 @@ def gt_change_transient_strides(
     transients in the optimal way.
     The function should run after all maps have been created.
 
+    After the strides have been adjusted the function will also propagate
+    the strides into nested SDFG. This propagation will happen with
+    `ignore_symbol_mapping` set to `True`, see `gt_propagate_strides_of()`
+    for more.
+
     Args:
         sdfg: The SDFG to process.
         gpu: If the SDFG is supposed to run on the GPU.
@@ -35,8 +56,6 @@ def gt_change_transient_strides(
 
     Todo:
         - Implement the estimation correctly.
-        - Handle the case of nested SDFGs correctly; on the outside a transient,
-            but on the inside a non transient.
     """
     # TODO(phimeull): Implement this function correctly.
 
@@ -46,54 +65,608 @@ def gt_change_transient_strides(
         return sdfg
 
     for nsdfg in sdfg.all_sdfgs_recursive():
-        # TODO(phimuell): Handle the case when transient goes into nested SDFG
-        #   on the inside it is a non transient, so it is ignored.
         _gt_change_transient_strides_non_recursive_impl(nsdfg)
 
 
 def _gt_change_transient_strides_non_recursive_impl(
     sdfg: dace.SDFG,
 ) -> None:
-    """Essentially this function just changes the stride to FORTRAN order."""
-    for top_level_transient in _find_toplevel_transients(sdfg, only_arrays=True):
+    """Set optimal strides of all transients in the SDFG.
+
+    The function will look for all top level transients, see `_gt_find_toplevel_data_accesses()`
+    and set their strides such that the access is optimal, see Note. The function
+    will also run `gt_propagate_strides_of()` to propagate the strides into nested SDFGs.
+
+    This function should never be called directly but always through
+    `gt_change_transient_strides()`!
+
+    Note:
+        Currently the function just reverses the strides of the data descriptor
+        it processes. Since DaCe generates `C` order by default this lead to
+        FORTRAN order, which is (for now) sufficient to optimize the memory
+        layout to GPU.
+
+    Todo:
+        Make this function more intelligent to analyse the access pattern and then
+        figuring out the best order.
+    """
+    # NOTE: Processing the transient here is enough. If we are inside a
+    #   NestedSDFG then they were handled before on the level above us.
+    top_level_transients_and_their_accesses = _gt_find_toplevel_data_accesses(
+        sdfg=sdfg,
+        only_transients=True,
+        only_arrays=True,
+    )
+    for top_level_transient, accesses in top_level_transients_and_their_accesses.items():
         desc: dace_data.Array = sdfg.arrays[top_level_transient]
+
+        # Setting the strides only make sense if we have more than one dimensions
         ndim = len(desc.shape)
         if ndim <= 1:
             continue
+
         # We assume that everything is in C order initially, to get FORTRAN order
         #  we simply have to reverse the order.
+        # TODO(phimuell): Improve this.
         new_stride_order = list(range(ndim))
         desc.set_strides_from_layout(*new_stride_order)
 
+        # Now we have to propagate the changed strides. Because we already have
+        #  collected all the AccessNodes we are using the
+        #  `gt_propagate_strides_from_access_node()` function, but we have to
+        #  create `processed_nsdfg` set already outside here.
+        #  Furthermore, the same comment as above applies here, we do not have to
+        #  propagate the non-transients, because they either come from outside,
+        #  or they were already handled in the levels above, where they were
+        #  defined and then propagated down.
+        # TODO(phimuell): Updated the functions such that only one scan is needed.
+        processed_nsdfgs: set[dace_nodes.NestedSDFG] = set()
+        for state, access_node in accesses:
+            gt_propagate_strides_from_access_node(
+                sdfg=sdfg,
+                state=state,
+                outer_node=access_node,
+                processed_nsdfgs=processed_nsdfgs,
+                ignore_symbol_mapping=True,
+            )
+
+
+def gt_propagate_strides_of(
+    sdfg: dace.SDFG,
+    data_name: str,
+    ignore_symbol_mapping: bool = True,
+) -> None:
+    """Propagates the strides of `data_name` within the whole SDFG.
+
+    This function will call `gt_propagate_strides_from_access_node()` for every
+    AccessNode that refers to `data_name`. It will also make sure that a descriptor
+    inside a NestedSDFG is only processed once.
+
+    Args:
+        sdfg: The SDFG on which we operate.
+        data_name: Name of the data descriptor that should be handled.
+        ignore_symbol_mapping: If `False` (default is `True`) try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+    """
+
+    # Defining it here ensures that we will not enter an NestedSDFG multiple times.
+    processed_nsdfgs: set[PropagatedStrideRecord] = set()
+
+    for state in sdfg.states():
+        for dnode in state.data_nodes():
+            if dnode.data != data_name:
+                continue
+            gt_propagate_strides_from_access_node(
+                sdfg=sdfg,
+                state=state,
+                outer_node=dnode,
+                processed_nsdfgs=processed_nsdfgs,
+                ignore_symbol_mapping=ignore_symbol_mapping,
+            )
+
+
+def gt_propagate_strides_from_access_node(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    outer_node: dace_nodes.AccessNode,
+    ignore_symbol_mapping: bool = True,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]] = None,
+) -> None:
+    """Propagates the stride of `outer_node` to any adjacent NestedSDFG.
+
+    The function will propagate the strides of the data descriptor `outer_node`
+    refers to along all adjacent edges of `outer_node`. If one of these edges
+    leads to a NestedSDFG then the function will modify the strides of data
+    descriptor within to match the strides on the outside. The function will then
+    recursively process NestedSDFG.
+
+    It is important that this function will only handle the NestedSDFGs that are
+    reachable from `outer_node`. To fully propagate the strides the
+    `gt_propagate_strides_of()` should be used.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that reads from the data node, the nested SDFG is expected as the destination.
+        outer_node: The data node whose strides should be propagated.
+        ignore_symbol_mapping: If `False` (default is `True`), try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+        processed_nsdfgs: Set of NestedSDFG that were already processed and will be ignored.
+            Only specify when you know what your are doing.
+    """
+    if processed_nsdfgs is None:
+        # For preventing the case that nested SDFGs are handled multiple time.
+        processed_nsdfgs = set()
+
+    for in_edge in state.in_edges(outer_node):
+        gt_map_strides_to_src_nested_sdfg(
+            sdfg=sdfg,
+            state=state,
+            edge=in_edge,
+            outer_node=outer_node,
+            processed_nsdfgs=processed_nsdfgs,
+            ignore_symbol_mapping=ignore_symbol_mapping,
+        )
+    for out_edge in state.out_edges(outer_node):
+        gt_map_strides_to_dst_nested_sdfg(
+            sdfg=sdfg,
+            state=state,
+            edge=out_edge,
+            outer_node=outer_node,
+            processed_nsdfgs=processed_nsdfgs,
+            ignore_symbol_mapping=ignore_symbol_mapping,
+        )
+
+
+def gt_map_strides_to_dst_nested_sdfg(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    edge: dace.sdfg.graph.Edge,
+    outer_node: dace.nodes.AccessNode,
+    ignore_symbol_mapping: bool = True,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]] = None,
+) -> None:
+    """Propagates the strides of `outer_node` along `edge` in the dataflow direction.
+
+    In this context "along the dataflow direction" means that `edge` is an outgoing
+    edge of `outer_node` and the strides are propagated into all NestedSDFGs that
+    are downstream of `outer_node`.
+
+    Except in certain cases this function should not be used directly. It is
+    instead recommended to use `gt_propagate_strides_of()`, which propagates
+    all edges in the SDFG.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that writes to the data node, the nested SDFG is expected as the source.
+        outer_node: The data node whose strides should be propagated.
+        ignore_symbol_mapping: If `False`, the default, try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+        processed_nsdfgs: Set of NestedSDFGs that were already processed. Only specify when
+            you know what your are doing.
+    """
+    assert edge.src is outer_node
+    _gt_map_strides_to_nested_sdfg_src_dst(
+        sdfg=sdfg,
+        state=state,
+        edge=edge,
+        outer_node=outer_node,
+        processed_nsdfgs=processed_nsdfgs,
+        propagate_along_dataflow=True,
+        ignore_symbol_mapping=ignore_symbol_mapping,
+    )
+
+
+def gt_map_strides_to_src_nested_sdfg(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    edge: dace.sdfg.graph.Edge,
+    outer_node: dace.nodes.AccessNode,
+    ignore_symbol_mapping: bool = False,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]] = None,
+) -> None:
+    """Propagates the strides of `outer_node` along `edge` in the opposite direction of the dataflow
+
+    In this context "in the opposite direction of the dataflow" means that `edge`
+    is an incoming edge of `outer_node` and the strides are propagated into all
+    NestedSDFGs that are upstream of `outer_node`.
+
+    Except in certain cases this function should not be used directly. It is
+    instead recommended to use `gt_propagate_strides_of()`, which propagates
+    all edges in the SDFG.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that writes to the data node, the nested SDFG is expected as the source.
+        outer_node: The data node whose strides should be propagated.
+        ignore_symbol_mapping: If `False`, the default, try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+        processed_nsdfgs: Set of NestedSDFGs that were already processed. Only specify when
+            you know what your are doing.
+    """
+    _gt_map_strides_to_nested_sdfg_src_dst(
+        sdfg=sdfg,
+        state=state,
+        edge=edge,
+        outer_node=outer_node,
+        processed_nsdfgs=processed_nsdfgs,
+        propagate_along_dataflow=False,
+        ignore_symbol_mapping=ignore_symbol_mapping,
+    )
+
+
+def _gt_map_strides_to_nested_sdfg_src_dst(
+    sdfg: dace.SDFG,
+    state: dace.SDFGState,
+    edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+    outer_node: dace.nodes.AccessNode,
+    processed_nsdfgs: Optional[set[PropagatedStrideRecord]],
+    propagate_along_dataflow: bool,
+    ignore_symbol_mapping: bool = False,
+) -> None:
+    """Propagates the stride of `outer_node` along `edge`.
+
+    The function will follow `edge`, the direction depends on the value of
+    `propagate_along_dataflow` and propagate the strides of `outer_node`
+    into every NestedSDFG that is reachable by following `edge`.
+
+    When the function encounters a NestedSDFG it will determine what data
+    the `outer_node` is mapped to on the inside of the NestedSDFG.
+    It will then replace the stride of the inner descriptor with the ones
+    of the outside. Afterwards it will recursively propagate the strides
+    inside the NestedSDFG.
+    During this propagation the function will follow any edges.
+
+    If the function reaches a NestedSDFG that is listed inside `processed_nsdfgs`
+    then it will be skipped. NestedSDFGs that have been processed will be added
+    to the `processed_nsdfgs`.
+
+    Args:
+        sdfg: The SDFG to process.
+        state: The state where the data node is used.
+        edge: The edge that reads from the data node, the nested SDFG is expected as the destination.
+        outer_node: The data node whose strides should be propagated.
+        processed_nsdfgs: Set of Nested SDFG that were already processed and will be ignored.
+            Only specify when you know what your are doing.
+        propagate_along_dataflow: Determine the direction of propagation. If `True` the
+            function follows the dataflow.
+        ignore_symbol_mapping: If `False`, the default, try to modify the `symbol_mapping`
+            of NestedSDFGs instead of manipulating the data descriptor.
+
+    Note:
+        A user should not use this function directly, instead `gt_propagate_strides_of()`,
+        `gt_map_strides_to_src_nested_sdfg()` (`propagate_along_dataflow == `False`)
+        or `gt_map_strides_to_dst_nested_sdfg()` (`propagate_along_dataflow == `True`)
+        should be used.
+
+    Todo:
+        Try using `MemletTree` for the propagation.
+    """
+    # If `processed_nsdfg` is `None` then this is the first call. We will now
+    #  allocate the `set` and pass it as argument to all recursive calls, this
+    #  ensures that the `set` is the same everywhere.
+    if processed_nsdfgs is None:
+        processed_nsdfgs = set()
+
+    if propagate_along_dataflow:
+        # Propagate along the dataflow or forward, so we are interested at the `dst` of the edge.
+        ScopeNode = dace_nodes.MapEntry
+
+        def get_node(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> dace_nodes.Node:
+            return edge.dst
+
+        def get_inner_data(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> str:
+            return edge.dst_conn
+
+        def get_subset(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> dace.subsets.Subset:
+            return edge.data.get_src_subset(edge, state)
 
-def _find_toplevel_transients(
+        def next_edges_by_connector(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> list[dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]]:
+            if edge.dst_conn is None or not edge.dst_conn.startswith("IN_"):
+                return []
+            return list(state.out_edges_by_connector(edge.dst, "OUT_" + edge.dst_conn[3:]))
+
+    else:
+        # Propagate against the dataflow or backward, so we are interested at the `src` of the edge.
+        ScopeNode = dace_nodes.MapExit
+
+        def get_node(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> dace_nodes.Node:
+            return edge.src
+
+        def get_inner_data(edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]) -> str:
+            return edge.src_conn
+
+        def get_subset(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> dace.subsets.Subset:
+            return edge.data.get_dst_subset(edge, state)
+
+        def next_edges_by_connector(
+            state: dace.SDFGState,
+            edge: dace.sdfg.graph.MultiConnectorEdge[dace.Memlet],
+        ) -> list[dace.sdfg.graph.MultiConnectorEdge[dace.Memlet]]:
+            return list(state.in_edges_by_connector(edge.src, "IN_" + edge.src_conn[4:]))
+
+    if isinstance(get_node(edge), ScopeNode):
+        for next_edge in next_edges_by_connector(state, edge):
+            _gt_map_strides_to_nested_sdfg_src_dst(
+                sdfg=sdfg,
+                state=state,
+                edge=next_edge,
+                outer_node=outer_node,
+                processed_nsdfgs=processed_nsdfgs,
+                propagate_along_dataflow=propagate_along_dataflow,
+                ignore_symbol_mapping=ignore_symbol_mapping,
+            )
+
+    elif isinstance(get_node(edge), dace.nodes.NestedSDFG):
+        nsdfg_node = get_node(edge)
+        inner_data = get_inner_data(edge)
+        process_record = (inner_data, nsdfg_node)
+
+        if process_record in processed_nsdfgs:
+            # We already handled this NestedSDFG and the inner data.
+            return
+
+        # Mark this nested SDFG as processed.
+        processed_nsdfgs.add(process_record)
+
+        # Now set the stride of the data descriptor inside the nested SDFG to
+        #  the ones it has outside.
+        _gt_map_strides_into_nested_sdfg(
+            sdfg=sdfg,
+            nsdfg_node=nsdfg_node,
+            inner_data=inner_data,
+            outer_subset=get_subset(state, edge),
+            outer_desc=outer_node.desc(sdfg),
+            ignore_symbol_mapping=ignore_symbol_mapping,
+        )
+
+        # Since the function call above is not recursive we have now to propagate
+        #  the change into the NestedSDFGs. Using `_gt_find_toplevel_data_accesses()`
+        #  is a bit overkill, but allows for a more uniform processing.
+        # TODO(phimuell): Instead of scanning every level for every data we modify
+        #   we should scan the whole SDFG once and then reuse this information.
+        accesses_in_nested_sdfg = _gt_find_toplevel_data_accesses(
+            sdfg=nsdfg_node.sdfg,
+            only_transients=False,  # Because on the nested levels they are globals.
+            only_arrays=True,
+        )
+        for nested_state, nested_access in accesses_in_nested_sdfg.get(inner_data, list()):
+            # We have to use `gt_propagate_strides_from_access_node()` here because we
+            #  have to handle its entirety. We could wait until the other branch processes
+            #  the nested SDFG, but this might not work, so let's do it fully now.
+            gt_propagate_strides_from_access_node(
+                sdfg=nsdfg_node.sdfg,
+                state=nested_state,
+                outer_node=nested_access,
+                processed_nsdfgs=processed_nsdfgs,
+                ignore_symbol_mapping=ignore_symbol_mapping,
+            )
+
+
+def _gt_map_strides_into_nested_sdfg(
     sdfg: dace.SDFG,
+    nsdfg_node: dace.nodes.NestedSDFG,
+    inner_data: str,
+    outer_subset: dace.subsets.Subset,
+    outer_desc: dace_data.Data,
+    ignore_symbol_mapping: bool,
+) -> None:
+    """Modify the strides of `inner_data` inside `nsdfg_node` to match `outer_desc`.
+
+    `inner_data` is the name of a data descriptor inside the NestedSDFG.
+    The function will then modify the strides of `inner_data`, assuming this
+    is an array, to match the ones of `outer_desc`.
+
+    Args:
+        sdfg: The SDFG containing the NestedSDFG.
+        nsdfg_node: The node in the parent SDFG that contains the NestedSDFG.
+        inner_data: The name of the data descriptor that should be processed
+            inside the NestedSDFG (by construction also a connector name).
+        outer_subset: The subset that describes what part of the outer data is
+            mapped into the NestedSDFG.
+        outer_desc: The data descriptor of the data on the outside.
+        ignore_symbol_mapping: If possible the function will perform the renaming
+            through the `symbol_mapping` of the nested SDFG. If `True` then
+            the function will always perform the renaming.
+            Note that setting this value to `False` might have negative side effects.
+
+    Todo:
+        - Handle explicit dimensions of size 1.
+        - What should we do if the stride symbol is used somewhere else, creating an
+            alias is probably not the right thing?
+        - Handle the case if the outer stride symbol is already used in another
+            context inside the Neste SDFG.
+    """
+    # We need to compute the new strides. In the following we assume that the
+    #  relative order of the dimensions does not change, but we support the case
+    #  where some dimensions of the outer data descriptor are not present on the
+    #  inside. For example this happens for the Memlet `a[__i0, 0:__a_size1]`. We
+    #  detect this case by checking if the Memlet subset in that dimension has size 1.
+    # TODO(phimuell): Handle the case were some additional size 1 dimensions are added.
+    inner_desc: dace_data.Data = nsdfg_node.sdfg.arrays[inner_data]
+    inner_shape = inner_desc.shape
+    inner_strides_init = inner_desc.strides
+
+    outer_strides = outer_desc.strides
+    outer_inflow = outer_subset.size()
+
+    new_strides: list = []
+    for dim_ostride, dim_oinflow in zip(outer_strides, outer_inflow, strict=True):
+        if dim_oinflow == 1:
+            # This is the case of implicit slicing along one dimension.
+            pass
+        else:
+            # There is inflow into the SDFG, so we need the stride.
+            new_strides.append(dim_ostride)
+        assert len(new_strides) <= len(inner_shape)
+
+    # If we have a scalar on the inside, then there is nothing to adjust.
+    #  We could have performed the test above, but doing it here, gives us
+    #  the chance of validating it.
+    if isinstance(inner_desc, dace_data.Scalar):
+        if len(new_strides) != 0:
+            raise ValueError(f"Dimensional error for '{inner_data}' in '{nsdfg_node.label}'.")
+        return
+
+    if not isinstance(inner_desc, dace_data.Array):
+        raise TypeError(
+            f"Expected that '{inner_data}' is an 'Array' but it is '{type(inner_desc).__name__}'."
+        )
+
+    if len(new_strides) != len(inner_shape):
+        raise ValueError("Failed to compute the inner strides.")
+
+    # Now we actually replace the strides, there are two ways of doing it.
+    #  The first is to create an alias in the `symbol_mapping`, however,
+    #  this is only possible if the current strides are singular symbols,
+    #  like `__a_strides_1`, but not expressions such as `horizontal_end - horizontal_start`
+    #  or literal values. Furthermore, this would change the meaning of the
+    #  old stride symbol in any context and not only in the one of the stride
+    #  of a single and isolated data descriptor.
+    #  The second way would be to replace `strides` attribute of the
+    #  inner data descriptor. In case the new stride consists of expressions
+    #  such as `value1 - value2` we have to make them available inside the
+    #  NestedSDFG. However, it could be that the strides is used somewhere else.
+    # We will do the following, if `ignore_symbol_mapping` is `False` and
+    #  the strides of the inner descriptors are symbols, we will use the
+    #  symbol mapping. Otherwise, we will replace the `strides` attribute
+    #  of the inner descriptor, in addition we will install a remapping,
+    #  for those values that were a symbol.
+    if (not ignore_symbol_mapping) and all(
+        isinstance(inner_stride, dace.symbol) for inner_stride in inner_strides_init
+    ):
+        # Use the symbol
+        for inner_stride, outer_stride in zip(inner_desc.strides, new_strides, strict=True):
+            nsdfg_node.symbol_mapping[inner_stride.name] = outer_stride
+    else:
+        # We have to replace the `strides` attribute of the inner descriptor.
+        inner_desc.set_shape(inner_desc.shape, new_strides)
+
+        # Now find the free symbols that the new strides need.
+        #  Note that usually `free_symbols` returns `set[str]`, but here, because
+        #  we fall back on SymPy, we get back symbols. We will keep them, because
+        #  then we can use them to extract the type form them, which we need later.
+        new_strides_symbols: list[dace.symbol] = []
+        for new_stride_dim in new_strides:
+            if dace.symbolic.issymbolic(new_stride_dim):
+                new_strides_symbols.extend(sym for sym in new_stride_dim.free_symbols)
+            else:
+                # It is not already a symbol, so we turn it into a symbol.
+                #  However, we only add it, if it is also a symbol, for example `1`.
+                #  should not be added.
+                new_stride_symbol = dace.symbolic.pystr_to_symbolic(new_stride_dim)
+                if new_stride_symbol.is_symbol:
+                    new_strides_symbols.append(new_stride_symbol)
+
+        # Now we determine the set of symbols that should be mapped inside the NestedSDFG.
+        #  We will exclude all that are already inside the `symbol_mapping` (we do not
+        #  check if they map to the same value, we just hope it). Furthermore,
+        #  we will exclude all symbols that are listed in the `symbols` property
+        #  of the SDFG that is nested, and hope that it has the same meaning.
+        # TODO(phimuell): Add better checks to avoid overwriting.
+        missing_symbol_mappings: set[dace.symbol] = {
+            sym
+            for sym in new_strides_symbols
+            if not (sym.name in nsdfg_node.sdfg.symbols or sym.name in nsdfg_node.symbol_mapping)
+        }
+
+        # Now propagate the symbols from the parent SDFG to the NestedSDFG.
+        for sym in missing_symbol_mappings:
+            assert sym.name in sdfg.symbols, f"Expected that '{sym}' is defined in the parent SDFG."
+            nsdfg_node.sdfg.add_symbol(sym.name, sdfg.symbols[sym.name])
+            nsdfg_node.symbol_mapping[sym.name] = sym
+
+
+def _gt_find_toplevel_data_accesses(
+    sdfg: dace.SDFG,
+    only_transients: bool,
     only_arrays: bool = False,
-) -> set[str]:
-    """Find all top level transients in the SDFG.
+) -> dict[str, list[tuple[dace.SDFGState, dace_nodes.AccessNode]]]:
+    """Find all data that is accessed on the top level.
 
     The function will scan the SDFG, ignoring nested one, and return the
-    name of all transients that have an access node at the top level.
-    However, it will ignore access nodes that refers to registers.
+    name of all data that only have AccessNodes on the top level. In data
+    is found that has an AccessNode on both the top level and in a nested
+    scope and error is generated.
+    By default the function will return transient and non transient data,
+    however, if `only_transients` is `True` then only transient data will
+    be returned.
+    Furthermore, the function will ignore an access in the following cases:
+    - The AccessNode refers to data that is a register.
+    - The AccessNode refers to a View.
+
+    Args:
+        sdfg: The SDFG to process.
+        only_transients: If `True` only include transients.
+        only_arrays: If `True`, defaults to `False`, only arrays are returned.
+
+    Returns:
+        A `dict` that maps the name of a data container, to a list of tuples
+        containing the state where the AccessNode was found and the AccessNode.
     """
-    top_level_transients: set[str] = set()
+    # List of data that is accessed on the top level and all its access node.
+    top_level_data: dict[str, list[tuple[dace.SDFGState, dace_nodes.AccessNode]]] = dict()
+
+    # List of all data that were found not on top level.
+    not_top_level_data: set[str] = set()
+
     for state in sdfg.states():
         scope_dict = state.scope_dict()
         for dnode in state.data_nodes():
             data: str = dnode.data
             if scope_dict[dnode] is not None:
-                if data in top_level_transients:
-                    top_level_transients.remove(data)
+                # The node was not found on the top level. So we can ignore it.
+                # We also check if it was ever found on the top level, this should
+                #  not happen, as everything should go through Maps. But some strange
+                #  DaCe transformation might do it.
+                assert (
+                    data not in top_level_data
+                ), f"Found {data} on the top level and inside a scope."
+                not_top_level_data.add(data)
                 continue
-            elif data in top_level_transients:
+
+            elif data in top_level_data:
+                # The data is already known to be in top level data, so we must add the
+                #  AccessNode to the list of known nodes. But nothing else.
+                top_level_data[data].append((state, dnode))
                 continue
+
             elif gtx_transformations.util.is_view(dnode, sdfg):
+                # The AccessNode refers to a View so we ignore it anyway.
                 continue
+
+            # We have found a new data node that is on the top node and is unknown.
+            assert (
+                data not in not_top_level_data
+            ), f"Found {data} on the top level and inside a scope."
             desc: dace_data.Data = dnode.desc(sdfg)
 
-            if not desc.transient:
+            # Check if we only accept arrays
+            if only_arrays and not isinstance(desc, dace_data.Array):
                 continue
-            elif only_arrays and not isinstance(desc, dace_data.Array):
+
+            # For now we ignore registers.
+            #  We do this because register are allocated on the stack, so the compiler
+            #  has all information and should organize the best thing possible.
+            # TODO(phimuell): verify this.
+            elif desc.storage is dace.StorageType.Register:
                 continue
-            top_level_transients.add(data)
-    return top_level_transients
+
+            # We are only interested in transients
+            if only_transients and (not desc.transient):
+                continue
+
+            # Now create the new entry in the list and record the AccessNode.
+            top_level_data[data] = [(state, dnode)]
+    return top_level_data
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py b/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py
index 118f0449..c46420c2 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/utility.py
@@ -45,17 +45,18 @@ def get_tuple_fields(
     ...     ("a_1_1", sty),
     ... ]
     """
+    assert all(isinstance(t, ts.DataType) for t in tuple_type.types)
     fields = [(f"{tuple_name}_{i}", field_type) for i, field_type in enumerate(tuple_type.types)]
     if flatten:
-        expanded_fields = [
+        expanded_fields: list[list[tuple[str, ts.DataType]]] = [
             get_tuple_fields(field_name, field_type)
             if isinstance(field_type, ts.TupleType)
-            else [(field_name, field_type)]
+            else [(field_name, field_type)]  # type: ignore[list-item] # checked in assert
             for field_name, field_type in fields
         ]
         return list(itertools.chain(*expanded_fields))
     else:
-        return fields
+        return fields  # type: ignore[return-value] # checked in assert
 
 
 def replace_invalid_symbols(sdfg: dace.SDFG, ir: gtir.Program) -> gtir.Program:
diff --git a/src/gt4py/next/program_processors/runners/dace_fieldview/workflow.py b/src/gt4py/next/program_processors/runners/dace_fieldview/workflow.py
index a38a50d8..779dc8a1 100644
--- a/src/gt4py/next/program_processors/runners/dace_fieldview/workflow.py
+++ b/src/gt4py/next/program_processors/runners/dace_fieldview/workflow.py
@@ -37,6 +37,7 @@ class DaCeTranslator(
 ):
     device_type: core_defs.DeviceType
     auto_optimize: bool
+    itir_transforms_off: bool = False
 
     def _language_settings(self) -> languages.LanguageSettings:
         return languages.LanguageSettings(
@@ -51,7 +52,8 @@ class DaCeTranslator(
         auto_opt: bool,
         on_gpu: bool,
     ) -> dace.SDFG:
-        ir = itir_transforms.apply_fieldview_transforms(ir, offset_provider=offset_provider)
+        if not self.itir_transforms_off:
+            ir = itir_transforms.apply_fieldview_transforms(ir, offset_provider=offset_provider)
         sdfg = gtir_sdfg.build_sdfg_from_gtir(
             ir, offset_provider_type=common.offset_provider_to_type(offset_provider)
         )
diff --git a/src/gt4py/next/type_system/type_info.py b/src/gt4py/next/type_system/type_info.py
index 66f8937d..983063a9 100644
--- a/src/gt4py/next/type_system/type_info.py
+++ b/src/gt4py/next/type_system/type_info.py
@@ -78,15 +78,15 @@ def type_class(symbol_type: ts.TypeSpec) -> Type[ts.TypeSpec]:
     >>> type_class(ts.TupleType(types=[])).__name__
     'TupleType'
     """
-    match symbol_type:
-        case ts.DeferredType(constraint):
-            if constraint is None:
-                raise ValueError(f"No type information available for '{symbol_type}'.")
-            elif isinstance(constraint, tuple):
-                raise ValueError(f"Not sufficient type information available for '{symbol_type}'.")
-            return constraint
-        case ts.TypeSpec() as concrete_type:
-            return concrete_type.__class__
+    if isinstance(symbol_type, ts.DeferredType):
+        constraint = symbol_type.constraint
+        if constraint is None:
+            raise ValueError(f"No type information available for '{symbol_type}'.")
+        elif isinstance(constraint, tuple):
+            raise ValueError(f"Not sufficient type information available for '{symbol_type}'.")
+        return constraint
+    if isinstance(symbol_type, ts.TypeSpec):
+        return symbol_type.__class__
     raise ValueError(
         f"Invalid type for TypeInfo: requires '{ts.TypeSpec}', got '{type(symbol_type)}'."
     )
@@ -197,7 +197,7 @@ def apply_to_primitive_constituents(
         return fun(*symbol_types)
 
 
-def extract_dtype(symbol_type: ts.TypeSpec) -> ts.ScalarType:
+def extract_dtype(symbol_type: ts.TypeSpec) -> ts.ScalarType | ts.ListType:
     """
     Extract the data type from ``symbol_type`` if it is either `FieldType` or `ScalarType`.
 
@@ -234,7 +234,10 @@ def is_floating_point(symbol_type: ts.TypeSpec) -> bool:
     >>> is_floating_point(ts.FieldType(dims=[], dtype=ts.ScalarType(kind=ts.ScalarKind.FLOAT32)))
     True
     """
-    return extract_dtype(symbol_type).kind in [ts.ScalarKind.FLOAT32, ts.ScalarKind.FLOAT64]
+    return isinstance(dtype := extract_dtype(symbol_type), ts.ScalarType) and dtype.kind in [
+        ts.ScalarKind.FLOAT32,
+        ts.ScalarKind.FLOAT64,
+    ]
 
 
 def is_integer(symbol_type: ts.TypeSpec) -> bool:
@@ -295,7 +298,10 @@ def is_number(symbol_type: ts.TypeSpec) -> bool:
 
 
 def is_logical(symbol_type: ts.TypeSpec) -> bool:
-    return extract_dtype(symbol_type).kind is ts.ScalarKind.BOOL
+    return (
+        isinstance(dtype := extract_dtype(symbol_type), ts.ScalarType)
+        and dtype.kind is ts.ScalarKind.BOOL
+    )
 
 
 def is_arithmetic(symbol_type: ts.TypeSpec) -> bool:
@@ -385,11 +391,10 @@ def extract_dims(symbol_type: ts.TypeSpec) -> list[common.Dimension]:
     >>> extract_dims(ts.FieldType(dims=[I, J], dtype=ts.ScalarType(kind=ts.ScalarKind.INT64)))
     [Dimension(value='I', kind=<DimensionKind.HORIZONTAL: 'horizontal'>), Dimension(value='J', kind=<DimensionKind.HORIZONTAL: 'horizontal'>)]
     """
-    match symbol_type:
-        case ts.ScalarType():
-            return []
-        case ts.FieldType(dims):
-            return dims
+    if isinstance(symbol_type, ts.ScalarType):
+        return []
+    if isinstance(symbol_type, ts.FieldType):
+        return symbol_type.dims
     raise ValueError(f"Can not extract dimensions from '{symbol_type}'.")
 
 
@@ -502,7 +507,9 @@ def promote(
         return types[0]
     elif all(isinstance(type_, (ts.ScalarType, ts.FieldType)) for type_ in types):
         dims = common.promote_dims(*(extract_dims(type_) for type_ in types))
-        dtype = cast(ts.ScalarType, promote(*(extract_dtype(type_) for type_ in types)))
+        extracted_dtypes = [extract_dtype(type_) for type_ in types]
+        assert all(isinstance(dtype, ts.ScalarType) for dtype in extracted_dtypes)
+        dtype = cast(ts.ScalarType, promote(*extracted_dtypes))  # type: ignore[arg-type] # checked is `ScalarType`
 
         return ts.FieldType(dims=dims, dtype=dtype)
     raise TypeError("Expected a 'FieldType' or 'ScalarType'.")
diff --git a/src/gt4py/next/type_system/type_specifications.py b/src/gt4py/next/type_system/type_specifications.py
index fa8c9b9a..060d56ae 100644
--- a/src/gt4py/next/type_system/type_specifications.py
+++ b/src/gt4py/next/type_system/type_specifications.py
@@ -6,21 +6,13 @@
 # Please, refer to the LICENSE file in the root directory.
 # SPDX-License-Identifier: BSD-3-Clause
 
-from dataclasses import dataclass
 from typing import Iterator, Optional, Sequence, Union
 
-from gt4py.eve.type_definitions import IntEnum
-from gt4py.eve.utils import content_hash
-from gt4py.next import common as func_common
+from gt4py.eve import datamodels as eve_datamodels, type_definitions as eve_types
+from gt4py.next import common
 
 
-@dataclass(frozen=True)
-class TypeSpec:
-    def __hash__(self) -> int:
-        return hash(content_hash(self))
-
-    def __init_subclass__(cls) -> None:
-        cls.__hash__ = TypeSpec.__hash__  # type: ignore[method-assign]
+class TypeSpec(eve_datamodels.DataModel, kw_only=False, frozen=True): ...  # type: ignore[call-arg]
 
 
 class DataType(TypeSpec):
@@ -40,14 +32,12 @@ class CallableType:
     """
 
 
-@dataclass(frozen=True)
 class DeferredType(TypeSpec):
     """Dummy used to represent a type not yet inferred."""
 
     constraint: Optional[type[TypeSpec] | tuple[type[TypeSpec], ...]]
 
 
-@dataclass(frozen=True)
 class VoidType(TypeSpec):
     """
     Return type of a function without return values.
@@ -56,22 +46,20 @@ class VoidType(TypeSpec):
     """
 
 
-@dataclass(frozen=True)
 class DimensionType(TypeSpec):
-    dim: func_common.Dimension
+    dim: common.Dimension
 
 
-@dataclass(frozen=True)
 class OffsetType(TypeSpec):
     # TODO(havogt): replace by ConnectivityType
-    source: func_common.Dimension
-    target: tuple[func_common.Dimension] | tuple[func_common.Dimension, func_common.Dimension]
+    source: common.Dimension
+    target: tuple[common.Dimension] | tuple[common.Dimension, common.Dimension]
 
     def __str__(self) -> str:
         return f"Offset[{self.source}, {self.target}]"
 
 
-class ScalarKind(IntEnum):
+class ScalarKind(eve_types.IntEnum):
     BOOL = 1
     INT32 = 32
     INT64 = 64
@@ -80,7 +68,6 @@ class ScalarKind(IntEnum):
     STRING = 3001
 
 
-@dataclass(frozen=True)
 class ScalarType(DataType):
     kind: ScalarKind
     shape: Optional[list[int]] = None
@@ -92,31 +79,43 @@ class ScalarType(DataType):
         return f"{kind_str}{self.shape}"
 
 
-@dataclass(frozen=True)
-class TupleType(DataType):
-    types: list[DataType]
-
-    def __str__(self) -> str:
-        return f"tuple[{', '.join(map(str, self.types))}]"
+class ListType(DataType):
+    """Represents a neighbor list in the ITIR representation.
 
-    def __iter__(self) -> Iterator[DataType]:
-        yield from self.types
+    Note: not used in the frontend.
+    """
 
-    def __len__(self) -> int:
-        return len(self.types)
+    element_type: DataType
+    # TODO(havogt): the `offset_type` is not yet used in type_inference,
+    # it is meant to describe the neighborhood (via the local dimension)
+    offset_type: Optional[common.Dimension] = None
 
 
-@dataclass(frozen=True)
 class FieldType(DataType, CallableType):
-    dims: list[func_common.Dimension]
-    dtype: ScalarType
+    dims: list[common.Dimension]
+    dtype: ScalarType | ListType
 
     def __str__(self) -> str:
         dims = "..." if self.dims is Ellipsis else f"[{', '.join(dim.value for dim in self.dims)}]"
         return f"Field[{dims}, {self.dtype}]"
 
 
-@dataclass(frozen=True)
+class TupleType(DataType):
+    # TODO(tehrengruber): Remove `DeferredType` again. This was erroneously
+    #  introduced before we checked the annotations at runtime. All attributes of
+    #  a type that are types themselves must be concrete.
+    types: list[DataType | DimensionType | DeferredType]
+
+    def __str__(self) -> str:
+        return f"tuple[{', '.join(map(str, self.types))}]"
+
+    def __iter__(self) -> Iterator[DataType | DimensionType | DeferredType]:
+        yield from self.types
+
+    def __len__(self) -> int:
+        return len(self.types)
+
+
 class FunctionType(TypeSpec, CallableType):
     pos_only_args: Sequence[TypeSpec]
     pos_or_kw_args: dict[str, TypeSpec]
diff --git a/src/gt4py/next/type_system/type_translation.py b/src/gt4py/next/type_system/type_translation.py
index 62a67813..e601556e 100644
--- a/src/gt4py/next/type_system/type_translation.py
+++ b/src/gt4py/next/type_system/type_translation.py
@@ -10,7 +10,6 @@ from __future__ import annotations
 
 import builtins
 import collections.abc
-import dataclasses
 import functools
 import types
 import typing
@@ -105,7 +104,7 @@ def from_type_hint(
                 raise ValueError(f"Unbound tuples '{type_hint}' are not allowed.")
             tuple_types = [recursive_make_symbol(arg) for arg in args]
             assert all(isinstance(elem, ts.DataType) for elem in tuple_types)
-            return ts.TupleType(types=tuple_types)  # type: ignore[arg-type] # checked in assert
+            return ts.TupleType(types=tuple_types)
 
         case common.Field:
             if (n_args := len(args)) != 2:
@@ -168,7 +167,6 @@ def from_type_hint(
     raise ValueError(f"'{type_hint}' type is not supported.")
 
 
-@dataclasses.dataclass(frozen=True)
 class UnknownPythonObject(ts.TypeSpec):
     _object: Any
 
@@ -217,9 +215,9 @@ def from_value(value: Any) -> ts.TypeSpec:
         # not needed anymore.
         elems = [from_value(el) for el in value]
         assert all(isinstance(elem, ts.DataType) for elem in elems)
-        return ts.TupleType(types=elems)  # type: ignore[arg-type] # checked in assert
+        return ts.TupleType(types=elems)
     elif isinstance(value, types.ModuleType):
-        return UnknownPythonObject(_object=value)
+        return UnknownPythonObject(value)
     else:
         type_ = xtyping.infer_type(value, annotate_callable_kwargs=True)
         symbol_type = from_type_hint(type_)
diff --git a/tox.ini b/tox.ini
index 8da0e458..e7bfd4a3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,19 +9,14 @@ envlist =
     storage-py{310}-{internal,dace}-{cpu}
     # docs
 labels =
-    test-cartesian-cpu = cartesian-py38-internal-cpu, cartesian-internal-py39-cpu, \
-    cartesian-internal-py310-cpu, cartesian-py311-internal-cpu, \
-    cartesian-py38-dace-cpu, cartesian-py39-dace-cpu, cartesian-py310-dace-cpu, cartesian-py311-dace-cpu
-    test-eve-cpu = eve-py38, eve-py39, eve-py310, eve-py311
+    test-cartesian-cpu = cartesian-internal-py310-cpu, cartesian-py311-internal-cpu, cartesian-py310-dace-cpu, cartesian-py311-dace-cpu
+    test-eve-cpu = eve-py310, eve-py311
     test-next-cpu = next-py310-nomesh-cpu, next-py311-nomesh-cpu, next-py310-atlas-cpu, next-py311-atlas-cpu
-    test-storage-cpu = storage-py38-internal-cpu, storage-py39-internal-cpu, storage-py310-internal-cpu, storage-py311-internal-cpu, \
-    storage-py38-dace-cpu, storage-py39-dace-cpu, storage-py310-dace-cpu, storage-py311-dace-cpu
-    test-cpu = cartesian-py38-internal-cpu, cartesian-py39-internal-cpu, cartesian-py310-internal-cpu, cartesian-py311-internal-cpu, \
-    cartesian-py38-dace-cpu, cartesian-py39-dace-cpu, cartesian-py310-dace-cpu, cartesian-py311-dace-cpu, \
-    eve-py38, eve-py39, eve-py310, eve-py311, \
+    test-storage-cpu = storage-py310-internal-cpu, storage-py311-internal-cpu, storage-py310-dace-cpu, storage-py311-dace-cpu
+    test-cpu = cartesian-py310-internal-cpu, cartesian-py311-internal-cpu, cartesian-py310-dace-cpu, cartesian-py311-dace-cpu, \
+    eve-py310, eve-py311, \
     next-py310-nomesh-cpu, next-py311-nomesh-cpu, next-py310-atlas-cpu, next-py311-atlas-cpu, \
-    storage-py38-internal-cpu, storage-py39-internal-cpu, storage-py310-internal-cpu, storage-py311-internal-cpu, \
-    storage-py38-dace-cpu, storage-py39-dace-cpu, storage-py310-dace-cpu, storage-py311-dace-cpu
+    storage-py310-internal-cpu, storage-py311-internal-cpu, storage-py310-dace-cpu, storage-py311-dace-cpu
 
 [testenv]
 deps = -r {tox_root}{/}{env:ENV_REQUIREMENTS_FILE:requirements-dev.txt}
@@ -42,7 +37,7 @@ set_env =
     PYTHONWARNINGS = {env:PYTHONWARNINGS:ignore:Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*:UserWarning,ignore:Field View Program:UserWarning}
 
 # -- Primary tests --
-[testenv:cartesian-py{38,39,310,311}-{internal,dace}-{cpu,cuda,cuda11x,cuda12x}]
+[testenv:cartesian-py{310,311}-{internal,dace}-{cpu,cuda,cuda11x,cuda12x}]
 description = Run 'gt4py.cartesian' tests
 pass_env = {[testenv]pass_env}, BOOST_ROOT, BOOST_HOME, CUDA_HOME, CUDA_PATH, CXX, CC, OPENMP_CPPFLAGS, OPENMP_LDFLAGS, PIP_USER, PYTHONUSERBASE
 allowlist_externals =
@@ -65,7 +60,7 @@ commands =
 #     coverage json --rcfile=setup.cfg
 #     coverage html --rcfile=setup.cfg --show-contexts
 
-[testenv:eve-py{38,39,310,311}]
+[testenv:eve-py{310,311}]
 description = Run 'gt4py.eve' tests
 commands =
     python -m pytest --cache-clear -v -n {env:NUM_PROCESSES:1} {posargs} tests{/}eve_tests
@@ -89,7 +84,7 @@ commands =
         " {posargs} tests{/}next_tests
     pytest --doctest-modules src{/}gt4py{/}next
 
-[testenv:storage-py{38,39,310,311}-{internal,dace}-{cpu,cuda,cuda11x,cuda12x}]
+[testenv:storage-py{310,311}-{internal,dace}-{cpu,cuda,cuda11x,cuda12x}]
 description = Run 'gt4py.storage' tests
 commands =
     python -m pytest --cache-clear -v -n {env:NUM_PROCESSES:1} -m "\
@@ -112,7 +107,7 @@ commands =
     python -m pytest --nbmake examples -v -n {env:NUM_PROCESSES:1}
 
 # -- Other artefacts --
-[testenv:dev-py{38,39,310,311}{-atlas,}]
+[testenv:dev-py{310,311}{-atlas,}]
 description = Initialize development environment for gt4py
 deps =
     -r {tox_root}{/}requirements-dev.txt
@@ -141,17 +136,13 @@ set_env =
 #     git add _static
 # commands_post =
 
-[testenv:requirements-{base,py38,py39,py310,py311}]
+[testenv:requirements-{base,py310,py311}]
 description =
     base: Update pinned development requirements
-    py38: Update requirements for testing a specific python version
-    py39: Update requirements for testing a specific python version
     py310: Update requirements for testing a specific python version
     py311: Update requirements for testing a specific python version
 base_python =
-    base: py38
-    py38: py38
-    py39: py39
+    base: py310
     py310: py310
     py311: py311
 deps =


Test patch:
diff --git a/.github/workflows/test-cartesian-fallback.yml b/.github/workflows/test-cartesian-fallback.yml
index 45bbdf27..76fd8981 100644
--- a/.github/workflows/test-cartesian-fallback.yml
+++ b/.github/workflows/test-cartesian-fallback.yml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         tox-factor: [internal, dace]
 
     steps:
diff --git a/.github/workflows/test-cartesian.yml b/.github/workflows/test-cartesian.yml
index aa59660a..fd896c3d 100644
--- a/.github/workflows/test-cartesian.yml
+++ b/.github/workflows/test-cartesian.yml
@@ -25,20 +25,14 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         tox-factor: [internal, dace]
     steps:
     - uses: actions/checkout@v4
-    - name: Install boost
+    - name: Install C++ libraries
       shell: bash
       run: |
-        wget https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.gz
-        echo 7bd7ddceec1a1dfdcbdb3e609b60d01739c38390a5f956385a12f3122049f0ca boost_1_76_0.tar.gz > boost_hash.txt
-        sha256sum -c boost_hash.txt
-        tar xzf boost_1_76_0.tar.gz
-        mkdir -p boost/include
-        mv boost_1_76_0/boost boost/include/
-        echo "BOOST_ROOT=${PWD}/boost" >> $GITHUB_ENV
+        sudo apt install libboost-dev
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v5
       with:
diff --git a/.github/workflows/test-eve-fallback.yml b/.github/workflows/test-eve-fallback.yml
index 661118e7..46140042 100644
--- a/.github/workflows/test-eve-fallback.yml
+++ b/.github/workflows/test-eve-fallback.yml
@@ -18,7 +18,7 @@ jobs:
   test-eve:
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         os: ["ubuntu-latest"]
 
     runs-on: ${{ matrix.os }}
diff --git a/.github/workflows/test-eve.yml b/.github/workflows/test-eve.yml
index bfd6d8e4..e83c4c56 100644
--- a/.github/workflows/test-eve.yml
+++ b/.github/workflows/test-eve.yml
@@ -22,7 +22,7 @@ jobs:
   test-eve:
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         os: ["ubuntu-latest"]
       fail-fast: false
 
diff --git a/.github/workflows/test-storage-fallback.yml b/.github/workflows/test-storage-fallback.yml
index df861c64..022c66b1 100644
--- a/.github/workflows/test-storage-fallback.yml
+++ b/.github/workflows/test-storage-fallback.yml
@@ -19,7 +19,7 @@ jobs:
   test-storage:
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         tox-factor: [internal, dace]
         os: ["ubuntu-latest"]
 
diff --git a/.github/workflows/test-storage.yml b/.github/workflows/test-storage.yml
index 2f85670e..bfe6e49d 100644
--- a/.github/workflows/test-storage.yml
+++ b/.github/workflows/test-storage.yml
@@ -23,7 +23,7 @@ jobs:
   test-storage:
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.10", "3.11"]
         tox-factor: [internal, dace]
         os: ["ubuntu-latest"]
       fail-fast: false
diff --git a/min-extra-requirements-test.txt b/min-extra-requirements-test.txt
index 6d754151..a4924cc0 100644
--- a/min-extra-requirements-test.txt
+++ b/min-extra-requirements-test.txt
@@ -49,7 +49,6 @@
 ##         result.append(str(make_min_req(r)))
 ## print("\n".join(sorted(result)))
 ##]]]
-astunparse==1.6.3; python_version < "3.9"
 attrs==21.3
 black==22.3
 boltons==20.1
@@ -71,8 +70,7 @@ filelock==3.16.1
 frozendict==2.3
 gridtools-cpp==2.3.8
 hypothesis==6.0.0
-importlib-resources==5.0; python_version < "3.9"
-jax[cpu]==0.4.18; python_version >= "3.10"
+jax[cpu]==0.4.18
 jinja2==3.0.0
 jupytext==1.14
 lark==1.1.2
diff --git a/min-requirements-test.txt b/min-requirements-test.txt
index 991b7a69..4b243854 100644
--- a/min-requirements-test.txt
+++ b/min-requirements-test.txt
@@ -46,7 +46,6 @@
 ##     result.append(str(make_min_req(r)))
 ## print("\n".join(sorted(result)))
 ##]]]
-astunparse==1.6.3; python_version < "3.9"
 attrs==21.3
 black==22.3
 boltons==20.1
@@ -67,7 +66,6 @@ filelock==3.16.1
 frozendict==2.3
 gridtools-cpp==2.3.8
 hypothesis==6.0.0
-importlib-resources==5.0; python_version < "3.9"
 jinja2==3.0.0
 jupytext==1.14
 lark==1.1.2
diff --git a/tests/cartesian_tests/integration_tests/multi_feature_tests/test_code_generation.py b/tests/cartesian_tests/integration_tests/multi_feature_tests/test_code_generation.py
index 46091845..5a43144b 100644
--- a/tests/cartesian_tests/integration_tests/multi_feature_tests/test_code_generation.py
+++ b/tests/cartesian_tests/integration_tests/multi_feature_tests/test_code_generation.py
@@ -582,17 +582,6 @@ def test_K_offset_write(backend):
     # Cuda generates bad code for the K offset
     if backend == "cuda":
         pytest.skip("cuda K-offset write generates bad code")
-    if backend in ["dace:gpu"]:
-        import cupy as cp
-
-        if cp.cuda.runtime.runtimeGetVersion() < 12000:
-            pytest.skip(
-                f"{backend} backend with CUDA 11 and/or GCC 10.3 is not capable of K offset write, update CUDA/GCC if possible"
-            )
-    if backend in ["gt:gpu"]:
-        pytest.skip(
-            f"{backend} backend is not capable of K offset write, bug remains unsolved: https://github.com/GridTools/gt4py/issues/1754"
-        )
 
     arraylib = get_array_library(backend)
     array_shape = (1, 1, 4)
@@ -664,17 +653,6 @@ def test_K_offset_write(backend):
 def test_K_offset_write_conditional(backend):
     if backend == "cuda":
         pytest.skip("Cuda backend is not capable of K offset write")
-    if backend in ["dace:gpu"]:
-        import cupy as cp
-
-        if cp.cuda.runtime.runtimeGetVersion() < 12000:
-            pytest.skip(
-                f"{backend} backend with CUDA 11 and/or GCC 10.3 is not capable of K offset write, update CUDA/GCC if possible"
-            )
-    if backend in ["gt:gpu"]:
-        pytest.skip(
-            f"{backend} backend is not capable of K offset write, bug remains unsolved: https://github.com/GridTools/gt4py/issues/1754"
-        )
 
     arraylib = get_array_library(backend)
     array_shape = (1, 1, 4)
@@ -682,7 +660,7 @@ def test_K_offset_write_conditional(backend):
 
     @gtscript.stencil(backend=backend)
     def column_physics_conditional(A: Field[np.float64], B: Field[np.float64], scalar: np.float64):
-        with computation(BACKWARD), interval(1, None):
+        with computation(BACKWARD), interval(1, -1):
             if A > 0 and B > 0:
                 A[0, 0, -1] = scalar
                 B[0, 0, 1] = A
@@ -700,6 +678,42 @@ def test_K_offset_write_conditional(backend):
         backend=backend, aligned_index=(0, 0, 0), shape=array_shape, dtype=np.float64
     )
     column_physics_conditional(A, B, 2.0)
+    # Manual unroll of the above
+    # Starts with
+    # - A[...] = [40, 41, 42, 43]
+    # - B[...] = [1, 1, 1, 1]
+    # Now in-stencil
+    # ITERATION k = 2 of [2:1]
+    # if condition
+    # - A[2] == 42 && B[2] == 1 => True
+    # - A[1] = 2.0
+    # - B[3] = A[2] = 42
+    # while
+    # - lev = 1
+    # - A[2] == 42 && B[2] == 1 => True
+    # - A[3] = -1
+    # - B[2] = -1
+    # - lev = 2
+    # - A[2] == 42 && B[2] == -1 => False
+    # End of iteration state
+    # - A[...] = A[40, 2.0, 2.0, -1]
+    # - B[...] = A[1, 1, -1, 42]
+    # ITERATION k = 1 of [2:1]
+    # if condition
+    # - A[1] == 2.0 && B[1] == 1 => True
+    # - A[0] = 2.0
+    # - B[2] = A[1] = 2.0
+    # while
+    # - lev = 1
+    # - A[1] == 2.0 && B[1] == 1 => True
+    # - A[2] = -1
+    # - B[1] = -1
+    # - lev = 2
+    # - A[1] == 2.0 && B[2] == -1 => False
+    # End of stencil state
+    # - A[...] = A[2.0, 2.0, -1, -1]
+    # - B[...] = A[1, -1, 2.0, 42]
+
     assert (A[0, 0, :] == arraylib.array([2, 2, -1, -1])).all()
     assert (B[0, 0, :] == arraylib.array([1, -1, 2, 42])).all()
 
diff --git a/tests/eve_tests/unit_tests/test_datamodels.py b/tests/eve_tests/unit_tests/test_datamodels.py
index 05be5f3d..75b07fd8 100644
--- a/tests/eve_tests/unit_tests/test_datamodels.py
+++ b/tests/eve_tests/unit_tests/test_datamodels.py
@@ -10,9 +10,9 @@ from __future__ import annotations
 
 import enum
 import numbers
+import sys
 import types
 import typing
-from typing import Set  # noqa: F401 [unused-import] used in exec() context
 from typing import (
     Any,
     Callable,
@@ -26,6 +26,7 @@ from typing import (
     MutableSequence,
     Optional,
     Sequence,
+    Set,  # noqa: F401 [unused-import] used in exec() context
     Tuple,
     Type,
     TypeVar,
@@ -555,6 +556,18 @@ SAMPLE_TYPE_DATA: Final = [
     ("typing.MutableSequence[int]", ([1, 2, 3], []), ((1, 2, 3), tuple(), 1, [1.0], {1})),
     ("typing.Set[int]", ({1, 2, 3}, set()), (1, [1], (1,), {1: None})),
     ("typing.Union[int, float, str]", [1, 3.0, "one"], [[1], [], 1j]),
+    pytest.param(
+        "int | float | str",
+        [1, 3.0, "one"],
+        [[1], [], 1j],
+        marks=pytest.mark.skipif(sys.version_info < (3, 10), reason="| union syntax not supported"),
+    ),
+    pytest.param(
+        "typing.List[int|float]",
+        [[1, 2.0], []],
+        [1, 2.0, [1, "2.0"]],
+        marks=pytest.mark.skipif(sys.version_info < (3, 10), reason="| union syntax not supported"),
+    ),
     ("typing.Optional[int]", [1, None], [[1], [], 1j]),
     (
         "typing.Dict[Union[int, float, str], Union[Tuple[int, Optional[float]], Set[int]]]",
diff --git a/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py b/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
index 08904c06..cd71c306 100644
--- a/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
+++ b/tests/next_tests/integration_tests/feature_tests/dace/test_orchestration.py
@@ -12,15 +12,16 @@ import pytest
 import gt4py.next as gtx
 from gt4py.next import allocators as gtx_allocators, common as gtx_common
 
+from gt4py._core import definitions as core_defs
 from next_tests.integration_tests import cases
-from next_tests.integration_tests.cases import cartesian_case, unstructured_case
+from next_tests.integration_tests.cases import cartesian_case, unstructured_case  # noqa: F401
 from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import (
     E2V,
     E2VDim,
     Edge,
     Vertex,
-    exec_alloc_descriptor,
-    mesh_descriptor,
+    exec_alloc_descriptor,  # noqa: F401
+    mesh_descriptor,  # noqa: F401
 )
 from next_tests.integration_tests.multi_feature_tests.ffront_tests.test_laplacian import (
     lap_program,
@@ -37,23 +38,17 @@ except ImportError:
 pytestmark = pytest.mark.requires_dace
 
 
-def test_sdfgConvertible_laplap(cartesian_case):
+def test_sdfgConvertible_laplap(cartesian_case):  # noqa: F811
     if not cartesian_case.backend or "dace" not in cartesian_case.backend.name:
         pytest.skip("DaCe-related test: Test SDFGConvertible interface for GT4Py programs")
 
-    # TODO(ricoh): enable test after adding GTIR support
-    pytest.skip("DaCe SDFGConvertible interface does not support GTIR program.")
-
-    allocator, backend = unstructured_case.allocator, unstructured_case.backend
-
-    if gtx_allocators.is_field_allocator_factory_for(allocator, gtx_allocators.CUPY_DEVICE):
-        import cupy as xp
-    else:
-        import numpy as xp
+    backend = cartesian_case.backend
 
     in_field = cases.allocate(cartesian_case, laplap_program, "in_field")()
     out_field = cases.allocate(cartesian_case, laplap_program, "out_field")()
 
+    xp = in_field.array_ns
+
     # Test DaCe closure support
     @dace.program
     def sdfg():
@@ -88,16 +83,13 @@ def testee(a: gtx.Field[gtx.Dims[Vertex], gtx.float64], b: gtx.Field[gtx.Dims[Ed
 
 
 @pytest.mark.uses_unstructured_shift
-def test_sdfgConvertible_connectivities(unstructured_case):
+def test_sdfgConvertible_connectivities(unstructured_case):  # noqa: F811
     if not unstructured_case.backend or "dace" not in unstructured_case.backend.name:
         pytest.skip("DaCe-related test: Test SDFGConvertible interface for GT4Py programs")
 
-    # TODO(ricoh): enable test after adding GTIR support
-    pytest.skip("DaCe SDFGConvertible interface does not support GTIR program.")
-
     allocator, backend = unstructured_case.allocator, unstructured_case.backend
 
-    if gtx_allocators.is_field_allocator_factory_for(allocator, gtx_allocators.CUPY_DEVICE):
+    if gtx_allocators.is_field_allocator_for(allocator, gtx_allocators.CUPY_DEVICE):
         import cupy as xp
 
         dace_storage_type = dace.StorageType.GPU_Global
@@ -113,6 +105,15 @@ def test_sdfgConvertible_connectivities(unstructured_case):
         name="OffsetProvider",
     )
 
+    e2v = gtx.as_connectivity(
+        [Edge, E2VDim],
+        codomain=Vertex,
+        data=xp.asarray([[0, 1], [1, 2], [2, 0]]),
+        allocator=allocator,
+    )
+
+    testee2 = testee.with_backend(backend).with_connectivities({"E2V": e2v})
+
     @dace.program
     def sdfg(
         a: dace.data.Array(dtype=dace.float64, shape=(rows,), storage=dace_storage_type),
@@ -120,17 +121,10 @@ def test_sdfgConvertible_connectivities(unstructured_case):
         offset_provider: OffsetProvider_t,
         connectivities: dace.compiletime,
     ):
-        testee.with_backend(backend).with_connectivities(connectivities)(
-            a, out, offset_provider=offset_provider
-        )
+        testee2.with_connectivities(connectivities)(a, out, offset_provider=offset_provider)
+        return out
 
-    e2v = gtx.as_connectivity(
-        [Edge, E2VDim],
-        codomain=Vertex,
-        data=xp.asarray([[0, 1], [1, 2], [2, 0]]),
-        allocator=allocator,
-    )
-    connectivities = {"E2V": e2v.__gt_type__()}
+    connectivities = {"E2V": e2v}  # replace 'e2v' with 'e2v.__gt_type__()' when GTIR is AOT
     offset_provider = OffsetProvider_t.dtype._typeclass.as_ctypes()(E2V=e2v.data_ptr())
 
     SDFG = sdfg.to_sdfg(connectivities=connectivities)
@@ -138,23 +132,21 @@ def test_sdfgConvertible_connectivities(unstructured_case):
 
     a = gtx.as_field([Vertex], xp.asarray([0.0, 1.0, 2.0]), allocator=allocator)
     out = gtx.zeros({Edge: 3}, allocator=allocator)
-    e2v_ndarray_copy = (
-        e2v.ndarray.copy()
-    )  # otherwise DaCe complains about the gt4py custom allocated view
-    # This is a low level interface to call the compiled SDFG.
-    # It is not supposed to be used in user code.
-    # The high level interface should be provided by a DaCe Orchestrator,
-    # i.e. decorator that hides the low level operations.
-    # This test checks only that the SDFGConvertible interface works correctly.
+
+    def get_stride_from_numpy_to_dace(arg: core_defs.NDArrayObject, axis: int) -> int:
+        # NumPy strides: number of bytes to jump
+        # DaCe strides: number of elements to jump
+        return arg.strides[axis] // arg.itemsize
+
     cSDFG(
         a,
         out,
         offset_provider,
         rows=3,
         cols=2,
-        connectivity_E2V=e2v_ndarray_copy,
-        __connectivity_E2V_stride_0=get_stride_from_numpy_to_dace(e2v_ndarray_copy, 0),
-        __connectivity_E2V_stride_1=get_stride_from_numpy_to_dace(e2v_ndarray_copy, 1),
+        connectivity_E2V=e2v,
+        __connectivity_E2V_stride_0=get_stride_from_numpy_to_dace(e2v.ndarray, 0),
+        __connectivity_E2V_stride_1=get_stride_from_numpy_to_dace(e2v.ndarray, 1),
     )
 
     e2v_np = e2v.asnumpy()
@@ -166,18 +158,19 @@ def test_sdfgConvertible_connectivities(unstructured_case):
         data=xp.asarray([[1, 0], [2, 1], [0, 2]]),
         allocator=allocator,
     )
-    e2v_ndarray_copy = e2v.ndarray.copy()
     offset_provider = OffsetProvider_t.dtype._typeclass.as_ctypes()(E2V=e2v.data_ptr())
-    cSDFG(
-        a,
-        out,
-        offset_provider,
-        rows=3,
-        cols=2,
-        connectivity_E2V=e2v_ndarray_copy,
-        __connectivity_E2V_stride_0=get_stride_from_numpy_to_dace(e2v_ndarray_copy, 0),
-        __connectivity_E2V_stride_1=get_stride_from_numpy_to_dace(e2v_ndarray_copy, 1),
-    )
+    with dace.config.temporary_config():
+        dace.config.Config.set("compiler", "allow_view_arguments", value=True)
+        cSDFG(
+            a,
+            out,
+            offset_provider,
+            rows=3,
+            cols=2,
+            connectivity_E2V=e2v,
+            __connectivity_E2V_stride_0=get_stride_from_numpy_to_dace(e2v.ndarray, 0),
+            __connectivity_E2V_stride_1=get_stride_from_numpy_to_dace(e2v.ndarray, 1),
+        )
 
     e2v_np = e2v.asnumpy()
     assert np.allclose(out.asnumpy(), a.asnumpy()[e2v_np[:, 0]])
diff --git a/tests/next_tests/integration_tests/feature_tests/dace/test_program.py b/tests/next_tests/integration_tests/feature_tests/dace/test_program.py
new file mode 100644
index 00000000..db0f90b4
--- /dev/null
+++ b/tests/next_tests/integration_tests/feature_tests/dace/test_program.py
@@ -0,0 +1,134 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2024, ETH Zurich
+# All rights reserved.
+#
+# Please, refer to the LICENSE file in the root directory.
+# SPDX-License-Identifier: BSD-3-Clause
+
+import pytest
+
+from gt4py import next as gtx
+from gt4py.next import common
+
+from next_tests.integration_tests import cases
+from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import (
+    Cell,
+    Edge,
+    IDim,
+    JDim,
+    KDim,
+    Vertex,
+    mesh_descriptor,  # noqa: F401
+)
+
+
+try:
+    import dace
+
+    from gt4py.next.program_processors.runners import dace as dace_backends
+except ImportError:
+    from types import ModuleType
+    from typing import Optional
+
+    from gt4py.next import backend as next_backend
+
+    dace: Optional[ModuleType] = None
+    dace_backends: Optional[ModuleType] = None
+
+
+@pytest.fixture(
+    params=[
+        pytest.param(dace_backends.run_dace_cpu, marks=pytest.mark.requires_dace),
+        pytest.param(
+            dace_backends.run_dace_gpu, marks=(pytest.mark.requires_gpu, pytest.mark.requires_dace)
+        ),
+    ]
+)
+def gtir_dace_backend(request):
+    yield request.param
+
+
+@pytest.fixture
+def cartesian(request, gtir_dace_backend):
+    if gtir_dace_backend is None:
+        yield None
+
+    yield cases.Case(
+        backend=gtir_dace_backend,
+        offset_provider={
+            "Ioff": IDim,
+            "Joff": JDim,
+            "Koff": KDim,
+        },
+        default_sizes={IDim: 10, JDim: 10, KDim: 10},
+        grid_type=common.GridType.CARTESIAN,
+        allocator=gtir_dace_backend.allocator,
+    )
+
+
+@pytest.fixture
+def unstructured(request, gtir_dace_backend, mesh_descriptor):  # noqa: F811
+    if gtir_dace_backend is None:
+        yield None
+
+    yield cases.Case(
+        backend=gtir_dace_backend,
+        offset_provider=mesh_descriptor.offset_provider,
+        default_sizes={
+            Vertex: mesh_descriptor.num_vertices,
+            Edge: mesh_descriptor.num_edges,
+            Cell: mesh_descriptor.num_cells,
+            KDim: 10,
+        },
+        grid_type=common.GridType.UNSTRUCTURED,
+        allocator=gtir_dace_backend.allocator,
+    )
+
+
+@pytest.mark.skipif(dace is None, reason="DaCe not found")
+def test_halo_exchange_helper_attrs(unstructured):
+    local_int = gtx.int
+
+    @gtx.field_operator(backend=unstructured.backend)
+    def testee_op(
+        a: gtx.Field[[Vertex, KDim], gtx.int],
+    ) -> gtx.Field[[Vertex, KDim], gtx.int]:
+        return a + local_int(10)
+
+    @gtx.program(backend=unstructured.backend)
+    def testee_prog(
+        a: gtx.Field[[Vertex, KDim], gtx.int],
+        b: gtx.Field[[Vertex, KDim], gtx.int],
+        c: gtx.Field[[Vertex, KDim], gtx.int],
+    ):
+        testee_op(b, out=c)
+        testee_op(a, out=b)
+
+    dace_storage_type = (
+        dace.StorageType.GPU_Global
+        if unstructured.backend == dace_backends.run_dace_gpu
+        else dace.StorageType.Default
+    )
+
+    rows = dace.symbol("rows")
+    cols = dace.symbol("cols")
+
+    @dace.program
+    def testee_dace(
+        a: dace.data.Array(dtype=dace.int64, shape=(rows, cols), storage=dace_storage_type),
+        b: dace.data.Array(dtype=dace.int64, shape=(rows, cols), storage=dace_storage_type),
+        c: dace.data.Array(dtype=dace.int64, shape=(rows, cols), storage=dace_storage_type),
+    ):
+        testee_prog(a, b, c)
+
+    # if simplify=True, DaCe might inline the nested SDFG coming from Program.__sdfg__,
+    # effectively erasing the attributes we want to test for here
+    sdfg = testee_dace.to_sdfg(simplify=False)
+
+    testee = next(
+        subgraph for subgraph in sdfg.all_sdfgs_recursive() if subgraph.name == "testee_prog"
+    )
+
+    assert testee.gt4py_program_input_fields == {"a": Vertex, "b": Vertex}
+    assert testee.gt4py_program_output_fields == {"b": Vertex, "c": Vertex}
diff --git a/tests/next_tests/integration_tests/feature_tests/iterator_tests/test_extractors.py b/tests/next_tests/integration_tests/feature_tests/iterator_tests/test_extractors.py
new file mode 100644
index 00000000..7358ab3d
--- /dev/null
+++ b/tests/next_tests/integration_tests/feature_tests/iterator_tests/test_extractors.py
@@ -0,0 +1,102 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2024, ETH Zurich
+# All rights reserved.
+#
+# Please, refer to the LICENSE file in the root directory.
+# SPDX-License-Identifier: BSD-3-Clause
+
+import typing
+
+import pytest
+
+from gt4py import next as gtx
+from gt4py.next import common
+from gt4py.next.iterator.transforms import extractors
+
+from next_tests.integration_tests import cases
+from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils import (
+    IDim,
+    JDim,
+    KDim,
+)
+
+
+if typing.TYPE_CHECKING:
+    from types import ModuleType
+    from typing import Optional
+
+try:
+    import dace
+
+    from gt4py.next.program_processors.runners.dace import run_dace_cpu
+except ImportError:
+    from gt4py.next import backend as next_backend
+
+    dace: Optional[ModuleType] = None
+    run_dace_cpu: Optional[next_backend.Backend] = None
+
+
+@pytest.fixture(params=[pytest.param(run_dace_cpu, marks=pytest.mark.requires_dace), gtx.gtfn_cpu])
+def gtir_dace_backend(request):
+    yield request.param
+
+
+@pytest.fixture
+def cartesian(request, gtir_dace_backend):
+    if gtir_dace_backend is None:
+        yield None
+
+    yield cases.Case(
+        backend=gtir_dace_backend,
+        offset_provider={
+            "Ioff": IDim,
+            "Joff": JDim,
+            "Koff": KDim,
+        },
+        default_sizes={IDim: 10, JDim: 10, KDim: 10},
+        grid_type=common.GridType.CARTESIAN,
+        allocator=gtir_dace_backend.allocator,
+    )
+
+
+@pytest.mark.skipif(dace is None, reason="DaCe not found")
+def test_input_names_extractor_cartesian(cartesian):
+    @gtx.field_operator(backend=cartesian.backend)
+    def testee_op(
+        a: gtx.Field[[IDim, JDim, KDim], gtx.int],
+    ) -> gtx.Field[[IDim, JDim, KDim], gtx.int]:
+        return a
+
+    @gtx.program(backend=cartesian.backend)
+    def testee(
+        a: gtx.Field[[IDim, JDim, KDim], gtx.int],
+        b: gtx.Field[[IDim, JDim, KDim], gtx.int],
+        c: gtx.Field[[IDim, JDim, KDim], gtx.int],
+    ):
+        testee_op(b, out=c)
+        testee_op(a, out=b)
+
+    input_field_names = extractors.InputNamesExtractor.only_fields(testee.gtir)
+    assert input_field_names == {"a", "b"}
+
+
+@pytest.mark.skipif(dace is None, reason="DaCe not found")
+def test_output_names_extractor(cartesian):
+    @gtx.field_operator(backend=cartesian.backend)
+    def testee_op(
+        a: gtx.Field[[IDim, JDim, KDim], gtx.int],
+    ) -> gtx.Field[[IDim, JDim, KDim], gtx.int]:
+        return a
+
+    @gtx.program(backend=cartesian.backend)
+    def testee(
+        a: gtx.Field[[IDim, JDim, KDim], gtx.int],
+        b: gtx.Field[[IDim, JDim, KDim], gtx.int],
+        c: gtx.Field[[IDim, JDim, KDim], gtx.int],
+    ):
+        testee_op(a, out=b)
+        testee_op(a, out=c)
+
+    output_field_names = extractors.OutputNamesExtractor.only_fields(testee.gtir)
+    assert output_field_names == {"b", "c"}
diff --git a/tests/next_tests/unit_tests/iterator_tests/test_type_inference.py b/tests/next_tests/unit_tests/iterator_tests/test_type_inference.py
index 7eb4e86a..b6b70af0 100644
--- a/tests/next_tests/unit_tests/iterator_tests/test_type_inference.py
+++ b/tests/next_tests/unit_tests/iterator_tests/test_type_inference.py
@@ -46,8 +46,8 @@ from next_tests.integration_tests.feature_tests.ffront_tests.ffront_test_utils i
 bool_type = ts.ScalarType(kind=ts.ScalarKind.BOOL)
 int_type = ts.ScalarType(kind=ts.ScalarKind.INT32)
 float64_type = ts.ScalarType(kind=ts.ScalarKind.FLOAT64)
-float64_list_type = it_ts.ListType(element_type=float64_type)
-int_list_type = it_ts.ListType(element_type=int_type)
+float64_list_type = ts.ListType(element_type=float64_type)
+int_list_type = ts.ListType(element_type=int_type)
 
 float_i_field = ts.FieldType(dims=[IDim], dtype=float64_type)
 float_vertex_k_field = ts.FieldType(dims=[Vertex, KDim], dtype=float64_type)
@@ -77,8 +77,8 @@ def expression_test_cases():
         (im.deref(im.ref("it", it_on_e_of_e_type)), it_on_e_of_e_type.element_type),
         (im.call("can_deref")(im.ref("it", it_on_e_of_e_type)), bool_type),
         (im.if_(True, 1, 2), int_type),
-        (im.call("make_const_list")(True), it_ts.ListType(element_type=bool_type)),
-        (im.call("list_get")(0, im.ref("l", it_ts.ListType(element_type=bool_type))), bool_type),
+        (im.call("make_const_list")(True), ts.ListType(element_type=bool_type)),
+        (im.call("list_get")(0, im.ref("l", ts.ListType(element_type=bool_type))), bool_type),
         (
             im.call("named_range")(itir.AxisLiteral(value="Vertex"), 0, 1),
             it_ts.NamedRangeType(dim=Vertex),
@@ -110,7 +110,7 @@ def expression_test_cases():
         # neighbors
         (
             im.neighbors("E2V", im.ref("a", it_on_e_of_e_type)),
-            it_ts.ListType(element_type=it_on_e_of_e_type.element_type),
+            ts.ListType(element_type=it_on_e_of_e_type.element_type),
         ),
         # cast
         (im.call("cast_")(1, "int32"), int_type),
diff --git a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py
index 1a4ce6d0..a98eac3c 100644
--- a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py
+++ b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_map_buffer_elimination.py
@@ -22,10 +22,6 @@ from . import util
 import dace
 
 
-def _make_test_data(names: list[str]) -> dict[str, np.ndarray]:
-    return {name: np.array(np.random.rand(10), dtype=np.float64, copy=True) for name in names}
-
-
 def _make_test_sdfg(
     output_name: str = "G",
     input_name: str = "G",
@@ -262,3 +258,92 @@ def test_map_buffer_elimination_not_apply():
         validate_all=True,
     )
     assert count == 0
+
+
+def test_map_buffer_elimination_with_nested_sdfgs():
+    """
+    After removing a transient connected to a nested SDFG node, ensure that the strides
+    are propagated to the arrays in nested SDFG.
+    """
+
+    stride1, stride2, stride3 = [dace.symbol(f"stride{i}", dace.int32) for i in range(3)]
+
+    # top-level sdfg
+    sdfg = dace.SDFG(util.unique_name("map_buffer"))
+    inp, inp_desc = sdfg.add_array("__inp", (10,), dace.float64)
+    out, out_desc = sdfg.add_array(
+        "__out", (10, 10, 10), dace.float64, strides=(stride1, stride2, stride3)
+    )
+    tmp, _ = sdfg.add_temp_transient_like(out_desc)
+    state = sdfg.add_state()
+    tmp_node = state.add_access(tmp)
+
+    nsdfg1 = dace.SDFG(util.unique_name("map_buffer"))
+    inp1, inp1_desc = nsdfg1.add_array("__inp", (10,), dace.float64)
+    out1, out1_desc = nsdfg1.add_array("__out", (10, 10), dace.float64)
+    tmp1, _ = nsdfg1.add_temp_transient_like(out1_desc)
+    state1 = nsdfg1.add_state()
+    tmp1_node = state1.add_access(tmp1)
+
+    nsdfg2 = dace.SDFG(util.unique_name("map_buffer"))
+    inp2, _ = nsdfg2.add_array("__inp", (10,), dace.float64)
+    out2, out2_desc = nsdfg2.add_array("__out", (10,), dace.float64)
+    tmp2, _ = nsdfg2.add_temp_transient_like(out2_desc)
+    state2 = nsdfg2.add_state()
+    tmp2_node = state2.add_access(tmp2)
+
+    state2.add_mapped_tasklet(
+        "broadcast2",
+        map_ranges={"__i": "0:10"},
+        code="__oval = __ival + 1.0",
+        inputs={
+            "__ival": dace.Memlet(f"{inp2}[__i]"),
+        },
+        outputs={
+            "__oval": dace.Memlet(f"{tmp2}[__i]"),
+        },
+        output_nodes={tmp2_node},
+        external_edges=True,
+    )
+    state2.add_nedge(tmp2_node, state2.add_access(out2), dace.Memlet.from_array(out2, out2_desc))
+
+    nsdfg2_node = state1.add_nested_sdfg(nsdfg2, nsdfg1, inputs={"__inp"}, outputs={"__out"})
+    me1, mx1 = state1.add_map("broadcast1", ndrange={"__i": "0:10"})
+    state1.add_memlet_path(
+        state1.add_access(inp1),
+        me1,
+        nsdfg2_node,
+        dst_conn="__inp",
+        memlet=dace.Memlet.from_array(inp1, inp1_desc),
+    )
+    state1.add_memlet_path(
+        nsdfg2_node, mx1, tmp1_node, src_conn="__out", memlet=dace.Memlet(f"{tmp1}[__i, 0:10]")
+    )
+    state1.add_nedge(tmp1_node, state1.add_access(out1), dace.Memlet.from_array(out1, out1_desc))
+
+    nsdfg1_node = state.add_nested_sdfg(nsdfg1, sdfg, inputs={"__inp"}, outputs={"__out"})
+    me, mx = state.add_map("broadcast", ndrange={"__i": "0:10"})
+    state.add_memlet_path(
+        state.add_access(inp),
+        me,
+        nsdfg1_node,
+        dst_conn="__inp",
+        memlet=dace.Memlet.from_array(inp, inp_desc),
+    )
+    state.add_memlet_path(
+        nsdfg1_node, mx, tmp_node, src_conn="__out", memlet=dace.Memlet(f"{tmp}[__i, 0:10, 0:10]")
+    )
+    state.add_nedge(tmp_node, state.add_access(out), dace.Memlet.from_array(out, out_desc))
+
+    sdfg.validate()
+
+    count = sdfg.apply_transformations_repeated(
+        gtx_transformations.GT4PyMapBufferElimination(
+            assume_pointwise=False,
+        ),
+        validate=True,
+        validate_all=True,
+    )
+    assert count == 3
+    assert out1_desc.strides == out_desc.strides[1:]
+    assert out2_desc.strides == out_desc.strides[2:]
diff --git a/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_strides.py b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_strides.py
new file mode 100644
index 00000000..5b16e41b
--- /dev/null
+++ b/tests/next_tests/unit_tests/program_processor_tests/runners_tests/dace_tests/transformation_tests/test_strides.py
@@ -0,0 +1,541 @@
+# GT4Py - GridTools Framework
+#
+# Copyright (c) 2014-2024, ETH Zurich
+# All rights reserved.
+#
+# Please, refer to the LICENSE file in the root directory.
+# SPDX-License-Identifier: BSD-3-Clause
+
+import pytest
+import numpy as np
+import copy
+
+dace = pytest.importorskip("dace")
+from dace import symbolic as dace_symbolic
+from dace.sdfg import nodes as dace_nodes
+
+from gt4py.next.program_processors.runners.dace_fieldview import (
+    transformations as gtx_transformations,
+)
+
+from . import util
+
+import dace
+
+
+def _make_strides_propagation_level3_sdfg() -> dace.SDFG:
+    """Generates the level 3 SDFG (nested-nested) SDFG for `test_strides_propagation()`."""
+    sdfg = dace.SDFG(util.unique_name("level3"))
+    state = sdfg.add_state(is_start_block=True)
+    names = ["a3", "c3"]
+
+    for name in names:
+        stride_name = name + "_stride"
+        stride_sym = dace_symbolic.pystr_to_symbolic(stride_name)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            transient=False,
+            strides=(stride_sym,),
+        )
+
+    state.add_mapped_tasklet(
+        "compL3",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("a3[__i0]")},
+        code="__out = __in1 + 10.",
+        outputs={"__out": dace.Memlet("c3[__i0]")},
+        external_edges=True,
+    )
+    sdfg.validate()
+    return sdfg
+
+
+def _make_strides_propagation_level2_sdfg() -> tuple[dace.SDFG, dace_nodes.NestedSDFG]:
+    """Generates the level 2 SDFG (nested) SDFG for `test_strides_propagation()`.
+
+    The function returns the level 2 SDFG and the NestedSDFG node that contains
+    the level 3 SDFG.
+    """
+    sdfg = dace.SDFG(util.unique_name("level2"))
+    state = sdfg.add_state(is_start_block=True)
+    names = ["a2", "a2_alias", "b2", "c2"]
+
+    for name in names:
+        stride_name = name + "_stride"
+        stride_sym = dace_symbolic.pystr_to_symbolic(stride_name)
+        sdfg.add_symbol(stride_name, dace.int64)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            transient=False,
+            strides=(stride_sym,),
+        )
+
+    state.add_mapped_tasklet(
+        "compL2_1",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("a2[__i0]")},
+        code="__out = __in1 + 10",
+        outputs={"__out": dace.Memlet("b2[__i0]")},
+        external_edges=True,
+    )
+
+    state.add_mapped_tasklet(
+        "compL2_2",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("c2[__i0]")},
+        code="__out = __in1",
+        outputs={"__out": dace.Memlet("a2_alias[__i0]")},
+        external_edges=True,
+    )
+
+    # This is the nested SDFG we have here.
+    sdfg_level3 = _make_strides_propagation_level3_sdfg()
+
+    nsdfg = state.add_nested_sdfg(
+        sdfg=sdfg_level3,
+        parent=sdfg,
+        inputs={"a3"},
+        outputs={"c3"},
+        symbol_mapping={s3: s3 for s3 in sdfg_level3.free_symbols},
+    )
+
+    state.add_edge(state.add_access("a2"), None, nsdfg, "a3", dace.Memlet("a2[0:10]"))
+    state.add_edge(nsdfg, "c3", state.add_access("c2"), None, dace.Memlet("c2[0:10]"))
+    sdfg.validate()
+
+    return sdfg, nsdfg
+
+
+def _make_strides_propagation_level1_sdfg() -> (
+    tuple[dace.SDFG, dace_nodes.NestedSDFG, dace_nodes.NestedSDFG]
+):
+    """Generates the level 1 SDFG (top) SDFG for `test_strides_propagation()`.
+
+    Note that the SDFG is valid, but will be indeterminate. The only point of
+    this SDFG is to have a lot of different situations that have to be handled
+    for renaming.
+
+    Returns:
+        A tuple of length three, with the following members:
+        - The top level SDFG.
+        - The NestedSDFG node that contains the level 2 SDFG (member of the top level SDFG).
+        - The NestedSDFG node that contains the lebel 3 SDFG (member of the level 2 SDFG).
+    """
+
+    sdfg = dace.SDFG(util.unique_name("level1"))
+    state = sdfg.add_state(is_start_block=True)
+    names = ["a1", "b1", "c1"]
+
+    for name in names:
+        stride_name = name + "_stride"
+        stride_sym = dace_symbolic.pystr_to_symbolic(stride_name)
+        sdfg.add_symbol(stride_name, dace.int64)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            transient=False,
+            strides=(stride_sym,),
+        )
+
+    sdfg_level2, nsdfg_level3 = _make_strides_propagation_level2_sdfg()
+
+    nsdfg_level2: dace_nodes.NestedSDFG = state.add_nested_sdfg(
+        sdfg=sdfg_level2,
+        parent=sdfg,
+        inputs={"a2", "c2"},
+        outputs={"a2_alias", "b2", "c2"},
+        symbol_mapping={s: s for s in sdfg_level2.free_symbols},
+    )
+
+    for inner_name in nsdfg_level2.in_connectors:
+        outer_name = inner_name[0] + "1"
+        state.add_edge(
+            state.add_access(outer_name),
+            None,
+            nsdfg_level2,
+            inner_name,
+            dace.Memlet(f"{outer_name}[0:10]"),
+        )
+    for inner_name in nsdfg_level2.out_connectors:
+        outer_name = inner_name[0] + "1"
+        state.add_edge(
+            nsdfg_level2,
+            inner_name,
+            state.add_access(outer_name),
+            None,
+            dace.Memlet(f"{outer_name}[0:10]"),
+        )
+
+    sdfg.validate()
+
+    return sdfg, nsdfg_level2, nsdfg_level3
+
+
+def test_strides_propagation_use_symbol_mapping():
+    # Note that the SDFG we are building here is not really meaningful.
+    sdfg_level1, nsdfg_level2, nsdfg_level3 = _make_strides_propagation_level1_sdfg()
+
+    # Tests if all strides are distinct in the beginning and match what we expect.
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            exp_stride = f"{aname}_stride"
+            actual_stride = adesc.strides[0]
+            assert len(adesc.strides) == 1
+            assert (
+                str(actual_stride) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                assert exp_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[exp_stride]) == exp_stride
+
+    # Now we propagate `a` and `b`, but not `c`.
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "a1", ignore_symbol_mapping=False)
+    sdfg_level1.validate()
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "b1", ignore_symbol_mapping=False)
+    sdfg_level1.validate()
+
+    # Because `ignore_symbol_mapping=False` the strides of the data descriptor should
+    #  not have changed. But the `symbol_mapping` has been updated for `a` and `b`.
+    #  However, the symbols will only point one level above.
+    for level, sdfg in enumerate([sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg], start=1):
+        for aname, adesc in sdfg.arrays.items():
+            nsdfg = sdfg.parent_nsdfg_node
+            original_stride = f"{aname}_stride"
+
+            if aname.startswith("c"):
+                target_symbol = f"{aname}_stride"
+            else:
+                target_symbol = f"{aname[0]}{level - 1}_stride"
+
+            if nsdfg is not None:
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == target_symbol
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == original_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+    # Now we also propagate `c` thus now all data descriptors have the same stride
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "c1", ignore_symbol_mapping=False)
+    sdfg_level1.validate()
+    for level, sdfg in enumerate([sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg], start=1):
+        for aname, adesc in sdfg.arrays.items():
+            nsdfg = sdfg.parent_nsdfg_node
+            original_stride = f"{aname}_stride"
+            target_symbol = f"{aname[0]}{level-1}_stride"
+            if nsdfg is not None:
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == target_symbol
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == original_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+
+def test_strides_propagation_ignore_symbol_mapping():
+    # Note that the SDFG we are building here is not really meaningful.
+    sdfg_level1, nsdfg_level2, nsdfg_level3 = _make_strides_propagation_level1_sdfg()
+
+    # Tests if all strides are distinct in the beginning and match what we expect.
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            exp_stride = f"{aname}_stride"
+            actual_stride = adesc.strides[0]
+            assert len(adesc.strides) == 1
+            assert (
+                str(actual_stride) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                assert exp_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[exp_stride]) == exp_stride
+
+    # Now we propagate `a` and `b`, but not `c`.
+    # TODO(phimuell): Create a version where we can set `ignore_symbol_mapping=False`.
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "a1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "b1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+
+    # After the propagation `a` and `b` should use the same stride (the one that
+    #  it has on level 1, but `c` should still be level depending.
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            original_stride = f"{aname}_stride"
+            if aname.startswith("c"):
+                exp_stride = f"{aname}_stride"
+            else:
+                exp_stride = f"{aname[0]}1_stride"
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == original_stride
+
+    # Now we also propagate `c` thus now all data descriptors have the same stride
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "c1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+    for sdfg in [sdfg_level1, nsdfg_level2.sdfg, nsdfg_level3.sdfg]:
+        for aname, adesc in sdfg.arrays.items():
+            exp_stride = f"{aname[0]}1_stride"
+            original_stride = f"{aname}_stride"
+            assert len(adesc.strides) == 1
+            assert (
+                str(adesc.strides[0]) == exp_stride
+            ), f"Expected that '{aname}' has strides '{exp_stride}', but found '{adesc.strides}'."
+
+            nsdfg = sdfg.parent_nsdfg_node
+            if nsdfg is not None:
+                # The symbol mapping must should not be updated.
+                assert original_stride in nsdfg.symbol_mapping
+                assert str(nsdfg.symbol_mapping[original_stride]) == original_stride
+
+
+def _make_strides_propagation_dependent_symbol_nsdfg() -> dace.SDFG:
+    sdfg = dace.SDFG(util.unique_name("strides_propagation_dependent_symbol_nsdfg"))
+    state = sdfg.add_state(is_start_block=True)
+
+    array_names = ["a2", "b2"]
+    for name in array_names:
+        stride_sym = dace.symbol(f"{name}_stride", dtype=dace.uint64)
+        sdfg.add_symbol(stride_sym.name, stride_sym.dtype)
+        sdfg.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            strides=(stride_sym,),
+            transient=False,
+        )
+
+    state.add_mapped_tasklet(
+        "nested_comp",
+        map_ranges={"__i0": "0:10"},
+        inputs={"__in1": dace.Memlet("a2[__i0]")},
+        code="__out = __in1 + 10.",
+        outputs={"__out": dace.Memlet("b2[__i0]")},
+        external_edges=True,
+    )
+    sdfg.validate()
+    return sdfg
+
+
+def _make_strides_propagation_dependent_symbol_sdfg() -> tuple[dace.SDFG, dace_nodes.NestedSDFG]:
+    sdfg_level1 = dace.SDFG(util.unique_name("strides_propagation_dependent_symbol_sdfg"))
+    state = sdfg_level1.add_state(is_start_block=True)
+
+    array_names = ["a1", "b1"]
+    for name in array_names:
+        stride_sym1 = dace.symbol(f"{name}_1stride", dtype=dace.uint64)
+        stride_sym2 = dace.symbol(f"{name}_2stride", dtype=dace.int64)
+        sdfg_level1.add_symbol(stride_sym1.name, stride_sym1.dtype)
+        sdfg_level1.add_symbol(stride_sym2.name, stride_sym2.dtype)
+        stride_sym = stride_sym1 * stride_sym2
+        sdfg_level1.add_array(
+            name,
+            shape=(10,),
+            dtype=dace.float64,
+            strides=(stride_sym,),
+            transient=False,
+        )
+
+    sdfg_level2 = _make_strides_propagation_dependent_symbol_nsdfg()
+
+    for sym, sym_dtype in sdfg_level2.symbols.items():
+        sdfg_level1.add_symbol(sym, sym_dtype)
+
+    nsdfg = state.add_nested_sdfg(
+        sdfg=sdfg_level2,
+        parent=sdfg_level1,
+        inputs={"a2"},
+        outputs={"b2"},
+        symbol_mapping={s: s for s in sdfg_level2.symbols},
+    )
+
+    state.add_edge(state.add_access("a1"), None, nsdfg, "a2", dace.Memlet("a1[0:10]"))
+    state.add_edge(nsdfg, "b2", state.add_access("b1"), None, dace.Memlet("b1[0:10]"))
+    sdfg_level1.validate()
+
+    return sdfg_level1, nsdfg
+
+
+def test_strides_propagation_dependent_symbol():
+    sdfg_level1, nsdfg_level2 = _make_strides_propagation_dependent_symbol_sdfg()
+    sym1_dtype = dace.uint64
+    sym2_dtype = dace.int64
+
+    # Ensure that the special symbols are not already present inside the nested SDFG.
+    for aname, adesc in sdfg_level1.arrays.items():
+        sym1 = f"{aname}_1stride"
+        sym2 = f"{aname}_2stride"
+        for sym, dtype in [(sym1, sym1_dtype), (sym2, sym2_dtype)]:
+            assert sym in {fs.name for fs in adesc.strides[0].free_symbols}
+            assert sym not in nsdfg_level2.symbol_mapping
+            assert sym not in nsdfg_level2.sdfg.symbols
+            assert sym in sdfg_level1.symbols
+            assert sdfg_level1.symbols[sym] == dtype
+
+    # Now propagate `a1` and `b1`.
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "a1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+    gtx_transformations.gt_propagate_strides_of(sdfg_level1, "b1", ignore_symbol_mapping=True)
+    sdfg_level1.validate()
+
+    # Now we check if the update has worked.
+    for aname, adesc in sdfg_level1.arrays.items():
+        sym1 = f"{aname}_1stride"
+        sym2 = f"{aname}_2stride"
+        adesc2 = nsdfg_level2.sdfg.arrays[aname.replace("1", "2")]
+        assert adesc2.strides == adesc.strides
+
+        for sym, dtype in [(sym1, sym1_dtype), (sym2, sym2_dtype)]:
+            assert sym in nsdfg_level2.symbol_mapping
+            assert nsdfg_level2.symbol_mapping[sym].name == sym
+            assert sym in sdfg_level1.symbols
+            assert sdfg_level1.symbols[sym] == dtype
+            assert sym in nsdfg_level2.sdfg.symbols
+            assert nsdfg_level2.sdfg.symbols[sym] == dtype
+
+
+def _make_strides_propagation_shared_symbols_nsdfg() -> dace.SDFG:
+    sdfg = dace.SDFG(util.unique_name("strides_propagation_shared_symbols_nsdfg"))
+    state = sdfg.add_state(is_start_block=True)
+
+    # NOTE: Both arrays have the same symbols used for strides.
+    array_names = ["a2", "b2"]
+    stride_sym0 = dace.symbol(f"__stride_0", dtype=dace.uint64)
+    stride_sym1 = dace.symbol(f"__stride_1", dtype=dace.uint64)
+    sdfg.add_symbol(stride_sym0.name, stride_sym0.dtype)
+    sdfg.add_symbol(stride_sym1.name, stride_sym1.dtype)
+    for name in array_names:
+        sdfg.add_array(
+            name,
+            shape=(10, 10),
+            dtype=dace.float64,
+            strides=(stride_sym0, stride_sym1),
+            transient=False,
+        )
+
+    state.add_mapped_tasklet(
+        "nested_comp",
+        map_ranges={
+            "__i0": "0:10",
+            "__i1": "0:10",
+        },
+        inputs={"__in1": dace.Memlet("a2[__i0, __i1]")},
+        code="__out = __in1 + 10.",
+        outputs={"__out": dace.Memlet("b2[__i0, __i1]")},
+        external_edges=True,
+    )
+    sdfg.validate()
+    return sdfg
+
+
+def _make_strides_propagation_shared_symbols_sdfg() -> tuple[dace.SDFG, dace_nodes.NestedSDFG]:
+    sdfg_level1 = dace.SDFG(util.unique_name("strides_propagation_shared_symbols_sdfg"))
+    state = sdfg_level1.add_state(is_start_block=True)
+
+    # NOTE: Both arrays use the same symbols as strides.
+    #   Furthermore, they are the same as in the nested SDFG, i.e. they are shared.
+    array_names = ["a1", "b1"]
+    stride_sym0 = dace.symbol(f"__stride_0", dtype=dace.uint64)
+    stride_sym1 = dace.symbol(f"__stride_1", dtype=dace.uint64)
+    sdfg_level1.add_symbol(stride_sym0.name, stride_sym0.dtype)
+    sdfg_level1.add_symbol(stride_sym1.name, stride_sym1.dtype)
+    for name in array_names:
+        sdfg_level1.add_array(
+            name,
+            shape=(10, 10),
+            dtype=dace.float64,
+            strides=(
+                stride_sym0,
+                stride_sym1,
+            ),
+            transient=False,
+        )
+
+    sdfg_level2 = _make_strides_propagation_shared_symbols_nsdfg()
+    nsdfg = state.add_nested_sdfg(
+        sdfg=sdfg_level2,
+        parent=sdfg_level1,
+        inputs={"a2"},
+        outputs={"b2"},
+        symbol_mapping={s: s for s in sdfg_level2.symbols},
+    )
+
+    state.add_edge(state.add_access("a1"), None, nsdfg, "a2", dace.Memlet("a1[0:10, 0:10]"))
+    state.add_edge(nsdfg, "b2", state.add_access("b1"), None, dace.Memlet("b1[0:10, 0:10]"))
+    sdfg_level1.validate()
+
+    return sdfg_level1, nsdfg
+
+
+def test_strides_propagation_shared_symbols_sdfg():
+    """Tests what happens if symbols are (unintentionally) shred between descriptor.
+
+    This test looks rather artificial, but it is actually quite likely. Because
+    transients will most likely have the same shape and if the strides are not
+    set explicitly, which is the case, the strides will also be related to their
+    shape. This test explores the situation, where we can, for whatever reason,
+    only propagate the strides of one such data descriptor.
+
+    Note:
+        If `ignore_symbol_mapping` is `False` then this test will fail.
+        This is because the `symbol_mapping` of the NestedSDFG will act on the
+        whole SDFG. Thus it will not only change the strides of `b` but as an
+        unintended side effect also the strides of `a`.
+    """
+
+    def ref(a1, b1):
+        for i in range(10):
+            for j in range(10):
+                b1[i, j] = a1[i, j] + 10.0
+
+    sdfg_level1, nsdfg_level2 = _make_strides_propagation_shared_symbols_sdfg()
+
+    res_args = {
+        "a1": np.array(np.random.rand(10, 10), order="C", dtype=np.float64, copy=True),
+        "b1": np.array(np.random.rand(10, 10), order="F", dtype=np.float64, copy=True),
+    }
+    ref_args = copy.deepcopy(res_args)
+
+    # Now we change the strides of `b1`, and then we propagate the new strides
+    #  into the nested SDFG. We want to keep (for whatever reasons) strides of `a1`.
+    stride_b1_sym0 = dace.symbol(f"__b1_stride_0", dtype=dace.uint64)
+    stride_b1_sym1 = dace.symbol(f"__b1_stride_1", dtype=dace.uint64)
+    sdfg_level1.add_symbol(stride_b1_sym0.name, stride_b1_sym0.dtype)
+    sdfg_level1.add_symbol(stride_b1_sym1.name, stride_b1_sym1.dtype)
+
+    desc_b1 = sdfg_level1.arrays["b1"]
+    desc_b1.set_shape((10, 10), (stride_b1_sym0, stride_b1_sym1))
+
+    # Now we propagate the data into it.
+    gtx_transformations.gt_propagate_strides_of(
+        sdfg=sdfg_level1,
+        data_name="b1",
+    )
+
+    # Now we have to prepare the call arguments, i.e. adding the strides
+    itemsize = res_args["b1"].itemsize
+    res_args.update(
+        {
+            "__b1_stride_0": res_args["b1"].strides[0] // itemsize,
+            "__b1_stride_1": res_args["b1"].strides[1] // itemsize,
+            "__stride_0": res_args["a1"].strides[0] // itemsize,
+            "__stride_1": res_args["a1"].strides[1] // itemsize,
+        }
+    )
+    ref(**ref_args)
+    sdfg_level1(**res_args)
+    assert np.allclose(ref_args["b1"], res_args["b1"])