[spirv-reader] Handle pointer to vector component

Add a new transform that replaces all uses of
pointers-to-vector-components with `load_vector_element` and
`store_vector_element` instructions.

Allow the parser to generate these pointers by enabling the IR
validation capability. Enable two tests that cover this.

Add new tests for the full SPIR-V to core IR flow.

Bug: tint:1907
Change-Id: Ic015cc855b7abb535d23376f88eb4c9f3ba5f086
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/170002
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/api/BUILD.bazel b/src/tint/api/BUILD.bazel
index 32a7773..9f218bd 100644
--- a/src/tint/api/BUILD.bazel
+++ b/src/tint/api/BUILD.bazel
@@ -52,7 +52,6 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -93,6 +92,7 @@
   }) + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader",
+      "//src/tint/lang/spirv/reader/common",
     ],
     "//conditions:default": [],
   }) + select({
diff --git a/src/tint/api/BUILD.cmake b/src/tint/api/BUILD.cmake
index af664e6..9551324 100644
--- a/src/tint/api/BUILD.cmake
+++ b/src/tint/api/BUILD.cmake
@@ -54,7 +54,6 @@
   tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -100,6 +99,7 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_api lib
     tint_lang_spirv_reader
+    tint_lang_spirv_reader_common
   )
 endif(TINT_BUILD_SPV_READER)
 
diff --git a/src/tint/api/BUILD.gn b/src/tint/api/BUILD.gn
index ff3f483..80382c0 100644
--- a/src/tint/api/BUILD.gn
+++ b/src/tint/api/BUILD.gn
@@ -51,7 +51,6 @@
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
-    "${tint_src_dir}/lang/spirv/reader/common",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
@@ -93,7 +92,10 @@
   }
 
   if (tint_build_spv_reader) {
-    deps += [ "${tint_src_dir}/lang/spirv/reader" ]
+    deps += [
+      "${tint_src_dir}/lang/spirv/reader",
+      "${tint_src_dir}/lang/spirv/reader/common",
+    ]
   }
 
   if (tint_build_spv_writer) {
diff --git a/src/tint/cmd/bench/BUILD.bazel b/src/tint/cmd/bench/BUILD.bazel
index a273bc9..743f284 100644
--- a/src/tint/cmd/bench/BUILD.bazel
+++ b/src/tint/cmd/bench/BUILD.bazel
@@ -49,7 +49,6 @@
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -74,6 +73,7 @@
   ] + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader",
+      "//src/tint/lang/spirv/reader/common",
     ],
     "//conditions:default": [],
   }) + select({
diff --git a/src/tint/cmd/bench/BUILD.cmake b/src/tint/cmd/bench/BUILD.cmake
index ee4b122..cdc5971 100644
--- a/src/tint/cmd/bench/BUILD.cmake
+++ b/src/tint/cmd/bench/BUILD.cmake
@@ -126,7 +126,6 @@
   tint_lang_core_constant
   tint_lang_core_ir
   tint_lang_core_type
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -156,6 +155,7 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_cmd_bench_bench bench
     tint_lang_spirv_reader
+    tint_lang_spirv_reader_common
   )
 endif(TINT_BUILD_SPV_READER)
 
diff --git a/src/tint/cmd/bench/BUILD.gn b/src/tint/cmd/bench/BUILD.gn
index 7eb287d..38bdc6a 100644
--- a/src/tint/cmd/bench/BUILD.gn
+++ b/src/tint/cmd/bench/BUILD.gn
@@ -54,7 +54,6 @@
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
-      "${tint_src_dir}/lang/spirv/reader/common",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
@@ -78,7 +77,10 @@
     ]
 
     if (tint_build_spv_reader) {
-      deps += [ "${tint_src_dir}/lang/spirv/reader" ]
+      deps += [
+        "${tint_src_dir}/lang/spirv/reader",
+        "${tint_src_dir}/lang/spirv/reader/common",
+      ]
     }
 
     if (tint_build_wgsl_reader) {
diff --git a/src/tint/cmd/common/BUILD.bazel b/src/tint/cmd/common/BUILD.bazel
index c768f73..affb277 100644
--- a/src/tint/cmd/common/BUILD.bazel
+++ b/src/tint/cmd/common/BUILD.bazel
@@ -53,7 +53,6 @@
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -78,6 +77,7 @@
   ] + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader",
+      "//src/tint/lang/spirv/reader/common",
     ],
     "//conditions:default": [],
   }) + select({
diff --git a/src/tint/cmd/common/BUILD.cmake b/src/tint/cmd/common/BUILD.cmake
index 43d472f..2c0d823 100644
--- a/src/tint/cmd/common/BUILD.cmake
+++ b/src/tint/cmd/common/BUILD.cmake
@@ -52,7 +52,6 @@
   tint_lang_core_constant
   tint_lang_core_ir
   tint_lang_core_type
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -79,6 +78,7 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_cmd_common lib
     tint_lang_spirv_reader
+    tint_lang_spirv_reader_common
   )
 endif(TINT_BUILD_SPV_READER)
 
diff --git a/src/tint/cmd/common/BUILD.gn b/src/tint/cmd/common/BUILD.gn
index 4b1f5e3..6026bf5 100644
--- a/src/tint/cmd/common/BUILD.gn
+++ b/src/tint/cmd/common/BUILD.gn
@@ -56,7 +56,6 @@
     "${tint_src_dir}/lang/core/constant",
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
-    "${tint_src_dir}/lang/spirv/reader/common",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
@@ -81,7 +80,10 @@
   ]
 
   if (tint_build_spv_reader) {
-    deps += [ "${tint_src_dir}/lang/spirv/reader" ]
+    deps += [
+      "${tint_src_dir}/lang/spirv/reader",
+      "${tint_src_dir}/lang/spirv/reader/common",
+    ]
   }
 
   if (tint_build_spv_reader || tint_build_spv_writer) {
diff --git a/src/tint/cmd/info/BUILD.bazel b/src/tint/cmd/info/BUILD.bazel
index c19a5da..6d0b8f9 100644
--- a/src/tint/cmd/info/BUILD.bazel
+++ b/src/tint/cmd/info/BUILD.bazel
@@ -47,7 +47,6 @@
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -70,6 +69,11 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_spv_reader_or_tint_build_spv_writer": [
       "@spirv_tools",
     ],
diff --git a/src/tint/cmd/info/BUILD.cmake b/src/tint/cmd/info/BUILD.cmake
index 74d28e0..dda56b9 100644
--- a/src/tint/cmd/info/BUILD.cmake
+++ b/src/tint/cmd/info/BUILD.cmake
@@ -48,7 +48,6 @@
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -72,6 +71,12 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_cmd_info_cmd cmd
+    tint_lang_spirv_reader_common
+  )
+endif(TINT_BUILD_SPV_READER)
+
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_cmd_info_cmd cmd
     "spirv-tools"
diff --git a/src/tint/cmd/info/BUILD.gn b/src/tint/cmd/info/BUILD.gn
index 6f3a945..5f06fcd 100644
--- a/src/tint/cmd/info/BUILD.gn
+++ b/src/tint/cmd/info/BUILD.gn
@@ -47,7 +47,6 @@
     "${tint_src_dir}/lang/core",
     "${tint_src_dir}/lang/core/constant",
     "${tint_src_dir}/lang/core/type",
-    "${tint_src_dir}/lang/spirv/reader/common",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
@@ -71,6 +70,10 @@
     "${tint_src_dir}/utils/traits",
   ]
 
+  if (tint_build_spv_reader) {
+    deps += [ "${tint_src_dir}/lang/spirv/reader/common" ]
+  }
+
   if (tint_build_spv_reader || tint_build_spv_writer) {
     deps += [
       "${tint_spirv_tools_dir}:spvtools_headers",
diff --git a/src/tint/cmd/loopy/BUILD.bazel b/src/tint/cmd/loopy/BUILD.bazel
index e0b4ebe..1b0859f 100644
--- a/src/tint/cmd/loopy/BUILD.bazel
+++ b/src/tint/cmd/loopy/BUILD.bazel
@@ -51,7 +51,6 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -95,6 +94,7 @@
   }) + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader",
+      "//src/tint/lang/spirv/reader/common",
     ],
     "//conditions:default": [],
   }) + select({
diff --git a/src/tint/cmd/loopy/BUILD.cmake b/src/tint/cmd/loopy/BUILD.cmake
index abf2e07..2597a47 100644
--- a/src/tint/cmd/loopy/BUILD.cmake
+++ b/src/tint/cmd/loopy/BUILD.cmake
@@ -52,7 +52,6 @@
   tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -101,6 +100,7 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_cmd_loopy_cmd cmd
     tint_lang_spirv_reader
+    tint_lang_spirv_reader_common
   )
 endif(TINT_BUILD_SPV_READER)
 
diff --git a/src/tint/cmd/loopy/BUILD.gn b/src/tint/cmd/loopy/BUILD.gn
index 3241429..5176168 100644
--- a/src/tint/cmd/loopy/BUILD.gn
+++ b/src/tint/cmd/loopy/BUILD.gn
@@ -51,7 +51,6 @@
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
-    "${tint_src_dir}/lang/spirv/reader/common",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
@@ -96,7 +95,10 @@
   }
 
   if (tint_build_spv_reader) {
-    deps += [ "${tint_src_dir}/lang/spirv/reader" ]
+    deps += [
+      "${tint_src_dir}/lang/spirv/reader",
+      "${tint_src_dir}/lang/spirv/reader/common",
+    ]
   }
 
   if (tint_build_spv_writer) {
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index a44e992..01d6cf6 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -52,6 +52,7 @@
     "//src/tint/lang/core:test",
     "//src/tint/lang/msl/ir:test",
     "//src/tint/lang/spirv/ir:test",
+    "//src/tint/lang/spirv/reader/lower:test",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/helpers:test",
     "//src/tint/lang/wgsl/program:test",
@@ -122,7 +123,9 @@
     "//conditions:default": [],
   }) + select({
     ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/common:test",
       "//src/tint/lang/spirv/reader/parser:test",
+      "//src/tint/lang/spirv/reader:test",
     ],
     "//conditions:default": [],
   }) + select({
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index e79c221..d81fdcb 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -53,6 +53,7 @@
   tint_lang_core_test
   tint_lang_msl_ir_test
   tint_lang_spirv_ir_test
+  tint_lang_spirv_reader_lower_test
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program_test
@@ -135,7 +136,9 @@
 
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_spirv_reader_common_test
     tint_lang_spirv_reader_parser_test
+    tint_lang_spirv_reader_test
   )
 endif(TINT_BUILD_SPV_READER)
 
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index 55c146b..abb1fd2 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -58,6 +58,7 @@
       "${tint_src_dir}/lang/core/type:unittests",
       "${tint_src_dir}/lang/msl/ir:unittests",
       "${tint_src_dir}/lang/spirv/ir:unittests",
+      "${tint_src_dir}/lang/spirv/reader/lower:unittests",
       "${tint_src_dir}/lang/wgsl:unittests",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
       "${tint_src_dir}/lang/wgsl/helpers:unittests",
@@ -128,7 +129,11 @@
     }
 
     if (tint_build_spv_reader) {
-      deps += [ "${tint_src_dir}/lang/spirv/reader/parser:unittests" ]
+      deps += [
+        "${tint_src_dir}/lang/spirv/reader:unittests",
+        "${tint_src_dir}/lang/spirv/reader/common:unittests",
+        "${tint_src_dir}/lang/spirv/reader/parser:unittests",
+      ]
     }
 
     if (tint_build_spv_reader && tint_build_wgsl_reader &&
diff --git a/src/tint/cmd/tint/BUILD.bazel b/src/tint/cmd/tint/BUILD.bazel
index 934ff47..fbc1286 100644
--- a/src/tint/cmd/tint/BUILD.bazel
+++ b/src/tint/cmd/tint/BUILD.bazel
@@ -51,7 +51,6 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/hlsl/writer/common",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
@@ -104,6 +103,11 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_spv_reader_or_tint_build_spv_writer": [
       "@spirv_tools",
     ],
diff --git a/src/tint/cmd/tint/BUILD.cmake b/src/tint/cmd/tint/BUILD.cmake
index 3924e7d..9a02dfe 100644
--- a/src/tint/cmd/tint/BUILD.cmake
+++ b/src/tint/cmd/tint/BUILD.cmake
@@ -52,7 +52,6 @@
   tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
@@ -110,6 +109,12 @@
   )
 endif(TINT_BUILD_MSL_WRITER)
 
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_cmd_tint_cmd cmd
+    tint_lang_spirv_reader_common
+  )
+endif(TINT_BUILD_SPV_READER)
+
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_cmd_tint_cmd cmd
     "spirv-tools"
diff --git a/src/tint/cmd/tint/BUILD.gn b/src/tint/cmd/tint/BUILD.gn
index b9fca30..5d0447f 100644
--- a/src/tint/cmd/tint/BUILD.gn
+++ b/src/tint/cmd/tint/BUILD.gn
@@ -51,7 +51,6 @@
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/hlsl/writer/common",
-    "${tint_src_dir}/lang/spirv/reader/common",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/ast/transform",
@@ -107,6 +106,10 @@
     ]
   }
 
+  if (tint_build_spv_reader) {
+    deps += [ "${tint_src_dir}/lang/spirv/reader/common" ]
+  }
+
   if (tint_build_spv_reader || tint_build_spv_writer) {
     deps += [
       "${tint_spirv_tools_dir}:spvtools_headers",
diff --git a/src/tint/lang/spirv/reader/BUILD.bazel b/src/tint/lang/spirv/reader/BUILD.bazel
index f7662cf..3f50893 100644
--- a/src/tint/lang/spirv/reader/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/BUILD.bazel
@@ -50,7 +50,7 @@
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/spirv/reader/lower",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -73,6 +73,7 @@
   ] + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader/ast_parser",
+      "//src/tint/lang/spirv/reader/common",
       "//src/tint/lang/spirv/reader/parser",
     ],
     "//conditions:default": [],
@@ -80,9 +81,70 @@
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "reader_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader",
+      "//src/tint/lang/spirv/reader/common",
+      "//src/tint/lang/spirv/reader/common:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
 
 alias(
   name = "tint_build_spv_reader",
   actual = "//src/tint:tint_build_spv_reader_true",
 )
 
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/reader/BUILD.cmake b/src/tint/lang/spirv/reader/BUILD.cmake
index 6e0d5ac..6b1a73c 100644
--- a/src/tint/lang/spirv/reader/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/BUILD.cmake
@@ -37,6 +37,7 @@
 include(lang/spirv/reader/ast_lower/BUILD.cmake)
 include(lang/spirv/reader/ast_parser/BUILD.cmake)
 include(lang/spirv/reader/common/BUILD.cmake)
+include(lang/spirv/reader/lower/BUILD.cmake)
 include(lang/spirv/reader/parser/BUILD.cmake)
 
 if(TINT_BUILD_SPV_READER)
@@ -56,7 +57,7 @@
   tint_lang_core_constant
   tint_lang_core_ir
   tint_lang_core_type
-  tint_lang_spirv_reader_common
+  tint_lang_spirv_reader_lower
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -81,8 +82,65 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_lang_spirv_reader lib
     tint_lang_spirv_reader_ast_parser
+    tint_lang_spirv_reader_common
     tint_lang_spirv_reader_parser
   )
 endif(TINT_BUILD_SPV_READER)
 
+endif(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_SPV_READER)
+################################################################################
+# Target:    tint_lang_spirv_reader_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_READER
+################################################################################
+tint_add_target(tint_lang_spirv_reader_test test
+  lang/spirv/reader/reader_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_common
+  tint_lang_wgsl_features
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_reader_test test
+  "gtest"
+)
+
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_test test
+    tint_lang_spirv_reader
+    tint_lang_spirv_reader_common
+    tint_lang_spirv_reader_common_test
+  )
+endif(TINT_BUILD_SPV_READER)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_external_dependencies(tint_lang_spirv_reader_test test
+    "spirv-tools"
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
 endif(TINT_BUILD_SPV_READER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/reader/BUILD.gn b/src/tint/lang/spirv/reader/BUILD.gn
index 82b4314..09788fc 100644
--- a/src/tint/lang/spirv/reader/BUILD.gn
+++ b/src/tint/lang/spirv/reader/BUILD.gn
@@ -37,6 +37,10 @@
 import("../../../../../scripts/tint_overrides_with_defaults.gni")
 
 import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
 if (tint_build_spv_reader) {
   libtint_source_set("reader") {
     sources = [
@@ -49,7 +53,7 @@
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
-      "${tint_src_dir}/lang/spirv/reader/common",
+      "${tint_src_dir}/lang/spirv/reader/lower",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
@@ -74,8 +78,58 @@
     if (tint_build_spv_reader) {
       deps += [
         "${tint_src_dir}/lang/spirv/reader/ast_parser",
+        "${tint_src_dir}/lang/spirv/reader/common",
         "${tint_src_dir}/lang/spirv/reader/parser",
       ]
     }
   }
 }
+if (tint_build_unittests) {
+  if (tint_build_spv_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [ "reader_test.cc" ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/ir",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/sem",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/id",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/reflection",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/symbol",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_spv_reader) {
+        deps += [
+          "${tint_src_dir}/lang/spirv/reader",
+          "${tint_src_dir}/lang/spirv/reader/common",
+          "${tint_src_dir}/lang/spirv/reader/common:unittests",
+        ]
+      }
+
+      if (tint_build_spv_reader || tint_build_spv_writer) {
+        deps += [
+          "${tint_spirv_tools_dir}:spvtools_headers",
+          "${tint_spirv_tools_dir}:spvtools_val",
+        ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
index 421ec97..c656285 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
@@ -67,7 +67,6 @@
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
@@ -92,6 +91,7 @@
   ] + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader/ast_lower",
+      "//src/tint/lang/spirv/reader/common",
     ],
     "//conditions:default": [],
   }) + select({
@@ -148,7 +148,6 @@
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
@@ -173,6 +172,7 @@
   ] + select({
     ":tint_build_spv_reader": [
       "//src/tint/lang/spirv/reader/ast_parser",
+      "//src/tint/lang/spirv/reader/common",
     ],
     "//conditions:default": [],
   }) + select({
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
index cf9191f..e4ab32d 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
@@ -68,7 +68,6 @@
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
@@ -95,6 +94,7 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_lang_spirv_reader_ast_parser lib
     tint_lang_spirv_reader_ast_lower
+    tint_lang_spirv_reader_common
   )
 endif(TINT_BUILD_SPV_READER)
 
@@ -154,7 +154,6 @@
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
@@ -184,6 +183,7 @@
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_lang_spirv_reader_ast_parser_test test
     tint_lang_spirv_reader_ast_parser
+    tint_lang_spirv_reader_common
   )
 endif(TINT_BUILD_SPV_READER)
 
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
index f82d2fb..5c8e0c1 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
@@ -70,7 +70,6 @@
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/type",
-      "${tint_src_dir}/lang/spirv/reader/common",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
@@ -95,7 +94,10 @@
     ]
 
     if (tint_build_spv_reader) {
-      deps += [ "${tint_src_dir}/lang/spirv/reader/ast_lower" ]
+      deps += [
+        "${tint_src_dir}/lang/spirv/reader/ast_lower",
+        "${tint_src_dir}/lang/spirv/reader/common",
+      ]
     }
 
     if (tint_build_spv_reader || tint_build_spv_writer) {
@@ -154,7 +156,6 @@
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/type",
-        "${tint_src_dir}/lang/spirv/reader/common",
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/common",
@@ -178,7 +179,10 @@
       ]
 
       if (tint_build_spv_reader) {
-        deps += [ "${tint_src_dir}/lang/spirv/reader/ast_parser" ]
+        deps += [
+          "${tint_src_dir}/lang/spirv/reader/ast_parser",
+          "${tint_src_dir}/lang/spirv/reader/common",
+        ]
       }
 
       if (tint_build_spv_reader || tint_build_spv_writer) {
diff --git a/src/tint/lang/spirv/reader/common/BUILD.bazel b/src/tint/lang/spirv/reader/common/BUILD.bazel
index b74c742..438bddd 100644
--- a/src/tint/lang/spirv/reader/common/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/common/BUILD.bazel
@@ -60,4 +60,48 @@
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "helper_test.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
 
diff --git a/src/tint/lang/spirv/reader/common/BUILD.cfg b/src/tint/lang/spirv/reader/common/BUILD.cfg
new file mode 100644
index 0000000..a460fd5
--- /dev/null
+++ b/src/tint/lang/spirv/reader/common/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_spv_reader"
+}
diff --git a/src/tint/lang/spirv/reader/common/BUILD.cmake b/src/tint/lang/spirv/reader/common/BUILD.cmake
index 23c0ff1..f0206ba 100644
--- a/src/tint/lang/spirv/reader/common/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/common/BUILD.cmake
@@ -34,9 +34,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
+if(TINT_BUILD_SPV_READER)
 ################################################################################
 # Target:    tint_lang_spirv_reader_common
 # Kind:      lib
+# Condition: TINT_BUILD_SPV_READER
 ################################################################################
 tint_add_target(tint_lang_spirv_reader_common lib
   lang/spirv/reader/common/common.cc
@@ -56,3 +58,35 @@
   tint_utils_rtti
   tint_utils_traits
 )
+
+endif(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_SPV_READER)
+################################################################################
+# Target:    tint_lang_spirv_reader_common_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_READER
+################################################################################
+tint_add_target(tint_lang_spirv_reader_common_test test
+  lang/spirv/reader/common/helper_test.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_common_test test
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_external_dependencies(tint_lang_spirv_reader_common_test test
+    "spirv-tools"
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_READER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/reader/common/BUILD.gn b/src/tint/lang/spirv/reader/common/BUILD.gn
index 9e75c83..8383ffe 100644
--- a/src/tint/lang/spirv/reader/common/BUILD.gn
+++ b/src/tint/lang/spirv/reader/common/BUILD.gn
@@ -38,22 +38,53 @@
 
 import("${tint_src_dir}/tint.gni")
 
-libtint_source_set("common") {
-  sources = [
-    "common.cc",
-    "options.h",
-  ]
-  deps = [
-    "${tint_src_dir}/lang/wgsl",
-    "${tint_src_dir}/lang/wgsl/common",
-    "${tint_src_dir}/lang/wgsl/features",
-    "${tint_src_dir}/utils/containers",
-    "${tint_src_dir}/utils/ice",
-    "${tint_src_dir}/utils/macros",
-    "${tint_src_dir}/utils/math",
-    "${tint_src_dir}/utils/memory",
-    "${tint_src_dir}/utils/reflection",
-    "${tint_src_dir}/utils/rtti",
-    "${tint_src_dir}/utils/traits",
-  ]
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_spv_reader) {
+  libtint_source_set("common") {
+    sources = [
+      "common.cc",
+      "options.h",
+    ]
+    deps = [
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/traits",
+    ]
+  }
+}
+if (tint_build_unittests) {
+  if (tint_build_spv_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [ "helper_test.h" ]
+      deps = [
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_spv_reader || tint_build_spv_writer) {
+        deps += [
+          "${tint_spirv_tools_dir}:spvtools_headers",
+          "${tint_spirv_tools_dir}:spvtools_val",
+        ]
+      }
+    }
+  }
 }
diff --git a/src/tint/lang/spirv/reader/common/helper_test.h b/src/tint/lang/spirv/reader/common/helper_test.h
new file mode 100644
index 0000000..6a0ae0e
--- /dev/null
+++ b/src/tint/lang/spirv/reader/common/helper_test.h
@@ -0,0 +1,58 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_READER_COMMON_HELPER_TEST_H_
+#define SRC_TINT_LANG_SPIRV_READER_COMMON_HELPER_TEST_H_
+
+#include <string>
+#include <vector>
+
+#include "spirv-tools/libspirv.hpp"
+#include "src/tint/utils/result/result.h"
+
+namespace tint::spirv::reader {
+
+/// Assemble a textual SPIR-V module into a SPIR-V binary.
+/// @param spirv_asm the textual SPIR-V assembly
+/// @returns the SPIR-V binary data, or an error string
+inline Result<std::vector<uint32_t>, std::string> Assemble(std::string spirv_asm) {
+    StringStream err;
+    std::vector<uint32_t> binary;
+    spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
+    tools.SetMessageConsumer(
+        [&err](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) {
+            err << "SPIR-V assembly failed:" << pos.line << ":" << pos.column << ": " << msg;
+        });
+    if (!tools.Assemble(spirv_asm, &binary, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
+        return err.str();
+    }
+    return binary;
+}
+
+}  // namespace tint::spirv::reader
+
+#endif  // SRC_TINT_LANG_SPIRV_READER_COMMON_HELPER_TEST_H_
diff --git a/src/tint/lang/spirv/reader/lower/BUILD.bazel b/src/tint/lang/spirv/reader/lower/BUILD.bazel
new file mode 100644
index 0000000..01fd572
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/BUILD.bazel
@@ -0,0 +1,106 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "lower",
+  srcs = [
+    "lower.cc",
+    "vector_element_pointer.cc",
+  ],
+  hdrs = [
+    "lower.h",
+    "vector_element_pointer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "vector_element_pointer_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir/transform:test",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/reader/lower",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/spirv/reader/lower/BUILD.cmake b/src/tint/lang/spirv/reader/lower/BUILD.cmake
new file mode 100644
index 0000000..5f0e866
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/BUILD.cmake
@@ -0,0 +1,104 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_lang_spirv_reader_lower
+# Kind:      lib
+################################################################################
+tint_add_target(tint_lang_spirv_reader_lower lib
+  lang/spirv/reader/lower/lower.cc
+  lang/spirv/reader/lower/lower.h
+  lang/spirv/reader/lower/vector_element_pointer.cc
+  lang/spirv/reader/lower/vector_element_pointer.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_lower lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+################################################################################
+# Target:    tint_lang_spirv_reader_lower_test
+# Kind:      test
+################################################################################
+tint_add_target(tint_lang_spirv_reader_lower_test test
+  lang/spirv/reader/lower/vector_element_pointer_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_lower_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_ir
+  tint_lang_core_ir_transform_test
+  tint_lang_core_type
+  tint_lang_spirv_reader_lower
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_reader_lower_test test
+  "gtest"
+)
diff --git a/src/tint/lang/spirv/reader/lower/BUILD.gn b/src/tint/lang/spirv/reader/lower/BUILD.gn
new file mode 100644
index 0000000..b5f0342
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/BUILD.gn
@@ -0,0 +1,102 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+
+libtint_source_set("lower") {
+  sources = [
+    "lower.cc",
+    "lower.h",
+    "vector_element_pointer.cc",
+    "vector_element_pointer.h",
+  ]
+  deps = [
+    "${tint_src_dir}/api/common",
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/intrinsic",
+    "${tint_src_dir}/lang/core/ir",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
+if (tint_build_unittests) {
+  tint_unittests_source_set("unittests") {
+    sources = [ "vector_element_pointer_test.cc" ]
+    deps = [
+      "${tint_src_dir}:gmock_and_gtest",
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/ir/transform:unittests",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/spirv/reader/lower",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/id",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/symbol",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+  }
+}
diff --git a/src/tint/lang/spirv/reader/lower/lower.cc b/src/tint/lang/spirv/reader/lower/lower.cc
new file mode 100644
index 0000000..25f546c
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/lower.cc
@@ -0,0 +1,54 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/lower/lower.h"
+
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/spirv/reader/lower/vector_element_pointer.h"
+
+namespace tint::spirv::reader {
+
+Result<SuccessType> Lower(core::ir::Module& mod) {
+#define RUN_TRANSFORM(name, ...)         \
+    do {                                 \
+        auto result = name(__VA_ARGS__); \
+        if (result != Success) {         \
+            return result;               \
+        }                                \
+    } while (false)
+
+    RUN_TRANSFORM(lower::VectorElementPointer, mod);
+
+    if (auto res = core::ir::ValidateAndDumpIfNeeded(mod, "end of lowering from SPIR-V");
+        res != Success) {
+        return res.Failure();
+    }
+
+    return Success;
+}
+
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/lower/lower.h b/src/tint/lang/spirv/reader/lower/lower.h
new file mode 100644
index 0000000..0d8e3b2
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/lower.h
@@ -0,0 +1,43 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_READER_LOWER_LOWER_H_
+#define SRC_TINT_LANG_SPIRV_READER_LOWER_LOWER_H_
+
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/utils/result/result.h"
+
+namespace tint::spirv::reader {
+
+/// Lower converts a SPIR-V-dialect IR module to a core-dialect IR module
+/// @param  mod the IR module
+/// @return the result of the operation
+Result<SuccessType> Lower(core::ir::Module& mod);
+
+}  // namespace tint::spirv::reader
+
+#endif  // SRC_TINT_LANG_SPIRV_READER_LOWER_LOWER_H_
diff --git a/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc b/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc
new file mode 100644
index 0000000..ad0d3f3
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/vector_element_pointer.cc
@@ -0,0 +1,178 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/lower/vector_element_pointer.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::spirv::reader::lower {
+
+namespace {
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    core::ir::Module& ir;
+
+    /// The IR builder.
+    core::ir::Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// Access is an access instruction and the type of the vector that it produces a pointer to.
+    struct Access {
+        /// The access instruction.
+        core::ir::Access* inst;
+        /// The vector type being accessed.
+        const core::type::Type* type;
+    };
+
+    /// Process the module.
+    void Process() {
+        // Find the access instructions that need to be replaced.
+        Vector<Access, 8> worklist;
+        for (auto* inst : ir.instructions.Objects()) {
+            if (!inst->Alive()) {
+                continue;
+            }
+            if (auto* access = inst->As<core::ir::Access>()) {
+                auto* source_ty = access->Object()->Type();
+                if (!source_ty->Is<core::type::Pointer>()) {
+                    continue;
+                }
+                source_ty = source_ty->UnwrapPtr();
+
+                // Step through the indices of the access instruction to check for vector types.
+                for (auto* idx : access->Indices()) {
+                    if (source_ty->Is<core::type::Vector>()) {
+                        // Found an access that is indexing into a vector pointer.
+                        worklist.Push(Access{access, source_ty});
+                        break;
+                    }
+
+                    // Update the current source type based on the next index.
+                    if (auto* constant = idx->As<core::ir::Constant>()) {
+                        auto i = constant->Value()->ValueAs<u32>();
+                        source_ty = source_ty->Element(i);
+                    } else {
+                        source_ty = source_ty->Elements().type;
+                    }
+                }
+            }
+        }
+
+        // Replace the access instructions that we found.
+        for (const auto& access : worklist) {
+            ReplaceAccess(access);
+        }
+    }
+
+    /// Replace an access instruction with {load,store}_vector_element instructions.
+    /// @param access the access instruction to replace
+    void ReplaceAccess(const Access& access) {
+        auto* object = access.inst->Object();
+
+        if (access.inst->Indices().Length() > 1) {
+            // Create a new access instruction that stops at the vector pointer.
+            Vector<core::ir::Value*, 8> partial_indices{access.inst->Indices()};
+            partial_indices.Pop();
+            auto addrspace = object->Type()->As<core::type::Pointer>()->AddressSpace();
+            auto* access_to_vec = b.Access(ty.ptr(addrspace, access.type), object, partial_indices);
+            access_to_vec->InsertBefore(access.inst);
+
+            object = access_to_vec->Result(0);
+        }
+
+        // Replace all uses of the original access instruction.
+        auto* index = access.inst->Indices().Back();
+        ReplaceAccessUses(access.inst, object, index);
+
+        // Destroy the original access instruction.
+        access.inst->Destroy();
+    }
+
+    /// Replace all uses of an access instruction with {load,store}_vector_element instructions.
+    /// @param access the access instruction to replace
+    /// @param object the pointer-to-vector source object
+    /// @param index the index of the vector element
+    void ReplaceAccessUses(core::ir::Access* access,
+                           core::ir::Value* object,
+                           core::ir::Value* index) {
+        Vector<core::ir::Instruction*, 4> to_destroy;
+        access->Result(0)->ForEachUse([&](core::ir::Usage use) {
+            Switch(
+                use.instruction,
+                [&](core::ir::Load* load) {
+                    auto* lve = b.LoadVectorElement(object, index);
+                    lve->InsertBefore(load);
+                    load->Result(0)->ReplaceAllUsesWith(lve->Result(0));
+                    to_destroy.Push(load);
+                },
+                [&](core::ir::Store* store) {
+                    auto* sve = b.StoreVectorElement(object, index, store->From());
+                    sve->InsertBefore(store);
+                    to_destroy.Push(store);
+                },
+                [&](core::ir::Access* noop_access) {
+                    TINT_ASSERT(noop_access->Indices().IsEmpty());
+                    ReplaceAccessUses(noop_access, object, index);
+                    to_destroy.Push(noop_access);
+                },
+                TINT_ICE_ON_NO_MATCH);
+        });
+
+        // Clean up old instructions.
+        for (auto* inst : to_destroy) {
+            inst->Destroy();
+        }
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> VectorElementPointer(core::ir::Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "VectorElementPointer transform",
+                                          EnumSet<core::ir::Capability>{
+                                              core::ir::Capability::kAllowVectorElementPointer,
+                                          });
+    if (result != Success) {
+        return result.Failure();
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::spirv::reader::lower
diff --git a/src/tint/lang/spirv/reader/lower/vector_element_pointer.h b/src/tint/lang/spirv/reader/lower/vector_element_pointer.h
new file mode 100644
index 0000000..e07a30c
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/vector_element_pointer.h
@@ -0,0 +1,48 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_READER_LOWER_VECTOR_ELEMENT_POINTER_H_
+#define SRC_TINT_LANG_SPIRV_READER_LOWER_VECTOR_ELEMENT_POINTER_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::spirv::reader::lower {
+
+/// VectorElementPointer is a transform that removes pointers to vector elements by replacing access
+/// instructions and their uses.
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> VectorElementPointer(core::ir::Module& module);
+
+}  // namespace tint::spirv::reader::lower
+
+#endif  // SRC_TINT_LANG_SPIRV_READER_LOWER_VECTOR_ELEMENT_POINTER_H_
diff --git a/src/tint/lang/spirv/reader/lower/vector_element_pointer_test.cc b/src/tint/lang/spirv/reader/lower/vector_element_pointer_test.cc
new file mode 100644
index 0000000..e7160a5
--- /dev/null
+++ b/src/tint/lang/spirv/reader/lower/vector_element_pointer_test.cc
@@ -0,0 +1,487 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/lower/vector_element_pointer.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::spirv::reader::lower {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using SpirvReader_VectorElementPointerTest = core::ir::transform::TransformTest;
+
+TEST_F(SpirvReader_VectorElementPointerTest, NonPointerAccess) {
+    auto* vec = b.FunctionParam("vec", ty.vec4<u32>());
+    auto* foo = b.Function("foo", ty.u32());
+    b.Append(foo->Block(), [&] {
+        auto* access = b.Access<u32>(vec, 2_u);
+        b.Return(foo, access);
+    });
+
+    auto* src = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %2:u32 = access %vec, 2u
+    ret %2
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, Access_NoIndices) {
+    auto* foo = b.Function("foo", ty.vec4<u32>());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        auto* access = b.Access<ptr<function, vec4<u32>>>(vec);
+        b.Return(foo, b.Load(access));
+    });
+
+    auto* src = R"(
+%foo = func():vec4<u32> -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, vec4<u32>, read_write> = access %vec
+    %4:vec4<u32> = load %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, Access_NoIndices_Chain) {
+    auto* foo = b.Function("foo", ty.vec4<u32>());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        auto* access_1 = b.Access<ptr<function, vec4<u32>>>(vec);
+        auto* access_2 = b.Access<ptr<function, vec4<u32>>>(access_1);
+        auto* access_3 = b.Access<ptr<function, vec4<u32>>>(access_2);
+        b.Return(foo, b.Load(access_3));
+    });
+
+    auto* src = R"(
+%foo = func():vec4<u32> -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, vec4<u32>, read_write> = access %vec
+    %4:ptr<function, vec4<u32>, read_write> = access %3
+    %5:ptr<function, vec4<u32>, read_write> = access %4
+    %6:vec4<u32> = load %5
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, Access_Component_NoUse) {
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        b.Access<ptr<function, u32>>(vec, 2_u);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %vec, 2u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, Load) {
+    auto* foo = b.Function("foo", ty.u32());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        auto* access = b.Access<ptr<function, u32>>(vec, 2_u);
+        auto* load = b.Load(access);
+        b.Return(foo, load);
+    });
+
+    auto* src = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %vec, 2u
+    %4:u32 = load %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:u32 = load_vector_element %vec, 2u
+    ret %3
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, Store) {
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        auto* access = b.Access<ptr<function, u32>>(vec, 2_u);
+        b.Store(access, 42_u);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %vec, 2u
+    store %3, 42u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %vec, 2u, 42u
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, AccessBeforeUse) {
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        auto* access_1 = b.Access<ptr<function, u32>>(vec, 2_u);
+        auto* access_2 = b.Access<ptr<function, u32>>(access_1);
+        b.Store(access_2, 42_u);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %vec, 2u
+    %4:ptr<function, u32, read_write> = access %3
+    store %4, 42u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %vec, 2u, 42u
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, MultipleUses) {
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* vec = b.Var<function, vec4<u32>>("vec");
+        auto* access = b.Access<ptr<function, u32>>(vec, 2_u);
+        auto* load = b.Load(access);
+        auto* add = b.Add<u32>(load, 1_u);
+        b.Store(access, add);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %vec, 2u
+    %4:u32 = load %3
+    %5:u32 = add %4, 1u
+    store %3, %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:u32 = load_vector_element %vec, 2u
+    %4:u32 = add %3, 1u
+    store_vector_element %vec, 2u, %4
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, ViaMatrix) {
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* mat = b.Var<function, mat4x4<f32>>("mat");
+        auto* access = b.Access<ptr<function, f32>>(mat, 1_u, 2_u);
+        b.Store(access, 42_f);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %3:ptr<function, f32, read_write> = access %mat, 1u, 2u
+    store %3, 42.0f
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %mat, 1u
+    store_vector_element %3, 2u, 42.0f
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, ViaArray) {
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* arr = b.Var<function, array<vec4<f32>, 4>>("arr");
+        auto* access = b.Access<ptr<function, f32>>(arr, 1_u, 2_u);
+        b.Store(access, 42_f);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<vec4<f32>, 4>, read_write> = var
+    %3:ptr<function, f32, read_write> = access %arr, 1u, 2u
+    store %3, 42.0f
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<vec4<f32>, 4>, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %arr, 1u
+    store_vector_element %3, 2u, 42.0f
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, ViaStruct) {
+    auto* str_ty = ty.Struct(mod.symbols.New("str"), {{
+                                                         mod.symbols.New("vec"),
+                                                         ty.vec4<f32>(),
+                                                     }});
+
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* str = b.Var("str", ty.ptr<function>(str_ty));
+        auto* access = b.Access<ptr<function, f32>>(str, 0_u, 2_u);
+        b.Store(access, 42_f);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+str = struct @align(16) {
+  vec:vec4<f32> @offset(0)
+}
+
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %str:ptr<function, str, read_write> = var
+    %3:ptr<function, f32, read_write> = access %str, 0u, 2u
+    store %3, 42.0f
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(16) {
+  vec:vec4<f32> @offset(0)
+}
+
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %str:ptr<function, str, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %str, 0u
+    store_vector_element %3, 2u, 42.0f
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_VectorElementPointerTest, DeeplyNested) {
+    auto* inner_arr = ty.array(ty.mat4x4<f32>(), 4);
+    auto* str_ty = ty.Struct(mod.symbols.New("str"), {{
+                                                         mod.symbols.New("inner"),
+                                                         inner_arr,
+                                                     }});
+    auto* outer_arr = ty.array(str_ty, 4);
+
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {
+        auto* arr = b.Var("arr", ty.ptr<function>(outer_arr));
+        auto* access = b.Access<ptr<function, f32>>(arr, 1_u, 0_u, 3_u, 2_u, 1_u);
+        b.Store(access, 42_f);
+        b.Return(foo);
+    });
+
+    auto* src = R"(
+str = struct @align(16) {
+  inner:array<mat4x4<f32>, 4> @offset(0)
+}
+
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<str, 4>, read_write> = var
+    %3:ptr<function, f32, read_write> = access %arr, 1u, 0u, 3u, 2u, 1u
+    store %3, 42.0f
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(16) {
+  inner:array<mat4x4<f32>, 4> @offset(0)
+}
+
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<str, 4>, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %arr, 1u, 0u, 3u, 2u
+    store_vector_element %3, 1u, 42.0f
+    ret
+  }
+}
+)";
+
+    Run(VectorElementPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader::lower
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.bazel b/src/tint/lang/spirv/reader/parser/BUILD.bazel
index d41f46f..2bfaad6 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/parser/BUILD.bazel
@@ -108,6 +108,7 @@
     "@gtest",
   ] + select({
     ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/common:test",
       "//src/tint/lang/spirv/reader/parser",
     ],
     "//conditions:default": [],
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.cmake b/src/tint/lang/spirv/reader/parser/BUILD.cmake
index e82f651..c8abe87 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/parser/BUILD.cmake
@@ -120,6 +120,7 @@
 
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_lang_spirv_reader_parser_test test
+    tint_lang_spirv_reader_common_test
     tint_lang_spirv_reader_parser
   )
 endif(TINT_BUILD_SPV_READER)
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.gn b/src/tint/lang/spirv/reader/parser/BUILD.gn
index 430c576..26c2954 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/parser/BUILD.gn
@@ -116,7 +116,10 @@
       ]
 
       if (tint_build_spv_reader) {
-        deps += [ "${tint_src_dir}/lang/spirv/reader/parser" ]
+        deps += [
+          "${tint_src_dir}/lang/spirv/reader/common:unittests",
+          "${tint_src_dir}/lang/spirv/reader/parser",
+        ]
       }
 
       if (tint_build_spv_reader || tint_build_spv_writer) {
diff --git a/src/tint/lang/spirv/reader/parser/helper_test.h b/src/tint/lang/spirv/reader/parser/helper_test.h
index decab00..c7fc02a 100644
--- a/src/tint/lang/spirv/reader/parser/helper_test.h
+++ b/src/tint/lang/spirv/reader/parser/helper_test.h
@@ -34,10 +34,10 @@
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
-#include "spirv-tools/libspirv.hpp"
 #include "src/tint/lang/core/ir/disassembler.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/spirv/reader/common/helper_test.h"
 #include "src/tint/lang/spirv/reader/parser/parser.h"
 
 namespace tint::spirv::reader {
@@ -56,23 +56,6 @@
 template <typename BASE>
 class SpirvParserTestHelperBase : public BASE {
   protected:
-    /// Assemble a textual SPIR-V module into a SPIR-V binary.
-    /// @param spirv_asm the textual SPIR-V assembly
-    /// @returns the SPIR-V binary data, or an error string
-    static Result<std::vector<uint32_t>, std::string> Assemble(std::string spirv_asm) {
-        StringStream err;
-        std::vector<uint32_t> binary;
-        spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
-        tools.SetMessageConsumer(
-            [&err](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) {
-                err << "SPIR-V assembly failed:" << pos.line << ":" << pos.column << ": " << msg;
-            });
-        if (!tools.Assemble(spirv_asm, &binary, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
-            return err.str();
-        }
-        return binary;
-    }
-
     /// Run the parser on a SPIR-V module and return the Tint IR or an error string.
     /// @param spirv_asm the SPIR-V assembly to parse
     /// @returns the disassembled Tint IR or an error
@@ -89,8 +72,11 @@
             return parsed.Failure();
         }
 
-        // Validate the IR module.
-        auto validated = core::ir::Validate(parsed.Get());
+        // Validate the IR module against the capabilities supported by the SPIR-V dialect.
+        auto validated =
+            core::ir::Validate(parsed.Get(), EnumSet<core::ir::Capability>{
+                                                 core::ir::Capability::kAllowVectorElementPointer,
+                                             });
         if (validated != Success) {
             return validated.Failure();
         }
diff --git a/src/tint/lang/spirv/reader/parser/memory_test.cc b/src/tint/lang/spirv/reader/parser/memory_test.cc
index 2031696..9f8c75f 100644
--- a/src/tint/lang/spirv/reader/parser/memory_test.cc
+++ b/src/tint/lang/spirv/reader/parser/memory_test.cc
@@ -83,8 +83,7 @@
 )");
 }
 
-// TODO(jrprice): We need to handle pointer-to-vector component somewhere.
-TEST_F(SpirvParserTest, DISABLED_Load_VectorComponent) {
+TEST_F(SpirvParserTest, Load_VectorComponent) {
     EXPECT_IR(R"(
                OpCapability Shader
                OpMemoryModel Logical GLSL450
@@ -350,8 +349,7 @@
 )");
 }
 
-// TODO(jrprice): We need to handle pointer-to-vector component somewhere.
-TEST_F(SpirvParserTest, DISABLED_Store_VectorComponent) {
+TEST_F(SpirvParserTest, Store_VectorComponent) {
     EXPECT_IR(R"(
                OpCapability Shader
                OpMemoryModel Logical GLSL450
@@ -359,7 +357,7 @@
                OpExecutionMode %main LocalSize 1 1 1
        %void = OpTypeVoid
         %u32 = OpTypeInt 32 0
-     %u32_42 = OpConstantInt %u32 42
+     %u32_42 = OpConstant %u32 42
       %vec4u = OpTypeVector %u32 4
     %u32_ptr = OpTypePointer Function %u32
   %vec4u_ptr = OpTypePointer Function %vec4u
diff --git a/src/tint/lang/spirv/reader/reader.cc b/src/tint/lang/spirv/reader/reader.cc
index 11199ea..d70c16c 100644
--- a/src/tint/lang/spirv/reader/reader.cc
+++ b/src/tint/lang/spirv/reader/reader.cc
@@ -31,17 +31,22 @@
 
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/spirv/reader/ast_parser/parse.h"
+#include "src/tint/lang/spirv/reader/lower/lower.h"
 #include "src/tint/lang/spirv/reader/parser/parser.h"
 
 namespace tint::spirv::reader {
 
 Result<core::ir::Module> ReadIR(const std::vector<uint32_t>& input) {
+    // Parse the input SPIR-V to the SPIR-V dialect of the IR.
     auto mod = Parse(Slice(input.data(), input.size()));
     if (mod != Success) {
         return mod.Failure();
     }
 
-    // TODO(crbug.com/tint/1907): Lower the module to core dialect.
+    // Lower the module to the core dialect of the IR.
+    if (auto res = Lower(mod.Get()); res != Success) {
+        return std::move(res.Failure());
+    }
 
     return mod;
 }
diff --git a/src/tint/lang/spirv/reader/reader_test.cc b/src/tint/lang/spirv/reader/reader_test.cc
new file mode 100644
index 0000000..1a086a8
--- /dev/null
+++ b/src/tint/lang/spirv/reader/reader_test.cc
@@ -0,0 +1,139 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/reader.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/tint/lang/core/ir/disassembler.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/spirv/reader/common/helper_test.h"
+
+namespace tint::spirv::reader {
+namespace {
+
+class SpirvReaderTest : public testing::Test {
+  protected:
+    /// Run the reader on a SPIR-V module and return the Tint IR or an error string.
+    /// @param spirv_asm the SPIR-V assembly to read
+    /// @returns the disassembled Tint IR or an error
+    Result<std::string> Run(std::string spirv_asm) {
+        // Assemble the SPIR-V input.
+        auto binary = Assemble(spirv_asm);
+        if (binary != Success) {
+            return binary.Failure();
+        }
+
+        // Read the SPIR-V to produce a core IR module.
+        auto ir = ReadIR(binary.Get());
+        if (ir != Success) {
+            return ir.Failure();
+        }
+
+        // Validate the IR module against the core dialect.
+        auto validated = core::ir::Validate(ir.Get());
+        if (validated != Success) {
+            return validated.Failure();
+        }
+
+        // Return the disassembled IR module.
+        return "\n" + core::ir::Disassemble(ir.Get());
+    }
+};
+
+TEST_F(SpirvReaderTest, Load_VectorComponent) {
+    auto got = Run(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %vec4u = OpTypeVector %u32 4
+    %u32_ptr = OpTypePointer Function %u32
+  %vec4u_ptr = OpTypePointer Function %vec4u
+      %u32_2 = OpConstant %u32 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %vec4u_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_2
+       %load = OpLoad %u32 %access
+               OpReturn
+               OpFunctionEnd
+)");
+    ASSERT_EQ(got, Success);
+    EXPECT_EQ(got, R"(
+%main = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
+  %b1 = block {
+    %2:ptr<function, vec4<u32>, read_write> = var
+    %3:u32 = load_vector_element %2, 2u
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvReaderTest, Store_VectorComponent) {
+    auto got = Run(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+     %u32_42 = OpConstant %u32 42
+      %vec4u = OpTypeVector %u32 4
+    %u32_ptr = OpTypePointer Function %u32
+  %vec4u_ptr = OpTypePointer Function %vec4u
+      %u32_2 = OpConstant %u32 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %vec4u_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_2
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)");
+    ASSERT_EQ(got, Success);
+    EXPECT_EQ(got, R"(
+%main = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
+  %b1 = block {
+    %2:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %2, 2u, 42u
+    ret
+  }
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader