Import Tint changes from Dawn

Contains manual fixes for:
 * BUILD.gn
 * CMakeLists.txt
 * DEPS
 * Doxyfile
 * build_overrides/build.gni
 * kokoro/linux/docker.sh
 * scripts/tint_overrides_with_defaults.gni
 * third_party/CMakeLists.txt

And two new files:
 * third_party/google_benchmark/BUILD.gn
 * third_party/google_benchmark/README.chromium

Changes: - 5784a4bd7a0ca25ff2ec57a2b9759a34621ba51c [tint][utils] Add support for large allocations by Ben Clayton <bclayton@google.com>
  - 48a4eabdad4985dff82e2dc897d8245a2df4a36f Fix Combine Sampler transform on function with unused tex... by Shrek Shao <shrekshao@google.com>
  - 6d370e591b68d9abb68eb25bf1638476fbe98f15 [ir] Process uncalled functions in DemoteToHelper by James Price <jrprice@google.com>
  - d037b6031f0b0e5b4a5122f660ac24bc454f5a55 [tint][gn] Fix cmd generation by Ben Clayton <bclayton@google.com>
  - 0c7be2d6360c30922096e3a1bc7e37610dc3013d [spirv-reader] Avoid nested struct deduplication by James Price <jrprice@google.com>
  - f1b8a01f97c5567d9f93feb7b276e567849240bb [tint][glsl] Put GLSL validation behind a build flag. by Ben Clayton <bclayton@google.com>
  - 49a564c40dd569b29fe9e569cd1ace173a170844 [tint][gn] Only build 'cmd' targets when 'tint_standalone' by Ben Clayton <bclayton@google.com>
  - fc9b2c489785c4154d4b8988f25a11ec3f349bc9 [tint][resolver] Error if ptr store type is not storable by Ben Clayton <bclayton@google.com>
  - 80b987eccbcb17fe679db6e5801cf956d17fcd36 [tint][build] Guard WGSL reader with tint_build_wgsl_reader by Ben Clayton <bclayton@google.com>
  - ddd2f5905b1d725a865e5b558ddf681f7d88ef7d [tint][resolver] Move handling of incomplete types by Ben Clayton <bclayton@google.com>
  - 8886d0b3bdb7b3f79d4899d1d9930d17a5d31865 [tint][resolver] Convert abstract-numerics for array() by Ben Clayton <bclayton@google.com>
  - 2550b49be0b0f59612e668db92c40dccab108cc4 [tint][build] Guard WGSL writer with tint_build_wgsl_writer by Ben Clayton <bclayton@google.com>
  - fe9e9d8c69c02df82bd75f376b8350c337dfa989 Support larger maxInterStageShader limits on D3D and Vulkan by Jiawei Shao <jiawei.shao@intel.com>
  - 9bbb27a1c5b945b3815c2f867564c20f34997957 [tint] Add missing build guards by Ben Clayton <bclayton@google.com>
  - 3a533674ffa6881844516706307a965cd967da89 [tint][utils] Assert vectors aren't mutated while iterating by Ben Clayton <bclayton@google.com>
  - 3cd439bfaf464c0ea5ab2d669febda4bc5651e0a [tint] Resolve types without recursion by Ben Clayton <bclayton@google.com>
  - d283e95ad3bfe7c307ede27c6593d5d0b4bfb091 [gn] Add new groups() for various targets, grouped by tar... by Ben Clayton <bclayton@google.com>
  - 535535bf1b3de33fa7fb146761afe14f352c256c [tint] Move override tracking into sem objects by Ben Clayton <bclayton@google.com>
  - b8ff13ee7e41f9e9d3a8480457c8eef350619b5b [tint] Add sem::Array, derives from core::type::Array by Ben Clayton <bclayton@google.com>
  - c3a47ef072710d83dcc6f313d3d96d2f8d65a6f0 [tint][resolver] Add UnresolvedIdentifier, IncompleteType by Ben Clayton <bclayton@google.com>
  - 0b4efc53e94e2199a0fc688a14e5f2ce764bec24 [tint] Add setters to sem variable types by Ben Clayton <bclayton@google.com>
  - ac72499c0a8a154fdfdc13a6442ef440d69f0ed0 Wrap fuzzer generation in GN tint_has_fuzzers check by Jim Van Verth <jvanverth@google.com>
  - 23f3c347a2ea07b88b41657178cd2ed36163f244 [spirv-writer] Clean up some obsolete TODOs by James Price <jrprice@google.com>
  - 01c15776a2e8e0b319f157d1e9670ae70a3a96d2 [spirv-writer] Move LiteralOperand to spirv::ir by James Price <jrprice@google.com>
  - dc5e5196a03c79fbd4490b58c72dcd8577125bfb [spirv-writer] Add pass-matrix-by-pointer option by James Price <jrprice@google.com>
  - ac001956d8395e7ec907b883aed0a9a3c7da023b [tint][IrToProgram] Reconstruct workgroupUniformLoad by Ben Clayton <bclayton@google.com>
  - abfb8e09a03b72d8fb03dc105f053336d399cae1 [spirv-writer] Fix ftoi conversion polyfill by James Price <jrprice@google.com>
  - 6f071847315a18825f6c217f784d672e75b5bf47 [spirv-writer] Don't hash `const char*` by Austin Eng <enga@chromium.org>
  - 5294cb0e9e7f4ca373b7638a92c44f2ed97fb045 Using binding information for SPIR-V/Tint interface by dan sinclair <dsinclair@chromium.org>
  - 66b75667e70724cef9912e78c50c25e164163a00 [gn] Disable fuzzers when '[dawn|tint]_has_build' is false by Ben Clayton <bclayton@google.com>
  - 99bc3e8ab7b60cad7909571b925d0ed48c75552a [tint][resolver] Rename builder_ to b and make a reference by Ben Clayton <bclayton@google.com>
  - c00c569fca7d3e1d40a179e340177815f3292733 [ir] Check for orphaned instructions in validator by James Price <jrprice@google.com>
  - 68c25b86e5e2ac299a4c84efa6ac75df31f9b545 [tint][resolver] Move lambdas to methods by Ben Clayton <bclayton@google.com>
  - 0eec270d3e1451cec2ffd712b00d3c28b3e47d92 [tint][utils] Add VectorIterator class by Ben Clayton <bclayton@google.com>
  - 7d95661d5b198f322edb31a507afecf54d74123a [kokoro] Update GCC to 13 by Ben Clayton <bclayton@google.com>
  - e82fe98bd900efceca25ee39099315c2ad8468e0 [tint][ir] Add Disassemble() free function by Ben Clayton <bclayton@google.com>
  - 89f9d6d4c49643b5d2d23ee3765d1f183ce38cf9 [tint][ir] Various fixes to IR -> Program by Ben Clayton <bclayton@google.com>
  - 1a1da4576f273526876d1b8e4fecfbcf619a9518 [spirv-writer] Combine access instruction chains by James Price <jrprice@google.com>
  - 6ab5462857c27c5f08f70a90c9a974d4bfc90b61 [tint][fuzzers] Add tint_wgsl_fuzzer executable by Ben Clayton <bclayton@google.com>
  - 415f1bed2988cdb6ae539093f1b86347c6861de6 [ir] Fix use-after-free in MultiplanarExternalTexture by James Price <jrprice@google.com>
  - 1d1205c77ca96f18be2cb77fe6278bac0223af77 [fuzzers] Skip *.expected.ir.spvasm files by James Price <jrprice@google.com>
  - 089347a5062e47e2761f8f262a21eafe14f7142a [tint] Don't link 'benchmark_main' target by Ben Clayton <bclayton@google.com>
  - ef3f5dc888e81edfdddd059b8a1ec06fb913890b [tint] Change signature of ApplySubstituteOverrides() by Ben Clayton <bclayton@google.com>
  - a9cc4c1ceb544d3e23e15ec70ee82b1a4b7c7e39 [tint] Add fuzzer target support to 'gen build' by Ben Clayton <bclayton@google.com>
  - a324e1a5e0e2a7c1b5e76247a783217c6c71cb65 [ir] Add polyfill for ftoi conversions by James Price <jrprice@google.com>
  - ddd6d24e437a54548ac446d009dcfeadf77dc6ad [wgsl][ir] Polyfill workgroupUniformLoad on lower by James Price <jrprice@google.com>
  - dcf6c42bed367b79377cdb335e29a6b849e351bb [spirv-writer] Use OpConstantNull for composites by James Price <jrprice@google.com>
  - fcda9f809f4d1092372379381c8f32f862fa8238 [tint][bench] Fix linker errors for certain build by Ben Clayton <bclayton@google.com>
  - 9d05868b851a00feb884b902f1756f0ed887019e [tint][gn] Remove `testonly = true` from emitted BUILD.gn... by Ben Clayton <bclayton@google.com>
  - d09eb2406339e8c4608b425fae3c7a78450b4cb3 [tint][utils] Use [[maybe_unused]] on TINT_STATIC_INIT by Ben Clayton <bclayton@google.com>
  - 997c2cbe94dd6a4bb5878d702926b0ef9b2a11ee [tint][utils] Add a TINT_STATIC_INIT() helper by Ben Clayton <bclayton@google.com>
  - 9d1b610d7666f3b8081fd83004c7376e8725790b [tint][build] Generate benchmarks for GN and Bazel by Ben Clayton <bclayton@google.com>
  - 77a51263418907750a14e1a6b55d31c2c57690c5 [tint] Move ApplySubstitueOverrides() fuzzers -> wgsl/hel... by Ben Clayton <bclayton@google.com>
  - 6f2ede60bf320ce42315de35eb257c289e1215f0 [tint] Remove blankline before copyright by Ben Clayton <bclayton@google.com>
  - 77350e94b50beb8366b0cfd06fc5afca228387f7 [tint] Support iteration while adding to BlockAllocator by Ben Clayton <bclayton@google.com>
  - ffcb82acc65d2c740ddf5ce463c2b52fead00d38 [tint][cmake] Rework targets for fuzzers by Ben Clayton <bclayton@google.com>
  - 8c71a15d9019dcfa98454c7dd76f5277ff341a05 [tint] Remove tint_spirv_tools_fuzzers by Ben Clayton <bclayton@google.com>
  - b099a8fc31eb4788bc144789c13dc6981055f389 [ir] Add PreservePadding transform by James Price <jrprice@google.com>
  - 9ff85267afccef5a8dbd86aea57a4da417fc8d6d [ir] Fix let instructions in BlockDecoratedStructs by James Price <jrprice@google.com>
  - c0476283ff86f358c76346d7f014330e5206bf76 [tint] Remove tint_black_box_fuzz_target by Ben Clayton <bclayton@google.com>
  - d5fcc3dd1751834721d2bd4b89b2e511f27b2a6b Add support for unorm10-10-10-2 vertex format by François Beaufort <beaufort.francois@gmail.com>
  - f744cf4a265f07aab17ad2605a46ec078afdacd6 [ir] Do not automatically add cloned functions by James Price <jrprice@google.com>
  - b1c7464f22074e923f90c3fc474cd16c19513ba0 [ir] Use Builder::LoopRange helper by James Price <jrprice@google.com>
  - a2bf4bdc476986228397d9584f6c78d08e606cd6 [ir] Fix use-after-free in ShaderIO transform by James Price <jrprice@google.com>
  - 8f403335073f241870c4bd9176146c4f20325331 [wgsl][lower] Destroy converted call instructions by James Price <jrprice@google.com>
  - d66eeb775802fcf92ddc9995ffd53096f13f9e96 [ir][spirv-writer] Add index to IO output names by James Price <jrprice@google.com>
  - 57d9ec4f87f22a562d4221d1e1ed15ecbabf18e3 [tint][fuzzers] Disable concurrency fuzzer for pixel-loca... by Ben Clayton <bclayton@google.com>
  - 1ec4835ca75d777a294587d50cb6640dcc6bd740 [ir] Change ir::BindingRemapper to take a set of options. by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 5784a4bd7a0ca25ff2ec57a2b9759a34621ba51c
Change-Id: Id46f334e607067a4b8bb341bd214dc2b33aa0b05
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/155921
Commit-Queue: Ben Clayton <bclayton@google.com>
Commit-Queue: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 1a1b671..685e9fe 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -14,14 +14,50 @@
 
 import("scripts/tint_overrides_with_defaults.gni")
 
+group("benchmarks") {
+  testonly = true
+  deps = [ "src/tint:benchmarks" ]
+}
+
+group("fuzzers") {
+  testonly = true
+  deps = [
+    "src/tint:fuzzers",
+  ]
+}
+
+group("libs") {
+  deps = [
+    "src/tint:libs",
+  ]
+}
+
+group("tests") {
+  testonly = true
+  deps = [
+    "src/tint:tests",
+  ]
+}
+
+group("cmds") {
+  deps = [
+    "src/tint:cmds",
+  ]
+}
+
+group("all") {
+  testonly = true
+  deps = [
+    ":benchmarks",
+    ":cmds",
+    ":fuzzers",
+    ":libs",
+    ":tests",
+  ]
+}
+
 # This target is built when no specific target is specified on the command line.
 group("default") {
   testonly = true
-  deps = [
-    "src/tint/api",
-    "src/tint/fuzzers",
-  ]
-  if (tint_build_unittests) {
-    deps += [ "src/tint/cmd/test" ]
-  }
+  deps = [ ":all" ]
 }
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e92ed63..fc95d64 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -285,6 +285,10 @@
   set(COMPILER_IS_CLANG TRUE)
 endif()
 
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+  set(COMPILER_IS_GNU TRUE)
+endif()
+
 if((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR COMPILER_IS_CLANG)
   set(COMPILER_IS_LIKE_GNU TRUE)
 endif()
diff --git a/DEPS b/DEPS
index 84ca13f..4138b73 100644
--- a/DEPS
+++ b/DEPS
@@ -38,15 +38,15 @@
 
   # Dependencies required to use GN/Clang in standalone
   'build': {
-    'url': '{chromium_git}/chromium/src/build@8cbb95464bb7f05b442f3ece4951efbe0825a131',
+    'url': '{chromium_git}/chromium/src/build@5885d3c24833ad72845a52a1b913a2b8bc651b56',
   },
 
   'buildtools': {
-    'url': '{chromium_git}/chromium/src/buildtools@70e9f44cbc8bc4c3dff18800ba5d962154a4f2a6',
+    'url': '{chromium_git}/chromium/src/buildtools@a9a6f0c49d0e8fa0cda37337430b4736ab3dc944',
   },
 
   'tools/clang': {
-    'url': '{chromium_git}/chromium/src/tools/clang@fff7f04d30a0687029ddc7e174d5548a525ddf0b',
+    'url': '{chromium_git}/chromium/src/tools/clang@8f75392b4aa947fb55c7c206b36804229595e4da',
   },
 
   'buildtools/clang_format/script': {
@@ -78,7 +78,7 @@
     'condition': 'host_os == "win"',
   },
 
- 'buildtools/reclient': {
+  'buildtools/reclient': {
     'packages': [
       {
         'package': Var('reclient_package') + '${{platform}}',
@@ -88,12 +88,12 @@
     'dep_type': 'cipd',
   },
 
-  'buildtools/third_party/libc++/trunk': {
-    'url': '{chromium_git}/external/github.com/llvm/llvm-project/libcxx.git@035440c7077237787869cb08ab99bcc8b5ddc97e',
+  'third_party/libc++/src': {
+    'url': '{chromium_git}/external/github.com/llvm/llvm-project/libcxx.git@84fb809dd6dae36d556dc0bb702c6cc2ce9d4b80',
   },
 
-  'buildtools/third_party/libc++abi/trunk': {
-    'url': '{chromium_git}/external/github.com/llvm/llvm-project/libcxxabi.git@b74d7716111d7eda5c03cb8f5dfc940e1c2c0030',
+  'third_party/libc++abi/src': {
+    'url': '{chromium_git}/external/github.com/llvm/llvm-project/libcxxabi.git@d4760c0af99ccc9bce077960d5ddde4d66146c05',
   },
 
   'third_party/ninja': {
@@ -113,14 +113,14 @@
 
   # Dependencies required for testing
   'testing': {
-    'url': '{chromium_git}/chromium/src/testing@d5ea1bf4b64781cfe38f207f56f264eb080d06b2',
+    'url': '{chromium_git}/chromium/src/testing@035a9b18047370df7403758b006e6c9696d6b84d',
   },
 
   'third_party/catapult': {
     'url': '{chromium_git}/catapult.git@37e879a7d13cbaa4925e09fc02b0f9276e060f0a',
   },
 
-  'third_party/benchmark': {
+  'third_party/google_benchmark/src': {
     'url': '{chromium_git}/external/github.com/google/benchmark.git@efc89f0b524780b1994d5dddd83a92718e5be492',
   },
 
diff --git a/Doxyfile b/Doxyfile
index 8f1bd7d..2cc7f05 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -786,11 +786,8 @@
 # Note: If this tag is empty the current directory is searched.
 
 INPUT                  = CODE_OF_CONDUCT.md \
-                         src/tint/fuzzers/tint_spirv_tools_fuzzer \
                          src \
-                         tools/src \
-                         src/tint/fuzzers/tint_spirv_tools_fuzzer \
-                         src/tint/fuzzers/tint_ast_fuzzer
+                         tools/src
 
 # This tag can be used to specify the character encoding of the source files
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
diff --git a/build_overrides/build.gni b/build_overrides/build.gni
index 11ce9f4..8070462 100644
--- a/build_overrides/build.gni
+++ b/build_overrides/build.gni
@@ -25,6 +25,9 @@
   ignore_elf32_limitations = false
 }
 
+# Enables assertions on safety checks in libc++.
+enable_safe_libcxx = true
+
 # Detect whether we can use the hermetic XCode like in Chromium and do so if
 # possible.
 if (host_os == "mac" && use_system_xcode == "") {
diff --git a/kokoro/linux/docker.sh b/kokoro/linux/docker.sh
index bea2fd9..a15ff37 100755
--- a/kokoro/linux/docker.sh
+++ b/kokoro/linux/docker.sh
@@ -105,7 +105,7 @@
         COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_AST_FUZZER=1"
         COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_REGEX_FUZZER=1"
     elif [ "$BUILD_TOOLCHAIN" == "gcc" ]; then
-        using gcc-10
+        using gcc-13
     fi
 
     if [ "$BUILD_SANITIZER" == "asan" ]; then
diff --git a/scripts/tint_overrides_with_defaults.gni b/scripts/tint_overrides_with_defaults.gni
index dca626e..0edfb87 100644
--- a/scripts/tint_overrides_with_defaults.gni
+++ b/scripts/tint_overrides_with_defaults.gni
@@ -16,6 +16,16 @@
 
 # This file contains Tint-related build flags.
 
+if (!defined(tint_standalone)) {
+  tint_standalone = false
+}
+
+if (!defined(tint_has_build)) {
+  tint_has_build = true
+}
+
+tint_has_fuzzers = tint_has_build
+
 declare_args() {
   # Path to tint checkout
   if (!defined(tint_root_dir)) {
@@ -42,6 +52,10 @@
     tint_spirv_headers_dir = "//third_party/vulkan-deps/spirv-headers/src"
   }
 
+  if (!defined(tint_build_cmds)) {
+    tint_build_cmds = tint_standalone
+  }
+
   # Build the SPIR-V input reader
   if (!defined(tint_build_spv_reader)) {
     tint_build_spv_reader = true
@@ -77,20 +91,25 @@
     tint_build_glsl_writer = true
   }
 
+  # Build the GLSL output validator
+  if (!defined(tint_build_glsl_validator)) {
+    tint_build_glsl_validator = true
+  }
+
   # Build the Syntax Tree writer
   if (!defined(tint_build_syntax_tree_writer)) {
     tint_build_syntax_tree_writer = false
   }
 
-  # Build the Tint IR
-  if (!defined(tint_build_ir)) {
-    tint_build_ir = false
-  }
-
   # Build unittests
   if (!defined(tint_build_unittests)) {
     tint_build_unittests = true
   }
+
+  # Build benchmarks
+  if (!defined(tint_build_benchmarks)) {
+    tint_build_benchmarks = true
+  }
 }
 
 declare_args() {
diff --git a/src/tint/BUILD.bazel b/src/tint/BUILD.bazel
index ca63402..d9b2f7e 100644
--- a/src/tint/BUILD.bazel
+++ b/src/tint/BUILD.bazel
@@ -17,14 +17,15 @@
 load(":flags.bzl", "declare_bool_flag", "declare_os_flag")
 
 # Declares the 'tint_build_*' flags that control what parts of Tint get built
-declare_bool_flag(name = "tint_build_glsl_writer", default = False)
-declare_bool_flag(name = "tint_build_hlsl_writer", default = True)
-declare_bool_flag(name = "tint_build_ir",          default = True)
-declare_bool_flag(name = "tint_build_msl_writer",  default = True)
-declare_bool_flag(name = "tint_build_spv_reader",  default = True)
-declare_bool_flag(name = "tint_build_spv_writer",  default = True)
-declare_bool_flag(name = "tint_build_wgsl_reader", default = True)
-declare_bool_flag(name = "tint_build_wgsl_writer", default = True)
+declare_bool_flag(name = "tint_build_glsl_writer",    default = False)
+declare_bool_flag(name = "tint_build_glsl_validator", default = False)
+declare_bool_flag(name = "tint_build_hlsl_writer",    default = True)
+declare_bool_flag(name = "tint_build_ir",             default = True)
+declare_bool_flag(name = "tint_build_msl_writer",     default = True)
+declare_bool_flag(name = "tint_build_spv_reader",     default = True)
+declare_bool_flag(name = "tint_build_spv_writer",     default = True)
+declare_bool_flag(name = "tint_build_wgsl_reader",    default = True)
+declare_bool_flag(name = "tint_build_wgsl_writer",    default = True)
 
 # Declares the 'os' flag that control what OS-specific Tint code gets built
 declare_os_flag()
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index d6570a1..1bcdbee 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -83,6 +83,12 @@
     defines += [ "TINT_BUILD_GLSL_WRITER=0" ]
   }
 
+  if (tint_build_glsl_validator) {
+    defines += [ "TINT_BUILD_GLSL_VALIDATOR=1" ]
+  } else {
+    defines += [ "TINT_BUILD_GLSL_VALIDATOR=0" ]
+  }
+
   if (tint_build_syntax_tree_writer) {
     defines += [ "TINT_BUILD_SYNTAX_TREE_WRITER=1" ]
   } else {
@@ -194,6 +200,13 @@
   }
 }
 
+if (tint_build_benchmarks) {
+  group("google_benchmark") {
+    testonly = true
+    public_deps = [ "//third_party/google_benchmark" ]
+  }
+}
+
 group("abseil") {
   # When build_with_chromium=true we need to include "//third_party/abseil-cpp:absl" while
   # it's beneficial to be more specific with standalone Dawn, especially when it comes to
@@ -210,11 +223,69 @@
 }
 
 ###############################################################################
-# Aliases.
+# Fuzzers
 ###############################################################################
-if (tint_build_unittests) {
-  group("tint_unittests") {
-    testonly = true
-    public_deps = [ "${tint_src_dir}/cmd/test" ]
+if (tint_has_fuzzers) {
+  action("tint_generate_wgsl_corpus") {
+    script = "${tint_src_dir}/cmd/fuzz/wgsl/generate_wgsl_corpus.py"
+    sources = [ "${tint_src_dir}/cmd/fuzz/wgsl/generate_wgsl_corpus.py" ]
+    args = [
+      "--stamp=" + rebase_path(fuzzer_corpus_wgsl_stamp, root_build_dir),
+      rebase_path("${tint_root_dir}/test", root_build_dir),
+      rebase_path(fuzzer_corpus_wgsl_dir, root_build_dir),
+    ]
+    outputs = [ fuzzer_corpus_wgsl_stamp ]
   }
 }
+
+###############################################################################
+# Groups
+###############################################################################
+group("libs") {
+  deps = [ "${tint_src_dir}/api" ]
+}
+
+group("cmds") {
+  deps = []
+  if (tint_build_cmds) {
+    deps += [
+      "${tint_src_dir}/cmd/remote_compile",
+      "${tint_src_dir}/cmd/tint",
+    ]
+  }
+}
+
+group("fuzzers") {
+  testonly = true
+  deps = []
+  if (tint_has_fuzzers) {
+    deps += [
+      "${tint_src_dir}/cmd/fuzz/wgsl",
+      "${tint_src_dir}/fuzzers",
+    ]
+  }
+}
+
+group("tests") {
+  testonly = true
+  deps = []
+  if (tint_build_unittests) {
+    deps += [ "${tint_src_dir}/cmd/test:test_cmd" ]
+  }
+}
+
+group("benchmarks") {
+  testonly = true
+  deps = []
+  if (tint_build_benchmarks) {
+    deps += [ "${tint_src_dir}/cmd/bench:bench_cmd" ]
+  }
+}
+
+###############################################################################
+# Aliases
+###############################################################################
+group("tint_unittests") {
+  testonly = true
+  public_deps = [ ":tests" ]
+}
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 2f3e893..d674fde 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -37,12 +37,17 @@
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_READER=$<BOOL:${TINT_BUILD_SPV_READER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_VALIDATOR=$<BOOL:${TINT_BUILD_GLSL_VALIDATOR}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_HLSL_WRITER=$<BOOL:${TINT_BUILD_HLSL_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_MSL_WRITER=$<BOOL:${TINT_BUILD_MSL_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_WRITER=$<BOOL:${TINT_BUILD_SPV_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SYNTAX_TREE_WRITER=$<BOOL:${TINT_BUILD_SYNTAX_TREE_WRITER}>)
 
+  if(TINT_BUILD_FUZZERS)
+    target_compile_options(${TARGET} PRIVATE "-fsanitize=fuzzer")
+  endif()
+
   common_compile_options(${TARGET})
 endfunction()
 
@@ -74,6 +79,12 @@
     -Weverything
   )
 
+  if(COMPILER_IS_GNU)
+    # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80635
+    # Despite the bug being closed, false-positives still seen in GCC-13.
+    target_compile_options(${TARGET} PRIVATE -Wno-maybe-uninitialized)
+  endif(COMPILER_IS_GNU)
+
   if(COMPILER_IS_LIKE_GNU)
     target_compile_options(${TARGET} PRIVATE
       -pedantic-errors
@@ -82,13 +93,9 @@
 
     if(COMPILER_IS_CLANG)
       if(IS_DEBUG_BUILD)
-        target_compile_options(${TARGET} PRIVATE
-          -fstandalone-debug
-        )
+        target_compile_options(${TARGET} PRIVATE -fstandalone-debug)
       endif()
-      target_compile_options(${TARGET} PRIVATE
-        ${COMMON_CLANG_OPTIONS}
-      )
+      target_compile_options(${TARGET} PRIVATE ${COMMON_CLANG_OPTIONS})
     endif()
   endif(COMPILER_IS_LIKE_GNU)
 
@@ -171,13 +178,11 @@
 
 function(tint_bench_compile_options TARGET)
   tint_core_compile_options(${TARGET})
-  set_target_properties(${TARGET} PROPERTIES FOLDER "Benchmarks")
-  target_link_libraries(${TARGET} PRIVATE benchmark::benchmark)
-  if(TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER)
-    target_compile_definitions(${TARGET} PRIVATE
-      "TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER=\"${TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER}\""
-    )
-  endif()
+endfunction()
+
+function(tint_fuzz_compile_options TARGET)
+  tint_core_compile_options(${TARGET})
+  set_target_properties(${TARGET} PROPERTIES FOLDER "Fuzzers")
 endfunction()
 
 function(tint_test_cmd_compile_options TARGET)
@@ -201,20 +206,28 @@
   target_link_libraries(${TARGET} PRIVATE benchmark::benchmark)
 endfunction()
 
-function(tint_fuzzer_compile_options TARGET)
-  tint_default_compile_options(${TARGET})
-  target_link_libraries(${TARGET} PRIVATE "tint_api${TINT_FUZZ_SUFFIX}")
+function(tint_fuzz_cmd_compile_options TARGET)
+  tint_fuzz_compile_options(${TARGET})
 
   if(NOT "${TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS}" STREQUAL "")
-    # This is set when the fuzzers are being built by OSS${TINT_FUZZ_SUFFIX}. In this case the
-    # variable provides the necessary linker flags, and OSS${TINT_FUZZ_SUFFIX} will take care
+    # This is set when the fuzzers are being built by OSS-Fuzz. In this case the
+    # variable provides the necessary linker flags, and OSS-Fuzz will take care
     # of passing suitable compiler flags.
     target_link_options(${TARGET} PUBLIC ${TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS})
   else()
-    # When the fuzzers are being built outside of OSS${TINT_FUZZ_SUFFIX}, specific libFuzzer
+    # When the fuzzers are being built outside of OSS-Fuzz, specific libFuzzer
     # arguments to enable fuzzing are used.
     target_link_options(${TARGET} PUBLIC -fsanitize=fuzzer -fsanitize-coverage=trace-cmp)
   endif()
+
+  # Link the version of tint_api with -sanitize=fuzzer enabled
+  target_link_libraries(${TARGET} PRIVATE "tint_api_sanitize_fuzzer")
+endfunction()
+
+# TODO(bclayton): Remove this when fuzzers fully migrated to gen build
+function(tint_fuzzer_compile_options TARGET)
+  tint_fuzz_cmd_compile_options(${TARGET})
+  target_link_libraries(${TARGET} PRIVATE "tint_api_sanitize_fuzzer")
 endfunction()
 
 if(TINT_ENABLE_BREAK_IN_DEBUGGER)
@@ -223,17 +236,6 @@
 endif()
 
 ################################################################################
-# Fuzzers
-################################################################################
-if(TINT_BUILD_FUZZERS)
-  if(NOT COMPILER_IS_CLANG)
-    message(FATAL_ERROR "TINT_BUILD_FUZZERS can only be enabled with the Clang toolchain")
-  endif()
-  add_subdirectory(fuzzers)
-  set(TINT_FUZZ_SUFFIX "_fuzz")
-endif()
-
-################################################################################
 # Benchmarks
 ################################################################################
 if(TINT_BUILD_BENCHMARKS AND TINT_EXTERNAL_BENCHMARK_CORPUS_DIR)
@@ -282,17 +284,16 @@
 # Functions used by BUILD.cmake files
 # The CMake build handles the target kinds in different ways:
 # 'cmd'       - Translates to a CMake executable target.
-# 'lib'       - Translates to CMake static library.
-#               If TINT_BUILD_FUZZERS is enabled, then a second static library with
-#               the ${TINT_FUZZ_SUFFIX} suffix is also created. This is done because
-#               the fuzzer build requires compilation with the '-fsanitize=fuzzer'
-#               flag, which results in a separate set of compilation units.
+# 'lib'       - Translates to a CMake static library.
 # 'test'      - Translates to a CMake object library, configured for compiling and
 #               linking against google-test.
 # 'bench'     - Translates to a CMake object library, configured for compiling and
 #               linking against google-benchmark.
+# 'fuzz'      - Translates to a CMake object library, configured for compiling and
+#               linking against libfuzzer.
 # 'test_cmd'  - Translates to a CMake executable target linked against google-test.
 # 'bench_cmd' - Translates to a CMake executable target linked against google-benchmark.
+# 'fuzz_cmd'  - Translates to a CMake executable target linked against libfuzz.
 # See also: docs/tint/gen.md
 ################################################################################
 
@@ -310,24 +311,20 @@
     if(TINT_BUILD_CMD_TOOLS)
       set(IS_ENABLED TRUE PARENT_SCOPE)
     endif()
-  elseif(${KIND} STREQUAL test_cmd)
-    if(TINT_BUILD_TESTS)
-      set(IS_ENABLED TRUE PARENT_SCOPE)
-    endif()
-  elseif(${KIND} STREQUAL bench_cmd)
-    if(TINT_BUILD_BENCHMARKS)
-      set(IS_ENABLED TRUE PARENT_SCOPE)
-    endif()
-  elseif(${KIND} STREQUAL test)
-    if(TINT_BUILD_TESTS)
-      set(IS_ENABLED TRUE PARENT_SCOPE)
-    endif()
-  elseif(${KIND} STREQUAL bench)
-    if(TINT_BUILD_BENCHMARKS)
-      set(IS_ENABLED TRUE PARENT_SCOPE)
-    endif()
   elseif(${KIND} STREQUAL lib)
     set(IS_ENABLED TRUE PARENT_SCOPE)
+  elseif((${KIND} STREQUAL test) OR (${KIND} STREQUAL test_cmd))
+    if(TINT_BUILD_TESTS)
+      set(IS_ENABLED TRUE PARENT_SCOPE)
+    endif()
+  elseif((${KIND} STREQUAL bench) OR (${KIND} STREQUAL bench_cmd))
+    if(TINT_BUILD_BENCHMARKS)
+      set(IS_ENABLED TRUE PARENT_SCOPE)
+    endif()
+  elseif((${KIND} STREQUAL fuzz) OR (${KIND} STREQUAL fuzz_cmd))
+    if(TINT_BUILD_FUZZERS)
+      set(IS_ENABLED TRUE PARENT_SCOPE)
+    endif()
   else()
     message(FATAL_ERROR "unhandled target kind ${KIND}")
   endif()
@@ -343,6 +340,7 @@
 #   KIND     - The target kind
 #   SOURCES  - a list of source files, relative to this directory
 function(tint_add_target TARGET KIND)
+  set(TARGET "${TARGET}${TINT_TARGET_SUFFIX}") # Apply suffix
   set(SOURCES ${ARGN})
 
   tint_check_target_is_enabled(IS_ENABLED ${KIND})
@@ -359,21 +357,6 @@
       )
     endif()
     tint_default_compile_options(${TARGET})
-
-    if(TINT_BUILD_FUZZERS)
-      # Create a second library target for use of the fuzzers, with the
-      # ${TINT_FUZZ_SUFFIX} suffix
-      set(FUZZ_TARGET "${TARGET}${TINT_FUZZ_SUFFIX}")
-      add_library(${FUZZ_TARGET} STATIC EXCLUDE_FROM_ALL)
-      target_sources(${FUZZ_TARGET} PRIVATE ${SOURCES})
-      if (TINT_ENABLE_INSTALL)
-        install(TARGETS ${FUZZ_TARGET}
-                LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
-                ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
-                )
-      endif()
-      tint_default_compile_options(${FUZZ_TARGET})
-    endif()
   elseif(${KIND} STREQUAL cmd)
     add_executable(${TARGET})
     tint_default_compile_options(${TARGET})
@@ -383,12 +366,18 @@
   elseif(${KIND} STREQUAL bench_cmd)
     add_executable(${TARGET})
     tint_bench_cmd_compile_options(${TARGET})
+  elseif(${KIND} STREQUAL fuzz_cmd)
+    add_executable(${TARGET})
+    tint_fuzz_cmd_compile_options(${TARGET})
   elseif(${KIND} STREQUAL test)
     add_library(${TARGET} OBJECT EXCLUDE_FROM_ALL)
     tint_test_compile_options(${TARGET})
   elseif(${KIND} STREQUAL bench)
     add_library(${TARGET} OBJECT EXCLUDE_FROM_ALL)
     tint_bench_compile_options(${TARGET})
+  elseif(${KIND} STREQUAL fuzz)
+    add_library(${TARGET} OBJECT EXCLUDE_FROM_ALL)
+    tint_fuzz_compile_options(${TARGET})
   else()
     message(FATAL_ERROR "unhandled target kind ${KIND}")
   endif()
@@ -406,6 +395,7 @@
 #   KIND     - The target kind
 #   SOURCES  - a list of source files, relative to this directory
 function(tint_target_add_sources TARGET KIND)
+  set(TARGET "${TARGET}${TINT_TARGET_SUFFIX}") # Apply suffix
   set(SOURCES ${ARGN})
 
   tint_check_target_is_enabled(IS_ENABLED ${KIND})
@@ -414,12 +404,6 @@
   endif()
 
   target_sources(${TARGET} PRIVATE ${SOURCES})
-
-  # If there's a corresponding fuzz target for this target, also append the files to that target
-  set(FUZZ_TARGET "${TARGET}${TINT_FUZZ_SUFFIX}")
-  if(TARGET "${FUZZ_TARGET}")
-    target_sources("${FUZZ_TARGET}" PRIVATE ${SOURCES})
-  endif()
 endfunction()
 
 # tint_target_add_dependencies(TARGET DEPENDENCIES...)
@@ -431,6 +415,7 @@
 #   KIND         - The target kind
 #   DEPENDENCIES - a list of target names
 function(tint_target_add_dependencies TARGET KIND)
+  set(TARGET "${TARGET}${TINT_TARGET_SUFFIX}") # Apply suffix
   set(DEPENDENCIES ${ARGN})
 
   tint_check_target_is_enabled(IS_ENABLED ${KIND})
@@ -438,21 +423,14 @@
     return() # Target is disabled via build flags
   endif()
 
+  # Apply target suffix
+  set(SUFFIXED_DEPENDENCIES "")
+  foreach(DEPENDENCY ${DEPENDENCIES})
+    list(APPEND SUFFIXED_DEPENDENCIES "${DEPENDENCY}${TINT_TARGET_SUFFIX}")
+  endforeach()
+
   # Register the dependencies
-  target_link_libraries(${TARGET} PRIVATE ${DEPENDENCIES})
-
-  # If there's a corresponding fuzz target for this target, add the corresponding fuzz dependencies
-  # to the fuzz target.
-  set(FUZZ_TARGET "${TARGET}${TINT_FUZZ_SUFFIX}")
-  if(TARGET "${FUZZ_TARGET}")
-    set(FUZZ_DEPENDENCIES "")
-
-    foreach(TARGET ${DEPENDENCIES})
-      list(APPEND FUZZ_DEPENDENCIES "${TARGET}${TINT_FUZZ_SUFFIX}")
-    endforeach()
-
-    target_link_libraries("${FUZZ_TARGET}" PRIVATE ${FUZZ_DEPENDENCIES})
-  endif()
+  target_link_libraries(${TARGET} PRIVATE ${SUFFIXED_DEPENDENCIES})
 endfunction()
 
 # tint_target_add_external_dependencies(TARGET KIND DEPENDENCIES...)
@@ -465,7 +443,8 @@
 #   DEPENDENCIES - a list of external target names
 #
 # See src/tint/externals.json for the list of external dependencies.
-function(tint_target_add_external_dependencies UNSUFFIXED_TARGET KIND)
+function(tint_target_add_external_dependencies TARGET KIND)
+  set(TARGET "${TARGET}${TINT_TARGET_SUFFIX}") # Apply suffix
   set(DEPENDENCIES ${ARGN})
 
   tint_check_target_is_enabled(IS_ENABLED ${KIND})
@@ -473,67 +452,67 @@
     return() # Target is disabled via build flags
   endif()
 
-  # Build a list of targets that we're going to operate on
-  set(TARGETS ${UNSUFFIXED_TARGET})
-  if(TARGET "${UNSUFFIXED_TARGET}${TINT_FUZZ_SUFFIX}")
-    list(APPEND TARGETS "${UNSUFFIXED_TARGET}${TINT_FUZZ_SUFFIX}")
-  endif()
-
-  foreach(TARGET ${TARGETS})
-    foreach(DEPENDENCY ${DEPENDENCIES})  # Each external dependency requires special handling...
-      if(${DEPENDENCY} STREQUAL "abseil")
-        target_link_libraries(${TARGET} PRIVATE
-          absl_strings
+  foreach(DEPENDENCY ${DEPENDENCIES})  # Each external dependency requires special handling...
+    if(${DEPENDENCY} STREQUAL "abseil")
+      target_link_libraries(${TARGET} PRIVATE
+        absl_strings
+      )
+    elseif(${DEPENDENCY} STREQUAL "glslang")
+      target_link_libraries(${TARGET} PRIVATE glslang)
+      if(NOT MSVC)
+        target_compile_options(${TARGET} PRIVATE
+          -Wno-reserved-id-macro
+          -Wno-shadow-field-in-constructor
+          -Wno-shadow
+          -Wno-weak-vtables
         )
-      elseif(${DEPENDENCY} STREQUAL "glslang")
-        target_link_libraries(${TARGET} PRIVATE glslang)
-        if(NOT MSVC)
-          target_compile_options(${TARGET} PRIVATE
-            -Wno-reserved-id-macro
-            -Wno-shadow-field-in-constructor
-            -Wno-shadow
-            -Wno-weak-vtables
-          )
-        endif()
-      elseif(${DEPENDENCY} STREQUAL "glslang-res-limits")
-        target_link_libraries(${TARGET} PRIVATE
-          glslang-default-resource-limits
-        )
-      elseif(${DEPENDENCY} STREQUAL "gtest")
-        target_include_directories(${TARGET} PRIVATE ${gmock_SOURCE_DIR}/include)
-        target_link_libraries(${TARGET} PRIVATE gmock)
-      elseif(${DEPENDENCY} STREQUAL "metal")
-        find_library(CoreGraphicsFramework CoreGraphics REQUIRED)
-        find_library(FoundationFramework Foundation REQUIRED)
-        find_library(MetalFramework Metal REQUIRED)
-        target_link_libraries(${TARGET} PRIVATE
-          ${CoreGraphicsFramework}
-          ${FoundationFramework}
-          ${MetalFramework}
-        )
-      elseif(${DEPENDENCY} STREQUAL "spirv-headers")
-        tint_spvheaders_compile_options(${TARGET})
-      elseif(${DEPENDENCY} STREQUAL "spirv-tools")
-        tint_spvtools_compile_options(${TARGET})
-      elseif(${DEPENDENCY} STREQUAL "spirv-opt-internal")
-        target_link_libraries(${TARGET} PRIVATE
-          SPIRV-Tools-opt
-        )
-        target_include_directories(${TARGET} PRIVATE
-          "${TINT_SPIRV_TOOLS_DIR}"
-          "${TINT_SPIRV_TOOLS_DIR}/include"
-          "${TINT_SPIRV_TOOLS_DIR}/source"
-          "${spirv-tools_BINARY_DIR}"
-        )
-      elseif(${DEPENDENCY} STREQUAL "thread")
-        find_package(Threads REQUIRED)
-        target_link_libraries(${TARGET} PRIVATE Threads::Threads)
-      elseif(${DEPENDENCY} STREQUAL "winsock")
-        target_link_libraries(${TARGET} PRIVATE ws2_32)
-      else()
-        message(FATAL_ERROR "unhandled external dependency ${DEPENDENCY}")
       endif()
-    endforeach()
+    elseif(${DEPENDENCY} STREQUAL "glslang-res-limits")
+      target_link_libraries(${TARGET} PRIVATE
+        glslang-default-resource-limits
+      )
+    elseif(${DEPENDENCY} STREQUAL "google-benchmark")
+      set_target_properties(${TARGET} PROPERTIES FOLDER "Benchmarks")
+      target_link_libraries(${TARGET} PRIVATE benchmark::benchmark)
+      if(TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER)
+        target_compile_definitions(${TARGET} PRIVATE
+          "TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER=\"${TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER}\""
+        )
+      endif()
+    elseif(${DEPENDENCY} STREQUAL "gtest")
+      target_include_directories(${TARGET} PRIVATE ${gmock_SOURCE_DIR}/include)
+      target_link_libraries(${TARGET} PRIVATE gmock)
+    elseif(${DEPENDENCY} STREQUAL "metal")
+      find_library(CoreGraphicsFramework CoreGraphics REQUIRED)
+      find_library(FoundationFramework Foundation REQUIRED)
+      find_library(MetalFramework Metal REQUIRED)
+      target_link_libraries(${TARGET} PRIVATE
+        ${CoreGraphicsFramework}
+        ${FoundationFramework}
+        ${MetalFramework}
+      )
+    elseif(${DEPENDENCY} STREQUAL "spirv-headers")
+      tint_spvheaders_compile_options(${TARGET})
+    elseif(${DEPENDENCY} STREQUAL "spirv-tools")
+      tint_spvtools_compile_options(${TARGET})
+    elseif(${DEPENDENCY} STREQUAL "spirv-opt-internal")
+      target_link_libraries(${TARGET} PRIVATE
+        SPIRV-Tools-opt
+      )
+      target_include_directories(${TARGET} PRIVATE
+        "${TINT_SPIRV_TOOLS_DIR}"
+        "${TINT_SPIRV_TOOLS_DIR}/include"
+        "${TINT_SPIRV_TOOLS_DIR}/source"
+        "${spirv-tools_BINARY_DIR}"
+      )
+    elseif(${DEPENDENCY} STREQUAL "thread")
+      find_package(Threads REQUIRED)
+      target_link_libraries(${TARGET} PRIVATE Threads::Threads)
+    elseif(${DEPENDENCY} STREQUAL "winsock")
+      target_link_libraries(${TARGET} PRIVATE ws2_32)
+    else()
+      message(FATAL_ERROR "unhandled external dependency ${DEPENDENCY}")
+    endif()
   endforeach()
 endfunction()
 
@@ -546,6 +525,7 @@
 #   KIND        - The target kind
 #   OUTPUT_NAME - the new name for the target output
 function(tint_target_set_output_name TARGET KIND OUTPUT_NAME)
+  set(TARGET "${TARGET}${TINT_TARGET_SUFFIX}") # Apply suffix
   tint_check_target_is_enabled(IS_ENABLED ${KIND})
   if(NOT IS_ENABLED)
     return() # Target is disabled via build flags
@@ -558,12 +538,18 @@
     add_library(${OUTPUT_NAME} ALIAS ${TARGET})
   elseif(${KIND} STREQUAL test)
     add_library(${OUTPUT_NAME} ALIAS ${TARGET})
+  elseif(${KIND} STREQUAL bench)
+    add_library(${OUTPUT_NAME} ALIAS ${TARGET})
+  elseif(${KIND} STREQUAL fuzz)
+    add_library(${OUTPUT_NAME} ALIAS ${TARGET})
   elseif(${KIND} STREQUAL cmd)
     add_executable(${OUTPUT_NAME} ALIAS ${TARGET})
   elseif(${KIND} STREQUAL test_cmd)
     add_executable(${OUTPUT_NAME} ALIAS ${TARGET})
   elseif(${KIND} STREQUAL bench_cmd)
     add_executable(${OUTPUT_NAME} ALIAS ${TARGET})
+  elseif(${KIND} STREQUAL fuzz_cmd)
+    add_executable(${OUTPUT_NAME} ALIAS ${TARGET})
   else()
     message(FATAL_ERROR "unhandled target kind ${KIND}")
   endif()
@@ -572,7 +558,44 @@
 ################################################################################
 # Include the generated build files
 ################################################################################
-include("BUILD.cmake")
+if(TINT_BUILD_FUZZERS)
+  if(NOT COMPILER_IS_CLANG)
+    message(FATAL_ERROR "TINT_BUILD_FUZZERS can only be enabled with the Clang toolchain")
+  endif()
+
+  # Save the current build flags
+  set(SAVE_TINT_BUILD_CMD_TOOLS ${TINT_BUILD_CMD_TOOLS})
+  set(SAVE_TINT_BUILD_TESTS ${TINT_BUILD_TESTS})
+  set(SAVE_TINT_BUILD_BENCHMARKS ${TINT_BUILD_BENCHMARKS})
+  set(SAVE_TINT_BUILD_FUZZERS ${TINT_BUILD_FUZZERS})
+  set(SAVE_TINT_TARGET_SUFFIX ${TINT_TARGET_SUFFIX})
+
+  # Declare the targets with fuzzers disabled
+  set(TINT_BUILD_FUZZERS FALSE)
+  include("BUILD.cmake")
+
+  # Now redeclare the fuzzers targets with a '_sanitize_fuzzer' target suffix
+  # Enabling TINT_BUILD_FUZZERS will enable the -fsanitize=fuzzer compilation flag for these
+  # targets.
+  set(TINT_TARGET_SUFFIX "_sanitize_fuzzer")
+  set(TINT_BUILD_FUZZERS TRUE)
+  set(TINT_BUILD_CMD_TOOLS FALSE)
+  set(TINT_BUILD_TESTS FALSE)
+  set(TINT_BUILD_BENCHMARKS FALSE)
+  include("BUILD.cmake")
+  add_subdirectory(fuzzers)
+
+  # Restore the build flags
+  set(TINT_BUILD_CMD_TOOLS ${SAVE_TINT_BUILD_CMD_TOOLS})
+  set(TINT_BUILD_TESTS ${SAVE_TINT_BUILD_TESTS})
+  set(TINT_BUILD_BENCHMARKS ${SAVE_TINT_BUILD_BENCHMARKS})
+  set(TINT_BUILD_FUZZERS ${SAVE_TINT_BUILD_FUZZERS})
+  set(TINT_TARGET_SUFFIX ${SAVE_TINT_TARGET_SUFFIX})
+else()
+  # Fuzzers not enabled. Just include BUILD.cmake with the current flags.
+  include("BUILD.cmake")
+endif(TINT_BUILD_FUZZERS)
+
 
 ################################################################################
 # Bespoke target settings
diff --git a/src/tint/api/BUILD.bazel b/src/tint/api/BUILD.bazel
index 885b21d..534fd1b 100644
--- a/src/tint/api/BUILD.bazel
+++ b/src/tint/api/BUILD.bazel
@@ -43,9 +43,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -87,6 +85,16 @@
       "//src/tint/lang/spirv/writer/common",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -117,3 +125,13 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/api/BUILD.cmake b/src/tint/api/BUILD.cmake
index 697a9f0..14e5cda 100644
--- a/src/tint/api/BUILD.cmake
+++ b/src/tint/api/BUILD.cmake
@@ -45,9 +45,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -95,3 +93,15 @@
     tint_lang_spirv_writer_common
   )
 endif(TINT_BUILD_SPV_WRITER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_api lib
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_api lib
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/api/BUILD.gn b/src/tint/api/BUILD.gn
index b1e798a..8e20ff6 100644
--- a/src/tint/api/BUILD.gn
+++ b/src/tint/api/BUILD.gn
@@ -42,9 +42,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/program",
-    "${tint_src_dir}/lang/wgsl/reader",
     "${tint_src_dir}/lang/wgsl/sem",
-    "${tint_src_dir}/lang/wgsl/writer",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
@@ -88,4 +86,12 @@
       "${tint_src_dir}/lang/spirv/writer/common",
     ]
   }
+
+  if (tint_build_wgsl_reader) {
+    deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+  }
 }
diff --git a/src/tint/cmd/BUILD.cmake b/src/tint/cmd/BUILD.cmake
index 2315cbe..889a33e 100644
--- a/src/tint/cmd/BUILD.cmake
+++ b/src/tint/cmd/BUILD.cmake
@@ -23,6 +23,7 @@
 
 include(cmd/bench/BUILD.cmake)
 include(cmd/common/BUILD.cmake)
+include(cmd/fuzz/BUILD.cmake)
 include(cmd/info/BUILD.cmake)
 include(cmd/loopy/BUILD.cmake)
 include(cmd/remote_compile/BUILD.cmake)
diff --git a/src/tint/cmd/bench/BUILD.bazel b/src/tint/cmd/bench/BUILD.bazel
index 62a4335..950f723 100644
--- a/src/tint/cmd/bench/BUILD.bazel
+++ b/src/tint/cmd/bench/BUILD.bazel
@@ -25,59 +25,22 @@
 load("@bazel_skylib//lib:selects.bzl", "selects")
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
-    "benchmark.cc",
-  ],
-  hdrs = [
+    "bench.cc",
     "bench.h",
   ],
   deps = [
-    "//src/tint/lang/core",
-    "//src/tint/lang/core/constant",
-    "//src/tint/lang/core/type",
-    "//src/tint/lang/wgsl",
-    "//src/tint/lang/wgsl/ast",
-    "//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/result",
-    "//src/tint/utils/rtti",
-    "//src/tint/utils/symbol",
-    "//src/tint/utils/text",
-    "//src/tint/utils/traits",
-  ],
-  copts = COPTS,
-  visibility = ["//visibility:public"],
-)
-cc_binary(
-  name = "bench_cmd",
-  srcs = [
-    "main_bench.cc",
-  ],
-  deps = [
     "//src/tint/api/common",
-    "//src/tint/cmd/bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/core:bench",
     "//src/tint/lang/spirv/reader/common",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
-    "//src/tint/lang/wgsl/reader:bench",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
-    "//src/tint/lang/wgsl/writer:bench",
-    "//src/tint/lang/wgsl:bench",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -88,10 +51,59 @@
     "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@benchmark",
+  ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_binary(
+  name = "bench_cmd",
+  srcs = [
+    "main_bench.cc",
+  ],
+  deps = [
+    "//src/tint/cmd/bench:bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/core:bench",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/lang/wgsl:bench",
+    "//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/result",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/rtti:bench",
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
+    "@benchmark",
   ] + select({
     ":tint_build_glsl_writer": [
       "//src/tint/lang/glsl/writer:bench",
@@ -108,13 +120,18 @@
     ],
     "//conditions:default": [],
   }) + select({
-    ":tint_build_spv_reader": [
-      "//src/tint/lang/spirv/reader",
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer:bench",
     ],
     "//conditions:default": [],
   }) + select({
-    ":tint_build_spv_writer": [
-      "//src/tint/lang/spirv/writer:bench",
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader:bench",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer:bench",
     ],
     "//conditions:default": [],
   }),
@@ -147,3 +164,13 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/cmd/bench/BUILD.cmake b/src/tint/cmd/bench/BUILD.cmake
index a6da553..03d6c47 100644
--- a/src/tint/cmd/bench/BUILD.cmake
+++ b/src/tint/cmd/bench/BUILD.cmake
@@ -22,37 +22,6 @@
 ################################################################################
 
 ################################################################################
-# Target:    tint_cmd_bench
-# Kind:      lib
-################################################################################
-tint_add_target(tint_cmd_bench lib
-  cmd/bench/bench.h
-  cmd/bench/benchmark.cc
-)
-
-tint_target_add_dependencies(tint_cmd_bench lib
-  tint_lang_core
-  tint_lang_core_constant
-  tint_lang_core_type
-  tint_lang_wgsl
-  tint_lang_wgsl_ast
-  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_result
-  tint_utils_rtti
-  tint_utils_symbol
-  tint_utils_text
-  tint_utils_traits
-)
-
-################################################################################
 # Target:    tint_cmd_bench_bench_cmd
 # Kind:      bench_cmd
 ################################################################################
@@ -61,22 +30,15 @@
 )
 
 tint_target_add_dependencies(tint_cmd_bench_bench_cmd bench_cmd
-  tint_api_common
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
-  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_core_bench
-  tint_lang_spirv_reader_common
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
-  tint_lang_wgsl_reader_bench
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
-  tint_lang_wgsl_writer_bench
   tint_lang_wgsl_bench
   tint_utils_containers
   tint_utils_diagnostic
@@ -85,7 +47,6 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
-  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_rtti_bench
@@ -94,6 +55,10 @@
   tint_utils_traits
 )
 
+tint_target_add_external_dependencies(tint_cmd_bench_bench_cmd bench_cmd
+  "google-benchmark"
+)
+
 if(TINT_BUILD_GLSL_WRITER)
   tint_target_add_dependencies(tint_cmd_bench_bench_cmd bench_cmd
     tint_lang_glsl_writer_bench
@@ -112,16 +77,79 @@
   )
 endif(TINT_BUILD_MSL_WRITER)
 
-if(TINT_BUILD_SPV_READER)
-  tint_target_add_dependencies(tint_cmd_bench_bench_cmd bench_cmd
-    tint_lang_spirv_reader
-  )
-endif(TINT_BUILD_SPV_READER)
-
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_cmd_bench_bench_cmd bench_cmd
     tint_lang_spirv_writer_bench
   )
 endif(TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_bench_bench_cmd bench_cmd
+    tint_lang_wgsl_reader_bench
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_bench_bench_cmd bench_cmd
+    tint_lang_wgsl_writer_bench
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 tint_target_set_output_name(tint_cmd_bench_bench_cmd bench_cmd "tint_benchmark")
+
+################################################################################
+# Target:    tint_cmd_bench_bench
+# Kind:      bench
+################################################################################
+tint_add_target(tint_cmd_bench_bench bench
+  cmd/bench/bench.cc
+  cmd/bench/bench.h
+)
+
+tint_target_add_dependencies(tint_cmd_bench_bench bench
+  tint_api_common
+  tint_lang_core
+  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_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_cmd_bench_bench bench
+  "google-benchmark"
+)
+
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_cmd_bench_bench bench
+    tint_lang_spirv_reader
+  )
+endif(TINT_BUILD_SPV_READER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_bench_bench bench
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_bench_bench bench
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/cmd/bench/BUILD.gn b/src/tint/cmd/bench/BUILD.gn
index e17e2de..49a79f4 100644
--- a/src/tint/cmd/bench/BUILD.gn
+++ b/src/tint/cmd/bench/BUILD.gn
@@ -25,30 +25,109 @@
 
 import("${tint_src_dir}/tint.gni")
 
-libtint_source_set("bench") {
-  sources = [
-    "bench.h",
-    "benchmark.cc",
-  ]
-  deps = [
-    "${tint_src_dir}/lang/core",
-    "${tint_src_dir}/lang/core/constant",
-    "${tint_src_dir}/lang/core/type",
-    "${tint_src_dir}/lang/wgsl",
-    "${tint_src_dir}/lang/wgsl/ast",
-    "${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/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_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_benchmarks) {
+  tint_unittests_source_set("bench") {
+    sources = [
+      "bench.cc",
+      "bench.h",
+    ]
+    deps = [
+      "${tint_src_dir}:google_benchmark",
+      "${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/spirv/reader/common",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${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" ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+    }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+    }
+  }
+}
+if (tint_build_benchmarks) {
+  test("bench_cmd") {
+    testonly = true
+    output_name = "tint_benchmark"
+    sources = [ "main_bench.cc" ]
+    deps = [
+      "${tint_src_dir}:google_benchmark",
+      "${tint_src_dir}/cmd/bench:bench",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core:bench",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl:bench",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${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/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/rtti:bench",
+      "${tint_src_dir}/utils/symbol",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_glsl_writer) {
+      deps += [ "${tint_src_dir}/lang/glsl/writer:bench" ]
+    }
+
+    if (tint_build_hlsl_writer) {
+      deps += [ "${tint_src_dir}/lang/hlsl/writer:bench" ]
+    }
+
+    if (tint_build_msl_writer) {
+      deps += [ "${tint_src_dir}/lang/msl/writer:bench" ]
+    }
+
+    if (tint_build_spv_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/writer:bench" ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader:bench" ]
+    }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer:bench" ]
+    }
+  }
 }
diff --git a/src/tint/cmd/bench/bench.cc b/src/tint/cmd/bench/bench.cc
new file mode 100644
index 0000000..f36a404
--- /dev/null
+++ b/src/tint/cmd/bench/bench.cc
@@ -0,0 +1,163 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <filesystem>
+#include <iostream>
+#include <utility>
+#include <vector>
+
+#include "src/tint/cmd/bench/bench.h"
+
+#if TINT_BUILD_SPV_READER
+#include "src/tint/lang/spirv/reader/reader.h"
+#endif
+
+#if TINT_BUILD_WGSL_WRITER
+#include "src/tint/lang/wgsl/writer/writer.h"
+#endif
+
+#if TINT_BUILD_WGSL_READER
+#include "src/tint/lang/wgsl/reader/reader.h"
+#endif
+
+#include "src/tint/utils/text/string.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace tint::bench {
+namespace {
+
+std::filesystem::path kInputFileDir;
+
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+Result<std::vector<T>> ReadFile(const std::string& input_file) {
+    FILE* file = nullptr;
+#if defined(_MSC_VER)
+    fopen_s(&file, input_file.c_str(), "rb");
+#else
+    file = fopen(input_file.c_str(), "rb");
+#endif
+    if (!file) {
+        return Failure{"Failed to open " + input_file};
+    }
+
+    fseek(file, 0, SEEK_END);
+    const auto file_size = static_cast<size_t>(ftell(file));
+    if (0 != (file_size % sizeof(T))) {
+        StringStream err;
+        err << "File " << input_file
+            << " does not contain an integral number of objects: " << file_size
+            << " bytes in the file, require " << sizeof(T) << " bytes per object";
+        fclose(file);
+        return Failure{err.str()};
+    }
+    fseek(file, 0, SEEK_SET);
+
+    std::vector<T> buffer;
+    buffer.resize(file_size / sizeof(T));
+
+    size_t bytes_read = fread(buffer.data(), 1, file_size, file);
+    fclose(file);
+    if (bytes_read != file_size) {
+        return Failure{"Failed to read " + input_file};
+    }
+
+    return buffer;
+}
+
+bool FindBenchmarkInputDir() {
+    // Attempt to find the benchmark input files by searching up from the current
+    // working directory.
+    auto path = std::filesystem::current_path();
+    while (std::filesystem::is_directory(path)) {
+        auto test = path / "test" / "tint" / "benchmark";
+        if (std::filesystem::is_directory(test)) {
+            kInputFileDir = test;
+            return true;
+        }
+        auto parent = path.parent_path();
+        if (path == parent) {
+            break;
+        }
+        path = parent;
+    }
+    return false;
+}
+
+}  // namespace
+
+bool Initialize() {
+    if (!FindBenchmarkInputDir()) {
+        std::cerr << "failed to locate benchmark input files" << std::endl;
+        return false;
+    }
+    return true;
+}
+
+Result<Source::File> LoadInputFile(std::string name) {
+    auto path = std::filesystem::path(name).is_absolute() ? name : (kInputFileDir / name).string();
+    if (tint::HasSuffix(path, ".wgsl")) {
+#if TINT_BUILD_WGSL_READER
+        auto data = ReadFile<uint8_t>(path);
+        if (!data) {
+            return data.Failure();
+        }
+        return tint::Source::File(path, std::string(data->begin(), data->end()));
+#else
+        return Failure{"cannot load " + path + " file as TINT_BUILD_WGSL_READER is not enabled"};
+#endif
+    }
+    if (tint::HasSuffix(path, ".spv")) {
+#if !TINT_BUILD_SPV_READER
+        return Failure{"cannot load " + path + " as TINT_BUILD_SPV_READER is not enabled"};
+#elif !TINT_BUILD_WGSL_WRITER
+        return Failure{"cannot load " + path + " as TINT_BUILD_WGSL_WRITER is not enabled"};
+#else
+
+        auto spirv = ReadFile<uint32_t>(path);
+        if (spirv) {
+            auto program = tint::spirv::reader::Read(spirv.Get(), {});
+            if (!program.IsValid()) {
+                return Failure{program.Diagnostics()};
+            }
+            auto result = tint::wgsl::writer::Generate(program, {});
+            if (!result) {
+                return result.Failure();
+            }
+            return tint::Source::File(path, result->wgsl);
+        }
+        return spirv.Failure();
+#endif
+    }
+    return Failure{"unsupported file extension: '" + name + "'"};
+}
+
+Result<ProgramAndFile> LoadProgram(std::string name) {
+    auto res = bench::LoadInputFile(name);
+    if (!res) {
+        return res.Failure();
+    }
+    auto file = std::make_unique<Source::File>(res.Get());
+    auto program = wgsl::reader::Parse(file.get());
+    if (!program.IsValid()) {
+        return Failure{program.Diagnostics()};
+    }
+    return ProgramAndFile{std::move(program), std::move(file)};
+}
+
+}  // namespace tint::bench
diff --git a/src/tint/cmd/bench/bench.h b/src/tint/cmd/bench/bench.h
index e813c4a..fc15e20 100644
--- a/src/tint/cmd/bench/bench.h
+++ b/src/tint/cmd/bench/bench.h
@@ -21,16 +21,12 @@
 
 #include "benchmark/benchmark.h"
 #include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/macros/concat.h"
+#include "src/tint/utils/result/result.h"
 
 namespace tint::bench {
 
-/// Error indicates an operation did not complete successfully.
-struct Error {
-    /// The error message.
-    std::string msg;
-};
-
 /// ProgramAndFile holds a Program and a Source::File.
 struct ProgramAndFile {
     /// The tint program parsed from file.
@@ -39,18 +35,23 @@
     std::unique_ptr<Source::File> file;
 };
 
+/// Initializes the internal state for benchmarking.
+/// Must be called once by the benchmark executable entry point.
+/// @returns true on success, false of failure
+bool Initialize();
+
 /// LoadInputFile attempts to load a benchmark input file with the given file
 /// name. Accepts files with the .wgsl and .spv extension.
 /// SPIR-V files are automatically converted to WGSL.
 /// @param name the file name
-/// @returns either the loaded Source::File or an Error
-std::variant<Source::File, Error> LoadInputFile(std::string name);
+/// @returns the loaded Source::File
+Result<Source::File> LoadInputFile(std::string name);
 
 /// LoadInputFile attempts to load a benchmark input program with the given file
 /// name.
 /// @param name the file name
-/// @returns either the loaded Program or an Error
-std::variant<ProgramAndFile, Error> LoadProgram(std::string name);
+/// @returns the loaded Program
+Result<ProgramAndFile> LoadProgram(std::string name);
 
 // If TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER is defined, include that to
 // declare the TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS() and TINT_BENCHMARK_EXTERNAL_SPV_PROGRAMS()
@@ -64,7 +65,7 @@
 #endif
 
 /// Declares a benchmark with the given function and WGSL file name
-#define TINT_BENCHMARK_WGSL_PROGRAM(FUNC, WGSL_NAME) BENCHMARK_CAPTURE(FUNC, WGSL_NAME, WGSL_NAME);
+#define TINT_BENCHMARK_WGSL_PROGRAM(FUNC, WGSL_NAME) BENCHMARK_CAPTURE(FUNC, WGSL_NAME, WGSL_NAME)
 
 /// Declares a set of benchmarks for the given function using a list of WGSL files.
 #define TINT_BENCHMARK_WGSL_PROGRAMS(FUNC)                                   \
@@ -83,7 +84,8 @@
 /// Declares a set of benchmarks for the given function using a list of WGSL and SPIR-V files.
 #define TINT_BENCHMARK_PROGRAMS(FUNC)  \
     TINT_BENCHMARK_WGSL_PROGRAMS(FUNC) \
-    TINT_BENCHMARK_SPV_PROGRAMS(FUNC)
+    TINT_BENCHMARK_SPV_PROGRAMS(FUNC)  \
+    TINT_REQUIRE_SEMICOLON
 
 }  // namespace tint::bench
 
diff --git a/src/tint/cmd/bench/main_bench.cc b/src/tint/cmd/bench/main_bench.cc
index e04dc38..99d6c48 100644
--- a/src/tint/cmd/bench/main_bench.cc
+++ b/src/tint/cmd/bench/main_bench.cc
@@ -12,134 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <filesystem>
-#include <iostream>
-#include <utility>
-#include <vector>
-
 #include "src/tint/cmd/bench/bench.h"
-#include "src/tint/lang/spirv/reader/reader.h"
-#include "src/tint/lang/wgsl/reader/reader.h"
-#include "src/tint/lang/wgsl/writer/writer.h"
-#include "src/tint/utils/text/string.h"
-#include "src/tint/utils/text/string_stream.h"
-
-namespace tint::bench {
-namespace {
-
-std::filesystem::path kInputFileDir;
-
-/// Copies the content from the file named `input_file` to `buffer`,
-/// assuming each element in the file is of type `T`.  If any error occurs,
-/// writes error messages to the standard error stream and returns false.
-/// Assumes the size of a `T` object is divisible by its required alignment.
-/// @returns true if we successfully read the file.
-template <typename T>
-std::variant<std::vector<T>, Error> ReadFile(const std::string& input_file) {
-    FILE* file = nullptr;
-#if defined(_MSC_VER)
-    fopen_s(&file, input_file.c_str(), "rb");
-#else
-    file = fopen(input_file.c_str(), "rb");
-#endif
-    if (!file) {
-        return Error{"Failed to open " + input_file};
-    }
-
-    fseek(file, 0, SEEK_END);
-    const auto file_size = static_cast<size_t>(ftell(file));
-    if (0 != (file_size % sizeof(T))) {
-        StringStream err;
-        err << "File " << input_file
-            << " does not contain an integral number of objects: " << file_size
-            << " bytes in the file, require " << sizeof(T) << " bytes per object";
-        fclose(file);
-        return Error{err.str()};
-    }
-    fseek(file, 0, SEEK_SET);
-
-    std::vector<T> buffer;
-    buffer.resize(file_size / sizeof(T));
-
-    size_t bytes_read = fread(buffer.data(), 1, file_size, file);
-    fclose(file);
-    if (bytes_read != file_size) {
-        return Error{"Failed to read " + input_file};
-    }
-
-    return buffer;
-}
-
-bool FindBenchmarkInputDir() {
-    // Attempt to find the benchmark input files by searching up from the current
-    // working directory.
-    auto path = std::filesystem::current_path();
-    while (std::filesystem::is_directory(path)) {
-        auto test = path / "test" / "tint" / "benchmark";
-        if (std::filesystem::is_directory(test)) {
-            kInputFileDir = test;
-            return true;
-        }
-        auto parent = path.parent_path();
-        if (path == parent) {
-            break;
-        }
-        path = parent;
-    }
-    return false;
-}
-
-}  // namespace
-
-std::variant<tint::Source::File, Error> LoadInputFile(std::string name) {
-    auto path = std::filesystem::path(name).is_absolute() ? name : (kInputFileDir / name).string();
-    if (tint::HasSuffix(path, ".wgsl")) {
-        auto data = ReadFile<uint8_t>(path);
-        if (auto* buf = std::get_if<std::vector<uint8_t>>(&data)) {
-            return tint::Source::File(path, std::string(buf->begin(), buf->end()));
-        }
-        return std::get<Error>(data);
-    }
-    if (tint::HasSuffix(path, ".spv")) {
-        auto spirv = ReadFile<uint32_t>(path);
-        if (auto* buf = std::get_if<std::vector<uint32_t>>(&spirv)) {
-            auto program = tint::spirv::reader::Read(*buf, {});
-            if (!program.IsValid()) {
-                return Error{program.Diagnostics().str()};
-            }
-            auto result = tint::wgsl::writer::Generate(program, {});
-            if (!result) {
-                return Error{result.Failure().reason.str()};
-            }
-            return tint::Source::File(path, result->wgsl);
-        }
-        return std::get<Error>(spirv);
-    }
-    return Error{"unsupported file extension: '" + name + "'"};
-}
-
-std::variant<ProgramAndFile, Error> LoadProgram(std::string name) {
-    auto res = bench::LoadInputFile(name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        return *err;
-    }
-    auto file = std::make_unique<Source::File>(std::move(std::get<Source::File>(res)));
-    auto program = wgsl::reader::Parse(file.get());
-    if (program.Diagnostics().contains_errors()) {
-        return Error{program.Diagnostics().str()};
-    }
-    return ProgramAndFile{std::move(program), std::move(file)};
-}
-
-}  // namespace tint::bench
 
 int main(int argc, char** argv) {
     benchmark::Initialize(&argc, argv);
     if (benchmark::ReportUnrecognizedArguments(argc, argv)) {
         return 1;
     }
-    if (!tint::bench::FindBenchmarkInputDir()) {
-        std::cerr << "failed to locate benchmark input files" << std::endl;
+    if (!tint::bench::Initialize()) {
         return 1;
     }
     benchmark::RunSpecifiedBenchmarks();
diff --git a/src/tint/cmd/common/BUILD.bazel b/src/tint/cmd/common/BUILD.bazel
index 5ff1ba3..8f0fa38 100644
--- a/src/tint/cmd/common/BUILD.bazel
+++ b/src/tint/cmd/common/BUILD.bazel
@@ -45,9 +45,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -71,6 +69,16 @@
       "@spirv_tools",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -122,6 +130,16 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
 selects.config_setting_group(
     name = "tint_build_spv_reader_or_tint_build_spv_writer",
     match_any = [
diff --git a/src/tint/cmd/common/BUILD.cmake b/src/tint/cmd/common/BUILD.cmake
index 7bd7799..dd7a96f 100644
--- a/src/tint/cmd/common/BUILD.cmake
+++ b/src/tint/cmd/common/BUILD.cmake
@@ -44,9 +44,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -74,6 +72,18 @@
   )
 endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_common lib
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_common lib
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 ################################################################################
 # Target:    tint_cmd_common_test
 # Kind:      test
diff --git a/src/tint/cmd/common/BUILD.gn b/src/tint/cmd/common/BUILD.gn
index 5bca6c4..d4d0ea3 100644
--- a/src/tint/cmd/common/BUILD.gn
+++ b/src/tint/cmd/common/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -48,9 +48,7 @@
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
-    "${tint_src_dir}/lang/wgsl/reader",
     "${tint_src_dir}/lang/wgsl/sem",
-    "${tint_src_dir}/lang/wgsl/writer",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
@@ -76,10 +74,17 @@
       "${tint_spirv_tools_dir}:spvtools_val",
     ]
   }
+
+  if (tint_build_wgsl_reader) {
+    deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+  }
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "generate_external_texture_bindings_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/cmd/fuzz/BUILD.bazel b/src/tint/cmd/fuzz/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/cmd/fuzz/BUILD.bazel
@@ -0,0 +1,26 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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")
+
diff --git a/src/tint/cmd/fuzz/BUILD.cmake b/src/tint/cmd/fuzz/BUILD.cmake
new file mode 100644
index 0000000..2f0dfdf
--- /dev/null
+++ b/src/tint/cmd/fuzz/BUILD.cmake
@@ -0,0 +1,24 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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
+################################################################################
+
+include(cmd/fuzz/wgsl/BUILD.cmake)
diff --git a/src/tint/cmd/fuzz/BUILD.gn b/src/tint/cmd/fuzz/BUILD.gn
new file mode 100644
index 0000000..b8a448e
--- /dev/null
+++ b/src/tint/cmd/fuzz/BUILD.gn
@@ -0,0 +1,26 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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")
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.bazel b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
new file mode 100644
index 0000000..1b32106
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
@@ -0,0 +1,31 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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")
+
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.cfg b/src/tint/cmd/fuzz/wgsl/BUILD.cfg
new file mode 100644
index 0000000..de92f71
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.cfg
@@ -0,0 +1,12 @@
+{
+    "condition": "tint_build_wgsl_reader",
+    "fuzz_cmd": {
+        /* The Tint fuzzer executable for WGSL-input. */
+        "OutputName": "tint_wgsl_fuzzer",
+        "AdditionalDependencies": {
+            /* Depend on all the fuzz targets to pull them all together. */
+            "Internal": [ "**:fuzz" ]
+        }
+    }
+}
+
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.cmake b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
new file mode 100644
index 0000000..8b294e9
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
@@ -0,0 +1,107 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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
+################################################################################
+
+if(TINT_BUILD_WGSL_READER)
+################################################################################
+# Target:    tint_cmd_fuzz_wgsl_fuzz_cmd
+# Kind:      fuzz_cmd
+# Condition: TINT_BUILD_WGSL_READER
+################################################################################
+tint_add_target(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+  cmd/fuzz/wgsl/main_fuzz.cc
+)
+
+tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  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_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_cmd_fuzz_wgsl_fuzz
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+tint_target_set_output_name(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd "tint_wgsl_fuzzer")
+
+endif(TINT_BUILD_WGSL_READER)
+if(TINT_BUILD_WGSL_READER)
+################################################################################
+# Target:    tint_cmd_fuzz_wgsl_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_WGSL_READER
+################################################################################
+tint_add_target(tint_cmd_fuzz_wgsl_fuzz fuzz
+  cmd/fuzz/wgsl/wgsl_fuzz.cc
+  cmd/fuzz/wgsl/wgsl_fuzz.h
+)
+
+tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz fuzz
+  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_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
+)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz fuzz
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.gn b/src/tint/cmd/fuzz/wgsl/BUILD.gn
new file mode 100644
index 0000000..cbf6274
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.gn
@@ -0,0 +1,93 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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_wgsl_reader) {
+  tint_fuzz_source_set("fuzz") {
+    sources = [
+      "wgsl_fuzz.cc",
+      "wgsl_fuzz.h",
+    ]
+    deps = [
+      "${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/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_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+    }
+  }
+}
+if (tint_build_wgsl_reader) {
+  tint_fuzzer_test("wgsl") {
+    output_name = "tint_wgsl_fuzzer"
+    sources = [ "main_fuzz.cc" ]
+    deps = [
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${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/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/symbol",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
+    }
+  }
+}
diff --git a/src/tint/cmd/fuzz/wgsl/dictionary.txt b/src/tint/cmd/fuzz/wgsl/dictionary.txt
new file mode 100644
index 0000000..59742d7
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/dictionary.txt
@@ -0,0 +1,296 @@
+"!"
+"!="
+"%"
+"%="
+"&"
+"&&"
+"&="
+"("
+")"
+"*"
+"*="
+"+"
+"++"
+"+="
+","
+"-"
+"--"
+"-="
+"->"
+"."
+"/"
+"/="
+":"
+";"
+"<"
+"<<"
+"<<="
+"<="
+"="
+"=="
+">"
+">="
+">>"
+">>="
+"@"
+"["
+"]"
+"^"
+"^="
+"_"
+"{"
+"|"
+"|="
+"||"
+"}"
+"~"
+"a"
+"abs"
+"acos"
+"acosh"
+"@align"
+"all"
+"any"
+"array"
+"arrayLength"
+"asin"
+"asinh"
+"atan"
+"atan2"
+"atanh"
+"atomic"
+"atomicAdd"
+"atomicAnd"
+"atomicLoad"
+"atomicMax"
+"atomicMin"
+"atomicOr"
+"atomicStore"
+"atomicSub"
+"atomicXor"
+"b"
+"@binding"
+"bitcast"
+"bool"
+"break"
+"@builtin"
+"@builtin(frag_depth)"
+"@builtin(front_facing)"
+"@builtin(global_invocation_id)"
+"@builtin(instance_index)"
+"@builtin(local_invocation_id)"
+"@builtin(local_invocation_index)"
+"@builtin(num_workgroups)"
+"@builtin(position)"
+"@builtin(sample_index)"
+"@builtin(sample_mask)"
+"@builtin(vertex_index)"
+"@builtin(workgroup_id)"
+"case"
+"ceil"
+"center"
+"centroid"
+"clamp"
+"@compute"
+"@const"
+"const"
+"continue"
+"continuing"
+"cos"
+"cosh"
+"countLeadingZeros"
+"countOneBits"
+"countTrailingZeros"
+"cross"
+"default"
+"degrees"
+"determinant"
+"discard"
+"distance"
+"dot"
+"dot4I8Packed"
+"dot4U8Packed"
+"dpdx"
+"dpdxCoarse"
+"dpdxFine"
+"dpdy"
+"dpdyCoarse"
+"dpdyFine"
+"else"
+"enable"
+"exp"
+"exp2"
+"extractBits"
+"f16"
+"f32"
+"faceForward"
+"fallthrough"
+"false"
+"firstLeadingBit"
+"firstTrailingBit"
+"flat"
+"floor"
+"fma"
+"fn"
+"for"
+"fract"
+"frag_depth"
+"@fragment"
+"frexp"
+"front_facing"
+"function"
+"fwidth"
+"fwidthCoarse"
+"fwidthFine"
+"g"
+"global_invocation_id"
+"@group"
+"i32"
+"@id"
+"if"
+"insertBits"
+"instance_index"
+"@interpolate"
+"@invariant"
+"inverseSqrt"
+"ldexp"
+"length"
+"let"
+"linear"
+"local_invocation_id"
+"local_invocation_index"
+"@location"
+"log"
+"log2"
+"loop"
+"mat2x2"
+"mat2x3"
+"mat2x4"
+"mat3x2"
+"mat3x3"
+"mat3x4"
+"mat4x2"
+"mat4x3"
+"mat4x4"
+"max"
+"min"
+"mix"
+"modf"
+"normalize"
+"num_workgroups"
+"override"
+"pack2x16float"
+"pack2x16snorm"
+"pack2x16unorm"
+"pack4x8snorm"
+"pack4x8unorm"
+"perspective"
+"position"
+"pow"
+"private"
+"ptr"
+"quantizeToF16"
+"r"
+"r32float"
+"r32sint"
+"r32uint"
+"radians"
+"read"
+"read_write"
+"reflect"
+"refract"
+"return"
+"reverseBits"
+"rg32float"
+"rg32sint"
+"rg32uint"
+"rgba16float"
+"rgba16sint"
+"rgba16uint"
+"rgba32float"
+"rgba32sint"
+"rgba32uint"
+"rgba8sint"
+"rgba8snorm"
+"rgba8uint"
+"rgba8unorm"
+"round"
+"sample"
+"sample_index"
+"sample_mask"
+"sampler"
+"sampler_comparison"
+"saturate"
+"select"
+"sign"
+"sin"
+"sinh"
+"@size"
+"smoothstep"
+"sqrt"
+"staticAssert"
+"step"
+"storage"
+"storageBarrier"
+"struct"
+"switch"
+"tan"
+"tanh"
+"texture_1d"
+"texture_2d"
+"texture_2d_array"
+"texture_3d"
+"texture_cube"
+"texture_cube_array"
+"texture_depth_2d"
+"texture_depth_2d_array"
+"texture_depth_cube"
+"texture_depth_cube_array"
+"texture_depth_multisampled_2d"
+"textureDimensions"
+"textureGather"
+"textureGatherCompare"
+"textureLoad"
+"texture_multisampled_2d"
+"textureNumLayers"
+"textureNumLevels"
+"textureNumSamples"
+"textureSample"
+"textureSampleBias"
+"textureSampleCompare"
+"textureSampleCompareLevel"
+"textureSampleGrad"
+"textureSampleLevel"
+"texture_storage_1d"
+"texture_storage_2d"
+"texture_storage_2d_array"
+"texture_storage_3d"
+"textureStore"
+"transpose"
+"true"
+"trunc"
+"type"
+"u32"
+"uniform"
+"unpack2x16float"
+"unpack2x16snorm"
+"unpack2x16unorm"
+"unpack4x8snorm"
+"unpack4x8unorm"
+"var"
+"vec2"
+"vec3"
+"vec4"
+"@vertex"
+"vertex_index"
+"w"
+"while"
+"workgroup"
+"workgroupBarrier"
+"workgroupUniformLoad"
+"workgroup_id"
+"@workgroup_size"
+"write"
+"x"
+"y"
+"z"
diff --git a/src/tint/cmd/fuzz/wgsl/generate_wgsl_corpus.py b/src/tint/cmd/fuzz/wgsl/generate_wgsl_corpus.py
new file mode 100644
index 0000000..983acd9
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/generate_wgsl_corpus.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Collect all .wgsl files under a given directory and copy them to a given
+# corpus directory, flattening their file names by replacing path
+# separators with underscores. If the output directory already exists, it
+# will be deleted and re-created. Files ending with ".expected.spvasm" are
+# skipped.
+#
+# The intended use of this script is to generate a corpus of WGSL shaders
+# for fuzzing.
+#
+# Usage:
+#    generate_wgsl_corpus.py <input_dir> <corpus_dir>
+
+import optparse
+import os
+import pathlib
+import shutil
+import sys
+
+
+def list_wgsl_files(root_search_dir):
+    for root, folders, files in os.walk(root_search_dir):
+        for filename in folders + files:
+            if pathlib.Path(filename).suffix == '.wgsl':
+                yield os.path.join(root, filename)
+
+
+def main():
+    parser = optparse.OptionParser(
+        usage="usage: %prog [option] input-dir output-dir")
+    parser.add_option('--stamp', dest='stamp', help='stamp file')
+    options, args = parser.parse_args(sys.argv[1:])
+    if len(args) != 2:
+        parser.error("incorrect number of arguments")
+    input_dir: str = os.path.abspath(args[0].rstrip(os.sep))
+    corpus_dir: str = os.path.abspath(args[1])
+    if os.path.exists(corpus_dir):
+        shutil.rmtree(corpus_dir)
+    os.makedirs(corpus_dir)
+    for in_file in list_wgsl_files(input_dir):
+        if in_file.endswith(".expected.wgsl"):
+            continue
+        out_file = in_file[len(input_dir) + 1:].replace(os.sep, '_')
+        shutil.copy(in_file, corpus_dir + os.sep + out_file)
+    if options.stamp:
+        pathlib.Path(options.stamp).touch(mode=0o644, exist_ok=True)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/src/tint/cmd/bench/benchmark.cc b/src/tint/cmd/fuzz/wgsl/main_fuzz.cc
similarity index 68%
rename from src/tint/cmd/bench/benchmark.cc
rename to src/tint/cmd/fuzz/wgsl/main_fuzz.cc
index ad8658b..8f3c3ab 100644
--- a/src/tint/cmd/bench/benchmark.cc
+++ b/src/tint/cmd/fuzz/wgsl/main_fuzz.cc
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#if defined(__clang__)
-#pragma clang diagnostic ignored "-Wmissing-variable-declarations"
-#endif
+#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
 
-// A placeholder symbol used to emit a symbol for this lib target.
-int tint_cmd_bench_symbol = 1;
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    if (size > 0) {
+        std::string_view wgsl(reinterpret_cast<const char*>(data), size);
+        tint::fuzz::wgsl::Run(wgsl);
+    }
+    return 0;
+}
diff --git a/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.cc b/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.cc
new file mode 100644
index 0000000..50907f6
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+
+#include <iostream>
+
+#include "src/tint/lang/wgsl/reader/reader.h"
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/macros/defer.h"
+#include "src/tint/utils/macros/static_init.h"
+
+namespace tint::fuzz::wgsl {
+namespace {
+
+Vector<ProgramFuzzer, 32> fuzzers;
+std::string_view currently_running;
+
+[[noreturn]] void TintInternalCompilerErrorReporter(const tint::InternalCompilerError& err) {
+    std::cerr << "ICE while running fuzzer: '" << currently_running << "'" << std::endl;
+    std::cerr << err.Error() << std::endl;
+    __builtin_trap();
+}
+
+}  // namespace
+
+void Register(const ProgramFuzzer& fuzzer) {
+    fuzzers.Push(fuzzer);
+}
+
+void Run(std::string_view wgsl) {
+    tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
+
+    // Ensure that fuzzers are sorted. Without this, the fuzzers may be registered in any order,
+    // leading to non-determinism, which we must avoid.
+    TINT_STATIC_INIT(fuzzers.Sort([](auto& a, auto& b) { return a.name < b.name; }));
+
+    // Create a Source::File to hand to the parser.
+    tint::Source::File file("test.wgsl", wgsl);
+
+    // Parse the WGSL program.
+    auto program = tint::wgsl::reader::Parse(&file);
+    if (!program.IsValid()) {
+        return;
+    }
+
+    // Run each of the program fuzzer functions
+    TINT_DEFER(currently_running = "");
+    for (auto& fuzzer : fuzzers) {
+        currently_running = fuzzer.name;
+        fuzzer.fn(program);
+    }
+}
+
+}  // namespace tint::fuzz::wgsl
diff --git a/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h b/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h
new file mode 100644
index 0000000..9fb5fd4
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h
@@ -0,0 +1,51 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_CMD_FUZZ_WGSL_WGSL_FUZZ_H_
+#define SRC_TINT_CMD_FUZZ_WGSL_WGSL_FUZZ_H_
+
+#include <string>
+
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/utils/containers/slice.h"
+#include "src/tint/utils/macros/static_init.h"
+
+namespace tint::fuzz::wgsl {
+
+/// ProgramFuzzer describes a fuzzer function that takes a WGSL program as input
+struct ProgramFuzzer {
+    /// The function signature
+    using Fn = void(const Program&);
+
+    /// Name of the fuzzer function
+    std::string_view name;
+    /// The fuzzer function pointer
+    Fn* fn = nullptr;
+};
+
+/// Runs all the registered WGSL fuzzers with the supplied WGSL
+/// @param wgsl the input WGSL
+void Run(std::string_view wgsl);
+
+/// Registers the fuzzer function with the WGSL fuzzer executable.
+/// @param fuzzer the fuzzer
+void Register(const ProgramFuzzer& fuzzer);
+
+/// TINT_WGSL_PROGRAM_FUZZER registers the fuzzer function to run as part of `tint_wgsl_fuzzer`
+#define TINT_WGSL_PROGRAM_FUZZER(FUNCTION) \
+    TINT_STATIC_INIT(::tint::fuzz::wgsl::Register({#FUNCTION, FUNCTION}))
+
+}  // namespace tint::fuzz::wgsl
+
+#endif  // SRC_TINT_CMD_FUZZ_WGSL_WGSL_FUZZ_H_
diff --git a/src/tint/cmd/info/BUILD.gn b/src/tint/cmd/info/BUILD.gn
index e9d84ce..79018c2 100644
--- a/src/tint/cmd/info/BUILD.gn
+++ b/src/tint/cmd/info/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-executable("info") {
+tint_executable("info") {
   output_name = "tint_info"
   sources = [ "main.cc" ]
   deps = [
diff --git a/src/tint/cmd/loopy/BUILD.bazel b/src/tint/cmd/loopy/BUILD.bazel
index 194e728..2725298 100644
--- a/src/tint/cmd/loopy/BUILD.bazel
+++ b/src/tint/cmd/loopy/BUILD.bazel
@@ -44,10 +44,7 @@
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -87,6 +84,18 @@
     ":tint_build_spv_writer": [
       "//src/tint/lang/spirv/writer",
       "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/helpers",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
     ],
     "//conditions:default": [],
   }),
@@ -119,3 +128,13 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/cmd/loopy/BUILD.cmake b/src/tint/cmd/loopy/BUILD.cmake
index 86eee38..3e559b7 100644
--- a/src/tint/cmd/loopy/BUILD.cmake
+++ b/src/tint/cmd/loopy/BUILD.cmake
@@ -45,10 +45,7 @@
   tint_lang_wgsl_helpers
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -94,7 +91,21 @@
   tint_target_add_dependencies(tint_cmd_loopy_cmd cmd
     tint_lang_spirv_writer
     tint_lang_spirv_writer_common
+    tint_lang_spirv_writer_helpers
   )
 endif(TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_loopy_cmd cmd
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_loopy_cmd cmd
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 tint_target_set_output_name(tint_cmd_loopy_cmd cmd "tint_loopy")
diff --git a/src/tint/cmd/loopy/BUILD.gn b/src/tint/cmd/loopy/BUILD.gn
index 4c54f72..f8b76c9 100644
--- a/src/tint/cmd/loopy/BUILD.gn
+++ b/src/tint/cmd/loopy/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-executable("loopy") {
+tint_executable("loopy") {
   output_name = "tint_loopy"
   sources = [ "main.cc" ]
   deps = [
@@ -44,10 +44,7 @@
     "${tint_src_dir}/lang/wgsl/helpers",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
-    "${tint_src_dir}/lang/wgsl/reader",
-    "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
     "${tint_src_dir}/lang/wgsl/sem",
-    "${tint_src_dir}/lang/wgsl/writer",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
@@ -89,6 +86,18 @@
     deps += [
       "${tint_src_dir}/lang/spirv/writer",
       "${tint_src_dir}/lang/spirv/writer/common",
+      "${tint_src_dir}/lang/spirv/writer/helpers",
     ]
   }
+
+  if (tint_build_wgsl_reader) {
+    deps += [
+      "${tint_src_dir}/lang/wgsl/reader",
+      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+    ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+  }
 }
diff --git a/src/tint/cmd/loopy/main.cc b/src/tint/cmd/loopy/main.cc
index e488fad..31cd2dc 100644
--- a/src/tint/cmd/loopy/main.cc
+++ b/src/tint/cmd/loopy/main.cc
@@ -18,7 +18,6 @@
 #include "src/tint/cmd/common/generate_external_texture_bindings.h"
 #include "src/tint/cmd/common/helper.h"
 #include "src/tint/lang/core/ir/module.h"
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 
 #if TINT_BUILD_GLSL_WRITER
 #include "src/tint/lang/glsl/writer/writer.h"
@@ -37,10 +36,12 @@
 #endif  // TINT_BUILD_SPV_READER
 
 #if TINT_BUILD_SPV_WRITER
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
 #if TINT_BUILD_WGSL_READER
+#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
 #endif  // TINT_BUILD_WGSL_READER
 
@@ -192,8 +193,7 @@
 bool GenerateSpirv(const tint::Program& program) {
 #if TINT_BUILD_SPV_WRITER
     tint::spirv::writer::Options gen_options;
-    gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(program);
+    gen_options.bindings = tint::spirv::writer::GenerateBindings(program);
     auto result = tint::spirv::writer::Generate(program, gen_options);
     if (!result) {
         tint::cmd::PrintWGSL(std::cerr, program);
@@ -231,12 +231,15 @@
 /// Generate MSL code for a program.
 /// @param program the program to generate
 /// @returns true on success
-bool GenerateMsl(const tint::Program& program) {
-#if TINT_BUILD_MSL_WRITER
+bool GenerateMsl([[maybe_unused]] const tint::Program& program) {
+#if !TINT_BUILD_MSL_WRITER
+    std::cerr << "MSL writer not enabled in tint build" << std::endl;
+    return false;
+#else
     // Remap resource numbers to a flat namespace.
     // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
     const tint::Program* input_program = &program;
-    auto flattened = tint::writer::FlattenBindings(program);
+    auto flattened = tint::wgsl::FlattenBindings(program);
     if (flattened) {
         input_program = &*flattened;
     }
@@ -257,11 +260,7 @@
     }
 
     return true;
-#else
-    (void)program;
-    std::cerr << "MSL writer not enabled in tint build" << std::endl;
-    return false;
-#endif  // TINT_BUILD_MSL_WRITER
+#endif
 }
 
 /// Generate HLSL code for a program.
@@ -381,6 +380,7 @@
     opts.filename = options.input_filename;
 
     auto info = tint::cmd::LoadProgramInfo(opts);
+#if TINT_BUILD_WGSL_READER
     {
         uint32_t loop_count = 1;
         if (options.loop == Looper::kIRGenerate) {
@@ -393,6 +393,7 @@
             }
         }
     }
+#endif  // TINT_BUILD_WGSL_READER
 
     bool success = false;
     {
diff --git a/src/tint/cmd/remote_compile/BUILD.gn b/src/tint/cmd/remote_compile/BUILD.gn
index 6d5be7d..58dfd2e 100644
--- a/src/tint/cmd/remote_compile/BUILD.gn
+++ b/src/tint/cmd/remote_compile/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-executable("remote_compile") {
+tint_executable("remote_compile") {
   output_name = "tint_remote_compile"
   sources = [ "main.cc" ]
   deps = [
diff --git a/src/tint/cmd/remote_compile/main.cc b/src/tint/cmd/remote_compile/main.cc
index 95e2c48..12611af 100644
--- a/src/tint/cmd/remote_compile/main.cc
+++ b/src/tint/cmd/remote_compile/main.cc
@@ -23,7 +23,10 @@
 #include <type_traits>
 #include <vector>
 
+#if TINT_BUILD_MSL_WRITER
 #include "src/tint/lang/msl/validate/val.h"
+#endif
+
 #include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/socket/socket.h"
 
@@ -421,7 +424,7 @@
                     DEBUG("%s\n", stream.error.c_str());
                     return;
                 }
-#ifdef __APPLE__
+#if TINT_BUILD_MSL_WRITER && defined(__APPLE__)
                 if (req.language == SourceLanguage::MSL) {
                     auto version = tint::msl::validate::MslVersion::kMsl_1_2;
                     if (req.version_major == 2 && req.version_minor == 1) {
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index 60aebb2..81c6552 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -38,17 +38,12 @@
     "//src/tint/lang/core/type:test",
     "//src/tint/lang/core:test",
     "//src/tint/lang/spirv/ir:test",
-    "//src/tint/lang/wgsl/ast/transform:test",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/helpers:test",
-    "//src/tint/lang/wgsl/inspector:test",
     "//src/tint/lang/wgsl/program:test",
     "//src/tint/lang/wgsl/reader/lower:test",
-    "//src/tint/lang/wgsl/reader/parser:test",
-    "//src/tint/lang/wgsl/reader/program_to_ir:test",
     "//src/tint/lang/wgsl/resolver:test",
     "//src/tint/lang/wgsl/sem:test",
-    "//src/tint/lang/wgsl/writer/ast_printer:test",
     "//src/tint/lang/wgsl/writer/ir_to_program:test",
     "//src/tint/lang/wgsl/writer/raise:test",
     "//src/tint/lang/wgsl:test",
@@ -74,38 +69,75 @@
   ] + select({
     ":tint_build_glsl_writer": [
       "//src/tint/lang/glsl/writer/ast_printer:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_glsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
       "//src/tint/lang/glsl/writer/ast_raise:test",
     ],
     "//conditions:default": [],
   }) + select({
     ":tint_build_hlsl_writer": [
       "//src/tint/lang/hlsl/writer/ast_printer:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_hlsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
       "//src/tint/lang/hlsl/writer/ast_raise:test",
     ],
     "//conditions:default": [],
   }) + select({
     ":tint_build_msl_writer": [
       "//src/tint/lang/msl/writer/ast_printer:test",
-      "//src/tint/lang/msl/writer/ast_raise:test",
       "//src/tint/lang/msl/writer/common:test",
       "//src/tint/lang/msl/writer/printer:test",
     ],
     "//conditions:default": [],
   }) + select({
-    ":tint_build_spv_reader": [
+    ":tint_build_msl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/msl/writer/ast_raise:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
       "//src/tint/lang/spirv/reader/ast_lower:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_and_tint_build_wgsl_writer": [
       "//src/tint/lang/spirv/reader/ast_parser:test",
     ],
     "//conditions:default": [],
   }) + select({
     ":tint_build_spv_writer": [
       "//src/tint/lang/spirv/writer/ast_printer:test",
-      "//src/tint/lang/spirv/writer/ast_raise:test",
       "//src/tint/lang/spirv/writer/common:test",
       "//src/tint/lang/spirv/writer/raise:test",
       "//src/tint/lang/spirv/writer:test",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/spirv/writer/ast_raise:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/inspector:test",
+      "//src/tint/lang/wgsl/reader/parser:test",
+      "//src/tint/lang/wgsl/reader/program_to_ir:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/ast/transform:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer/ast_printer:test",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -136,3 +168,68 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_glsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_glsl_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_hlsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_hlsl_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_msl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_msl_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_spv_reader_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_spv_reader",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_spv_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_spv_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_spv_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_spv_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index 108f7af..e0f6550 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -39,17 +39,12 @@
   tint_lang_core_type_test
   tint_lang_core_test
   tint_lang_spirv_ir_test
-  tint_lang_wgsl_ast_transform_test
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_helpers_test
-  tint_lang_wgsl_inspector_test
   tint_lang_wgsl_program_test
   tint_lang_wgsl_reader_lower_test
-  tint_lang_wgsl_reader_parser_test
-  tint_lang_wgsl_reader_program_to_ir_test
   tint_lang_wgsl_resolver_test
   tint_lang_wgsl_sem_test
-  tint_lang_wgsl_writer_ast_printer_test
   tint_lang_wgsl_writer_ir_to_program_test
   tint_lang_wgsl_writer_raise_test
   tint_lang_wgsl_test
@@ -80,41 +75,86 @@
 if(TINT_BUILD_GLSL_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_glsl_writer_ast_printer_test
-    tint_lang_glsl_writer_ast_raise_test
   )
 endif(TINT_BUILD_GLSL_WRITER)
 
+if(TINT_BUILD_GLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_glsl_writer_ast_raise_test
+  )
+endif(TINT_BUILD_GLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
 if(TINT_BUILD_HLSL_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_hlsl_writer_ast_printer_test
-    tint_lang_hlsl_writer_ast_raise_test
   )
 endif(TINT_BUILD_HLSL_WRITER)
 
+if(TINT_BUILD_HLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_hlsl_writer_ast_raise_test
+  )
+endif(TINT_BUILD_HLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_msl_writer_ast_printer_test
-    tint_lang_msl_writer_ast_raise_test
     tint_lang_msl_writer_common_test
     tint_lang_msl_writer_printer_test
   )
 endif(TINT_BUILD_MSL_WRITER)
 
-if(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_MSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_msl_writer_ast_raise_test
+  )
+endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_spirv_reader_ast_lower_test
+  )
+endif(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_spirv_reader_ast_parser_test
   )
-endif(TINT_BUILD_SPV_READER)
+endif(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER)
 
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_spirv_writer_ast_printer_test
-    tint_lang_spirv_writer_ast_raise_test
     tint_lang_spirv_writer_common_test
     tint_lang_spirv_writer_raise_test
     tint_lang_spirv_writer_test
   )
 endif(TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_spirv_writer_ast_raise_test
+  )
+endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_wgsl_inspector_test
+    tint_lang_wgsl_reader_parser_test
+    tint_lang_wgsl_reader_program_to_ir_test
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_wgsl_ast_transform_test
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_wgsl_writer_ast_printer_test
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 tint_target_set_output_name(tint_cmd_test_test_cmd test_cmd "tint_unittests")
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index 5461cfb..09acee9 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -25,11 +25,12 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_unittests) {
-  test("test") {
+  test("test_cmd") {
+    testonly = true
     output_name = "tint_unittests"
     sources = [ "main_test.cc" ]
     deps = [
@@ -45,16 +46,11 @@
       "${tint_src_dir}/lang/spirv/ir:unittests",
       "${tint_src_dir}/lang/wgsl:unittests",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
-      "${tint_src_dir}/lang/wgsl/ast/transform:unittests",
       "${tint_src_dir}/lang/wgsl/helpers:unittests",
-      "${tint_src_dir}/lang/wgsl/inspector:unittests",
       "${tint_src_dir}/lang/wgsl/program:unittests",
       "${tint_src_dir}/lang/wgsl/reader/lower:unittests",
-      "${tint_src_dir}/lang/wgsl/reader/parser:unittests",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir:unittests",
       "${tint_src_dir}/lang/wgsl/resolver:unittests",
       "${tint_src_dir}/lang/wgsl/sem:unittests",
-      "${tint_src_dir}/lang/wgsl/writer/ast_printer:unittests",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program:unittests",
       "${tint_src_dir}/lang/wgsl/writer/raise:unittests",
       "${tint_src_dir}/utils/cli:unittests",
@@ -78,45 +74,74 @@
     ]
 
     if (tint_build_glsl_writer) {
-      deps += [
-        "${tint_src_dir}/lang/glsl/writer/ast_printer:unittests",
-        "${tint_src_dir}/lang/glsl/writer/ast_raise:unittests",
-      ]
+      deps += [ "${tint_src_dir}/lang/glsl/writer/ast_printer:unittests" ]
+    }
+
+    if (tint_build_glsl_writer && tint_build_wgsl_reader &&
+        tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/glsl/writer/ast_raise:unittests" ]
     }
 
     if (tint_build_hlsl_writer) {
-      deps += [
-        "${tint_src_dir}/lang/hlsl/writer/ast_printer:unittests",
-        "${tint_src_dir}/lang/hlsl/writer/ast_raise:unittests",
-      ]
+      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_printer:unittests" ]
+    }
+
+    if (tint_build_hlsl_writer && tint_build_wgsl_reader &&
+        tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_raise:unittests" ]
     }
 
     if (tint_build_msl_writer) {
       deps += [
         "${tint_src_dir}/lang/msl/writer/ast_printer:unittests",
-        "${tint_src_dir}/lang/msl/writer/ast_raise:unittests",
         "${tint_src_dir}/lang/msl/writer/common:unittests",
         "${tint_src_dir}/lang/msl/writer/printer:unittests",
       ]
     }
 
-    if (tint_build_spv_reader) {
-      deps += [
-        "${tint_src_dir}/lang/spirv/reader/ast_lower:unittests",
-        "${tint_src_dir}/lang/spirv/reader/ast_parser:unittests",
-      ]
+    if (tint_build_msl_writer && tint_build_wgsl_reader &&
+        tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/msl/writer/ast_raise:unittests" ]
+    }
+
+    if (tint_build_spv_reader && tint_build_wgsl_reader &&
+        tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/reader/ast_lower:unittests" ]
+    }
+
+    if (tint_build_spv_reader && tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/reader/ast_parser:unittests" ]
     }
 
     if (tint_build_spv_writer) {
       deps += [
         "${tint_src_dir}/lang/spirv/writer:unittests",
         "${tint_src_dir}/lang/spirv/writer/ast_printer:unittests",
-        "${tint_src_dir}/lang/spirv/writer/ast_raise:unittests",
         "${tint_src_dir}/lang/spirv/writer/common:unittests",
         "${tint_src_dir}/lang/spirv/writer/raise:unittests",
       ]
     }
-    testonly = true
+
+    if (tint_build_spv_writer && tint_build_wgsl_reader &&
+        tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/writer/ast_raise:unittests" ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      deps += [
+        "${tint_src_dir}/lang/wgsl/inspector:unittests",
+        "${tint_src_dir}/lang/wgsl/reader/parser:unittests",
+        "${tint_src_dir}/lang/wgsl/reader/program_to_ir:unittests",
+      ]
+    }
+
+    if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/ast/transform:unittests" ]
+    }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer:unittests" ]
+    }
     configs += [ "${tint_src_dir}:tint_unittests_config" ]
 
     if (build_with_chromium) {
diff --git a/src/tint/cmd/tint/BUILD.bazel b/src/tint/cmd/tint/BUILD.bazel
index 321c15e..e333aea 100644
--- a/src/tint/cmd/tint/BUILD.bazel
+++ b/src/tint/cmd/tint/BUILD.bazel
@@ -45,10 +45,7 @@
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/cli",
     "//src/tint/utils/command",
     "//src/tint/utils/containers",
@@ -66,11 +63,14 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ] + select({
+    ":tint_build_glsl_validator": [
+      "//src/tint/lang/glsl/validate",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_glsl_writer": [
       "//src/tint/lang/glsl/writer",
       "//src/tint/lang/glsl/writer/common",
-      
-      
     ],
     "//conditions:default": [],
   }) + select({
@@ -100,6 +100,18 @@
     ":tint_build_spv_writer": [
       "//src/tint/lang/spirv/writer",
       "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/helpers",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
     ],
     "//conditions:default": [],
   }),
@@ -108,6 +120,11 @@
 )
 
 alias(
+  name = "tint_build_glsl_validator",
+  actual = "//src/tint:tint_build_glsl_validator_true",
+)
+
+alias(
   name = "tint_build_glsl_writer",
   actual = "//src/tint:tint_build_glsl_writer_true",
 )
@@ -132,6 +149,16 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
 selects.config_setting_group(
     name = "tint_build_spv_reader_or_tint_build_spv_writer",
     match_any = [
diff --git a/src/tint/cmd/tint/BUILD.cmake b/src/tint/cmd/tint/BUILD.cmake
index 1d500ac..b70b52b 100644
--- a/src/tint/cmd/tint/BUILD.cmake
+++ b/src/tint/cmd/tint/BUILD.cmake
@@ -46,10 +46,7 @@
   tint_lang_wgsl_helpers
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_cli
   tint_utils_command
   tint_utils_containers
@@ -68,15 +65,17 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_GLSL_VALIDATOR)
+  tint_target_add_dependencies(tint_cmd_tint_cmd cmd
+    tint_lang_glsl_validate
+  )
+endif(TINT_BUILD_GLSL_VALIDATOR)
+
 if(TINT_BUILD_GLSL_WRITER)
   tint_target_add_dependencies(tint_cmd_tint_cmd cmd
     tint_lang_glsl_writer
     tint_lang_glsl_writer_common
   )
-  tint_target_add_external_dependencies(tint_cmd_tint_cmd cmd
-    "glslang"
-    "glslang-res-limits"
-  )
 endif(TINT_BUILD_GLSL_WRITER)
 
 if(TINT_BUILD_HLSL_WRITER)
@@ -110,7 +109,21 @@
   tint_target_add_dependencies(tint_cmd_tint_cmd cmd
     tint_lang_spirv_writer
     tint_lang_spirv_writer_common
+    tint_lang_spirv_writer_helpers
   )
 endif(TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_tint_cmd cmd
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_tint_cmd cmd
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 tint_target_set_output_name(tint_cmd_tint_cmd cmd "tint")
diff --git a/src/tint/cmd/tint/BUILD.gn b/src/tint/cmd/tint/BUILD.gn
index e6c897c..50a05f7 100644
--- a/src/tint/cmd/tint/BUILD.gn
+++ b/src/tint/cmd/tint/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-executable("tint") {
+tint_executable("tint") {
   output_name = "tint"
   sources = [ "main.cc" ]
   deps = [
@@ -45,10 +45,7 @@
     "${tint_src_dir}/lang/wgsl/helpers",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
-    "${tint_src_dir}/lang/wgsl/reader",
-    "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
     "${tint_src_dir}/lang/wgsl/sem",
-    "${tint_src_dir}/lang/wgsl/writer",
     "${tint_src_dir}/utils/cli",
     "${tint_src_dir}/utils/command",
     "${tint_src_dir}/utils/containers",
@@ -67,10 +64,12 @@
     "${tint_src_dir}/utils/traits",
   ]
 
+  if (tint_build_glsl_validator) {
+    deps += [ "${tint_src_dir}/lang/glsl/validate" ]
+  }
+
   if (tint_build_glsl_writer) {
     deps += [
-      "${tint_glslang_dir}:glslang_default_resource_limits_sources",
-      "${tint_glslang_dir}:glslang_lib_sources",
       "${tint_src_dir}/lang/glsl/writer",
       "${tint_src_dir}/lang/glsl/writer/common",
     ]
@@ -106,6 +105,18 @@
     deps += [
       "${tint_src_dir}/lang/spirv/writer",
       "${tint_src_dir}/lang/spirv/writer/common",
+      "${tint_src_dir}/lang/spirv/writer/helpers",
     ]
   }
+
+  if (tint_build_wgsl_reader) {
+    deps += [
+      "${tint_src_dir}/lang/wgsl/reader",
+      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+    ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+  }
 }
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 1a48a8d..31e4522 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -24,11 +24,6 @@
 #include <unordered_map>
 #include <vector>
 
-#if TINT_BUILD_GLSL_WRITER
-#include "glslang/Public/ResourceLimits.h"
-#include "glslang/Public/ShaderLang.h"
-#endif  // TINT_BUILD_GLSL_WRITER
-
 #if TINT_BUILD_SPV_READER || TINT_BUILD_SPV_WRITER
 #include "spirv-tools/libspirv.hpp"
 #endif  // TINT_BUILD_SPV_READER || TINT_BUILD_SPV_WRITER
@@ -46,7 +41,6 @@
 #include "src/tint/lang/wgsl/ast/transform/single_entry_point.h"
 #include "src/tint/lang/wgsl/ast/transform/substitute_override.h"
 #include "src/tint/lang/wgsl/helpers/flatten_bindings.h"
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/utils/cli/cli.h"
 #include "src/tint/utils/command/command.h"
 #include "src/tint/utils/containers/transform.h"
@@ -61,10 +55,12 @@
 #endif  // TINT_BUILD_SPV_READER
 
 #if TINT_BUILD_WGSL_READER
+#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
 #endif  // TINT_BUILD_WGSL_READER
 
 #if TINT_BUILD_SPV_WRITER
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
@@ -86,6 +82,10 @@
 #include "src/tint/lang/glsl/writer/writer.h"
 #endif  // TINT_BUILD_GLSL_WRITER
 
+#if TINT_BUILD_GLSL_VALIDATOR
+#include "src/tint/lang/glsl/validate/validate.h"
+#endif  // TINT_BUILD_GLSL_VALIDATOR
+
 #if TINT_BUILD_SPV_WRITER
 #define SPV_WRITER_ONLY(x) x
 #else
@@ -495,7 +495,9 @@
 /// like `std::string` and `std::vector` do.
 /// @returns true on success
 template <typename ContainerT>
-bool WriteFile(const std::string& output_file, const std::string mode, const ContainerT& buffer) {
+[[maybe_unused]] bool WriteFile(const std::string& output_file,
+                                const std::string mode,
+                                const ContainerT& buffer) {
     const bool use_stdout = output_file.empty() || output_file == "-";
     FILE* file = stdout;
 
@@ -579,8 +581,7 @@
     tint::spirv::writer::Options gen_options;
     gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
-    gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(program);
+    gen_options.bindings = tint::spirv::writer::GenerateBindings(program);
     gen_options.use_tint_ir = options.use_ir;
 
     auto result = tint::spirv::writer::Generate(program, gen_options);
@@ -631,7 +632,8 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateWgsl(const tint::Program& program, const Options& options) {
+bool GenerateWgsl([[maybe_unused]] const tint::Program& program,
+                  [[maybe_unused]] const Options& options) {
 #if TINT_BUILD_WGSL_WRITER
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::wgsl::writer::Options gen_options;
@@ -650,6 +652,7 @@
         PrintHash(hash);
     }
 
+#if TINT_BUILD_WGSL_READER
     if (options.validate && options.skip_hash.count(hash) == 0) {
         // Attempt to re-parse the output program with Tint's WGSL reader.
         auto source = std::make_unique<tint::Source::File>(options.input_filename, result->wgsl);
@@ -661,11 +664,10 @@
             return false;
         }
     }
+#endif  // TINT_BUILD_WGSL_READER
 
     return true;
 #else
-    (void)program;
-    (void)options;
     std::cerr << "WGSL writer not enabled in tint build" << std::endl;
     return false;
 #endif  // TINT_BUILD_WGSL_WRITER
@@ -675,12 +677,16 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateMsl(const tint::Program& program, const Options& options) {
-#if TINT_BUILD_MSL_WRITER
+bool GenerateMsl([[maybe_unused]] const tint::Program& program,
+                 [[maybe_unused]] const Options& options) {
+#if !TINT_BUILD_MSL_WRITER
+    std::cerr << "MSL writer not enabled in tint build" << std::endl;
+    return false;
+#else
     // Remap resource numbers to a flat namespace.
     // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
     const tint::Program* input_program = &program;
-    auto flattened = tint::writer::FlattenBindings(program);
+    auto flattened = tint::wgsl::FlattenBindings(program);
     if (flattened) {
         input_program = &*flattened;
     }
@@ -752,11 +758,6 @@
     }
 
     return true;
-#else
-    (void)program;
-    (void)options;
-    std::cerr << "MSL writer not enabled in tint build" << std::endl;
-    return false;
 #endif  // TINT_BUILD_MSL_WRITER
 }
 
@@ -883,32 +884,16 @@
 #endif  // TINT_BUILD_HLSL_WRITER
 }
 
-#if TINT_BUILD_GLSL_WRITER
-EShLanguage pipeline_stage_to_esh_language(tint::ast::PipelineStage stage) {
-    switch (stage) {
-        case tint::ast::PipelineStage::kFragment:
-            return EShLangFragment;
-        case tint::ast::PipelineStage::kVertex:
-            return EShLangVertex;
-        case tint::ast::PipelineStage::kCompute:
-            return EShLangCompute;
-        default:
-            TINT_UNREACHABLE();
-            return EShLangVertex;
-    }
-}
-#endif
-
 /// Generate GLSL code for a program.
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateGlsl(const tint::Program& program, const Options& options) {
-#if TINT_BUILD_GLSL_WRITER
-    if (options.validate) {
-        glslang::InitializeProcess();
-    }
-
+bool GenerateGlsl([[maybe_unused]] const tint::Program& program,
+                  [[maybe_unused]] const Options& options) {
+#if !TINT_BUILD_GLSL_WRITER
+    std::cerr << "GLSL writer not enabled in tint build" << std::endl;
+    return false;
+#else
     auto generate = [&](const tint::Program& prg, const std::string entry_point_name) -> bool {
         tint::glsl::writer::Options gen_options;
         gen_options.disable_robustness = !options.enable_robustness;
@@ -935,22 +920,16 @@
         }
 
         if (options.validate && options.skip_hash.count(hash) == 0) {
-            for (auto entry_pt : result->entry_points) {
-                EShLanguage lang = pipeline_stage_to_esh_language(entry_pt.second);
-                glslang::TShader shader(lang);
-                const char* strings[1] = {result->glsl.c_str()};
-                int lengths[1] = {static_cast<int>(result->glsl.length())};
-                shader.setStringsWithLengths(strings, lengths, 1);
-                shader.setEntryPoint("main");
-                bool glslang_result = shader.parse(GetDefaultResources(), 310, EEsProfile, false,
-                                                   false, EShMsgDefault);
-                if (!glslang_result) {
-                    std::cerr << "Error parsing GLSL shader:\n"
-                              << shader.getInfoLog() << "\n"
-                              << shader.getInfoDebugLog() << "\n";
-                    return false;
-                }
+#if !TINT_BUILD_GLSL_VALIDATOR
+            std::cerr << "GLSL validator not enabled in tint build" << std::endl;
+            return false;
+#else
+            auto val = tint::glsl::validate::Validate(result->glsl, result->entry_points);
+            if (!val) {
+                std::cerr << "Error parsing GLSL shader:\n" << val.Failure();
+                return false;
             }
+#endif
         }
         return true;
     };
@@ -968,11 +947,6 @@
         success &= generate(program, entry_point.name);
     }
     return success;
-#else
-    (void)program;
-    (void)options;
-    std::cerr << "GLSL writer not enabled in tint build" << std::endl;
-    return false;
 #endif  // TINT_BUILD_GLSL_WRITER
 }
 
@@ -1116,8 +1090,7 @@
         } else {
             auto mod = result.Move();
             if (options.dump_ir) {
-                tint::core::ir::Disassembler d(mod);
-                std::cout << d.Disassemble() << std::endl;
+                std::cout << tint::core::ir::Disassemble(mod) << std::endl;
             }
         }
     }
diff --git a/src/tint/externals.json b/src/tint/externals.json
index 649a650..6cdd1b2 100644
--- a/src/tint/externals.json
+++ b/src/tint/externals.json
@@ -8,6 +8,11 @@
             "absl/**"
         ],
     },
+    "google-benchmark": {
+        "IncludePatterns": [
+            "benchmark/benchmark.h"
+        ]
+    },
     "metal": {
         "IncludePatterns": [
             "Metal/Metal.h"
diff --git a/src/tint/fuzzers/BUILD.gn b/src/tint/fuzzers/BUILD.gn
index f15436a..1f576ad 100644
--- a/src/tint/fuzzers/BUILD.gn
+++ b/src/tint/fuzzers/BUILD.gn
@@ -18,7 +18,7 @@
 # Fuzzers - Libfuzzer based fuzzing targets for Chromium
 # To run the fuzzers outside of Chromium, use the CMake based builds.
 
-if (build_with_chromium) {
+if (tint_has_fuzzers) {
   import("//testing/libfuzzer/fuzzer_test.gni")
   import("../../../scripts/dawn_overrides_with_defaults.gni")
 
@@ -40,13 +40,15 @@
     "max_len=10000",
   ]
 
-  tint_ast_fuzzer_common_libfuzzer_options =
-      tint_fuzzer_common_libfuzzer_options + [
-        "cross_over=0",
-        "mutate_depth=1",
-        "tint_enable_all_mutations=false",
-        "tint_mutation_batch_size=5",
-      ]
+  if (build_with_chromium) {
+    tint_ast_fuzzer_common_libfuzzer_options =
+        tint_fuzzer_common_libfuzzer_options + [
+          "cross_over=0",
+          "mutate_depth=1",
+          "tint_enable_all_mutations=false",
+          "tint_mutation_batch_size=5",
+        ]
+  }
 
   tint_regex_fuzzer_common_libfuzzer_options =
       tint_fuzzer_common_libfuzzer_options + [
@@ -72,6 +74,7 @@
       "${tint_src_dir}/lang/hlsl/writer",
       "${tint_src_dir}/lang/msl/writer",
       "${tint_src_dir}/lang/spirv/writer",
+      "${tint_src_dir}/lang/spirv/writer/helpers",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/helpers",
@@ -86,8 +89,6 @@
     ]
 
     sources = [
-      "apply_substitute_overrides.cc",
-      "apply_substitute_overrides.h",
       "data_builder.h",
       "mersenne_twister_engine.cc",
       "mersenne_twister_engine.h",
@@ -134,12 +135,14 @@
       seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
     }
 
-    fuzzer_test("tint_ast_wgsl_writer_fuzzer") {
-      sources = [ "tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc" ]
-      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
-      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
-      seed_corpus = fuzzer_corpus_wgsl_dir
-      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    if (build_with_chromium) {
+      fuzzer_test("tint_ast_wgsl_writer_fuzzer") {
+        sources = [ "tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc" ]
+        deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+        libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+        seed_corpus = fuzzer_corpus_wgsl_dir
+        seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+      }
     }
 
     fuzzer_test("tint_regex_wgsl_writer_fuzzer") {
@@ -170,12 +173,14 @@
       seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
     }
 
-    fuzzer_test("tint_ast_spv_writer_fuzzer") {
-      sources = [ "tint_ast_fuzzer/tint_ast_spv_writer_fuzzer.cc" ]
-      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
-      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
-      seed_corpus = fuzzer_corpus_wgsl_dir
-      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    if (build_with_chromium) {
+      fuzzer_test("tint_ast_spv_writer_fuzzer") {
+        sources = [ "tint_ast_fuzzer/tint_ast_spv_writer_fuzzer.cc" ]
+        deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+        libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+        seed_corpus = fuzzer_corpus_wgsl_dir
+        seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+      }
     }
 
     fuzzer_test("tint_binding_remapper_fuzzer") {
@@ -263,12 +268,14 @@
   }
 
   if (tint_build_wgsl_reader && tint_build_hlsl_writer) {
-    fuzzer_test("tint_ast_hlsl_writer_fuzzer") {
-      sources = [ "tint_ast_fuzzer/tint_ast_hlsl_writer_fuzzer.cc" ]
-      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
-      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
-      seed_corpus = fuzzer_corpus_wgsl_dir
-      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    if (build_with_chromium) {
+      fuzzer_test("tint_ast_hlsl_writer_fuzzer") {
+        sources = [ "tint_ast_fuzzer/tint_ast_hlsl_writer_fuzzer.cc" ]
+        deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+        libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+        seed_corpus = fuzzer_corpus_wgsl_dir
+        seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+      }
     }
 
     fuzzer_test("tint_regex_hlsl_writer_fuzzer") {
@@ -290,12 +297,14 @@
   }
 
   if (tint_build_wgsl_reader && tint_build_msl_writer) {
-    fuzzer_test("tint_ast_msl_writer_fuzzer") {
-      sources = [ "tint_ast_fuzzer/tint_ast_msl_writer_fuzzer.cc" ]
-      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
-      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
-      seed_corpus = fuzzer_corpus_wgsl_dir
-      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    if (build_with_chromium) {
+      fuzzer_test("tint_ast_msl_writer_fuzzer") {
+        sources = [ "tint_ast_fuzzer/tint_ast_msl_writer_fuzzer.cc" ]
+        deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+        libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+        seed_corpus = fuzzer_corpus_wgsl_dir
+        seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+      }
     }
 
     fuzzer_test("tint_regex_msl_writer_fuzzer") {
@@ -316,15 +325,6 @@
     }
   }
 
-  if (tint_build_wgsl_reader && tint_build_hlsl_writer &&
-      tint_build_msl_writer && tint_build_spv_writer &&
-      tint_build_wgsl_writer) {
-    executable("tint_black_box_fuzz_target") {
-      sources = [ "tint_black_box_fuzz_target.cc" ]
-      deps = [ ":tint_fuzzer_common_src" ]
-    }
-  }
-
   group("fuzzers") {
     testonly = true
     deps = []
@@ -332,15 +332,16 @@
     if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
       deps += [
         ":tint_ast_clone_fuzzer",
-        ":tint_ast_wgsl_writer_fuzzer",
         ":tint_regex_wgsl_writer_fuzzer",
         ":tint_wgsl_reader_wgsl_writer_fuzzer",
       ]
+      if (build_with_chromium) {
+        deps += [ ":tint_ast_wgsl_writer_fuzzer" ]
+      }
     }
     if (tint_build_wgsl_reader && tint_build_spv_writer) {
       deps += [
         ":tint_all_transforms_fuzzer",
-        ":tint_ast_spv_writer_fuzzer",
         ":tint_binding_remapper_fuzzer",
         ":tint_first_index_offset_fuzzer",
         ":tint_regex_spv_writer_fuzzer",
@@ -350,25 +351,27 @@
         ":tint_vertex_pulling_fuzzer",
         ":tint_wgsl_reader_spv_writer_fuzzer",
       ]
+      if (build_with_chromium) {
+        deps += [ ":tint_ast_spv_writer_fuzzer" ]
+      }
     }
     if (tint_build_wgsl_reader && tint_build_hlsl_writer) {
       deps += [
-        ":tint_ast_hlsl_writer_fuzzer",
         ":tint_regex_hlsl_writer_fuzzer",
         ":tint_wgsl_reader_hlsl_writer_fuzzer",
       ]
+      if (build_with_chromium) {
+        deps += [ ":tint_ast_hlsl_writer_fuzzer" ]
+      }
     }
     if (tint_build_wgsl_reader && tint_build_msl_writer) {
       deps += [
-        ":tint_ast_msl_writer_fuzzer",
         ":tint_regex_msl_writer_fuzzer",
         ":tint_wgsl_reader_msl_writer_fuzzer",
       ]
-    }
-    if (tint_build_wgsl_reader && tint_build_hlsl_writer &&
-        tint_build_msl_writer && tint_build_spv_writer &&
-        tint_build_wgsl_writer) {
-      deps += [ ":tint_black_box_fuzz_target" ]
+      if (build_with_chromium) {
+        deps += [ ":tint_ast_msl_writer_fuzzer" ]
+      }
     }
   }
 } else {
diff --git a/src/tint/fuzzers/CMakeLists.txt b/src/tint/fuzzers/CMakeLists.txt
index 6fb5e6e..97674f2 100644
--- a/src/tint/fuzzers/CMakeLists.txt
+++ b/src/tint/fuzzers/CMakeLists.txt
@@ -15,8 +15,6 @@
 function(add_tint_fuzzer NAME)
   add_executable(${NAME}
     ${NAME}.cc
-    apply_substitute_overrides.cc
-    apply_substitute_overrides.h
     cli.cc
     cli.h
     data_builder.h
@@ -40,6 +38,7 @@
     tint_spvheaders_compile_options(${NAME})
     tint_spvtools_compile_options(${NAME})
   endif()
+  target_link_libraries(${NAME} PRIVATE tint_lang_spirv_writer_helpers)
   target_compile_options(${NAME} PRIVATE -Wno-missing-prototypes)
 endfunction()
 
@@ -92,10 +91,6 @@
   add_tint_fuzzer(tint_ast_clone_fuzzer)
 endif()
 
-if (${TINT_BUILD_SPIRV_TOOLS_FUZZER})
-  add_subdirectory(tint_spirv_tools_fuzzer)
-endif()
-
 if (${TINT_BUILD_AST_FUZZER})
   add_subdirectory(tint_ast_fuzzer)
 endif()
@@ -103,26 +98,3 @@
 if (${TINT_BUILD_REGEX_FUZZER})
   add_subdirectory(tint_regex_fuzzer)
 endif()
-
-if (${TINT_BUILD_WGSL_READER}
-    AND ${TINT_BUILD_HLSL_WRITER}
-    AND ${TINT_BUILD_MSL_WRITER}
-    AND ${TINT_BUILD_SPV_WRITER}
-    AND ${TINT_BUILD_WGSL_WRITER})
-  add_executable(tint_black_box_fuzz_target
-    apply_substitute_overrides.cc
-    apply_substitute_overrides.h
-    mersenne_twister_engine.cc
-    mersenne_twister_engine.h
-    random_generator.cc
-    random_generator.h
-    random_generator_engine.cc
-    random_generator_engine.h
-    tint_black_box_fuzz_target.cc
-    tint_common_fuzzer.cc
-    tint_common_fuzzer.h
-  )
-  target_link_libraries(tint_black_box_fuzz_target PRIVATE libtint)
-  tint_default_compile_options(tint_black_box_fuzz_target)
-  tint_spvtools_compile_options(tint_black_box_fuzz_target)
-endif()
diff --git a/src/tint/fuzzers/generate_spirv_corpus.py b/src/tint/fuzzers/generate_spirv_corpus.py
index 6a865e0..53d5ae3 100644
--- a/src/tint/fuzzers/generate_spirv_corpus.py
+++ b/src/tint/fuzzers/generate_spirv_corpus.py
@@ -62,7 +62,7 @@
     logged_errors = ""
 
     for in_file in list_spvasm_files(input_dir):
-        if in_file.endswith(".expected.spvasm"):
+        if ".expected." in in_file:
             continue
         out_file = os.path.splitext(
             corpus_dir + os.sep +
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
index 1cb440a..85881e4 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
@@ -14,7 +14,7 @@
 
 function(add_tint_ast_fuzzer NAME)
   add_executable(${NAME} ${NAME}.cc ${AST_FUZZER_SOURCES})
-  target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer)
+  target_link_libraries(${NAME} PRIVATE libtint_ast_fuzzer tint_lang_spirv_writer_helpers)
   tint_fuzzer_compile_options(${NAME})
   if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
     tint_spvheaders_compile_options(${NAME})
@@ -96,7 +96,7 @@
 
 # Add static library target.
 add_library(libtint_ast_fuzzer STATIC ${LIBTINT_AST_FUZZER_SOURCES})
-target_link_libraries(libtint_ast_fuzzer protobuf::libprotobuf libtint)
+target_link_libraries(libtint_ast_fuzzer protobuf::libprotobuf tint_api_sanitize_fuzzer)
 tint_default_compile_options(libtint_ast_fuzzer)
 target_include_directories(libtint_ast_fuzzer PRIVATE ${CMAKE_BINARY_DIR})
 target_compile_options(libtint_ast_fuzzer PRIVATE
@@ -111,8 +111,6 @@
         cli.h
         fuzzer.cc
         override_cli_params.h
-        ../apply_substitute_overrides.cc
-        ../apply_substitute_overrides.h
         ../tint_common_fuzzer.cc
         ../tint_common_fuzzer.h)
 
diff --git a/src/tint/fuzzers/tint_black_box_fuzz_target.cc b/src/tint/fuzzers/tint_black_box_fuzz_target.cc
deleted file mode 100644
index 1f0877a..0000000
--- a/src/tint/fuzzers/tint_black_box_fuzz_target.cc
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-#include <cstdio>
-#include <fstream>
-#include <iostream>
-#include <string>
-#include <vector>
-
-#include "src/tint/fuzzers/tint_common_fuzzer.h"
-
-namespace {
-
-/// Controls the target language in which code will be generated.
-enum class TargetLanguage {
-    kHlsl,
-    kMsl,
-    kSpv,
-    kWgsl,
-    kTargetLanguageMax,
-};
-
-/// Copies the content from the file named `input_file` to `buffer`,
-/// assuming each element in the file is of type `T`.  If any error occurs,
-/// writes error messages to the standard error stream and returns false.
-/// Assumes the size of a `T` object is divisible by its required alignment.
-/// @returns true if we successfully read the file.
-template <typename T>
-bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
-    if (!buffer) {
-        std::cerr << "The buffer pointer was null" << std::endl;
-        return false;
-    }
-
-    FILE* file = nullptr;
-#if defined(_MSC_VER)
-    fopen_s(&file, input_file.c_str(), "rb");
-#else
-    file = fopen(input_file.c_str(), "rb");
-#endif
-    if (!file) {
-        std::cerr << "Failed to open " << input_file << std::endl;
-        return false;
-    }
-
-    fseek(file, 0, SEEK_END);
-    const auto file_size = static_cast<size_t>(ftell(file));
-    if (0 != (file_size % sizeof(T))) {
-        std::cerr << "File " << input_file
-                  << " does not contain an integral number of objects: " << file_size
-                  << " bytes in the file, require " << sizeof(T) << " bytes per object"
-                  << std::endl;
-        fclose(file);
-        return false;
-    }
-    fseek(file, 0, SEEK_SET);
-
-    buffer->clear();
-    buffer->resize(file_size / sizeof(T));
-
-    size_t bytes_read = fread(buffer->data(), 1, file_size, file);
-    fclose(file);
-    if (bytes_read != file_size) {
-        std::cerr << "Failed to read " << input_file << std::endl;
-        return false;
-    }
-
-    return true;
-}
-
-}  // namespace
-
-int main(int argc, const char** argv) {
-    if (argc < 2 || argc > 3) {
-        std::cerr << "Usage: " << argv[0] << " <input file> [hlsl|msl|spv|wgsl]" << std::endl;
-        return 1;
-    }
-
-    std::string input_filename(argv[1]);
-
-    std::vector<uint8_t> data;
-    if (!ReadFile<uint8_t>(input_filename, &data)) {
-        return 1;
-    }
-
-    if (data.empty()) {
-        return 0;
-    }
-
-    tint::fuzzers::DataBuilder builder(data.data(), data.size());
-
-    TargetLanguage target_language;
-
-    if (argc == 3) {
-        std::string target_language_string = argv[2];
-        if (target_language_string == "hlsl") {
-            target_language = TargetLanguage::kHlsl;
-        } else if (target_language_string == "msl") {
-            target_language = TargetLanguage::kMsl;
-        } else if (target_language_string == "spv") {
-            target_language = TargetLanguage::kSpv;
-        } else {
-            assert(target_language_string == "wgsl" && "Unknown target language.");
-            target_language = TargetLanguage::kWgsl;
-        }
-    } else {
-        target_language = builder.enum_class<TargetLanguage>(
-            static_cast<uint32_t>(TargetLanguage::kTargetLanguageMax));
-    }
-
-    switch (target_language) {
-        case TargetLanguage::kHlsl: {
-            tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
-                                               tint::fuzzers::OutputFormat::kHLSL);
-            return fuzzer.Run(data.data(), data.size());
-        }
-        case TargetLanguage::kMsl: {
-            tint::msl::writer::Options options;
-            GenerateMslOptions(&builder, &options);
-            tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
-                                               tint::fuzzers::OutputFormat::kMSL);
-            fuzzer.SetOptionsMsl(options);
-            return fuzzer.Run(data.data(), data.size());
-        }
-        case TargetLanguage::kSpv: {
-            tint::spirv::writer::Options options;
-            GenerateSpirvOptions(&builder, &options);
-            tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
-                                               tint::fuzzers::OutputFormat::kSpv);
-            fuzzer.SetOptionsSpirv(options);
-            return fuzzer.Run(data.data(), data.size());
-        }
-        case TargetLanguage::kWgsl: {
-            tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
-                                               tint::fuzzers::OutputFormat::kWGSL);
-            return fuzzer.Run(data.data(), data.size());
-        }
-        default:
-            std::cerr << "Aborting due to unknown target language; fuzzer must be "
-                         "misconfigured."
-                      << std::endl;
-            abort();
-    }
-}
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index a9e2e83..07df07b 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -31,9 +31,9 @@
 #endif  // TINT_BUILD_SPV_READER || TINT_BUILD_SPV_WRITER
 
 #include "src/tint/api/common/binding_point.h"
-#include "src/tint/fuzzers/apply_substitute_overrides.h"
 #include "src/tint/lang/core/type/external_texture.h"
 #include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
 #include "src/tint/lang/wgsl/helpers/flatten_bindings.h"
 #include "src/tint/lang/wgsl/program/program.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -41,6 +41,10 @@
 #include "src/tint/utils/diagnostic/printer.h"
 #include "src/tint/utils/math/hash.h"
 
+#if TINT_BUILD_SPV_WRITER
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
+#endif  // TINT_BUILD_SPV_WRITER
+
 namespace tint::fuzzers {
 
 namespace {
@@ -237,15 +241,30 @@
     }
 
     // Run SubstituteOverride if required
-    program = ApplySubstituteOverrides(std::move(program));
-    if (!program.IsValid()) {
-        return 0;
+    if (auto transformed = tint::wgsl::ApplySubstituteOverrides(program)) {
+        program = std::move(*transformed);
+        if (!program.IsValid()) {
+            return 0;
+        }
+    }
+
+    switch (output_) {
+        case OutputFormat::kMSL:
+            break;
+        case OutputFormat::kHLSL:
+            break;
+        case OutputFormat::kSpv:
+#if TINT_BUILD_SPV_WRITER
+            options_spirv_.bindings = tint::spirv::writer::GenerateBindings(program);
+#endif  // TINT_BUILD_SPV_WRITER
+            break;
+        case OutputFormat::kWGSL:
+            break;
     }
 
     // For the generates which use MultiPlanar, make sure the configuration options are provided so
     // that the transformer will execute.
-    if (output_ == OutputFormat::kMSL || output_ == OutputFormat::kHLSL ||
-        output_ == OutputFormat::kSpv) {
+    if (output_ == OutputFormat::kMSL || output_ == OutputFormat::kHLSL) {
         // Gather external texture binding information
         // Collect next valid binding number per group
         std::unordered_map<uint32_t, uint32_t> group_to_next_binding_number;
@@ -282,7 +301,6 @@
                 break;
             }
             case OutputFormat::kSpv: {
-                options_spirv_.external_texture_options.bindings_map = new_bindings_map;
                 break;
             }
             default:
@@ -335,7 +353,7 @@
 
             // Remap resource numbers to a flat namespace.
             // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
-            if (auto flattened = tint::writer::FlattenBindings(program)) {
+            if (auto flattened = tint::wgsl::FlattenBindings(program)) {
                 program = std::move(*flattened);
             }
 
diff --git a/src/tint/fuzzers/tint_concurrency_fuzzer.cc b/src/tint/fuzzers/tint_concurrency_fuzzer.cc
index 9301399..c09ab46 100644
--- a/src/tint/fuzzers/tint_concurrency_fuzzer.cc
+++ b/src/tint/fuzzers/tint_concurrency_fuzzer.cc
@@ -19,14 +19,15 @@
 
 #include <thread>
 
-#include "src/tint/fuzzers/apply_substitute_overrides.h"
 #include "src/tint/lang/glsl/writer/writer.h"
 #include "src/tint/lang/hlsl/writer/writer.h"
 #include "src/tint/lang/msl/writer/writer.h"
 #include "src/tint/lang/spirv/writer/writer.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
 #include "src/tint/lang/wgsl/helpers/flatten_bindings.h"
 #include "src/tint/lang/wgsl/inspector/inspector.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
+#include "src/tint/lang/wgsl/sem/module.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
 #include "src/tint/utils/math/hash.h"
 
@@ -47,9 +48,16 @@
         return 0;
     }
 
-    program = tint::fuzzers::ApplySubstituteOverrides(std::move(program));
-    if (!program.IsValid()) {
-        return 0;
+    if (program.Sem().Module()->Extensions().Contains(
+            tint::wgsl::Extension::kChromiumExperimentalPixelLocal)) {
+        return 0;  // Not supported
+    }
+
+    if (auto transformed = tint::wgsl::ApplySubstituteOverrides(program)) {
+        program = std::move(*transformed);
+        if (!program.IsValid()) {
+            return 0;
+        }
     }
 
     tint::inspector::Inspector inspector(program);
@@ -110,7 +118,7 @@
 #if TINT_BUILD_MSL_WRITER
                 case Writer::kMSL: {
                     // Remap resource numbers to a flat namespace.
-                    if (auto flattened = tint::writer::FlattenBindings(program)) {
+                    if (auto flattened = tint::wgsl::FlattenBindings(program)) {
                         (void)tint::msl::writer::Generate(flattened.value(), {});
                     }
                     break;
diff --git a/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc b/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
index 0f1cc86..47fb9ea 100644
--- a/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
+++ b/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
@@ -16,7 +16,7 @@
 #include <string>
 #include <unordered_set>
 
-#include "src/tint/fuzzers/apply_substitute_overrides.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
 #include "src/tint/lang/wgsl/reader/parser/parser.h"
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
@@ -68,9 +68,11 @@
         return 0;
     }
 
-    src = tint::fuzzers::ApplySubstituteOverrides(std::move(src));
-    if (!src.IsValid()) {
-        return 0;
+    if (auto transformed = tint::wgsl::ApplySubstituteOverrides(src)) {
+        src = std::move(*transformed);
+        if (!src.IsValid()) {
+            return 0;
+        }
     }
 
     auto ir = tint::wgsl::reader::ProgramToIR(src);
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn b/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn
index 78a3623..cddb936 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn
+++ b/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn
@@ -15,7 +15,7 @@
 import("//build_overrides/build.gni")
 import("../../../../scripts/tint_overrides_with_defaults.gni")
 
-if (build_with_chromium) {
+if (tint_has_fuzzers) {
   source_set("tint_regex_fuzzer") {
     public_configs = [
       "${tint_src_dir}:tint_config",
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
index 45802cd..90e7e21 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
@@ -14,7 +14,7 @@
 
 function(add_tint_regex_fuzzer NAME)
   add_executable(${NAME} ${NAME}.cc ${REGEX_FUZZER_SOURCES})
-  target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer)
+  target_link_libraries(${NAME} PRIVATE libtint_regex_fuzzer tint_lang_spirv_writer_helpers)
   tint_fuzzer_compile_options(${NAME})
   tint_spvtools_compile_options(${NAME})
   target_compile_definitions(${NAME} PRIVATE CUSTOM_MUTATOR)
@@ -40,8 +40,6 @@
         cli.h
         fuzzer.cc
         override_cli_params.h
-        ../apply_substitute_overrides.cc
-        ../apply_substitute_overrides.h
         ../tint_common_fuzzer.cc
         ../tint_common_fuzzer.h)
 
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/BUILD.gn b/src/tint/fuzzers/tint_spirv_tools_fuzzer/BUILD.gn
deleted file mode 100644
index a42c2d5..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/BUILD.gn
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright 2022 The Dawn Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import("../../../../scripts/dawn_overrides_with_defaults.gni")
-
-# Target aliases to ease merging Tint->Dawn
-
-group("tint_spirv_tools_fuzzer") {
-  deps = [ "${dawn_tint_dir}/src/tint/fuzzers/tint_spirv_tools_fuzzer:tint_spirv_tools_fuzzer" ]
-  testonly = true
-}
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt
deleted file mode 100644
index 0e2f98f..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt
+++ /dev/null
@@ -1,115 +0,0 @@
-# Copyright 2021 The Tint Authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set(FUZZER_SOURCES
-        ../mersenne_twister_engine.cc
-        ../random_generator.cc
-        ../random_generator_engine.cc
-        cli.cc
-        fuzzer.cc
-        mutator.cc
-        mutator_cache.cc
-        spirv_fuzz_mutator.cc
-        spirv_opt_mutator.cc
-        spirv_reduce_mutator.cc
-        util.cc)
-
-set(FUZZER_SOURCES ${FUZZER_SOURCES}
-        ../mersenne_twister_engine.h
-        ../random_generator.h
-        ../random_generator_engine.h
-        cli.h
-        mutator.h
-        mutator_cache.h
-        override_cli_params.h
-        spirv_fuzz_mutator.h
-        spirv_opt_mutator.h
-        spirv_reduce_mutator.h
-        util.h)
-
-set(FUZZER_SOURCES ${FUZZER_SOURCES}
-        ../apply_substitute_overrides.cc
-        ../apply_substitute_overrides.h
-        ../tint_common_fuzzer.cc
-        ../tint_common_fuzzer.h)
-
-function(configure_spirv_tools_fuzzer_target NAME SOURCES)
-    add_executable(${NAME} ${SOURCES})
-    target_link_libraries(${NAME} PRIVATE
-        SPIRV-Tools
-        SPIRV-Tools-opt
-        SPIRV-Tools-fuzz
-        SPIRV-Tools-reduce
-    )
-    tint_default_compile_options(${NAME})
-    target_compile_options(${NAME} PRIVATE
-        -Wno-conditional-uninitialized
-        -Wno-covered-switch-default
-        -Wno-missing-prototypes
-        -Wno-zero-as-null-pointer-constant
-        -Wno-reserved-id-macro
-        -Wno-sign-conversion
-        -Wno-extra-semi-stmt
-        -Wno-inconsistent-missing-destructor-override
-        -Wno-newline-eof
-        -Wno-old-style-cast
-        -Wno-weak-vtables
-        -Wno-undef)
-    target_include_directories(${NAME} PRIVATE
-        ${spirv-tools_SOURCE_DIR}
-        ${spirv-tools_BINARY_DIR})
-endfunction()
-
-function(add_tint_spirv_tools_fuzzer NAME)
-  set(FUZZER_TARGET_SOURCES ${NAME}.cc ${FUZZER_SOURCES})
-  configure_spirv_tools_fuzzer_target(${NAME} "${FUZZER_TARGET_SOURCES}")
-  tint_fuzzer_compile_options(${NAME})
-  target_compile_definitions(tint_spirv_tools_fuzzer PUBLIC CUSTOM_MUTATOR)
-  target_compile_definitions(tint_spirv_tools_fuzzer PRIVATE TARGET_FUZZER)
-endfunction()
-
-# Add libfuzzer targets.
-# Targets back-ends according to command line arguments.
-add_tint_spirv_tools_fuzzer(tint_spirv_tools_fuzzer)
-# Targets back-ends individually.
-add_tint_spirv_tools_fuzzer(tint_spirv_tools_hlsl_writer_fuzzer)
-add_tint_spirv_tools_fuzzer(tint_spirv_tools_msl_writer_fuzzer)
-add_tint_spirv_tools_fuzzer(tint_spirv_tools_spv_writer_fuzzer)
-add_tint_spirv_tools_fuzzer(tint_spirv_tools_wgsl_writer_fuzzer)
-
-set(DEBUGGER_SOURCES
-    ../mersenne_twister_engine.cc
-    ../random_generator.cc
-    ../random_generator_engine.cc
-    cli.cc
-    mutator.cc
-    mutator_debugger.cc
-    spirv_fuzz_mutator.cc
-    spirv_opt_mutator.cc
-    spirv_reduce_mutator.cc
-    util.cc)
-
-set(DEBUGGER_SOURCES ${DEBUGGER_SOURCES}
-    ../mersenne_twister_engine.h
-    ../random_generator.h
-    ../random_generator_engine.h
-    cli.h
-    mutator.h
-    spirv_fuzz_mutator.h
-    spirv_opt_mutator.h
-    spirv_reduce_mutator.h
-    util.h)
-
-configure_spirv_tools_fuzzer_target(tint_spirv_tools_mutator_debugger "${DEBUGGER_SOURCES}")
-target_compile_definitions(tint_spirv_tools_mutator_debugger PRIVATE TARGET_DEBUGGER)
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.cc
deleted file mode 100644
index 9e7c973..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.cc
+++ /dev/null
@@ -1,470 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-
-#include <fstream>
-#include <limits>
-#include <sstream>
-#include <string>
-#include <utility>
-#include <vector>
-
-#include "source/opt/build_module.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-namespace {
-
-const char* const kMutatorParameters = R"(
-Mutators' parameters:
-
-  -tint_donors=
-                       A path to the text file with a list of paths to the
-                       SPIR-V donor files. Check out the doc for the spirv-fuzz
-                       to learn more about donor binaries. Donors are not used
-                       by default.
-
-  -tint_enable_all_fuzzer_passes=
-                       Whether to use all fuzzer passes or a randomly selected subset
-                       of them. This must be one of `true` or `false` (without `).
-                       By default it's `false`.
-
-  -tint_enable_all_reduce_passes=
-                       Whether to use all reduction passes or a randomly selected subset
-                       of them. This must be one of `true` or `false` (without `).
-                       By default it's `false`.
-
-  -tint_opt_batch_size=
-                       The maximum number of spirv-opt optimizations that
-                       will be applied in a single mutation session (i.e.
-                       a call to LLVMFuzzerCustomMutator). This must fit in
-                       uint32_t. By default it's 6.
-
-  -tint_reduction_batch_size=
-                       The maximum number of spirv-reduce reductions that
-                       will be applied in a single mutation session (i.e.
-                       a call to LLVMFuzzerCustomMutator). This must fit in
-                       uint32_t. By default it's 3.
-
-  -tint_repeated_pass_strategy=
-                       The strategy that will be used to recommend the next fuzzer
-                       pass. This must be one of `simple`, `looped` or `random`
-                       (without `). By default it's `simple`. Check out the doc for
-                       spirv-fuzz to learn more.
-
-  -tint_transformation_batch_size=
-                       The maximum number of spirv-fuzz transformations
-                       that will be applied during a single mutation
-                       session (i.e. a call to LLVMFuzzerCustomMutator).
-                       This must fit in uint32_t. By default it's 3.
-
-  -tint_validate_after_each_fuzzer_pass=
-                       Whether to validate SPIR-V binary after each fuzzer pass.
-                       This must be one of `true` or `false` (without `).
-                       By default it's `true`. Switch this to `false` if you experience
-                       bad performance.
-
-  -tint_validate_after_each_opt_pass=
-                       Whether to validate SPIR-V binary after each optimization pass.
-                       This must be one of `true` or `false` (without `).
-                       By default it's `true`. Switch this to `false` if you experience
-                       bad performance.
-
-  -tint_validate_after_each_reduce_pass=
-                       Whether to validate SPIR-V binary after each reduction pass.
-                       This must be one of `true` or `false` (without `).
-                       By default it's `true`. Switch this to `false` if you experience
-                       bad performance.
-)";
-
-const char* const kFuzzerHelpMessage = R"(
-This fuzzer uses SPIR-V binaries to fuzz the Tint compiler. It uses SPIRV-Tools
-to mutate those binaries. The fuzzer works on a corpus of SPIR-V shaders.
-For each shader from the corpus it uses one of `spirv-fuzz`, `spirv-reduce` or
-`spirv-opt` to mutate it and then runs the shader through the Tint compiler in
-two steps:
-- Converts the mutated shader to WGSL.
-- Converts WGSL to some target language specified in the CLI arguments.
-
-Below is a list of all supported parameters for this fuzzer. You may want to
-run it with -help=1 to check out libfuzzer parameters.
-
-Fuzzer parameters:
-
-  -tint_error_dir
-                       The directory that will be used to output invalid SPIR-V
-                       binaries to. This is especially useful during debugging
-                       mutators. The directory must have the following subdirectories:
-                       - spv/ - will be used to output errors, produced during
-                         the conversion from the SPIR-V to WGSL.
-                       - wgsl/ - will be used to output errors, produced during
-                         the conversion from the WGSL to `--fuzzing_target`.
-                       - mutator/ - will be used to output errors, produced by
-                         the mutators.
-                       By default invalid files are not printed out.
-
-  -tint_fuzzing_target
-                       The type of backend to target during fuzzing. This must
-                       be one or a combination of `wgsl`, `spv`, `msl` or `hlsl`
-                       (without `) separated by commas. By default it's
-                       `wgsl,spv,msl,hlsl`.
-
-  -tint_help
-                       Show this message. Note that there is also a -help=1
-                       parameter that will display libfuzzer's help message.
-
-  -tint_mutator_cache_size=
-                       The maximum size of the cache that stores
-                       mutation sessions. This must fit in uint32_t.
-                       By default it's 20.
-
-  -tint_mutator_type=
-                       Determines types of the mutators to run. This must be one or
-                       a combination of `fuzz`, `opt`, `reduce` (without `) separated by
-                       comma. If a combination is specified, each element in the
-                       combination will have an equal chance of mutating a SPIR-V
-                       binary during a mutation session (i.e. if no mutator exists
-                       for that binary in the mutator cache). By default, the
-                       parameter's value is `fuzz,opt,reduce`.
-)";
-
-const char* const kMutatorDebuggerHelpMessage = R"(
-This tool is used to debug *mutators*. It uses CLI arguments similar to the
-ones used by the fuzzer. To debug some mutator you just need to specify the
-mutator type, the seed and the path to the SPIR-V binary that triggered the
-error. This tool will run the mutator on the binary until the error is
-produced or the mutator returns `kLimitReached`.
-
-Note that this is different from debugging the fuzzer by specifying input
-files to test. The difference is that the latter will not execute any
-mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this
-tool is useful when one of the SPIRV-Tools mutators crashes or produces an
-invalid binary in LLVMFuzzerCustomMutator.
-
-Debugger parameters:
-
-  --help
-                       Show this message.
-
-  --mutator_type=
-                       Determines the type of the mutator to debug. This must be
-                       one of `fuzz`, `reduce` or `opt` (without `). This parameter
-                       is REQUIRED.
-
-  --original_binary=
-                       The path to the SPIR-V binary that the faulty mutator was
-                       initialized with. This will be dumped on errors by the fuzzer
-                       if `--error_dir` is specified. This parameter is REQUIRED.
-
-  --seed=
-                       The seed for the random number generator that was used to
-                       initialize the mutator. This value is usually printed to
-                       the console when the mutator produces an invalid binary.
-                       It is also dumped into the log file if `--error_dir` is
-                       specified. This must fit in uint32_t. This parameter is
-                       REQUIRED.
-)";
-
-void PrintHelpMessage(const char* help_message) {
-    std::cout << help_message << std::endl << kMutatorParameters << std::endl;
-}
-
-[[noreturn]] void InvalidParameter(const char* help_message, const char* param) {
-    std::cout << "Invalid value for " << param << std::endl;
-    PrintHelpMessage(help_message);
-    exit(1);
-}
-
-bool ParseUint32(const char* param, uint32_t* out) {
-    uint64_t value = static_cast<uint64_t>(strtoul(param, nullptr, 10));
-    if (value > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
-        return false;
-    }
-    *out = static_cast<uint32_t>(value);
-    return true;
-}
-
-std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> ParseDonors(const char* file_name) {
-    std::ifstream fin(file_name);
-    if (!fin) {
-        std::cout << "Can't open donors list file: " << file_name << std::endl;
-        exit(1);
-    }
-
-    std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> result;
-    for (std::string donor_file_name; fin >> donor_file_name;) {
-        if (!std::ifstream(donor_file_name)) {
-            std::cout << "Can't open donor file: " << donor_file_name << std::endl;
-            exit(1);
-        }
-
-        result.emplace_back([donor_file_name] {
-            std::vector<uint32_t> binary;
-            if (!util::ReadBinary(donor_file_name, &binary)) {
-                std::cout << "Failed to read donor from: " << donor_file_name << std::endl;
-                exit(1);
-            }
-            return spvtools::BuildModule(kDefaultTargetEnv,
-                                         spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
-                                         binary.data(), binary.size());
-        });
-    }
-
-    return result;
-}
-
-bool ParseRepeatedPassStrategy(const char* param, spvtools::fuzz::RepeatedPassStrategy* out) {
-    if (!strcmp(param, "simple")) {
-        *out = spvtools::fuzz::RepeatedPassStrategy::kSimple;
-    } else if (!strcmp(param, "looped")) {
-        *out = spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations;
-    } else if (!strcmp(param, "random")) {
-        *out = spvtools::fuzz::RepeatedPassStrategy::kRandomWithRecommendations;
-    } else {
-        return false;
-    }
-    return true;
-}
-
-bool ParseBool(const char* param, bool* out) {
-    if (!strcmp(param, "true")) {
-        *out = true;
-    } else if (!strcmp(param, "false")) {
-        *out = false;
-    } else {
-        return false;
-    }
-    return true;
-}
-
-bool ParseMutatorType(const char* param, MutatorType* out) {
-    if (!strcmp(param, "fuzz")) {
-        *out = MutatorType::kFuzz;
-    } else if (!strcmp(param, "opt")) {
-        *out = MutatorType::kOpt;
-    } else if (!strcmp(param, "reduce")) {
-        *out = MutatorType::kReduce;
-    } else {
-        return false;
-    }
-    return true;
-}
-
-bool ParseFuzzingTarget(const char* param, FuzzingTarget* out) {
-    if (!strcmp(param, "wgsl")) {
-        *out = FuzzingTarget::kWgsl;
-    } else if (!strcmp(param, "spv")) {
-        *out = FuzzingTarget::kSpv;
-    } else if (!strcmp(param, "msl")) {
-        *out = FuzzingTarget::kMsl;
-    } else if (!strcmp(param, "hlsl")) {
-        *out = FuzzingTarget::kHlsl;
-    } else {
-        return false;
-    }
-    return true;
-}
-
-bool HasPrefix(const char* str, const char* prefix) {
-    return strncmp(str, prefix, strlen(prefix)) == 0;
-}
-
-bool ParseMutatorCliParam(const char* param, const char* help_message, MutatorCliParams* out) {
-    if (HasPrefix(param, "-tint_transformation_batch_size=")) {
-        if (!ParseUint32(param + sizeof("-tint_transformation_batch_size=") - 1,
-                         &out->transformation_batch_size)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_reduction_batch_size=")) {
-        if (!ParseUint32(param + sizeof("-tint_reduction_batch_size=") - 1,
-                         &out->reduction_batch_size)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_opt_batch_size=")) {
-        if (!ParseUint32(param + sizeof("-tint_opt_batch_size=") - 1, &out->opt_batch_size)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_donors=")) {
-        out->donors = ParseDonors(param + sizeof("-tint_donors=") - 1);
-    } else if (HasPrefix(param, "-tint_repeated_pass_strategy=")) {
-        if (!ParseRepeatedPassStrategy(param + sizeof("-tint_repeated_pass_strategy=") - 1,
-                                       &out->repeated_pass_strategy)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_enable_all_fuzzer_passes=")) {
-        if (!ParseBool(param + sizeof("-tint_enable_all_fuzzer_passes=") - 1,
-                       &out->enable_all_fuzzer_passes)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_enable_all_reduce_passes=")) {
-        if (!ParseBool(param + sizeof("-tint_enable_all_reduce_passes=") - 1,
-                       &out->enable_all_reduce_passes)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_validate_after_each_opt_pass=")) {
-        if (!ParseBool(param + sizeof("-tint_validate_after_each_opt_pass=") - 1,
-                       &out->validate_after_each_opt_pass)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_validate_after_each_fuzzer_pass=")) {
-        if (!ParseBool(param + sizeof("-tint_validate_after_each_fuzzer_pass=") - 1,
-                       &out->validate_after_each_fuzzer_pass)) {
-            InvalidParameter(help_message, param);
-        }
-    } else if (HasPrefix(param, "-tint_validate_after_each_reduce_pass=")) {
-        if (!ParseBool(param + sizeof("-tint_validate_after_each_reduce_pass=") - 1,
-                       &out->validate_after_each_reduce_pass)) {
-            InvalidParameter(help_message, param);
-        }
-    } else {
-        return false;
-    }
-    return true;
-}
-
-}  // namespace
-
-FuzzerCliParams ParseFuzzerCliParams(int* argc, char** argv) {
-    FuzzerCliParams cli_params;
-    const auto* help_message = kFuzzerHelpMessage;
-    auto help = false;
-
-    for (int i = *argc - 1; i > 0; --i) {
-        auto param = argv[i];
-        auto recognized_param = true;
-
-        if (HasPrefix(param, "-tint_mutator_cache_size=")) {
-            if (!ParseUint32(param + sizeof("-tint_mutator_cache_size=") - 1,
-                             &cli_params.mutator_cache_size)) {
-                InvalidParameter(help_message, param);
-            }
-        } else if (HasPrefix(param, "-tint_mutator_type=")) {
-            auto result = MutatorType::kNone;
-
-            std::stringstream ss(param + sizeof("-tint_mutator_type=") - 1);
-            for (std::string value; std::getline(ss, value, ',');) {
-                auto out = MutatorType::kNone;
-                if (!ParseMutatorType(value.c_str(), &out)) {
-                    InvalidParameter(help_message, param);
-                }
-                result = result | out;
-            }
-
-            if (result == MutatorType::kNone) {
-                InvalidParameter(help_message, param);
-            }
-
-            cli_params.mutator_type = result;
-        } else if (HasPrefix(param, "-tint_fuzzing_target=")) {
-            auto result = FuzzingTarget::kNone;
-
-            std::stringstream ss(param + sizeof("-tint_fuzzing_target=") - 1);
-            for (std::string value; std::getline(ss, value, ',');) {
-                auto tmp = FuzzingTarget::kNone;
-                if (!ParseFuzzingTarget(value.c_str(), &tmp)) {
-                    InvalidParameter(help_message, param);
-                }
-                result = result | tmp;
-            }
-
-            if (result == FuzzingTarget::kNone) {
-                InvalidParameter(help_message, param);
-            }
-
-            cli_params.fuzzing_target = result;
-        } else if (HasPrefix(param, "-tint_error_dir=")) {
-            cli_params.error_dir = param + sizeof("-tint_error_dir=") - 1;
-        } else if (!strcmp(param, "-tint_help")) {
-            help = true;
-        } else {
-            recognized_param =
-                ParseMutatorCliParam(param, help_message, &cli_params.mutator_params);
-        }
-
-        if (recognized_param) {
-            // Remove the recognized parameter from the list of all parameters by
-            // swapping it with the last one. This will suppress warnings in the
-            // libFuzzer about unrecognized parameters. By default, libFuzzer thinks
-            // that all user-defined parameters start with two dashes. However, we are
-            // forced to use a single one to make the fuzzer compatible with the
-            // ClusterFuzz.
-            std::swap(argv[i], argv[*argc - 1]);
-            *argc -= 1;
-        }
-    }
-
-    if (help) {
-        PrintHelpMessage(help_message);
-        exit(0);
-    }
-
-    return cli_params;
-}
-
-MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(int argc, const char* const* argv) {
-    MutatorDebuggerCliParams cli_params;
-    bool seed_param_present = false;
-    bool original_binary_param_present = false;
-    bool mutator_type_param_present = false;
-    const auto* help_message = kMutatorDebuggerHelpMessage;
-    auto help = false;
-
-    for (int i = 0; i < argc; ++i) {
-        auto param = argv[i];
-        ParseMutatorCliParam(param, help_message, &cli_params.mutator_params);
-
-        if (HasPrefix(param, "--mutator_type=")) {
-            if (!ParseMutatorType(param + sizeof("--mutator_type=") - 1,
-                                  &cli_params.mutator_type)) {
-                InvalidParameter(help_message, param);
-            }
-            mutator_type_param_present = true;
-        } else if (HasPrefix(param, "--original_binary=")) {
-            if (!util::ReadBinary(param + sizeof("--original_binary=") - 1,
-                                  &cli_params.original_binary)) {
-                InvalidParameter(help_message, param);
-            }
-            original_binary_param_present = true;
-        } else if (HasPrefix(param, "--seed=")) {
-            if (!ParseUint32(param + sizeof("--seed=") - 1, &cli_params.seed)) {
-                InvalidParameter(help_message, param);
-            }
-            seed_param_present = true;
-        } else if (!strcmp(param, "--help")) {
-            help = true;
-        }
-    }
-
-    if (help) {
-        PrintHelpMessage(help_message);
-        exit(0);
-    }
-
-    std::pair<bool, const char*> required_params[] = {
-        {seed_param_present, "--seed"},
-        {original_binary_param_present, "--original_binary"},
-        {mutator_type_param_present, "--mutator_type"}};
-
-    for (auto required_param : required_params) {
-        if (!required_param.first) {
-            std::cout << required_param.second << " is missing" << std::endl;
-            exit(1);
-        }
-    }
-
-    return cli_params;
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h
deleted file mode 100644
index c10c1a7..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
-
-#include <string>
-#include <vector>
-
-#include "source/fuzz/fuzzer.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// Default SPIR-V environment that will be used during fuzzing.
-const auto kDefaultTargetEnv = SPV_ENV_VULKAN_1_1;
-
-/// The type of the mutator to run.
-enum class MutatorType {
-    kNone = 0,
-    kFuzz = 1 << 0,
-    kReduce = 1 << 1,
-    kOpt = 1 << 2,
-    kAll = kFuzz | kReduce | kOpt
-};
-
-inline MutatorType operator|(MutatorType a, MutatorType b) {
-    return static_cast<MutatorType>(static_cast<int>(a) | static_cast<int>(b));
-}
-
-inline MutatorType operator&(MutatorType a, MutatorType b) {
-    return static_cast<MutatorType>(static_cast<int>(a) & static_cast<int>(b));
-}
-
-/// Shading language to target during fuzzing.
-enum class FuzzingTarget {
-    kNone = 0,
-    kHlsl = 1 << 0,
-    kMsl = 1 << 1,
-    kSpv = 1 << 2,
-    kWgsl = 1 << 3,
-    kAll = kHlsl | kMsl | kSpv | kWgsl
-};
-
-inline FuzzingTarget operator|(FuzzingTarget a, FuzzingTarget b) {
-    return static_cast<FuzzingTarget>(static_cast<int>(a) | static_cast<int>(b));
-}
-
-inline FuzzingTarget operator&(FuzzingTarget a, FuzzingTarget b) {
-    return static_cast<FuzzingTarget>(static_cast<int>(a) & static_cast<int>(b));
-}
-
-/// These parameters are accepted by various mutators and thus they are accepted
-/// by both the fuzzer and the mutator debugger.
-struct MutatorCliParams {
-    /// SPIR-V target environment for fuzzing.
-    spv_target_env target_env = kDefaultTargetEnv;
-
-    /// The number of spirv-fuzz transformations to apply at a time.
-    uint32_t transformation_batch_size = 3;
-
-    /// The number of spirv-reduce reductions to apply at a time.
-    uint32_t reduction_batch_size = 3;
-
-    /// The number of spirv-opt optimizations to apply at a time.
-    uint32_t opt_batch_size = 6;
-
-    /// The vector of donors to use in spirv-fuzz (see the doc for spirv-fuzz to
-    /// learn more).
-    std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> donors = {};
-
-    /// The strategy to use during fuzzing in spirv-fuzz (see the doc for
-    /// spirv-fuzz to learn more).
-    spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy =
-        spvtools::fuzz::RepeatedPassStrategy::kSimple;
-
-    /// Whether to use all fuzzer passes or a randomly selected subset of them.
-    bool enable_all_fuzzer_passes = false;
-
-    /// Whether to use all reduction passes or a randomly selected subset of them.
-    bool enable_all_reduce_passes = false;
-
-    /// Whether to validate the SPIR-V binary after each optimization pass.
-    bool validate_after_each_opt_pass = true;
-
-    /// Whether to validate the SPIR-V binary after each fuzzer pass.
-    bool validate_after_each_fuzzer_pass = true;
-
-    /// Whether to validate the SPIR-V binary after each reduction pass.
-    bool validate_after_each_reduce_pass = true;
-};
-
-/// Parameters specific to the fuzzer. Type `-tint_help` in the CLI to learn
-/// more.
-struct FuzzerCliParams {
-    /// The size of the cache that records ongoing mutation sessions.
-    uint32_t mutator_cache_size = 20;
-
-    /// The type of the mutator to run.
-    MutatorType mutator_type = MutatorType::kAll;
-
-    /// Tint backend to fuzz.
-    FuzzingTarget fuzzing_target = FuzzingTarget::kAll;
-
-    /// The path to the directory, that will be used to output buggy shaders.
-    std::string error_dir = "";
-
-    /// Parameters for various mutators.
-    MutatorCliParams mutator_params;
-};
-
-/// Parameters specific to the mutator debugger. Type `--help` in the CLI to
-/// learn more.
-struct MutatorDebuggerCliParams {
-    /// The type of the mutator to debug.
-    MutatorType mutator_type = MutatorType::kNone;
-
-    /// The seed that was used to initialize the mutator.
-    uint32_t seed = 0;
-
-    /// The binary that triggered a bug in the mutator.
-    std::vector<uint32_t> original_binary;
-
-    /// Parameters for various mutators.
-    MutatorCliParams mutator_params;
-};
-
-/// Parses CLI parameters for the fuzzer. This function exits with an error code
-/// and a message is printed to the console if some parameter has invalid
-/// format. You can pass `-tint_help` to check out all available parameters.
-/// This function will remove recognized parameters from the `argv` and adjust
-/// the `argc` accordingly.
-///
-/// @param argc - the number of parameters (identical to the `argc` in `main`
-///     function).
-/// @param argv - array of C strings of parameters.
-/// @return the parsed parameters.
-FuzzerCliParams ParseFuzzerCliParams(int* argc, char** argv);
-
-/// Parses CLI parameters for the mutator debugger. This function exits with an
-/// error code and a message is printed to the console if some parameter has
-/// invalid format. You can pass `--help` to check out all available parameters.
-///
-/// @param argc - the number of parameters (identical to the `argc` in `main`
-///     function).
-/// @param argv - array of C strings of parameters.
-/// @return the parsed parameters.
-MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(int argc, const char* const* argv);
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc
deleted file mode 100644
index 41e417b..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc
+++ /dev/null
@@ -1,249 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-#include <memory>
-#include <string>
-#include <vector>
-
-#include "spirv-tools/libspirv.hpp"
-#include "src/tint/fuzzers/random_generator.h"
-#include "src/tint/fuzzers/tint_common_fuzzer.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-#include "testing/libfuzzer/libfuzzer_exports.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-namespace {
-
-struct Context {
-    FuzzerCliParams params;
-    std::unique_ptr<MutatorCache> mutator_cache;
-};
-
-Context* context = nullptr;
-
-extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
-    auto params = ParseFuzzerCliParams(argc, *argv);
-    auto mutator_cache = params.mutator_cache_size
-                             ? std::make_unique<MutatorCache>(params.mutator_cache_size)
-                             : nullptr;
-    context = new Context{std::move(params), std::move(mutator_cache)};
-    OverrideCliParams(context->params);
-    return 0;
-}
-
-std::unique_ptr<Mutator> CreateMutator(const std::vector<uint32_t>& binary, unsigned seed) {
-    std::vector<MutatorType> types;
-    types.reserve(3);
-
-    // Determine which mutator we will be using for `binary` at random.
-    auto cli_mutator_type = context->params.mutator_type;
-    if ((MutatorType::kFuzz & cli_mutator_type) == MutatorType::kFuzz) {
-        types.push_back(MutatorType::kFuzz);
-    }
-    if ((MutatorType::kReduce & cli_mutator_type) == MutatorType::kReduce) {
-        types.push_back(MutatorType::kReduce);
-    }
-    if ((MutatorType::kOpt & cli_mutator_type) == MutatorType::kOpt) {
-        types.push_back(MutatorType::kOpt);
-    }
-
-    assert(!types.empty() && "At least one mutator type must be specified");
-    RandomGenerator generator(seed);
-    auto mutator_type = types[generator.GetUInt32(static_cast<uint32_t>(types.size()))];
-
-    const auto& mutator_params = context->params.mutator_params;
-    switch (mutator_type) {
-        case MutatorType::kFuzz:
-            return std::make_unique<SpirvFuzzMutator>(
-                mutator_params.target_env, binary, seed, mutator_params.donors,
-                mutator_params.enable_all_fuzzer_passes, mutator_params.repeated_pass_strategy,
-                mutator_params.validate_after_each_fuzzer_pass,
-                mutator_params.transformation_batch_size);
-        case MutatorType::kReduce:
-            return std::make_unique<SpirvReduceMutator>(
-                mutator_params.target_env, binary, seed, mutator_params.reduction_batch_size,
-                mutator_params.enable_all_reduce_passes,
-                mutator_params.validate_after_each_reduce_pass);
-        case MutatorType::kOpt:
-            return std::make_unique<SpirvOptMutator>(mutator_params.target_env, seed, binary,
-                                                     mutator_params.validate_after_each_opt_pass,
-                                                     mutator_params.opt_batch_size);
-        default:
-            assert(false && "All mutator types must be handled above");
-            return nullptr;
-    }
-}
-
-void CLIMessageConsumer(spv_message_level_t level,
-                        const char*,
-                        const spv_position_t& position,
-                        const char* message) {
-    switch (level) {
-        case SPV_MSG_FATAL:
-        case SPV_MSG_INTERNAL_ERROR:
-        case SPV_MSG_ERROR:
-            std::cerr << "error: line " << position.index << ": " << message << std::endl;
-            break;
-        case SPV_MSG_WARNING:
-            std::cout << "warning: line " << position.index << ": " << message << std::endl;
-            break;
-        case SPV_MSG_INFO:
-            std::cout << "info: line " << position.index << ": " << message << std::endl;
-            break;
-        default:
-            break;
-    }
-}
-
-bool IsValid(const std::vector<uint32_t>& binary) {
-    spvtools::SpirvTools tools(context->params.mutator_params.target_env);
-    tools.SetMessageConsumer(CLIMessageConsumer);
-    return tools.IsValid() &&
-           tools.Validate(binary.data(), binary.size(), spvtools::ValidatorOptions());
-}
-
-extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data,
-                                          size_t size,
-                                          size_t max_size,
-                                          unsigned seed) {
-    if ((size % sizeof(uint32_t)) != 0) {
-        // A valid SPIR-V binary's size must be a multiple of the size of a 32-bit
-        // word, and the SPIR-V Tools fuzzer is only designed to work with valid
-        // binaries.
-        return 0;
-    }
-
-    std::vector<uint32_t> binary(size / sizeof(uint32_t));
-    std::memcpy(binary.data(), data, size);
-
-    MutatorCache placeholder_cache(1);
-    auto* mutator_cache = context->mutator_cache.get();
-    if (!mutator_cache) {
-        // Use a placeholder cache if the user has decided not to use a real cache.
-        // The placeholder cache will be destroyed when we return from this function
-        // but it will save us from writing all the `if (mutator_cache)` below.
-        mutator_cache = &placeholder_cache;
-    }
-
-    if (!mutator_cache->Get(binary)) {
-        // This is an unknown binary, so its validity must be checked before
-        // proceeding.
-        if (!IsValid(binary)) {
-            return 0;
-        }
-        // Assign a mutator to the binary if it doesn't have one yet.
-        mutator_cache->Put(binary, CreateMutator(binary, seed));
-    }
-
-    auto* mutator = mutator_cache->Get(binary);
-    assert(mutator && "Mutator must be present in the cache");
-
-    auto result = mutator->Mutate();
-
-    if (result.GetStatus() == Mutator::Status::kInvalid) {
-        // The binary is invalid - log the error and remove the mutator from the
-        // cache.
-        util::LogMutatorError(*mutator, context->params.error_dir);
-        mutator_cache->Remove(binary);
-        return 0;
-    }
-
-    if (!result.IsChanged()) {
-        // The mutator didn't change the binary this time. This could be due to the
-        // fact that we've reached the number of mutations we can apply (e.g. the
-        // number of transformations in spirv-fuzz) or the mutator was just unlucky.
-        // Either way, there is no harm in destroying mutator and maybe trying again
-        // later (i.e. if libfuzzer decides to do so).
-        mutator_cache->Remove(binary);
-        return 0;
-    }
-
-    // At this point the binary is valid and was changed by the mutator.
-
-    auto mutated = mutator->GetBinary();
-    auto mutated_bytes_size = mutated.size() * sizeof(uint32_t);
-    if (mutated_bytes_size > max_size) {
-        // The binary is too big. It's unlikely that we'll reduce its size by
-        // applying the mutator one more time.
-        mutator_cache->Remove(binary);
-        return 0;
-    }
-
-    if (result.GetStatus() == Mutator::Status::kComplete) {
-        // Reassign the mutator to the mutated binary in the cache so that we can
-        // access later.
-        mutator_cache->Put(mutated, mutator_cache->Remove(binary));
-    } else {
-        // If the binary is valid and was changed but is not `kComplete`, then the
-        // mutator has reached some limit on the number of mutations.
-        mutator_cache->Remove(binary);
-    }
-
-    std::memcpy(data, mutated.data(), mutated_bytes_size);
-    return mutated_bytes_size;
-}
-
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    if (size == 0) {
-        return 0;
-    }
-
-    if ((size % sizeof(uint32_t)) != 0) {
-        // The SPIR-V Tools fuzzer has been designed to work with valid
-        // SPIR-V binaries, whose sizes should be multiples of the size of a 32-bit
-        // word.
-        return 0;
-    }
-
-    CommonFuzzer spv_to_wgsl(InputFormat::kSpv, OutputFormat::kWGSL);
-    spv_to_wgsl.Run(data, size);
-    if (spv_to_wgsl.HasErrors()) {
-        auto error = spv_to_wgsl.Diagnostics().str();
-        util::LogSpvError(error, data, size, context ? context->params.error_dir : "");
-        return 0;
-    }
-
-    const auto& wgsl = spv_to_wgsl.GetGeneratedWgsl();
-
-    std::pair<FuzzingTarget, OutputFormat> targets[] = {
-        {FuzzingTarget::kHlsl, OutputFormat::kHLSL},
-        {FuzzingTarget::kMsl, OutputFormat::kMSL},
-        {FuzzingTarget::kSpv, OutputFormat::kSpv},
-        {FuzzingTarget::kWgsl, OutputFormat::kWGSL}};
-
-    for (auto target : targets) {
-        if ((target.first & context->params.fuzzing_target) != target.first) {
-            continue;
-        }
-
-        CommonFuzzer fuzzer(InputFormat::kWGSL, target.second);
-        fuzzer.Run(reinterpret_cast<const uint8_t*>(wgsl.data()), wgsl.size());
-        if (fuzzer.HasErrors()) {
-            auto error = spv_to_wgsl.Diagnostics().str();
-            util::LogWgslError(error, data, size, wgsl, target.second, context->params.error_dir);
-        }
-    }
-
-    return 0;
-}
-
-}  // namespace
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.cc
deleted file mode 100644
index 6bb7809..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.cc
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-// We need to define destructor here so that vtable is produced in this
-// translation unit (see -Wweak-vtables clang flag).
-Mutator::~Mutator() = default;
-
-Mutator::Result::Result(Status status, bool is_changed) : status_(status), is_changed_(is_changed) {
-    assert((is_changed || status == Status::kStuck || status == Status::kLimitReached) &&
-           "Returning invalid result state");
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h
deleted file mode 100644
index 1e81dff..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
-
-#include <cassert>
-#include <cstdint>
-#include <string>
-#include <vector>
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// This is an interface that is used to define custom mutators based on the
-/// SPIR-V tools.
-class Mutator {
-  public:
-    /// The status of the mutation.
-    enum class Status {
-        /// Binary is valid, the limit is not reached - can mutate further.
-        kComplete,
-
-        /// The binary is valid, the limit of mutations has been reached -
-        /// can't mutate further.
-        kLimitReached,
-
-        /// The binary is valid, the limit is not reached but the mutator has spent
-        /// too much time without mutating anything - better to restart to make sure
-        /// we can make any progress.
-        kStuck,
-
-        /// The binary is invalid - this is likely a bug in the mutator - must
-        /// abort.
-        kInvalid
-    };
-
-    /// Represents the result of the mutation. The following states are possible:
-    /// - if `IsChanged() == false`, then `GetStatus()` can be either
-    ///   `kLimitReached` or `kStuck`.
-    /// - otherwise, any value of `Status` is possible.
-    class Result {
-      public:
-        /// Constructor.
-        /// @param status - the status of the mutation.
-        /// @param is_changed - whether the module was changed during mutation.
-        Result(Status status, bool is_changed);
-
-        /// @return the status of the mutation.
-        Status GetStatus() const { return status_; }
-
-        /// @return whether the module was changed during mutation.
-        bool IsChanged() const { return is_changed_; }
-
-      private:
-        Status status_;
-        bool is_changed_;
-    };
-
-    /// Virtual destructor.
-    virtual ~Mutator();
-
-    /// Causes the mutator to apply a mutation. This method can be called
-    /// multiple times as long as the previous call didn't return
-    /// `Status::kInvalid`.
-    ///
-    /// @return the status of the mutation (e.g. success, error etc) and whether
-    ///     the binary was changed during mutation.
-    virtual Result Mutate() = 0;
-
-    /// Returns the mutated binary. The returned binary is guaranteed to be valid
-    /// iff the previous call to the `Mutate` method returned didn't return
-    /// `Status::kInvalid`.
-    ///
-    /// @return the mutated SPIR-V binary. It might be identical to the original
-    ///     binary if `Result::IsChanged` returns `false`.
-    virtual std::vector<uint32_t> GetBinary() const = 0;
-
-    /// Returns errors, produced by the mutator.
-    ///
-    /// @param path - the directory to which the errors are printed to. No files
-    ///     are created if the `path` is nullptr.
-    /// @param count - the number of the error. Files for this error will be
-    ///     prefixed with `count`.
-    virtual void LogErrors(const std::string* path, uint32_t count) const = 0;
-
-    /// @return errors encountered during the mutation. The returned string is
-    ///     if there were no errors during mutation.
-    virtual std::string GetErrors() const = 0;
-};
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc
deleted file mode 100644
index b85bdc1..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-MutatorCache::MutatorCache(size_t max_size) : map_(), entries_(), max_size_(max_size) {
-    assert(max_size && "`max_size` may not be 0");
-}
-
-MutatorCache::Value::pointer MutatorCache::Get(const Key& key) {
-    auto it = map_.find(key);
-    if (it == map_.end()) {
-        return nullptr;
-    }
-    UpdateUsage(it);
-    return entries_.front().second.get();
-}
-
-void MutatorCache::Put(const Key& key, Value value) {
-    assert(value && "Mutator cache can't have nullptr unique_ptr");
-    auto it = map_.find(key);
-    if (it != map_.end()) {
-        it->second->second = std::move(value);
-        UpdateUsage(it);
-    } else {
-        if (map_.size() == max_size_) {
-            Remove(*entries_.back().first);
-        }
-
-        entries_.emplace_front(nullptr, std::move(value));
-        auto pair = map_.emplace(key, entries_.begin());
-        assert(pair.second && "The key must be unique");
-        entries_.front().first = &pair.first->first;
-    }
-}
-
-MutatorCache::Value MutatorCache::Remove(const Key& key) {
-    auto it = map_.find(key);
-    if (it == map_.end()) {
-        return nullptr;
-    }
-    auto result = std::move(it->second->second);
-    entries_.erase(it->second);
-    map_.erase(it);
-    return result;
-}
-
-size_t MutatorCache::KeyHash::operator()(const std::vector<uint32_t>& vec) const {
-    return std::hash<std::u32string>()({vec.begin(), vec.end()});
-}
-
-void MutatorCache::UpdateUsage(Map::iterator it) {
-    auto entry = std::move(*it->second);
-    entries_.erase(it->second);
-    entries_.push_front(std::move(entry));
-    it->second = entries_.begin();
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h
deleted file mode 100644
index cf9b148..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
-
-#include <cassert>
-#include <list>
-#include <memory>
-#include <unordered_map>
-#include <utility>
-#include <vector>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// Implementation of a fixed size LRU cache. That is, when the number of
-/// elements reaches a certain threshold, the element that wasn't used for the
-/// longest period of time is removed from the cache when a new element is
-/// inserted. All operations have amortized constant time complexity.
-class MutatorCache {
-  public:
-    /// SPIR-V binary that is being mutated.
-    using Key = std::vector<uint32_t>;
-
-    /// Mutator that is used to mutate the `Key`.
-    using Value = std::unique_ptr<Mutator>;
-
-    /// Constructor.
-    /// @param max_size - the maximum number of elements the cache can store. May
-    ///     not be equal to 0.
-    explicit MutatorCache(size_t max_size);
-
-    /// Retrieves a pointer to a value, associated with a given `key`.
-    ///
-    /// If the key is present in the cache, its usage is updated and the
-    /// (non-null) pointer to the value is returned. Otherwise, `nullptr` is
-    /// returned.
-    ///
-    /// @param key - may not exist in this cache.
-    /// @return non-`nullptr` pointer to a value if `key` exists in the cache.
-    /// @return `nullptr` if `key` doesn't exist in this cache.
-    Value::pointer Get(const Key& key);
-
-    /// Inserts a `key`-`value` pair into the cache.
-    ///
-    /// If the `key` is already present, the `value` replaces the old value and
-    /// the usage of `key` is updated. If the `key` is not present, then:
-    /// - if the number of elements in the cache is equal to `max_size`, the
-    ///   key-value pair, where the usage of the key wasn't updated for the
-    ///   longest period of time, is removed from the cache.
-    /// - a new `key`-`value` pair is inserted into the cache.
-    ///
-    /// @param key - a key.
-    /// @param value - may not be a `nullptr`.
-    void Put(const Key& key, Value value);
-
-    /// Removes `key` and an associated value from the cache.
-    ///
-    /// @param key - a key.
-    /// @return a non-`nullptr` pointer to the removed value, associated with
-    ///     `key`.
-    /// @return `nullptr` if `key` is not present in the cache.
-    Value Remove(const Key& key);
-
-  private:
-    struct KeyHash {
-        size_t operator()(const std::vector<uint32_t>& vec) const;
-    };
-
-    using Entry = std::pair<const Key*, Value>;
-    using Map = std::unordered_map<Key, std::list<Entry>::iterator, KeyHash>;
-
-    void UpdateUsage(Map::iterator it);
-
-    Map map_;
-    std::list<Entry> entries_;
-    const size_t max_size_;
-};
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc
deleted file mode 100644
index d8d6550..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <memory>
-#include <string>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-
-/// This tool is used to debug *mutators*. It uses CLI arguments similar to the
-/// ones used by the fuzzer. To debug some mutator you just need to specify the
-/// mutator type, the seed and the path to the SPIR-V binary that triggered the
-/// error. This tool will run the mutator on the binary until the error is
-/// produced or the mutator returns `kLimitReached`.
-///
-/// Note that this is different from debugging the fuzzer by specifying input
-/// files to test. The difference is that the latter will not execute any
-/// mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this
-/// tool is useful when one of the spirv-tools mutators crashes or produces an
-/// invalid binary in LLVMFuzzerCustomMutator.
-int main(int argc, const char** argv) {
-    auto params = tint::fuzzers::spvtools_fuzzer::ParseMutatorDebuggerCliParams(argc, argv);
-
-    std::unique_ptr<tint::fuzzers::spvtools_fuzzer::Mutator> mutator;
-    const auto& mutator_params = params.mutator_params;
-    switch (params.mutator_type) {
-        case tint::fuzzers::spvtools_fuzzer::MutatorType::kFuzz:
-            mutator = std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvFuzzMutator>(
-                mutator_params.target_env, params.original_binary, params.seed,
-                mutator_params.donors, mutator_params.enable_all_fuzzer_passes,
-                mutator_params.repeated_pass_strategy,
-                mutator_params.validate_after_each_fuzzer_pass,
-                mutator_params.transformation_batch_size);
-            break;
-        case tint::fuzzers::spvtools_fuzzer::MutatorType::kReduce:
-            mutator = std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvReduceMutator>(
-                mutator_params.target_env, params.original_binary, params.seed,
-                mutator_params.reduction_batch_size, mutator_params.enable_all_reduce_passes,
-                mutator_params.validate_after_each_reduce_pass);
-            break;
-        case tint::fuzzers::spvtools_fuzzer::MutatorType::kOpt:
-            mutator = std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvOptMutator>(
-                mutator_params.target_env, params.seed, params.original_binary,
-                mutator_params.validate_after_each_opt_pass, mutator_params.opt_batch_size);
-            break;
-        default:
-            assert(false && "All mutator types must've been handled");
-            return 1;
-    }
-
-    while (true) {
-        auto result = mutator->Mutate();
-        if (result.GetStatus() == tint::fuzzers::spvtools_fuzzer::Mutator::Status::kInvalid) {
-            std::cerr << mutator->GetErrors() << std::endl;
-            return 0;
-        }
-        if (result.GetStatus() == tint::fuzzers::spvtools_fuzzer::Mutator::Status::kLimitReached) {
-            break;
-        }
-    }
-}
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h
deleted file mode 100644
index da05a4d..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_OVERRIDE_CLI_PARAMS_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_OVERRIDE_CLI_PARAMS_H_
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// @brief Allows CLI parameters to be overridden.
-///
-/// This function allows fuzz targets to override particular CLI parameters,
-/// for example forcing a particular back-end to be targeted.
-///
-/// @param cli_params - the parsed CLI parameters to be updated.
-void OverrideCliParams(FuzzerCliParams& cli_params);
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_OVERRIDE_CLI_PARAMS_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc
deleted file mode 100644
index b726ec3..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
-
-#include <fstream>
-#include <utility>
-
-#include "source/opt/build_module.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-SpirvFuzzMutator::SpirvFuzzMutator(
-    spv_target_env target_env,
-    std::vector<uint32_t> binary,
-    unsigned seed,
-    const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
-    bool enable_all_passes,
-    spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
-    bool validate_after_each_pass,
-    uint32_t transformation_batch_size)
-    : transformation_batch_size_(transformation_batch_size),
-      errors_(std::make_unique<std::stringstream>()),
-      fuzzer_(nullptr),
-      validator_options_(),
-      original_binary_(std::move(binary)),
-      seed_(seed) {
-    auto ir_context =
-        spvtools::BuildModule(target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
-                              original_binary_.data(), original_binary_.size());
-    assert(ir_context && "|binary| is invalid");
-
-    auto transformation_context = std::make_unique<spvtools::fuzz::TransformationContext>(
-        std::make_unique<spvtools::fuzz::FactManager>(ir_context.get()), validator_options_);
-
-    auto fuzzer_context = std::make_unique<spvtools::fuzz::FuzzerContext>(
-        std::make_unique<spvtools::fuzz::PseudoRandomGenerator>(seed),
-        spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()), false);
-    fuzzer_ = std::make_unique<spvtools::fuzz::Fuzzer>(
-        std::move(ir_context), std::move(transformation_context), std::move(fuzzer_context),
-        util::GetBufferMessageConsumer(errors_.get()), donors, enable_all_passes,
-        repeated_pass_strategy, validate_after_each_pass, validator_options_);
-}
-
-Mutator::Result SpirvFuzzMutator::Mutate() {
-    // The assertion will fail in |fuzzer_->Run| if the previous fuzzing led to
-    // invalid module.
-    auto result = fuzzer_->Run(transformation_batch_size_);
-    switch (result.status) {
-        case spvtools::fuzz::Fuzzer::Status::kComplete:
-            return {Mutator::Status::kComplete, result.is_changed};
-        case spvtools::fuzz::Fuzzer::Status::kModuleTooBig:
-        case spvtools::fuzz::Fuzzer::Status::kTransformationLimitReached:
-            return {Mutator::Status::kLimitReached, result.is_changed};
-        case spvtools::fuzz::Fuzzer::Status::kFuzzerStuck:
-            return {Mutator::Status::kStuck, result.is_changed};
-        case spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule:
-            return {Mutator::Status::kInvalid, result.is_changed};
-    }
-}
-
-std::vector<uint32_t> SpirvFuzzMutator::GetBinary() const {
-    std::vector<uint32_t> result;
-    fuzzer_->GetIRContext()->module()->ToBinary(&result, true);
-    return result;
-}
-
-std::string SpirvFuzzMutator::GetErrors() const {
-    return errors_->str();
-}
-
-void SpirvFuzzMutator::LogErrors(const std::string* path, uint32_t count) const {
-    auto message = GetErrors();
-    std::cout << count << " | SpirvFuzzMutator (seed: " << seed_ << ")" << std::endl;
-    std::cout << message << std::endl;
-
-    if (path) {
-        auto prefix = *path + std::to_string(count);
-
-        // Write errors to file.
-        std::ofstream(prefix + ".fuzzer.log") << "seed: " << seed_ << std::endl
-                                              << message << std::endl;
-
-        // Write the invalid SPIR-V binary.
-        util::WriteBinary(prefix + ".fuzzer.invalid.spv", GetBinary());
-
-        // Write the original SPIR-V binary.
-        util::WriteBinary(prefix + ".fuzzer.original.spv", original_binary_);
-
-        // Write transformations.
-        google::protobuf::util::JsonOptions options;
-        options.add_whitespace = true;
-        std::string json;
-        google::protobuf::util::MessageToJsonString(fuzzer_->GetTransformationSequence(), &json,
-                                                    options);
-        std::ofstream(prefix + ".fuzzer.transformations.json") << json << std::endl;
-
-        std::ofstream binary_transformations(prefix + ".fuzzer.transformations.binary",
-                                             std::ios::binary | std::ios::out);
-        fuzzer_->GetTransformationSequence().SerializeToOstream(&binary_transformations);
-    }
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h
deleted file mode 100644
index 1f94111..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
-
-#include <memory>
-#include <sstream>
-#include <string>
-#include <vector>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-
-#include "source/fuzz/fuzzer.h"
-#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
-#include "source/fuzz/pseudo_random_generator.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// The mutator that uses spirv-fuzz to mutate SPIR-V.
-///
-/// The initial `binary` must be valid according to `target_env`. All other
-/// parameters (except for the `seed` which just initializes the RNG) are from
-/// the `spvtools::fuzz::Fuzzer` class.
-class SpirvFuzzMutator : public Mutator {
-  public:
-    /// Constructor.
-    /// @param target_env - the target environment for the `binary`.
-    /// @param binary - the SPIR-V binary. Must be valid.
-    /// @param seed - seed for the RNG.
-    /// @param donors - vector of donor suppliers.
-    /// @param enable_all_passes - whether to use all fuzzer passes.
-    /// @param repeated_pass_strategy - the strategy to use when selecting the
-    ///     next fuzzer pass.
-    /// @param validate_after_each_pass - whether to validate the binary after
-    ///     each fuzzer pass.
-    /// @param transformation_batch_size - the maximum number of transformations
-    ///     that will be applied during a single call to `Mutate`. It it's equal
-    ///     to 0 then we apply as much transformations as we can until the
-    ///     threshold in the spvtools::fuzz::Fuzzer is reached (see the doc for
-    ///     that class for more info).
-    SpirvFuzzMutator(spv_target_env target_env,
-                     std::vector<uint32_t> binary,
-                     uint32_t seed,
-                     const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
-                     bool enable_all_passes,
-                     spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
-                     bool validate_after_each_pass,
-                     uint32_t transformation_batch_size);
-
-    Result Mutate() override;
-    std::vector<uint32_t> GetBinary() const override;
-    void LogErrors(const std::string* path, uint32_t count) const override;
-    std::string GetErrors() const override;
-
-  private:
-    // The number of transformations that will be applied during a single call to
-    // the `Mutate` method. Is this only a lower bound since transformations are
-    // applied in batches by fuzzer passes (see docs for the
-    // `spvtools::fuzz::Fuzzer` for more info).
-    const uint32_t transformation_batch_size_;
-
-    // The errors produced by the `spvtools::fuzz::Fuzzer`.
-    std::unique_ptr<std::stringstream> errors_;
-    std::unique_ptr<spvtools::fuzz::Fuzzer> fuzzer_;
-    spvtools::ValidatorOptions validator_options_;
-
-    // The following fields are useful for debugging.
-
-    // The binary that the mutator is constructed with.
-    const std::vector<uint32_t> original_binary_;
-
-    // The seed that the mutator is constructed with.
-    const uint32_t seed_;
-};
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc
deleted file mode 100644
index 22fc517..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
-
-#include <fstream>
-#include <iostream>
-#include <unordered_set>
-#include <utility>
-
-#include "spirv-tools/optimizer.hpp"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-SpirvOptMutator::SpirvOptMutator(spv_target_env target_env,
-                                 uint32_t seed,
-                                 std::vector<uint32_t> binary,
-                                 bool validate_after_each_opt,
-                                 uint32_t opt_batch_size)
-    : num_executions_(0),
-      is_valid_(true),
-      target_env_(target_env),
-      original_binary_(std::move(binary)),
-      seed_(seed),
-      opt_passes_({"--combine-access-chains",
-                   "--loop-unroll",
-                   "--merge-blocks",
-                   "--cfg-cleanup",
-                   "--eliminate-dead-functions",
-                   "--merge-return",
-                   "--wrap-opkill",
-                   "--eliminate-dead-code-aggressive",
-                   "--if-conversion",
-                   "--eliminate-local-single-store",
-                   "--eliminate-local-single-block",
-                   "--eliminate-dead-branches",
-                   "--scalar-replacement=0",
-                   "--eliminate-dead-inserts",
-                   "--eliminate-dead-members",
-                   "--simplify-instructions",
-                   "--private-to-local",
-                   "--ssa-rewrite",
-                   "--ccp",
-                   "--reduce-load-size",
-                   "--vector-dce",
-                   "--scalar-replacement=100",
-                   "--inline-entry-points-exhaustive",
-                   "--redundancy-elimination",
-                   "--convert-local-access-chains",
-                   "--copy-propagate-arrays",
-                   "--fix-storage-class"}),
-      optimized_binary_(),
-      validate_after_each_opt_(validate_after_each_opt),
-      opt_batch_size_(opt_batch_size),
-      generator_(seed) {
-    assert(spvtools::SpirvTools(target_env).Validate(original_binary_) &&
-           "Initial binary is invalid");
-    assert(!opt_passes_.empty() && "Must be at least one pass");
-}
-
-SpirvOptMutator::Result SpirvOptMutator::Mutate() {
-    assert(is_valid_ && "The optimizer is not longer valid");
-
-    const uint32_t kMaxNumExecutions = 100;
-    const uint32_t kMaxNumStuck = 10;
-
-    if (num_executions_ == kMaxNumExecutions) {
-        // We've applied this mutator many times already. Indicate to the user that
-        // it might be better to try a different mutator.
-        return {Status::kLimitReached, false};
-    }
-
-    num_executions_++;
-
-    // Get the input binary. If this is the first time we run this mutator, use
-    // the `original_binary_`. Otherwise, one of the following will be true:
-    // - the `optimized_binary_` is not empty.
-    // - the previous call to the `Mutate` method returned `kStuck`.
-    auto binary = num_executions_ == 1 ? original_binary_ : optimized_binary_;
-    optimized_binary_.clear();
-
-    assert(!binary.empty() && "Can't run the optimizer on an empty binary");
-
-    // Number of times spirv-opt wasn't able to produce any new result.
-    uint32_t num_stuck = 0;
-    do {
-        // Randomly select `opt_batch_size` optimization passes. If `opt_batch_size`
-        // is equal to 0, we will use the number of passes equal to the number of
-        // all available passes.
-        auto num_of_passes = opt_batch_size_ ? opt_batch_size_ : opt_passes_.size();
-        std::vector<std::string> passes;
-
-        while (passes.size() < num_of_passes) {
-            auto idx = generator_.GetUInt32(static_cast<uint32_t>(opt_passes_.size()));
-            passes.push_back(opt_passes_[idx]);
-        }
-
-        // Run the `binary` into the `optimized_binary_`.
-        spvtools::Optimizer optimizer(target_env_);
-        optimizer.SetMessageConsumer(util::GetBufferMessageConsumer(&errors_));
-        optimizer.SetValidateAfterAll(validate_after_each_opt_);
-        optimizer.RegisterPassesFromFlags(passes);
-        if (!optimizer.Run(binary.data(), binary.size(), &optimized_binary_)) {
-            is_valid_ = false;
-            return {Status::kInvalid, true};
-        }
-    } while (optimized_binary_.empty() && ++num_stuck < kMaxNumStuck);
-
-    return {optimized_binary_.empty() ? Status::kStuck : Status::kComplete,
-            !optimized_binary_.empty()};
-}
-
-std::vector<uint32_t> SpirvOptMutator::GetBinary() const {
-    return optimized_binary_;
-}
-
-std::string SpirvOptMutator::GetErrors() const {
-    return errors_.str();
-}
-
-void SpirvOptMutator::LogErrors(const std::string* path, uint32_t count) const {
-    auto message = GetErrors();
-    std::cout << count << " | SpirvOptMutator (seed: " << seed_ << ")" << std::endl;
-    std::cout << message << std::endl;
-
-    if (path) {
-        auto prefix = *path + std::to_string(count);
-
-        // Write errors to file.
-        std::ofstream(prefix + ".opt.log") << "seed: " << seed_ << std::endl
-                                           << message << std::endl;
-
-        // Write the invalid SPIR-V binary.
-        util::WriteBinary(prefix + ".opt.invalid.spv", optimized_binary_);
-
-        // Write the original SPIR-V binary.
-        util::WriteBinary(prefix + ".opt.original.spv", original_binary_);
-    }
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h
deleted file mode 100644
index b8e15f0..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
-
-#include <sstream>
-#include <string>
-#include <vector>
-
-#include "spirv-tools/libspirv.h"
-#include "src/tint/fuzzers/random_generator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// Mutates the SPIR-V module using the spirv-opt tool.
-///
-/// The initial `binary` must be valid according to `target_env`. On each call
-/// to the `Mutate` method the mutator selects `opt_batch_size` random
-/// optimization passes (with substitutions) and applies them to the binary.
-class SpirvOptMutator : public Mutator {
-  public:
-    /// Constructor.
-    /// @param target_env - target environment for the `binary`.
-    /// @param seed - seed for the RNG.
-    /// @param binary - SPIR-V binary. Must be valid.
-    /// @param validate_after_each_opt - whether to validate the binary after each
-    ///     optimization pass.
-    /// @param opt_batch_size - the maximum number of optimization passes that
-    ///     will be applied in a single call to `Mutate`. If it's equal to 0 then
-    ///     all available optimization passes are applied.
-    SpirvOptMutator(spv_target_env target_env,
-                    uint32_t seed,
-                    std::vector<uint32_t> binary,
-                    bool validate_after_each_opt,
-                    uint32_t opt_batch_size);
-
-    Result Mutate() override;
-    std::vector<uint32_t> GetBinary() const override;
-    void LogErrors(const std::string* path, uint32_t count) const override;
-    std::string GetErrors() const override;
-
-  private:
-    // Number of times this mutator was executed.
-    uint32_t num_executions_;
-
-    // Whether the last execution left it in a valid state.
-    bool is_valid_;
-
-    // Target environment for the SPIR-V binary.
-    const spv_target_env target_env_;
-
-    // The original SPIR-V binary. Useful for debugging.
-    const std::vector<uint32_t> original_binary_;
-
-    // The seed for the RNG. Useful for debugging.
-    const uint32_t seed_;
-
-    // All the optimization passes available.
-    const std::vector<std::string> opt_passes_;
-
-    // The result of the optimization.
-    std::vector<uint32_t> optimized_binary_;
-
-    // Whether we need to validate the binary after each optimization pass.
-    const bool validate_after_each_opt_;
-
-    // The number of optimization passes to apply at once.
-    const uint32_t opt_batch_size_;
-
-    // All the errors produced by the optimizer.
-    std::stringstream errors_;
-
-    // The random number generator initialized with `seed_`.
-    RandomGenerator generator_;
-};
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc
deleted file mode 100644
index 221799c..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
-
-#include <fstream>
-
-#include "source/fuzz/fuzzer_util.h"
-#include "source/opt/build_module.h"
-#include "source/reduce/conditional_branch_to_simple_conditional_branch_opportunity_finder.h"
-#include "source/reduce/merge_blocks_reduction_opportunity_finder.h"
-#include "source/reduce/operand_to_const_reduction_opportunity_finder.h"
-#include "source/reduce/operand_to_dominating_id_reduction_opportunity_finder.h"
-#include "source/reduce/operand_to_undef_reduction_opportunity_finder.h"
-#include "source/reduce/remove_block_reduction_opportunity_finder.h"
-#include "source/reduce/remove_function_reduction_opportunity_finder.h"
-#include "source/reduce/remove_selection_reduction_opportunity_finder.h"
-#include "source/reduce/remove_unused_instruction_reduction_opportunity_finder.h"
-#include "source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h"
-#include "source/reduce/simple_conditional_branch_to_branch_opportunity_finder.h"
-#include "source/reduce/structured_loop_to_selection_reduction_opportunity_finder.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-SpirvReduceMutator::SpirvReduceMutator(spv_target_env target_env,
-                                       std::vector<uint32_t> binary,
-                                       uint32_t seed,
-                                       uint32_t reductions_batch_size,
-                                       bool enable_all_reductions,
-                                       bool validate_after_each_reduction)
-    : ir_context_(nullptr),
-      finders_(),
-      generator_(seed),
-      errors_(),
-      is_valid_(true),
-      reductions_batch_size_(reductions_batch_size),
-      total_applied_reductions_(0),
-      enable_all_reductions_(enable_all_reductions),
-      validate_after_each_reduction_(validate_after_each_reduction),
-      original_binary_(std::move(binary)),
-      seed_(seed) {
-    ir_context_ =
-        spvtools::BuildModule(target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
-                              original_binary_.data(), original_binary_.size());
-    assert(ir_context_ && "|binary| is invalid");
-
-    do {
-        MaybeAddFinder<
-            spvtools::reduce::ConditionalBranchToSimpleConditionalBranchOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::MergeBlocksReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::OperandToConstReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::OperandToDominatingIdReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::OperandToUndefReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::RemoveBlockReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::RemoveFunctionReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::RemoveSelectionReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::RemoveUnusedInstructionReductionOpportunityFinder>(true);
-        MaybeAddFinder<spvtools::reduce::RemoveUnusedStructMemberReductionOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::SimpleConditionalBranchToBranchOpportunityFinder>();
-        MaybeAddFinder<spvtools::reduce::StructuredLoopToSelectionReductionOpportunityFinder>();
-    } while (finders_.empty());
-}
-
-Mutator::Result SpirvReduceMutator::Mutate() {
-    assert(is_valid_ && "Can't mutate invalid module");
-
-    // The upper limit on the number of applied reduction passes.
-    const uint32_t kMaxAppliedReductions = 500;
-    const auto old_applied_reductions = total_applied_reductions_;
-
-    // The upper limit on the number of failed attempts to apply reductions (i.e.
-    // when no reduction was returned by the reduction finder).
-    const uint32_t kMaxConsecutiveFailures = 10;
-    uint32_t num_consecutive_failures = 0;
-
-    // Iterate while we haven't exceeded the limit on the total number of applied
-    // reductions, the limit on the number of reductions applied at once and limit
-    // on the number of consecutive failed attempts.
-    while (total_applied_reductions_ < kMaxAppliedReductions &&
-           (reductions_batch_size_ == 0 ||
-            total_applied_reductions_ - old_applied_reductions < reductions_batch_size_) &&
-           num_consecutive_failures < kMaxConsecutiveFailures) {
-        // Select an opportunity finder and get some reduction opportunities from
-        // it.
-        auto finder = GetRandomElement(&finders_);
-        auto reduction_opportunities = finder->GetAvailableOpportunities(ir_context_.get(), 0);
-
-        if (reduction_opportunities.empty()) {
-            // There is nothing to reduce. We increase the counter to make sure we
-            // don't stuck in this situation.
-            num_consecutive_failures++;
-        } else {
-            // Apply a random reduction opportunity. The latter should be applicable.
-            auto opportunity = GetRandomElement(&reduction_opportunities);
-            assert(opportunity->PreconditionHolds() && "Preconditions should hold");
-            total_applied_reductions_++;
-            num_consecutive_failures = 0;
-            if (!ApplyReduction(opportunity)) {
-                // The module became invalid as a result of the applied reduction.
-                is_valid_ = false;
-                return {Mutator::Status::kInvalid,
-                        total_applied_reductions_ != old_applied_reductions};
-            }
-        }
-    }
-
-    auto is_changed = total_applied_reductions_ != old_applied_reductions;
-    if (total_applied_reductions_ == kMaxAppliedReductions) {
-        return {Mutator::Status::kLimitReached, is_changed};
-    }
-
-    if (num_consecutive_failures == kMaxConsecutiveFailures) {
-        return {Mutator::Status::kStuck, is_changed};
-    }
-
-    assert(is_changed && "This is the only way left to break the loop");
-    return {Mutator::Status::kComplete, is_changed};
-}
-
-bool SpirvReduceMutator::ApplyReduction(
-    spvtools::reduce::ReductionOpportunity* reduction_opportunity) {
-    reduction_opportunity->TryToApply();
-    return !validate_after_each_reduction_ || spvtools::fuzz::fuzzerutil::IsValidAndWellFormed(
-                                                  ir_context_.get(), spvtools::ValidatorOptions(),
-                                                  util::GetBufferMessageConsumer(&errors_));
-}
-
-std::vector<uint32_t> SpirvReduceMutator::GetBinary() const {
-    std::vector<uint32_t> result;
-    ir_context_->module()->ToBinary(&result, true);
-    return result;
-}
-
-std::string SpirvReduceMutator::GetErrors() const {
-    return errors_.str();
-}
-
-void SpirvReduceMutator::LogErrors(const std::string* path, uint32_t count) const {
-    auto message = GetErrors();
-    std::cout << count << " | SpirvReduceMutator (seed: " << seed_ << ")" << std::endl;
-    std::cout << message << std::endl;
-
-    if (path) {
-        auto prefix = *path + std::to_string(count);
-
-        // Write errors to file.
-        std::ofstream(prefix + ".reducer.log") << "seed: " << seed_ << std::endl
-                                               << message << std::endl;
-
-        // Write the invalid SPIR-V binary.
-        util::WriteBinary(prefix + ".reducer.invalid.spv", GetBinary());
-
-        // Write the original SPIR-V binary.
-        util::WriteBinary(prefix + ".reducer.original.spv", original_binary_);
-    }
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h
deleted file mode 100644
index ba7bef4..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
-
-#include <memory>
-#include <sstream>
-#include <string>
-#include <utility>
-#include <vector>
-
-#include "src/tint/fuzzers/random_generator.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-
-#include "source/reduce/reduction_opportunity_finder.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-/// Mutates SPIR-V binary by running spirv-reduce tool.
-///
-/// The initial `binary` must be valid according to `target_env`. Applies at
-/// most `reductions_batch_size` reductions at a time. This parameter is ignored
-/// if its value is 0. Uses a random subset of reduction opportunity finders by
-/// default. This can be overridden with the `enable_all_reductions` parameter.
-class SpirvReduceMutator : public Mutator {
-  public:
-    /// Constructor.
-    /// @param target_env - the target environment for the `binary`.
-    /// @param binary - SPIR-V binary. Must be valid.
-    /// @param seed - the seed for the RNG.
-    /// @param reductions_batch_size - the number of reduction passes that will be
-    ///     applied during a single call to `Mutate`. If it's equal to 0 then we
-    ///     apply the passes until we reach the threshold for the total number of
-    ///     applied passes.
-    /// @param enable_all_reductions - whether to use all reduction passes or only
-    ///     a randomly selected subset of them.
-    /// @param validate_after_each_reduction - whether to validate after each
-    ///     applied reduction.
-    SpirvReduceMutator(spv_target_env target_env,
-                       std::vector<uint32_t> binary,
-                       uint32_t seed,
-                       uint32_t reductions_batch_size,
-                       bool enable_all_reductions,
-                       bool validate_after_each_reduction);
-
-    Result Mutate() override;
-    std::vector<uint32_t> GetBinary() const override;
-    void LogErrors(const std::string* path, uint32_t count) const override;
-    std::string GetErrors() const override;
-
-  private:
-    template <typename T, typename... Args>
-    void MaybeAddFinder(Args&&... args) {
-        if (enable_all_reductions_ || generator_.GetBool()) {
-            finders_.push_back(std::make_unique<T>(std::forward<Args>(args)...));
-        }
-    }
-
-    template <typename T>
-    T* GetRandomElement(std::vector<T>* arr) {
-        assert(!arr->empty() && "Can't get random element from an empty vector");
-        auto index = generator_.GetUInt32(static_cast<uint32_t>(arr->size()));
-        return &(*arr)[index];
-    }
-
-    template <typename T>
-    T* GetRandomElement(std::vector<std::unique_ptr<T>>* arr) {
-        assert(!arr->empty() && "Can't get random element from an empty vector");
-        auto index = generator_.GetUInt32(static_cast<uint32_t>(arr->size()));
-        return (*arr)[index].get();
-    }
-
-    bool ApplyReduction(spvtools::reduce::ReductionOpportunity* reduction_opportunity);
-
-    // The SPIR-V binary that is being reduced.
-    std::unique_ptr<spvtools::opt::IRContext> ir_context_;
-
-    // The selected subset of reduction opportunity finders.
-    std::vector<std::unique_ptr<spvtools::reduce::ReductionOpportunityFinder>> finders_;
-
-    // Random number generator initialized with `seed_`.
-    RandomGenerator generator_;
-
-    // All the errors produced by the reducer.
-    std::stringstream errors_;
-
-    // Whether the last call to the `Mutate` method produced the valid binary.
-    bool is_valid_;
-
-    // The number of reductions to apply on a single call to `Mutate`.
-    const uint32_t reductions_batch_size_;
-
-    // The total number of applied reductions.
-    uint32_t total_applied_reductions_;
-
-    // Whether we want to use all the reduction opportunity finders and not just a
-    // subset of them.
-    const bool enable_all_reductions_;
-
-    // Whether we want to validate all the binary after each reduction.
-    const bool validate_after_each_reduction_;
-
-    // The original binary that was used to initialize this mutator.
-    // Useful for debugging.
-    const std::vector<uint32_t> original_binary_;
-
-    // The seed that was used to initialize the random number generator.
-    // Useful for debugging.
-    const uint32_t seed_;
-};
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_fuzzer.cc
deleted file mode 100644
index 6ba973a..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_fuzzer.cc
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-void OverrideCliParams(FuzzerCliParams& /*unused*/) {
-    // Leave the CLI parameters unchanged.
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_hlsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_hlsl_writer_fuzzer.cc
deleted file mode 100644
index 2c76157..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_hlsl_writer_fuzzer.cc
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-void OverrideCliParams(FuzzerCliParams& cli_params) {
-    assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
-           "The fuzzing target should not have been set by a CLI parameter: it "
-           "should have its default value.");
-    cli_params.fuzzing_target = FuzzingTarget::kHlsl;
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_msl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_msl_writer_fuzzer.cc
deleted file mode 100644
index 5d70ad3..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_msl_writer_fuzzer.cc
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-void OverrideCliParams(FuzzerCliParams& cli_params) {
-    assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
-           "The fuzzing target should not have been set by a CLI parameter: it "
-           "should have its default value.");
-    cli_params.fuzzing_target = FuzzingTarget::kMsl;
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_spv_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_spv_writer_fuzzer.cc
deleted file mode 100644
index 53b4e45..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_spv_writer_fuzzer.cc
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-void OverrideCliParams(FuzzerCliParams& cli_params) {
-    assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
-           "The fuzzing target should not have been set by a CLI parameter: it "
-           "should have its default value.");
-    cli_params.fuzzing_target = FuzzingTarget::kSpv;
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_wgsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_wgsl_writer_fuzzer.cc
deleted file mode 100644
index 0593f6e..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_wgsl_writer_fuzzer.cc
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <cassert>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
-
-namespace tint::fuzzers::spvtools_fuzzer {
-
-void OverrideCliParams(FuzzerCliParams& cli_params) {
-    assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
-           "The fuzzing target should not have been set by a CLI parameter: it "
-           "should have its default value.");
-    cli_params.fuzzing_target = FuzzingTarget::kWgsl;
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.cc
deleted file mode 100644
index a9e9e2b..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.cc
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <fstream>
-#include <iostream>
-
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
-
-namespace tint::fuzzers::spvtools_fuzzer::util {
-namespace {
-
-bool WriteBinary(const std::string& path, const uint8_t* data, size_t size) {
-    std::ofstream spv(path, std::ios::binary);
-    return spv &&
-           spv.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
-}
-
-void LogError(uint32_t index,
-              const std::string& type,
-              const std::string& message,
-              const std::string* path,
-              const uint8_t* data,
-              size_t size,
-              const std::string* wgsl) {
-    std::cout << index << " | " << type << ": " << message << std::endl;
-
-    if (path) {
-        auto prefix = *path + std::to_string(index);
-        std::ofstream(prefix + ".log") << message << std::endl;
-
-        WriteBinary(prefix + ".spv", data, size);
-
-        if (wgsl) {
-            std::ofstream(prefix + ".wgsl") << *wgsl << std::endl;
-        }
-    }
-}
-
-}  // namespace
-
-spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer) {
-    return [buffer](spv_message_level_t level, const char*, const spv_position_t& position,
-                    const char* message) {
-        std::string status;
-        switch (level) {
-            case SPV_MSG_FATAL:
-            case SPV_MSG_INTERNAL_ERROR:
-            case SPV_MSG_ERROR:
-                status = "ERROR";
-                break;
-            case SPV_MSG_WARNING:
-            case SPV_MSG_INFO:
-            case SPV_MSG_DEBUG:
-                status = "INFO";
-                break;
-        }
-        *buffer << status << " " << position.line << ":" << position.column << ":" << position.index
-                << ": " << message << std::endl;
-    };
-}
-
-void LogMutatorError(const Mutator& mutator, const std::string& error_dir) {
-    static uint32_t mutator_count = 0;
-    auto error_path = error_dir.empty() ? error_dir : error_dir + "/mutator/";
-    mutator.LogErrors(error_dir.empty() ? nullptr : &error_path, mutator_count++);
-}
-
-void LogWgslError(const std::string& message,
-                  const uint8_t* data,
-                  size_t size,
-                  const std::string& wgsl,
-                  OutputFormat output_format,
-                  const std::string& error_dir) {
-    static uint32_t wgsl_count = 0;
-    std::string error_type;
-    switch (output_format) {
-        case OutputFormat::kSpv:
-            error_type = "WGSL -> SPV";
-            break;
-        case OutputFormat::kMSL:
-            error_type = "WGSL -> MSL";
-            break;
-        case OutputFormat::kHLSL:
-            error_type = "WGSL -> HLSL";
-            break;
-        case OutputFormat::kWGSL:
-            error_type = "WGSL -> WGSL";
-            break;
-    }
-    auto error_path = error_dir.empty() ? error_dir : error_dir + "/wgsl/";
-    LogError(wgsl_count++, error_type, message, error_dir.empty() ? nullptr : &error_path, data,
-             size, &wgsl);
-}
-
-void LogSpvError(const std::string& message,
-                 const uint8_t* data,
-                 size_t size,
-                 const std::string& error_dir) {
-    static uint32_t spv_count = 0;
-    auto error_path = error_dir.empty() ? error_dir : error_dir + "/spv/";
-    LogError(spv_count++, "SPV -> WGSL", message, error_dir.empty() ? nullptr : &error_path, data,
-             size, nullptr);
-}
-
-bool ReadBinary(const std::string& path, std::vector<uint32_t>* out) {
-    if (!out) {
-        return false;
-    }
-
-    std::ifstream file(path, std::ios::binary | std::ios::ate);
-    if (!file) {
-        return false;
-    }
-
-    size_t size = static_cast<size_t>(file.tellg());
-    if (!file) {
-        return false;
-    }
-
-    file.seekg(0);
-    if (!file) {
-        return false;
-    }
-
-    std::vector<char> binary(size);
-    if (!file.read(binary.data(), size)) {
-        return false;
-    }
-
-    out->resize(binary.size() / sizeof(uint32_t));
-    std::memcpy(out->data(), binary.data(), binary.size());
-    return true;
-}
-
-bool WriteBinary(const std::string& path, const std::vector<uint32_t>& binary) {
-    return WriteBinary(path, reinterpret_cast<const uint8_t*>(binary.data()),
-                       binary.size() * sizeof(uint32_t));
-}
-
-}  // namespace tint::fuzzers::spvtools_fuzzer::util
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h
deleted file mode 100644
index 28b1774..0000000
--- a/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
-#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
-
-#include <sstream>
-#include <string>
-#include <vector>
-
-#include "src/tint/fuzzers/tint_common_fuzzer.h"
-#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
-
-#include "spirv-tools/libspirv.hpp"
-
-namespace tint::fuzzers::spvtools_fuzzer::util {
-
-/// @param buffer will be used to output errors by the returned message
-///     consumer. Must remain in scope as long as the returned consumer is in
-///     scope.
-/// @return the message consumer that will print errors to the `buffer`.
-spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer);
-
-/// Output errors from the SPV -> WGSL conversion.
-///
-/// @param message - the error message.
-/// @param data - invalid SPIR-V binary.
-/// @param size - the size of `data`.
-/// @param error_dir - the directory, to which the binary will be printed to.
-///     If it's empty, the invalid binary and supplemental files will not be
-///     printed. Otherwise, it must have a `spv/` subdirectory.
-void LogSpvError(const std::string& message,
-                 const uint8_t* data,
-                 size_t size,
-                 const std::string& error_dir);
-
-/// Output errors from the WGSL -> `output_format` conversion.
-///
-/// @param message - the error message.
-/// @param data - the SPIR-V binary that generated the WGSL binary.
-/// @param size - the size of `data`.
-/// @param wgsl - the invalid WGSL binary.
-/// @param output_format - the format which we attempted to convert `wgsl` to.
-/// @param error_dir - the directory, to which the binary will be printed out.
-///     If it's empty, the invalid binary and supplemental files will not be
-///     printed. Otherwise, it must have a `wgsl/` subdirectory.
-void LogWgslError(const std::string& message,
-                  const uint8_t* data,
-                  size_t size,
-                  const std::string& wgsl,
-                  OutputFormat output_format,
-                  const std::string& error_dir);
-
-/// Output errors produced by the mutator.
-///
-/// @param mutator - the mutator with invalid state.
-/// @param error_dir - the directory, to which invalid files will be printed to.
-///     If it's empty, the invalid binary and supplemental files will not be
-///     printed. Otherwise, it must have a `mutator/` subdirectory.
-void LogMutatorError(const Mutator& mutator, const std::string& error_dir);
-
-/// Reads SPIR-V binary from `path` into `out`. Returns `true` if successful and
-/// `false` otherwise (in this case, `out` is unchanged).
-///
-/// @param path - the path to the SPIR-V binary.
-/// @param out - may be a `nullptr`. In this case, `false` is returned.
-/// @return `true` if successful and `false` otherwise.
-bool ReadBinary(const std::string& path, std::vector<uint32_t>* out);
-
-/// Writes `binary` into `path`.
-///
-/// @param path - the path to write `binary` to.
-/// @param binary - SPIR-V binary.
-/// @return whether the operation was successful.
-bool WriteBinary(const std::string& path, const std::vector<uint32_t>& binary);
-
-}  // namespace tint::fuzzers::spvtools_fuzzer::util
-
-#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
diff --git a/src/tint/lang/core/BUILD.bazel b/src/tint/lang/core/BUILD.bazel
index 8b4f3e9..f3b42a9 100644
--- a/src/tint/lang/core/BUILD.bazel
+++ b/src/tint/lang/core/BUILD.bazel
@@ -116,6 +116,7 @@
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "access_bench.cc",
     "address_space_bench.cc",
@@ -129,6 +130,7 @@
   deps = [
     "//src/tint/lang/core",
     "//src/tint/utils/traits",
+    "@benchmark",
   ],
   copts = COPTS,
   visibility = ["//visibility:public"],
diff --git a/src/tint/lang/core/BUILD.cmake b/src/tint/lang/core/BUILD.cmake
index ec43104..3bc86ee 100644
--- a/src/tint/lang/core/BUILD.cmake
+++ b/src/tint/lang/core/BUILD.cmake
@@ -138,3 +138,7 @@
   tint_lang_core
   tint_utils_traits
 )
+
+tint_target_add_external_dependencies(tint_lang_core_bench bench
+  "google-benchmark"
+)
diff --git a/src/tint/lang/core/BUILD.gn b/src/tint/lang/core/BUILD.gn
index 78c9e73..ba5a75c 100644
--- a/src/tint/lang/core/BUILD.gn
+++ b/src/tint/lang/core/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -76,7 +76,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "access_test.cc",
       "address_space_test.cc",
@@ -114,3 +113,22 @@
     ]
   }
 }
+if (tint_build_benchmarks) {
+  tint_unittests_source_set("bench") {
+    sources = [
+      "access_bench.cc",
+      "address_space_bench.cc",
+      "attribute_bench.cc",
+      "builtin_type_bench.cc",
+      "builtin_value_bench.cc",
+      "interpolation_sampling_bench.cc",
+      "interpolation_type_bench.cc",
+      "texel_format_bench.cc",
+    ]
+    deps = [
+      "${tint_src_dir}:google_benchmark",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/utils/traits",
+    ]
+  }
+}
diff --git a/src/tint/lang/core/builtin_fn.cc b/src/tint/lang/core/builtin_fn.cc
index c74963f..3388c19 100644
--- a/src/tint/lang/core/builtin_fn.cc
+++ b/src/tint/lang/core/builtin_fn.cc
@@ -284,9 +284,6 @@
     if (name == "workgroupBarrier") {
         return BuiltinFn::kWorkgroupBarrier;
     }
-    if (name == "workgroupUniformLoad") {
-        return BuiltinFn::kWorkgroupUniformLoad;
-    }
     if (name == "textureBarrier") {
         return BuiltinFn::kTextureBarrier;
     }
@@ -556,8 +553,6 @@
             return "unpack4x8unorm";
         case BuiltinFn::kWorkgroupBarrier:
             return "workgroupBarrier";
-        case BuiltinFn::kWorkgroupUniformLoad:
-            return "workgroupUniformLoad";
         case BuiltinFn::kTextureBarrier:
             return "textureBarrier";
         case BuiltinFn::kTextureDimensions:
@@ -702,7 +697,6 @@
         case BuiltinFn::kAtomicSub:
         case BuiltinFn::kAtomicXor:
         case BuiltinFn::kTextureStore:
-        case BuiltinFn::kWorkgroupUniformLoad:
             return true;
         default:
             break;
diff --git a/src/tint/lang/core/builtin_fn.cc.tmpl b/src/tint/lang/core/builtin_fn.cc.tmpl
index 0a4ba6d..376b33f 100644
--- a/src/tint/lang/core/builtin_fn.cc.tmpl
+++ b/src/tint/lang/core/builtin_fn.cc.tmpl
@@ -121,7 +121,6 @@
         case BuiltinFn::kAtomicSub:
         case BuiltinFn::kAtomicXor:
         case BuiltinFn::kTextureStore:
-        case BuiltinFn::kWorkgroupUniformLoad:
             return true;
         default:
             break;
diff --git a/src/tint/lang/core/builtin_fn.h b/src/tint/lang/core/builtin_fn.h
index 5e58b5e..7754750 100644
--- a/src/tint/lang/core/builtin_fn.h
+++ b/src/tint/lang/core/builtin_fn.h
@@ -120,7 +120,6 @@
     kUnpack4X8Snorm,
     kUnpack4X8Unorm,
     kWorkgroupBarrier,
-    kWorkgroupUniformLoad,
     kTextureBarrier,
     kTextureDimensions,
     kTextureGather,
@@ -259,7 +258,6 @@
     BuiltinFn::kUnpack4X8Snorm,
     BuiltinFn::kUnpack4X8Unorm,
     BuiltinFn::kWorkgroupBarrier,
-    BuiltinFn::kWorkgroupUniformLoad,
     BuiltinFn::kTextureBarrier,
     BuiltinFn::kTextureDimensions,
     BuiltinFn::kTextureGather,
@@ -380,7 +378,6 @@
     "unpack4x8snorm",
     "unpack4x8unorm",
     "workgroupBarrier",
-    "workgroupUniformLoad",
     "textureBarrier",
     "textureDimensions",
     "textureGather",
diff --git a/src/tint/lang/core/constant/BUILD.bazel b/src/tint/lang/core/constant/BUILD.bazel
index 6904781..a78387e 100644
--- a/src/tint/lang/core/constant/BUILD.bazel
+++ b/src/tint/lang/core/constant/BUILD.bazel
@@ -96,7 +96,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/resolver:test",
     "//src/tint/lang/wgsl/sem",
@@ -114,8 +113,18 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/core/constant/BUILD.cmake b/src/tint/lang/core/constant/BUILD.cmake
index d77761f..c6d7ff9 100644
--- a/src/tint/lang/core/constant/BUILD.cmake
+++ b/src/tint/lang/core/constant/BUILD.cmake
@@ -95,7 +95,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_resolver
   tint_lang_wgsl_resolver_test
   tint_lang_wgsl_sem
@@ -117,3 +116,9 @@
 tint_target_add_external_dependencies(tint_lang_core_constant_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_core_constant_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
diff --git a/src/tint/lang/core/constant/BUILD.gn b/src/tint/lang/core/constant/BUILD.gn
index 2f15285..a4d035e 100644
--- a/src/tint/lang/core/constant/BUILD.gn
+++ b/src/tint/lang/core/constant/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -66,7 +66,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "composite_test.cc",
       "eval_binary_op_test.cc",
@@ -98,7 +97,6 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/resolver:unittests",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -116,5 +114,9 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+    }
   }
 }
diff --git a/src/tint/lang/core/constant/eval_binary_op_test.cc b/src/tint/lang/core/constant/eval_binary_op_test.cc
index 8860d48..6f62abb 100644
--- a/src/tint/lang/core/constant/eval_binary_op_test.cc
+++ b/src/tint/lang/core/constant/eval_binary_op_test.cc
@@ -14,9 +14,12 @@
 
 #include "src/tint/lang/core/constant/eval_test.h"
 
-#include "src/tint/lang/wgsl/reader/reader.h"
 #include "src/tint/utils/result/result.h"
 
+#if TINT_BUILD_WGSL_READER
+#include "src/tint/lang/wgsl/reader/reader.h"
+#endif
+
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
 using ::testing::HasSubstr;
diff --git a/src/tint/lang/core/constant/eval_construction_test.cc b/src/tint/lang/core/constant/eval_construction_test.cc
index 36b5ace..0e5a3aa 100644
--- a/src/tint/lang/core/constant/eval_construction_test.cc
+++ b/src/tint/lang/core/constant/eval_construction_test.cc
@@ -1446,6 +1446,7 @@
     ASSERT_NE(sem, nullptr);
     auto* arr = sem->Type()->As<core::type::Array>();
     ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 2u);
     EXPECT_TRUE(arr->ElemType()->Is<core::type::Struct>());
     EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
     EXPECT_TRUE(sem->ConstantValue()->AnyZero());
@@ -1478,6 +1479,7 @@
     ASSERT_NE(sem, nullptr);
     auto* arr = sem->Type()->As<core::type::Array>();
     ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 4u);
     EXPECT_TRUE(arr->ElemType()->Is<core::type::I32>());
     EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
     EXPECT_FALSE(sem->ConstantValue()->AnyZero());
@@ -1500,6 +1502,131 @@
     EXPECT_EQ(sem->ConstantValue()->Index(3)->ValueAs<i32>(), 40_i);
 }
 
+TEST_F(ConstEvalTest, Array_Infer_i32_i32) {
+    auto* expr = Call<array<Infer>>(10_i, 20_i);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<core::type::Array>();
+    ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 2u);
+    EXPECT_TRUE(arr->ElemType()->Is<core::type::I32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->ValueAs<i32>(), 10_i);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->ValueAs<i32>(), 20_i);
+}
+
+TEST_F(ConstEvalTest, Array_Infer_ai_ai) {
+    auto* expr = Call<array<Infer>>(10_a, 20_a);
+    GlobalConst("C", expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<core::type::Array>();
+    ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 2u);
+    EXPECT_TRUE(arr->ElemType()->Is<core::type::AbstractInt>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->ValueAs<AInt>(), 10_a);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->ValueAs<AInt>(), 20_a);
+}
+
+TEST_F(ConstEvalTest, Array_Infer_af_af) {
+    auto* expr = Call<array<Infer>>(10.0_a, 20.0_a);
+    GlobalConst("C", expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<core::type::Array>();
+    ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 2u);
+    EXPECT_TRUE(arr->ElemType()->Is<core::type::AbstractFloat>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->ValueAs<AFloat>(), 10_a);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->ValueAs<AFloat>(), 20_a);
+}
+
+TEST_F(ConstEvalTest, Array_Infer_af_ai) {
+    auto* expr = Call<array<Infer>>(10.0_a, 20_a);
+    GlobalConst("C", expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<core::type::Array>();
+    ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 2u);
+    EXPECT_TRUE(arr->ElemType()->Is<core::type::AbstractFloat>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->ValueAs<AFloat>(), 10_a);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->ValueAs<AFloat>(), 20_a);
+}
+
+TEST_F(ConstEvalTest, Array_Infer_ai_af) {
+    auto* expr = Call<array<Infer>>(10_a, 20.0_a);
+    GlobalConst("C", expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<core::type::Array>();
+    ASSERT_NE(arr, nullptr);
+    ASSERT_EQ(arr->ConstantCount(), 2u);
+    EXPECT_TRUE(arr->ElemType()->Is<core::type::AbstractFloat>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->ValueAs<AFloat>(), 10_a);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->ValueAs<AFloat>(), 20_a);
+}
+
 namespace ArrayInit {
 struct Case {
     Value input;
diff --git a/src/tint/lang/core/constant/eval_test.h b/src/tint/lang/core/constant/eval_test.h
index b874287..83fbefa 100644
--- a/src/tint/lang/core/constant/eval_test.h
+++ b/src/tint/lang/core/constant/eval_test.h
@@ -113,9 +113,9 @@
             [&](const auto& expected) {
                 using T = std::decay_t<decltype(expected)>;
 
-                ASSERT_TRUE(std::holds_alternative<T>(got_scalar))
-                    << "Scalar variant index: " << got_scalar.index();
-                auto got = std::get<T>(got_scalar);
+                ASSERT_TRUE(std::holds_alternative<T>(got_scalar.value))
+                    << "Scalar variant index: " << got_scalar.value.index();
+                auto got = std::get<T>(got_scalar.value);
 
                 if constexpr (std::is_same_v<bool, T>) {
                     EXPECT_EQ(got, expected) << "index: " << i;
@@ -148,7 +148,7 @@
                     EXPECT_EQ(AInt(got), AInt(expected)) << "index: " << i;
                 }
             },
-            expected_scalar);
+            expected_scalar.value);
     }
 }
 
diff --git a/src/tint/lang/core/core.def b/src/tint/lang/core/core.def
index 3ca17b9..83a9a84 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -659,7 +659,6 @@
 @must_use @const fn unpack4x8snorm(u32) -> vec4<f32>
 @must_use @const fn unpack4x8unorm(u32) -> vec4<f32>
 @stage("compute") fn workgroupBarrier()
-@must_use @stage("compute") fn workgroupUniformLoad<T>(ptr<workgroup, T, read_write>) -> T
 
 @stage("compute") fn textureBarrier()
 @must_use fn textureDimensions<T: fiu32>(texture: texture_1d<T>) -> u32
diff --git a/src/tint/lang/core/intrinsic/BUILD.gn b/src/tint/lang/core/intrinsic/BUILD.gn
index 2ee3356..e5e9984 100644
--- a/src/tint/lang/core/intrinsic/BUILD.gn
+++ b/src/tint/lang/core/intrinsic/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -60,7 +60,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "table_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/lang/core/intrinsic/data.cc b/src/tint/lang/core/intrinsic/data.cc
index 804e52f..9a4568b 100644
--- a/src/tint/lang/core/intrinsic/data.cc
+++ b/src/tint/lang/core/intrinsic/data.cc
@@ -1871,150 +1871,148 @@
   /* [27] */ TypeMatcherIndex(9),
   /* [28] */ TypeMatcherIndex(13),
   /* [29] */ TypeMatcherIndex(9),
-  /* [30] */ TypeMatcherIndex(25),
+  /* [30] */ TypeMatcherIndex(30),
   /* [31] */ TypeMatcherIndex(0),
-  /* [32] */ TypeMatcherIndex(30),
-  /* [33] */ TypeMatcherIndex(0),
-  /* [34] */ TypeMatcherIndex(11),
-  /* [35] */ TypeMatcherIndex(8),
-  /* [36] */ TypeMatcherIndex(31),
+  /* [32] */ TypeMatcherIndex(11),
+  /* [33] */ TypeMatcherIndex(8),
+  /* [34] */ TypeMatcherIndex(31),
+  /* [35] */ TypeMatcherIndex(0),
+  /* [36] */ TypeMatcherIndex(32),
   /* [37] */ TypeMatcherIndex(0),
-  /* [38] */ TypeMatcherIndex(32),
-  /* [39] */ TypeMatcherIndex(0),
-  /* [40] */ TypeMatcherIndex(12),
-  /* [41] */ TypeMatcherIndex(8),
-  /* [42] */ TypeMatcherIndex(33),
+  /* [38] */ TypeMatcherIndex(12),
+  /* [39] */ TypeMatcherIndex(8),
+  /* [40] */ TypeMatcherIndex(33),
+  /* [41] */ TypeMatcherIndex(0),
+  /* [42] */ TypeMatcherIndex(34),
   /* [43] */ TypeMatcherIndex(0),
-  /* [44] */ TypeMatcherIndex(34),
+  /* [44] */ TypeMatcherIndex(35),
   /* [45] */ TypeMatcherIndex(0),
-  /* [46] */ TypeMatcherIndex(35),
+  /* [46] */ TypeMatcherIndex(36),
   /* [47] */ TypeMatcherIndex(0),
-  /* [48] */ TypeMatcherIndex(36),
+  /* [48] */ TypeMatcherIndex(13),
   /* [49] */ TypeMatcherIndex(0),
-  /* [50] */ TypeMatcherIndex(13),
-  /* [51] */ TypeMatcherIndex(0),
-  /* [52] */ TypeMatcherIndex(11),
-  /* [53] */ TypeMatcherIndex(7),
-  /* [54] */ TypeMatcherIndex(12),
+  /* [50] */ TypeMatcherIndex(11),
+  /* [51] */ TypeMatcherIndex(7),
+  /* [52] */ TypeMatcherIndex(12),
+  /* [53] */ TypeMatcherIndex(9),
+  /* [54] */ TypeMatcherIndex(30),
   /* [55] */ TypeMatcherIndex(9),
-  /* [56] */ TypeMatcherIndex(30),
+  /* [56] */ TypeMatcherIndex(31),
   /* [57] */ TypeMatcherIndex(9),
-  /* [58] */ TypeMatcherIndex(31),
+  /* [58] */ TypeMatcherIndex(32),
   /* [59] */ TypeMatcherIndex(9),
-  /* [60] */ TypeMatcherIndex(32),
+  /* [60] */ TypeMatcherIndex(33),
   /* [61] */ TypeMatcherIndex(9),
-  /* [62] */ TypeMatcherIndex(33),
-  /* [63] */ TypeMatcherIndex(9),
-  /* [64] */ TypeMatcherIndex(12),
-  /* [65] */ TypeMatcherIndex(7),
-  /* [66] */ TypeMatcherIndex(34),
+  /* [62] */ TypeMatcherIndex(12),
+  /* [63] */ TypeMatcherIndex(7),
+  /* [64] */ TypeMatcherIndex(34),
+  /* [65] */ TypeMatcherIndex(9),
+  /* [66] */ TypeMatcherIndex(35),
   /* [67] */ TypeMatcherIndex(9),
-  /* [68] */ TypeMatcherIndex(35),
-  /* [69] */ TypeMatcherIndex(9),
-  /* [70] */ TypeMatcherIndex(11),
-  /* [71] */ TypeMatcherIndex(0),
+  /* [68] */ TypeMatcherIndex(11),
+  /* [69] */ TypeMatcherIndex(0),
+  /* [70] */ TypeMatcherIndex(13),
+  /* [71] */ TypeMatcherIndex(7),
   /* [72] */ TypeMatcherIndex(13),
-  /* [73] */ TypeMatcherIndex(7),
-  /* [74] */ TypeMatcherIndex(13),
-  /* [75] */ TypeMatcherIndex(8),
-  /* [76] */ TypeMatcherIndex(11),
+  /* [73] */ TypeMatcherIndex(8),
+  /* [74] */ TypeMatcherIndex(11),
+  /* [75] */ TypeMatcherIndex(1),
+  /* [76] */ TypeMatcherIndex(12),
   /* [77] */ TypeMatcherIndex(1),
-  /* [78] */ TypeMatcherIndex(12),
-  /* [79] */ TypeMatcherIndex(1),
-  /* [80] */ TypeMatcherIndex(52),
-  /* [81] */ TypeMatcherIndex(0),
-  /* [82] */ TypeMatcherIndex(23),
-  /* [83] */ TypeMatcherIndex(8),
+  /* [78] */ TypeMatcherIndex(52),
+  /* [79] */ TypeMatcherIndex(0),
+  /* [80] */ TypeMatcherIndex(23),
+  /* [81] */ TypeMatcherIndex(8),
+  /* [82] */ TypeMatcherIndex(11),
+  /* [83] */ TypeMatcherIndex(5),
   /* [84] */ TypeMatcherIndex(11),
-  /* [85] */ TypeMatcherIndex(5),
+  /* [85] */ TypeMatcherIndex(10),
   /* [86] */ TypeMatcherIndex(11),
-  /* [87] */ TypeMatcherIndex(10),
-  /* [88] */ TypeMatcherIndex(11),
-  /* [89] */ TypeMatcherIndex(4),
+  /* [87] */ TypeMatcherIndex(4),
+  /* [88] */ TypeMatcherIndex(12),
+  /* [89] */ TypeMatcherIndex(5),
   /* [90] */ TypeMatcherIndex(12),
-  /* [91] */ TypeMatcherIndex(5),
+  /* [91] */ TypeMatcherIndex(10),
   /* [92] */ TypeMatcherIndex(12),
-  /* [93] */ TypeMatcherIndex(10),
-  /* [94] */ TypeMatcherIndex(12),
-  /* [95] */ TypeMatcherIndex(4),
+  /* [93] */ TypeMatcherIndex(4),
+  /* [94] */ TypeMatcherIndex(13),
+  /* [95] */ TypeMatcherIndex(5),
   /* [96] */ TypeMatcherIndex(13),
-  /* [97] */ TypeMatcherIndex(5),
+  /* [97] */ TypeMatcherIndex(1),
   /* [98] */ TypeMatcherIndex(13),
-  /* [99] */ TypeMatcherIndex(1),
+  /* [99] */ TypeMatcherIndex(10),
   /* [100] */ TypeMatcherIndex(13),
-  /* [101] */ TypeMatcherIndex(10),
-  /* [102] */ TypeMatcherIndex(13),
-  /* [103] */ TypeMatcherIndex(4),
+  /* [101] */ TypeMatcherIndex(4),
+  /* [102] */ TypeMatcherIndex(14),
+  /* [103] */ TypeMatcherIndex(0),
   /* [104] */ TypeMatcherIndex(14),
-  /* [105] */ TypeMatcherIndex(0),
+  /* [105] */ TypeMatcherIndex(10),
   /* [106] */ TypeMatcherIndex(14),
-  /* [107] */ TypeMatcherIndex(10),
-  /* [108] */ TypeMatcherIndex(14),
-  /* [109] */ TypeMatcherIndex(9),
+  /* [107] */ TypeMatcherIndex(9),
+  /* [108] */ TypeMatcherIndex(15),
+  /* [109] */ TypeMatcherIndex(0),
   /* [110] */ TypeMatcherIndex(15),
-  /* [111] */ TypeMatcherIndex(0),
+  /* [111] */ TypeMatcherIndex(10),
   /* [112] */ TypeMatcherIndex(15),
-  /* [113] */ TypeMatcherIndex(10),
-  /* [114] */ TypeMatcherIndex(15),
-  /* [115] */ TypeMatcherIndex(9),
+  /* [113] */ TypeMatcherIndex(9),
+  /* [114] */ TypeMatcherIndex(16),
+  /* [115] */ TypeMatcherIndex(0),
   /* [116] */ TypeMatcherIndex(16),
-  /* [117] */ TypeMatcherIndex(0),
+  /* [117] */ TypeMatcherIndex(10),
   /* [118] */ TypeMatcherIndex(16),
-  /* [119] */ TypeMatcherIndex(10),
-  /* [120] */ TypeMatcherIndex(16),
-  /* [121] */ TypeMatcherIndex(9),
+  /* [119] */ TypeMatcherIndex(9),
+  /* [120] */ TypeMatcherIndex(17),
+  /* [121] */ TypeMatcherIndex(0),
   /* [122] */ TypeMatcherIndex(17),
-  /* [123] */ TypeMatcherIndex(0),
+  /* [123] */ TypeMatcherIndex(10),
   /* [124] */ TypeMatcherIndex(17),
-  /* [125] */ TypeMatcherIndex(10),
-  /* [126] */ TypeMatcherIndex(17),
-  /* [127] */ TypeMatcherIndex(9),
+  /* [125] */ TypeMatcherIndex(9),
+  /* [126] */ TypeMatcherIndex(18),
+  /* [127] */ TypeMatcherIndex(0),
   /* [128] */ TypeMatcherIndex(18),
-  /* [129] */ TypeMatcherIndex(0),
+  /* [129] */ TypeMatcherIndex(10),
   /* [130] */ TypeMatcherIndex(18),
-  /* [131] */ TypeMatcherIndex(10),
-  /* [132] */ TypeMatcherIndex(18),
-  /* [133] */ TypeMatcherIndex(9),
+  /* [131] */ TypeMatcherIndex(9),
+  /* [132] */ TypeMatcherIndex(19),
+  /* [133] */ TypeMatcherIndex(0),
   /* [134] */ TypeMatcherIndex(19),
-  /* [135] */ TypeMatcherIndex(0),
+  /* [135] */ TypeMatcherIndex(10),
   /* [136] */ TypeMatcherIndex(19),
-  /* [137] */ TypeMatcherIndex(10),
-  /* [138] */ TypeMatcherIndex(19),
-  /* [139] */ TypeMatcherIndex(9),
+  /* [137] */ TypeMatcherIndex(9),
+  /* [138] */ TypeMatcherIndex(20),
+  /* [139] */ TypeMatcherIndex(0),
   /* [140] */ TypeMatcherIndex(20),
-  /* [141] */ TypeMatcherIndex(0),
+  /* [141] */ TypeMatcherIndex(10),
   /* [142] */ TypeMatcherIndex(20),
-  /* [143] */ TypeMatcherIndex(10),
-  /* [144] */ TypeMatcherIndex(20),
-  /* [145] */ TypeMatcherIndex(9),
+  /* [143] */ TypeMatcherIndex(9),
+  /* [144] */ TypeMatcherIndex(21),
+  /* [145] */ TypeMatcherIndex(0),
   /* [146] */ TypeMatcherIndex(21),
-  /* [147] */ TypeMatcherIndex(0),
+  /* [147] */ TypeMatcherIndex(10),
   /* [148] */ TypeMatcherIndex(21),
-  /* [149] */ TypeMatcherIndex(10),
-  /* [150] */ TypeMatcherIndex(21),
-  /* [151] */ TypeMatcherIndex(9),
+  /* [149] */ TypeMatcherIndex(9),
+  /* [150] */ TypeMatcherIndex(22),
+  /* [151] */ TypeMatcherIndex(0),
   /* [152] */ TypeMatcherIndex(22),
-  /* [153] */ TypeMatcherIndex(0),
+  /* [153] */ TypeMatcherIndex(10),
   /* [154] */ TypeMatcherIndex(22),
-  /* [155] */ TypeMatcherIndex(10),
-  /* [156] */ TypeMatcherIndex(22),
-  /* [157] */ TypeMatcherIndex(9),
-  /* [158] */ TypeMatcherIndex(47),
-  /* [159] */ TypeMatcherIndex(0),
-  /* [160] */ TypeMatcherIndex(37),
-  /* [161] */ TypeMatcherIndex(38),
-  /* [162] */ TypeMatcherIndex(39),
-  /* [163] */ TypeMatcherIndex(40),
-  /* [164] */ TypeMatcherIndex(41),
-  /* [165] */ TypeMatcherIndex(42),
-  /* [166] */ TypeMatcherIndex(43),
-  /* [167] */ TypeMatcherIndex(44),
-  /* [168] */ TypeMatcherIndex(45),
-  /* [169] */ TypeMatcherIndex(46),
-  /* [170] */ TypeMatcherIndex(28),
-  /* [171] */ TypeMatcherIndex(2),
-  /* [172] */ TypeMatcherIndex(29),
-  /* [173] */ TypeMatcherIndex(3),
+  /* [155] */ TypeMatcherIndex(9),
+  /* [156] */ TypeMatcherIndex(47),
+  /* [157] */ TypeMatcherIndex(0),
+  /* [158] */ TypeMatcherIndex(37),
+  /* [159] */ TypeMatcherIndex(38),
+  /* [160] */ TypeMatcherIndex(39),
+  /* [161] */ TypeMatcherIndex(40),
+  /* [162] */ TypeMatcherIndex(41),
+  /* [163] */ TypeMatcherIndex(42),
+  /* [164] */ TypeMatcherIndex(43),
+  /* [165] */ TypeMatcherIndex(44),
+  /* [166] */ TypeMatcherIndex(45),
+  /* [167] */ TypeMatcherIndex(46),
+  /* [168] */ TypeMatcherIndex(28),
+  /* [169] */ TypeMatcherIndex(2),
+  /* [170] */ TypeMatcherIndex(29),
+  /* [171] */ TypeMatcherIndex(3),
 };
 
 static_assert(TypeMatcherIndex::CanIndex(kTypeMatcherIndices),
@@ -2027,24 +2025,22 @@
   /* [3] */ NumberMatcherIndex(1),
   /* [4] */ NumberMatcherIndex(0),
   /* [5] */ NumberMatcherIndex(7),
-  /* [6] */ NumberMatcherIndex(13),
-  /* [7] */ NumberMatcherIndex(7),
-  /* [8] */ NumberMatcherIndex(3),
+  /* [6] */ NumberMatcherIndex(3),
+  /* [7] */ NumberMatcherIndex(9),
+  /* [8] */ NumberMatcherIndex(4),
   /* [9] */ NumberMatcherIndex(9),
-  /* [10] */ NumberMatcherIndex(4),
+  /* [10] */ NumberMatcherIndex(5),
   /* [11] */ NumberMatcherIndex(9),
-  /* [12] */ NumberMatcherIndex(5),
-  /* [13] */ NumberMatcherIndex(9),
-  /* [14] */ NumberMatcherIndex(3),
+  /* [12] */ NumberMatcherIndex(3),
+  /* [13] */ NumberMatcherIndex(8),
+  /* [14] */ NumberMatcherIndex(4),
   /* [15] */ NumberMatcherIndex(8),
-  /* [16] */ NumberMatcherIndex(4),
+  /* [16] */ NumberMatcherIndex(5),
   /* [17] */ NumberMatcherIndex(8),
-  /* [18] */ NumberMatcherIndex(5),
-  /* [19] */ NumberMatcherIndex(8),
-  /* [20] */ NumberMatcherIndex(1),
+  /* [18] */ NumberMatcherIndex(1),
+  /* [19] */ NumberMatcherIndex(2),
+  /* [20] */ NumberMatcherIndex(0),
   /* [21] */ NumberMatcherIndex(2),
-  /* [22] */ NumberMatcherIndex(0),
-  /* [23] */ NumberMatcherIndex(2),
 };
 
 static_assert(NumberMatcherIndex::CanIndex(kNumberMatcherIndices),
@@ -2156,25 +2152,25 @@
   {
     /* [17] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [18] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [19] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [20] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2204,7 +2200,7 @@
   {
     /* [25] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2216,13 +2212,13 @@
   {
     /* [27] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(38),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [28] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2234,25 +2230,25 @@
   {
     /* [30] */
     /* usage */ core::ParameterUsage::kArrayIndex,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [31] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [32] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(159),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [33] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2276,19 +2272,19 @@
   {
     /* [37] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [38] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [39] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2312,19 +2308,19 @@
   {
     /* [43] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [44] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(56),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [45] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2348,61 +2344,61 @@
   {
     /* [49] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [50] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [51] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [52] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [53] */
     /* usage */ core::ParameterUsage::kDdx,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [54] */
     /* usage */ core::ParameterUsage::kDdy,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [55] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [56] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [57] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [58] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2414,25 +2410,25 @@
   {
     /* [60] */
     /* usage */ core::ParameterUsage::kDdx,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [61] */
     /* usage */ core::ParameterUsage::kDdy,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [62] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [63] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2456,19 +2452,19 @@
   {
     /* [67] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [68] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(159),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [69] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2492,7 +2488,7 @@
   {
     /* [73] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2504,13 +2500,13 @@
   {
     /* [75] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(34),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [76] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2522,7 +2518,7 @@
   {
     /* [78] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2534,37 +2530,37 @@
   {
     /* [80] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(46),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(44),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [81] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [82] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [83] */
     /* usage */ core::ParameterUsage::kArrayIndex,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [84] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(159),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [85] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2582,19 +2578,19 @@
   {
     /* [88] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [89] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(158),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [90] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2612,25 +2608,25 @@
   {
     /* [93] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [94] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [95] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [96] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2648,13 +2644,13 @@
   {
     /* [99] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [100] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2672,19 +2668,19 @@
   {
     /* [103] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [104] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(56),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [105] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2702,25 +2698,25 @@
   {
     /* [108] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [109] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [110] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [111] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2732,25 +2728,25 @@
   {
     /* [113] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [114] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [115] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [116] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2768,43 +2764,43 @@
   {
     /* [119] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [120] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [121] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [122] */
     /* usage */ core::ParameterUsage::kDdx,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [123] */
     /* usage */ core::ParameterUsage::kDdy,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [124] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(56),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [125] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2822,25 +2818,25 @@
   {
     /* [128] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [129] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [130] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [131] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2852,25 +2848,25 @@
   {
     /* [133] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [134] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [135] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [136] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2888,13 +2884,13 @@
   {
     /* [139] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(158),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [140] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2912,25 +2908,25 @@
   {
     /* [143] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [144] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [145] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [146] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2960,13 +2956,13 @@
   {
     /* [151] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [152] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -2978,31 +2974,31 @@
   {
     /* [154] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(44),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(42),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [155] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [156] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [157] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(158),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [158] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3014,25 +3010,25 @@
   {
     /* [160] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [161] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [162] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [163] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3044,13 +3040,13 @@
   {
     /* [165] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(56),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [166] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3062,49 +3058,49 @@
   {
     /* [168] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [169] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [170] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [171] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [172] */
     /* usage */ core::ParameterUsage::kOffset,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [173] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [174] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [175] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3116,19 +3112,19 @@
   {
     /* [177] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [178] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [179] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3140,19 +3136,19 @@
   {
     /* [181] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [182] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [183] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3164,13 +3160,13 @@
   {
     /* [185] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(6),
   },
   {
     /* [186] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3188,13 +3184,13 @@
   {
     /* [189] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [190] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3206,19 +3202,19 @@
   {
     /* [192] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [193] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [194] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3230,43 +3226,43 @@
   {
     /* [196] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [197] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(38),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [198] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [199] */
     /* usage */ core::ParameterUsage::kArrayIndex,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [200] */
     /* usage */ core::ParameterUsage::kLevel,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(173),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [201] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(159),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [202] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3278,7 +3274,7 @@
   {
     /* [204] */
     /* usage */ core::ParameterUsage::kLevel,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3308,25 +3304,25 @@
   {
     /* [209] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [210] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [211] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [212] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3356,25 +3352,25 @@
   {
     /* [217] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [218] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [219] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [220] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3464,13 +3460,13 @@
   {
     /* [235] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(56),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [236] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3482,13 +3478,13 @@
   {
     /* [238] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [239] */
     /* usage */ core::ParameterUsage::kSampler,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3500,8 +3496,8 @@
   {
     /* [241] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(6),
   },
   {
     /* [242] */
@@ -3518,13 +3514,13 @@
   {
     /* [244] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(6),
   },
   {
     /* [245] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3536,8 +3532,8 @@
   {
     /* [247] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(6),
   },
   {
     /* [248] */
@@ -3554,8 +3550,8 @@
   {
     /* [250] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [251] */
@@ -3566,32 +3562,32 @@
   {
     /* [252] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [253] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [254] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [255] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [256] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [257] */
@@ -3602,14 +3598,14 @@
   {
     /* [258] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [259] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [260] */
@@ -3620,32 +3616,32 @@
   {
     /* [261] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [262] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [263] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [264] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [265] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [266] */
@@ -3656,13 +3652,13 @@
   {
     /* [267] */
     /* usage */ core::ParameterUsage::kValue,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [268] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(32),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(30),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3674,73 +3670,73 @@
   {
     /* [270] */
     /* usage */ core::ParameterUsage::kLevel,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [271] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(34),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [272] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [273] */
     /* usage */ core::ParameterUsage::kLevel,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [274] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(42),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(40),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [275] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(78),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [276] */
     /* usage */ core::ParameterUsage::kLevel,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [277] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(48),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(46),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [278] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [279] */
     /* usage */ core::ParameterUsage::kSampleIndex,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [280] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(158),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [281] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3752,13 +3748,13 @@
   {
     /* [283] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [284] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3770,13 +3766,13 @@
   {
     /* [286] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [287] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3788,13 +3784,13 @@
   {
     /* [289] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [290] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3806,13 +3802,13 @@
   {
     /* [292] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [293] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3824,7 +3820,7 @@
   {
     /* [295] */
     /* usage */ core::ParameterUsage::kXy,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3848,7 +3844,7 @@
   {
     /* [299] */
     /* usage */ core::ParameterUsage::kYz,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3872,7 +3868,7 @@
   {
     /* [303] */
     /* usage */ core::ParameterUsage::kZw,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3902,7 +3898,7 @@
   {
     /* [308] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(32),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(30),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3914,7 +3910,7 @@
   {
     /* [310] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(34),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3926,7 +3922,7 @@
   {
     /* [312] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(38),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3938,7 +3934,7 @@
   {
     /* [314] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(42),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(40),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3950,7 +3946,7 @@
   {
     /* [316] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(44),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(42),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3962,7 +3958,7 @@
   {
     /* [318] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(46),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(44),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3974,7 +3970,7 @@
   {
     /* [320] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(158),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3986,7 +3982,7 @@
   {
     /* [322] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(159),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -3998,7 +3994,7 @@
   {
     /* [324] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -4010,7 +4006,7 @@
   {
     /* [326] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -4022,20 +4018,20 @@
   {
     /* [328] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [329] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [330] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [331] */
@@ -4046,8 +4042,8 @@
   {
     /* [332] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [333] */
@@ -4058,8 +4054,8 @@
   {
     /* [334] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [335] */
@@ -4070,44 +4066,44 @@
   {
     /* [336] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [337] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [338] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [339] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [340] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [341] */
     /* usage */ core::ParameterUsage::kCoords,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [342] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [343] */
@@ -4118,8 +4114,8 @@
   {
     /* [344] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [345] */
@@ -4130,8 +4126,8 @@
   {
     /* [346] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [347] */
@@ -4148,7 +4144,7 @@
   {
     /* [349] */
     /* usage */ core::ParameterUsage::kSourceLaneIndex,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -4166,7 +4162,7 @@
   {
     /* [352] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(82),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(80),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
@@ -4215,7 +4211,7 @@
     /* [360] */
     /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(22),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(20),
   },
   {
     /* [361] */
@@ -4226,13 +4222,13 @@
   {
     /* [362] */
     /* usage */ core::ParameterUsage::kXy,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [363] */
     /* usage */ core::ParameterUsage::kZw,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -4297,44 +4293,44 @@
   },
   {
     /* [374] */
-    /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(30),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(6),
+    /* usage */ core::ParameterUsage::kTexture,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [375] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [376] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [377] */
     /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [378] */
-    /* usage */ core::ParameterUsage::kTexture,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(51),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [379] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(53),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(85),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [380] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(87),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
@@ -4346,175 +4342,169 @@
   {
     /* [382] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(78),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(96),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [383] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(98),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [384] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(104),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(106),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [385] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(108),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(104),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [386] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(106),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [387] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(110),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(112),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [388] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(114),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(110),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [389] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(112),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [390] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(116),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(118),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [391] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(120),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(116),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [392] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(118),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [393] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(122),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(124),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [394] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(126),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(122),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [395] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(124),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [396] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(128),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(130),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [397] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(132),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(128),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [398] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(130),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [399] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(134),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(136),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [400] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(138),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(134),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [401] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(136),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [402] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(140),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(142),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [403] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(144),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(140),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [404] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(142),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [405] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(146),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(148),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [406] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(150),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(146),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [407] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(148),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [408] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(152),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(154),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [409] */
     /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(156),
-    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-  },
-  {
-    /* [410] */
-    /* usage */ core::ParameterUsage::kNone,
-    /* type_matcher_indices */ TypeMatcherIndicesIndex(154),
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(152),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
 };
@@ -4885,7 +4875,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(268),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4898,7 +4888,7 @@
     /* template_types */ TemplateTypeIndex(12),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(308),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4911,7 +4901,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(75),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4924,7 +4914,7 @@
     /* template_types */ TemplateTypeIndex(12),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(310),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4937,7 +4927,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(27),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4950,7 +4940,7 @@
     /* template_types */ TemplateTypeIndex(12),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(312),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4963,7 +4953,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(274),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(40),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4976,7 +4966,7 @@
     /* template_types */ TemplateTypeIndex(12),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(314),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(40),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -4989,7 +4979,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(154),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5002,7 +4992,7 @@
     /* template_types */ TemplateTypeIndex(12),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(316),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5015,7 +5005,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(80),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5028,7 +5018,7 @@
     /* template_types */ TemplateTypeIndex(12),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(318),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5041,7 +5031,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(277),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5054,7 +5044,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(89),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5067,7 +5057,7 @@
     /* template_types */ TemplateTypeIndex(3),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(320),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5080,7 +5070,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(32),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5093,7 +5083,7 @@
     /* template_types */ TemplateTypeIndex(3),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(322),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5106,7 +5096,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(161),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5119,7 +5109,7 @@
     /* template_types */ TemplateTypeIndex(3),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(324),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5132,7 +5122,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(94),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5145,7 +5135,7 @@
     /* template_types */ TemplateTypeIndex(3),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(326),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5158,7 +5148,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(283),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5170,8 +5160,8 @@
     /* num_template_numbers */ 2,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(6),
-    /* parameters */ ParameterIndex(375),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* parameters */ ParameterIndex(374),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5183,8 +5173,8 @@
     /* num_template_numbers */ 2,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(6),
-    /* parameters */ ParameterIndex(376),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* parameters */ ParameterIndex(375),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5196,8 +5186,8 @@
     /* num_template_numbers */ 2,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(6),
-    /* parameters */ ParameterIndex(377),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* parameters */ ParameterIndex(376),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5209,8 +5199,8 @@
     /* num_template_numbers */ 2,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(6),
-    /* parameters */ ParameterIndex(378),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(40),
+    /* parameters */ ParameterIndex(377),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5223,7 +5213,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(238),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5236,7 +5226,7 @@
     /* template_types */ TemplateTypeIndex(4),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(268),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5249,7 +5239,7 @@
     /* template_types */ TemplateTypeIndex(4),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(271),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5262,7 +5252,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(197),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5275,7 +5265,7 @@
     /* template_types */ TemplateTypeIndex(4),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(274),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5288,7 +5278,7 @@
     /* template_types */ TemplateTypeIndex(7),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(277),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5366,7 +5356,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(332),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5379,7 +5369,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(334),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5405,7 +5395,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(338),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5418,7 +5408,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(340),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5444,7 +5434,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(289),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5457,7 +5447,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(292),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5483,7 +5473,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(344),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5496,7 +5486,7 @@
     /* template_types */ TemplateTypeIndex(1),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(346),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -5509,7 +5499,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(96),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(94),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -5522,7 +5512,7 @@
     /* template_types */ TemplateTypeIndex(35),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -5535,7 +5525,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(217),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -5548,7 +5538,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(100),
   },
@@ -5561,7 +5551,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(205),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(101),
   },
@@ -5574,7 +5564,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(295),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
@@ -5587,7 +5577,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(298),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
@@ -5600,7 +5590,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(301),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
@@ -5613,7 +5603,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(362),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
@@ -5626,7 +5616,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(364),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
@@ -5639,7 +5629,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(366),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
@@ -5651,7 +5641,7 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(383),
+    /* parameters */ ParameterIndex(382),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(28),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
@@ -5664,8 +5654,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(383),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(100),
+    /* parameters */ ParameterIndex(382),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(98),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -5677,8 +5667,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(18),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(383),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
+    /* parameters */ ParameterIndex(382),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -5690,8 +5680,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(20),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(383),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* parameters */ ParameterIndex(382),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -5703,8 +5693,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(22),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(383),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
+    /* parameters */ ParameterIndex(382),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(100),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6094,7 +6084,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(74),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6107,7 +6097,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(74),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6120,7 +6110,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(26),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6133,7 +6123,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(26),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6146,7 +6136,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(153),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6159,7 +6149,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(79),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6406,7 +6396,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(90),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(88),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -6496,8 +6486,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(382),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(54),
+    /* parameters */ ParameterIndex(381),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6509,8 +6499,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(382),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(92),
+    /* parameters */ ParameterIndex(381),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(90),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6522,8 +6512,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(18),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(382),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(64),
+    /* parameters */ ParameterIndex(381),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6535,8 +6525,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(20),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(382),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(40),
+    /* parameters */ ParameterIndex(381),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6548,8 +6538,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(22),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(382),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(94),
+    /* parameters */ ParameterIndex(381),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(92),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6562,7 +6552,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(268),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6575,7 +6565,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(75),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6588,7 +6578,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(27),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6601,7 +6591,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(274),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6614,7 +6604,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(154),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6627,7 +6617,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(80),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6640,7 +6630,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(89),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6653,7 +6643,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(32),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6666,7 +6656,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(161),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6679,7 +6669,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(94),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -6692,7 +6682,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(84),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(82),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -6705,7 +6695,7 @@
     /* template_types */ TemplateTypeIndex(35),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -6718,7 +6708,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(209),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -6731,7 +6721,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(100),
   },
@@ -6744,7 +6734,7 @@
     /* template_types */ TemplateTypeIndex(27),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(205),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(70),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(101),
   },
@@ -6756,7 +6746,7 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(381),
+    /* parameters */ ParameterIndex(380),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
@@ -6769,8 +6759,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(381),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(86),
+    /* parameters */ ParameterIndex(380),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(84),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6782,8 +6772,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(18),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(381),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(52),
+    /* parameters */ ParameterIndex(380),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6795,8 +6785,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(20),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(381),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(34),
+    /* parameters */ ParameterIndex(380),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6808,8 +6798,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(22),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(381),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(88),
+    /* parameters */ ParameterIndex(380),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(86),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -6927,7 +6917,7 @@
     /* template_numbers */ TemplateNumberIndex(0),
     /* parameters */ ParameterIndex(360),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(12),
-    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(20),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(18),
     /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
@@ -7381,7 +7371,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(104),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7393,8 +7383,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(384),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(104),
+    /* parameters */ ParameterIndex(383),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7407,7 +7397,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(104),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7420,7 +7410,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(209),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(104),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7432,8 +7422,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(385),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(106),
+    /* parameters */ ParameterIndex(384),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(104),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7445,8 +7435,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(386),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
+    /* parameters */ ParameterIndex(385),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(106),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7459,7 +7449,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(110),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7471,8 +7461,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(387),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(110),
+    /* parameters */ ParameterIndex(386),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7485,7 +7475,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(110),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7498,7 +7488,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(213),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(110),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7510,8 +7500,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(388),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(112),
+    /* parameters */ ParameterIndex(387),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(110),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7523,8 +7513,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(389),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
+    /* parameters */ ParameterIndex(388),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(112),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7537,7 +7527,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(116),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7549,8 +7539,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(390),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(116),
+    /* parameters */ ParameterIndex(389),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7563,7 +7553,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(116),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7576,7 +7566,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(217),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(116),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7588,8 +7578,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(391),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(118),
+    /* parameters */ ParameterIndex(390),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(116),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7601,8 +7591,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(392),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
+    /* parameters */ ParameterIndex(391),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(118),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7615,7 +7605,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(122),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7627,8 +7617,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(393),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(122),
+    /* parameters */ ParameterIndex(392),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7641,7 +7631,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(122),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7654,7 +7644,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(209),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(122),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7666,8 +7656,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(394),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(124),
+    /* parameters */ ParameterIndex(393),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(122),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7679,8 +7669,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(395),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
+    /* parameters */ ParameterIndex(394),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(124),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7693,7 +7683,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(128),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7705,8 +7695,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(396),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(128),
+    /* parameters */ ParameterIndex(395),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7719,7 +7709,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(128),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7732,7 +7722,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(213),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(128),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7744,8 +7734,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(397),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(130),
+    /* parameters */ ParameterIndex(396),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(128),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7757,8 +7747,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(398),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
+    /* parameters */ ParameterIndex(397),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(130),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7771,7 +7761,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(134),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7783,8 +7773,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(399),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(134),
+    /* parameters */ ParameterIndex(398),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7797,7 +7787,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(134),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7810,7 +7800,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(217),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(134),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7822,8 +7812,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(400),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(136),
+    /* parameters */ ParameterIndex(399),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(134),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7835,8 +7825,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(401),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
+    /* parameters */ ParameterIndex(400),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(136),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7849,7 +7839,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(140),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7861,8 +7851,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(402),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(140),
+    /* parameters */ ParameterIndex(401),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7875,7 +7865,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(140),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7888,7 +7878,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(209),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(140),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7900,8 +7890,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(403),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(142),
+    /* parameters */ ParameterIndex(402),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(140),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7913,8 +7903,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(404),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
+    /* parameters */ ParameterIndex(403),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(142),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7927,7 +7917,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(146),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -7939,8 +7929,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(405),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(146),
+    /* parameters */ ParameterIndex(404),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -7953,7 +7943,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(146),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -7966,7 +7956,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(213),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(146),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -7978,8 +7968,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(406),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(148),
+    /* parameters */ ParameterIndex(405),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(146),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -7991,8 +7981,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(407),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
+    /* parameters */ ParameterIndex(406),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(148),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -8005,7 +7995,7 @@
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(152),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -8017,8 +8007,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(36),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(408),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(152),
+    /* parameters */ ParameterIndex(407),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -8031,7 +8021,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(152),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
@@ -8044,7 +8034,7 @@
     /* template_types */ TemplateTypeIndex(10),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(217),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(152),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
@@ -8056,8 +8046,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(16),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(409),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(154),
+    /* parameters */ ParameterIndex(408),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(152),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -8069,8 +8059,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(14),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(410),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(156),
+    /* parameters */ ParameterIndex(409),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(154),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -8083,7 +8073,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(27),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -8096,7 +8086,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(80),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -8109,7 +8099,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(32),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -8122,7 +8112,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(94),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -8134,8 +8124,8 @@
     /* num_template_numbers */ 2,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(6),
-    /* parameters */ ParameterIndex(377),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* parameters */ ParameterIndex(376),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -8564,7 +8554,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(53),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(51),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -8576,8 +8566,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(379),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(53),
+    /* parameters */ ParameterIndex(378),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(51),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -8590,7 +8580,7 @@
     /* template_types */ TemplateTypeIndex(30),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(53),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(51),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -8603,7 +8593,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -8616,7 +8606,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -8629,7 +8619,7 @@
     /* template_types */ TemplateTypeIndex(31),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -8681,7 +8671,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(87),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(85),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
@@ -8693,8 +8683,8 @@
     /* num_template_numbers */ 0,
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(380),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(87),
+    /* parameters */ ParameterIndex(379),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(85),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
@@ -8707,7 +8697,7 @@
     /* template_types */ TemplateTypeIndex(33),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(87),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(85),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -10137,7 +10127,7 @@
     /* template_types */ TemplateTypeIndex(0),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(277),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -10150,7 +10140,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(283),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -10501,7 +10491,7 @@
     /* template_types */ TemplateTypeIndex(25),
     /* template_numbers */ TemplateNumberIndex(7),
     /* parameters */ ParameterIndex(368),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -10553,7 +10543,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(53),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(51),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -10566,7 +10556,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(17),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
@@ -10605,7 +10595,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(372),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(43),
   },
@@ -10618,7 +10608,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(372),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(44),
   },
@@ -10631,7 +10621,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(372),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(45),
   },
@@ -10644,7 +10634,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(373),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(46),
   },
@@ -10657,7 +10647,7 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(373),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(35),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(47),
   },
@@ -10780,19 +10770,6 @@
   },
   {
     /* [454] */
-    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
-    /* num_parameters */ 1,
-    /* num_template_types */ 1,
-    /* num_template_numbers */ 0,
-    /* template_types */ TemplateTypeIndex(25),
-    /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(374),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
-    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
-  },
-  {
-    /* [455] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
     /* num_parameters */ 1,
     /* num_template_types */ 1,
@@ -10805,7 +10782,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
   {
-    /* [456] */
+    /* [455] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
     /* num_parameters */ 2,
     /* num_template_types */ 1,
@@ -10818,7 +10795,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
   {
-    /* [457] */
+    /* [456] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
     /* num_parameters */ 2,
     /* num_template_types */ 1,
@@ -10831,7 +10808,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
   {
-    /* [458] */
+    /* [457] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
     /* num_parameters */ 3,
     /* num_template_types */ 1,
@@ -10839,12 +10816,12 @@
     /* template_types */ TemplateTypeIndex(26),
     /* template_numbers */ TemplateNumberIndex(8),
     /* parameters */ ParameterIndex(0),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(80),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(78),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
   {
-    /* [459] */
+    /* [458] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* num_parameters */ 0,
     /* num_template_types */ 0,
@@ -10852,12 +10829,12 @@
     /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(/* invalid */),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(74),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
   {
-    /* [460] */
+    /* [459] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* num_parameters */ 2,
     /* num_template_types */ 1,
@@ -10870,7 +10847,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
   },
   {
-    /* [461] */
+    /* [460] */
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
     /* num_parameters */ 1,
     /* num_template_types */ 1,
@@ -10883,7 +10860,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(73),
   },
   {
-    /* [462] */
+    /* [461] */
     /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* num_parameters */ 2,
     /* num_template_types */ 0,
@@ -10896,7 +10873,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(88),
   },
   {
-    /* [463] */
+    /* [462] */
     /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* num_parameters */ 2,
     /* num_template_types */ 0,
@@ -10909,7 +10886,7 @@
     /* const_eval_fn */ ConstEvalFunctionIndex(89),
   },
   {
-    /* [464] */
+    /* [463] */
     /* flags */ OverloadFlags(OverloadFlag::kIsConverter, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* num_parameters */ 1,
     /* num_template_types */ 1,
@@ -10917,7 +10894,7 @@
     /* template_types */ TemplateTypeIndex(35),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
     /* parameters */ ParameterIndex(213),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(158),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(156),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
     /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
@@ -11510,18 +11487,12 @@
   },
   {
     /* [86] */
-    /* fn workgroupUniformLoad<T>(ptr<workgroup, T, read_write>) -> T */
-    /* num overloads */ 1,
-    /* overloads */ OverloadIndex(454),
-  },
-  {
-    /* [87] */
     /* fn textureBarrier() */
     /* num overloads */ 1,
     /* overloads */ OverloadIndex(447),
   },
   {
-    /* [88] */
+    /* [87] */
     /* fn textureDimensions<T : fiu32>(texture: texture_1d<T>) -> u32 */
     /* fn textureDimensions<T : fiu32, L : iu32>(texture: texture_1d<T>, level: L) -> u32 */
     /* fn textureDimensions<T : fiu32>(texture: texture_2d<T>) -> vec2<u32> */
@@ -11553,7 +11524,7 @@
     /* overloads */ OverloadIndex(0),
   },
   {
-    /* [89] */
+    /* [88] */
     /* fn textureGather<T : fiu32, C : iu32>(@const component: C, texture: texture_2d<T>, sampler: sampler, coords: vec2<f32>) -> vec4<T> */
     /* fn textureGather<T : fiu32, C : iu32>(@const component: C, texture: texture_2d<T>, sampler: sampler, coords: vec2<f32>, @const offset: vec2<i32>) -> vec4<T> */
     /* fn textureGather<T : fiu32, C : iu32, A : iu32>(@const component: C, texture: texture_2d_array<T>, sampler: sampler, coords: vec2<f32>, array_index: A) -> vec4<T> */
@@ -11570,7 +11541,7 @@
     /* overloads */ OverloadIndex(93),
   },
   {
-    /* [90] */
+    /* [89] */
     /* fn textureGatherCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> vec4<f32> */
     /* fn textureGatherCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, @const offset: vec2<i32>) -> vec4<f32> */
     /* fn textureGatherCompare<A : iu32>(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: A, depth_ref: f32) -> vec4<f32> */
@@ -11581,7 +11552,7 @@
     /* overloads */ OverloadIndex(174),
   },
   {
-    /* [91] */
+    /* [90] */
     /* fn textureNumLayers<T : fiu32>(texture: texture_2d_array<T>) -> u32 */
     /* fn textureNumLayers<T : fiu32>(texture: texture_cube_array<T>) -> u32 */
     /* fn textureNumLayers(texture: texture_depth_2d_array) -> u32 */
@@ -11591,7 +11562,7 @@
     /* overloads */ OverloadIndex(246),
   },
   {
-    /* [92] */
+    /* [91] */
     /* fn textureNumLevels<T : fiu32>(texture: texture_1d<T>) -> u32 */
     /* fn textureNumLevels<T : fiu32>(texture: texture_2d<T>) -> u32 */
     /* fn textureNumLevels<T : fiu32>(texture: texture_2d_array<T>) -> u32 */
@@ -11606,14 +11577,14 @@
     /* overloads */ OverloadIndex(129),
   },
   {
-    /* [93] */
+    /* [92] */
     /* fn textureNumSamples<T : fiu32>(texture: texture_multisampled_2d<T>) -> u32 */
     /* fn textureNumSamples(texture: texture_depth_multisampled_2d) -> u32 */
     /* num overloads */ 2,
     /* overloads */ OverloadIndex(404),
   },
   {
-    /* [94] */
+    /* [93] */
     /* fn textureSample(texture: texture_1d<f32>, sampler: sampler, coords: f32) -> vec4<f32> */
     /* fn textureSample(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>) -> vec4<f32> */
     /* fn textureSample(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, @const offset: vec2<i32>) -> vec4<f32> */
@@ -11633,7 +11604,7 @@
     /* overloads */ OverloadIndex(64),
   },
   {
-    /* [95] */
+    /* [94] */
     /* fn textureSampleBias(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, bias: f32) -> vec4<f32> */
     /* fn textureSampleBias(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, bias: f32, @const offset: vec2<i32>) -> vec4<f32> */
     /* fn textureSampleBias<A : iu32>(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: A, bias: f32) -> vec4<f32> */
@@ -11646,7 +11617,7 @@
     /* overloads */ OverloadIndex(158),
   },
   {
-    /* [96] */
+    /* [95] */
     /* fn textureSampleCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32 */
     /* fn textureSampleCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, @const offset: vec2<i32>) -> f32 */
     /* fn textureSampleCompare<A : iu32>(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: A, depth_ref: f32) -> f32 */
@@ -11657,7 +11628,7 @@
     /* overloads */ OverloadIndex(180),
   },
   {
-    /* [97] */
+    /* [96] */
     /* fn textureSampleCompareLevel(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32 */
     /* fn textureSampleCompareLevel(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, @const offset: vec2<i32>) -> f32 */
     /* fn textureSampleCompareLevel<A : iu32>(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: A, depth_ref: f32) -> f32 */
@@ -11668,7 +11639,7 @@
     /* overloads */ OverloadIndex(186),
   },
   {
-    /* [98] */
+    /* [97] */
     /* fn textureSampleGrad(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> */
     /* fn textureSampleGrad(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>, @const offset: vec2<i32>) -> vec4<f32> */
     /* fn textureSampleGrad<A : iu32>(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: A, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> */
@@ -11681,7 +11652,7 @@
     /* overloads */ OverloadIndex(166),
   },
   {
-    /* [99] */
+    /* [98] */
     /* fn textureSampleLevel(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, level: f32) -> vec4<f32> */
     /* fn textureSampleLevel(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, level: f32, @const offset: vec2<i32>) -> vec4<f32> */
     /* fn textureSampleLevel<A : iu32>(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: A, level: f32) -> vec4<f32> */
@@ -11700,14 +11671,14 @@
     /* overloads */ OverloadIndex(79),
   },
   {
-    /* [100] */
+    /* [99] */
     /* fn textureSampleBaseClampToEdge(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>) -> vec4<f32> */
     /* fn textureSampleBaseClampToEdge(texture: texture_external, sampler: sampler, coords: vec2<f32>) -> vec4<f32> */
     /* num overloads */ 2,
     /* overloads */ OverloadIndex(406),
   },
   {
-    /* [101] */
+    /* [100] */
     /* fn textureStore<C : iu32>(texture: texture_storage_1d<f32_texel_format, writable>, coords: C, value: vec4<f32>) */
     /* fn textureStore<C : iu32>(texture: texture_storage_2d<f32_texel_format, writable>, coords: vec2<C>, value: vec4<f32>) */
     /* fn textureStore<C : iu32, A : iu32>(texture: texture_storage_2d_array<f32_texel_format, writable>, coords: vec2<C>, array_index: A, value: vec4<f32>) */
@@ -11724,7 +11695,7 @@
     /* overloads */ OverloadIndex(105),
   },
   {
-    /* [102] */
+    /* [101] */
     /* fn textureLoad<T : fiu32, C : iu32, L : iu32>(texture: texture_1d<T>, coords: C, level: L) -> vec4<T> */
     /* fn textureLoad<T : fiu32, C : iu32, L : iu32>(texture: texture_2d<T>, coords: vec2<C>, level: L) -> vec4<T> */
     /* fn textureLoad<T : fiu32, C : iu32, A : iu32, L : iu32>(texture: texture_2d_array<T>, coords: vec2<C>, array_index: A, level: L) -> vec4<T> */
@@ -11750,88 +11721,88 @@
     /* overloads */ OverloadIndex(27),
   },
   {
-    /* [103] */
+    /* [102] */
     /* fn atomicLoad<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>) -> T */
     /* num overloads */ 1,
+    /* overloads */ OverloadIndex(454),
+  },
+  {
+    /* [103] */
+    /* fn atomicStore<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) */
+    /* num overloads */ 1,
     /* overloads */ OverloadIndex(455),
   },
   {
     /* [104] */
-    /* fn atomicStore<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) */
+    /* fn atomicAdd<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
     /* overloads */ OverloadIndex(456),
   },
   {
     /* [105] */
-    /* fn atomicAdd<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicSub<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [106] */
-    /* fn atomicSub<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicMax<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [107] */
-    /* fn atomicMax<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicMin<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [108] */
-    /* fn atomicMin<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicAnd<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [109] */
-    /* fn atomicAnd<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicOr<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [110] */
-    /* fn atomicOr<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicXor<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [111] */
-    /* fn atomicXor<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicExchange<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(457),
+    /* overloads */ OverloadIndex(456),
   },
   {
     /* [112] */
-    /* fn atomicExchange<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* fn atomicCompareExchangeWeak<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T, T) -> __atomic_compare_exchange_result<T> */
     /* num overloads */ 1,
     /* overloads */ OverloadIndex(457),
   },
   {
     /* [113] */
-    /* fn atomicCompareExchangeWeak<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T, T) -> __atomic_compare_exchange_result<T> */
+    /* fn subgroupBallot() -> vec4<u32> */
     /* num overloads */ 1,
     /* overloads */ OverloadIndex(458),
   },
   {
     /* [114] */
-    /* fn subgroupBallot() -> vec4<u32> */
+    /* fn subgroupBroadcast<T : fiu32>(value: T, @const sourceLaneIndex: u32) -> T */
     /* num overloads */ 1,
     /* overloads */ OverloadIndex(459),
   },
   {
     /* [115] */
-    /* fn subgroupBroadcast<T : fiu32>(value: T, @const sourceLaneIndex: u32) -> T */
-    /* num overloads */ 1,
-    /* overloads */ OverloadIndex(460),
-  },
-  {
-    /* [116] */
     /* fn _tint_materialize<T>(T) -> T */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(461),
+    /* overloads */ OverloadIndex(460),
   },
 };
 
@@ -11944,13 +11915,13 @@
     /* [8] */
     /* op &&(bool, bool) -> bool */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(462),
+    /* overloads */ OverloadIndex(461),
   },
   {
     /* [9] */
     /* op ||(bool, bool) -> bool */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(463),
+    /* overloads */ OverloadIndex(462),
   },
   {
     /* [10] */
@@ -12225,7 +12196,7 @@
     /* [17] */
     /* conv packedVec3<T : concrete_scalar>(vec3<T>) -> packedVec3<T> */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(464),
+    /* overloads */ OverloadIndex(463),
   },
 };
 
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index ff12f1e..928d9c3 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -153,7 +153,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "access_test.cc",
       "binary_test.cc",
diff --git a/src/tint/lang/core/ir/block.cc b/src/tint/lang/core/ir/block.cc
index 00654b9..3a8f8e3 100644
--- a/src/tint/lang/core/ir/block.cc
+++ b/src/tint/lang/core/ir/block.cc
@@ -186,4 +186,10 @@
     inst->next = nullptr;
 }
 
+void Block::Destroy() {
+    while (instructions_.first) {
+        instructions_.first->Destroy();
+    }
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/block.h b/src/tint/lang/core/ir/block.h
index a01e8a0..3dea344 100644
--- a/src/tint/lang/core/ir/block.h
+++ b/src/tint/lang/core/ir/block.h
@@ -142,6 +142,9 @@
     /// @param parent the parent instruction that owns this block
     void SetParent(ControlInstruction* parent) { parent_ = parent; }
 
+    /// Destroys the block and all of its instructions.
+    void Destroy();
+
   private:
     struct {
         Instruction* first = nullptr;
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 64448cf..75f7266 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -356,6 +356,11 @@
             ir.constant_values.Composite(ty, Vector{ConstantValue(std::forward<ARGS>(values))...}));
     }
 
+    /// Creates a new zero-value ir::Constant
+    /// @param ty the constant type
+    /// @returns the new constant
+    ir::Constant* Zero(const core::type::Type* ty) { return Constant(ir.constant_values.Zero(ty)); }
+
     /// @param in the input value. One of: nullptr, ir::Value*, ir::Instruction* or a numeric value.
     /// @returns an ir::Value* from the given argument.
     template <typename T>
diff --git a/src/tint/lang/core/ir/control_instruction.cc b/src/tint/lang/core/ir/control_instruction.cc
index af0670a..31a4078 100644
--- a/src/tint/lang/core/ir/control_instruction.cc
+++ b/src/tint/lang/core/ir/control_instruction.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/core/ir/control_instruction.h"
 
+#include "src/tint/lang/core/ir/block.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::ControlInstruction);
 
 namespace tint::core::ir {
@@ -32,4 +34,9 @@
     exits_.Remove(exit);
 }
 
+void ControlInstruction::Destroy() {
+    Base::Destroy();
+    ForeachBlock([](ir::Block* blk) { blk->Destroy(); });
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/control_instruction.h b/src/tint/lang/core/ir/control_instruction.h
index fe8f6ce..74a4558 100644
--- a/src/tint/lang/core/ir/control_instruction.h
+++ b/src/tint/lang/core/ir/control_instruction.h
@@ -77,6 +77,9 @@
     /// @param exit the exit instruction
     void RemoveExit(Exit* exit);
 
+    /// @copydoc Instruction::Destroy
+    void Destroy() override;
+
   protected:
     /// The flow control exits
     Hashset<Exit*, 2> exits_;
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 6625466..0cbf490 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -74,6 +74,10 @@
 
 }  // namespace
 
+std::string Disassemble(Module& mod) {
+    return Disassembler{mod}.Disassemble();
+}
+
 Disassembler::Disassembler(Module& mod) : mod_(mod) {}
 
 Disassembler::~Disassembler() = default;
@@ -487,6 +491,9 @@
             if (v->Attributes().location.has_value()) {
                 out_ << " @location(" << v->Attributes().location.value() << ")";
             }
+            if (v->Attributes().index.has_value()) {
+                out_ << " @index(" << v->Attributes().index.value() << ")";
+            }
             if (v->Attributes().interpolation.has_value()) {
                 auto& interp = v->Attributes().interpolation.value();
                 out_ << " @interpolate(" << interp.type;
diff --git a/src/tint/lang/core/ir/disassembler.h b/src/tint/lang/core/ir/disassembler.h
index bb0bd44..e0cbc7b 100644
--- a/src/tint/lang/core/ir/disassembler.h
+++ b/src/tint/lang/core/ir/disassembler.h
@@ -36,6 +36,10 @@
 
 namespace tint::core::ir {
 
+/// @returns the disassembly for the module @p mod
+/// @param mod the module to disassemble
+std::string Disassemble(Module& mod);
+
 /// Helper class to disassemble the IR
 class Disassembler {
   public:
diff --git a/src/tint/lang/core/ir/function.cc b/src/tint/lang/core/ir/function.cc
index 57309c0..7b68cca 100644
--- a/src/tint/lang/core/ir/function.cc
+++ b/src/tint/lang/core/ir/function.cc
@@ -46,7 +46,6 @@
     block_->CloneInto(ctx, new_func->block_);
 
     ctx.ir.SetName(new_func, ctx.ir.NameOf(this).Name());
-    ctx.ir.functions.Push(new_func);
     return new_func;
 }
 
@@ -60,6 +59,11 @@
     TINT_ASSERT(!params_.Any(IsNull));
 }
 
+void Function::Destroy() {
+    Base::Destroy();
+    block_->Destroy();
+}
+
 std::string_view ToString(Function::PipelineStage value) {
     switch (value) {
         case Function::PipelineStage::kVertex:
diff --git a/src/tint/lang/core/ir/function.h b/src/tint/lang/core/ir/function.h
index 34a9276..ba3263f 100644
--- a/src/tint/lang/core/ir/function.h
+++ b/src/tint/lang/core/ir/function.h
@@ -140,6 +140,9 @@
     /// @returns the function root block
     ir::Block* Block() { return block_; }
 
+    /// Destroys the function and all of its instructions.
+    void Destroy() override;
+
   private:
     PipelineStage pipeline_stage_;
     std::optional<std::array<uint32_t, 3>> workgroup_size_;
diff --git a/src/tint/lang/core/ir/function_test.cc b/src/tint/lang/core/ir/function_test.cc
index 6739793..b2b58d3 100644
--- a/src/tint/lang/core/ir/function_test.cc
+++ b/src/tint/lang/core/ir/function_test.cc
@@ -111,7 +111,8 @@
     EXPECT_EQ(new_param1, new_f->Params()[0]);
     EXPECT_EQ(new_param2, new_f->Params()[1]);
 
-    EXPECT_EQ(new_f, mod.functions.Back());
+    // Cloned functions are not automatically added to the module.
+    EXPECT_EQ(mod.functions.Length(), 1u);
 }
 
 TEST_F(IR_FunctionTest, CloneWithExits) {
diff --git a/src/tint/lang/core/ir/operand_instruction.h b/src/tint/lang/core/ir/operand_instruction.h
index 980c696..afa699a 100644
--- a/src/tint/lang/core/ir/operand_instruction.h
+++ b/src/tint/lang/core/ir/operand_instruction.h
@@ -75,6 +75,9 @@
         operands_.Clear();
     }
 
+    /// Removes all results from the instruction.
+    void ClearResults() { results_.Clear(); }
+
     /// @returns the operands of the instruction
     VectorRef<ir::Value*> Operands() override { return operands_; }
 
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index e4c3240..fd23d7f 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -32,9 +32,12 @@
     "binding_remapper.cc",
     "block_decorated_structs.cc",
     "builtin_polyfill.cc",
+    "combine_access_instructions.cc",
+    "conversion_polyfill.cc",
     "demote_to_helper.cc",
     "direct_variable_access.cc",
     "multiplanar_external_texture.cc",
+    "preserve_padding.cc",
     "robustness.cc",
     "shader_io.cc",
     "std140.cc",
@@ -47,9 +50,12 @@
     "binding_remapper.h",
     "block_decorated_structs.h",
     "builtin_polyfill.h",
+    "combine_access_instructions.h",
+    "conversion_polyfill.h",
     "demote_to_helper.h",
     "direct_variable_access.h",
     "multiplanar_external_texture.h",
+    "preserve_padding.h",
     "robustness.h",
     "shader_io.h",
     "std140.h",
@@ -90,18 +96,25 @@
     "binding_remapper_test.cc",
     "block_decorated_structs_test.cc",
     "builtin_polyfill_test.cc",
+    "combine_access_instructions_test.cc",
+    "conversion_polyfill_test.cc",
     "demote_to_helper_test.cc",
     "direct_variable_access_test.cc",
     "helper_test.h",
     "multiplanar_external_texture_test.cc",
+    "preserve_padding_test.cc",
     "robustness_test.cc",
     "std140_test.cc",
     "zero_init_workgroup_memory_test.cc",
   ] + select({
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
       "direct_variable_access_wgsl_test.cc",
     ],
     "//conditions:default": [],
+  }) + select({
+    "//conditions:default": [],
   }),
   deps = [
     "//src/tint/api/common",
@@ -115,10 +128,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/lang/wgsl/writer/ir_to_program",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -134,7 +144,20 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 1164934..2533c85 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -38,12 +38,18 @@
   lang/core/ir/transform/block_decorated_structs.h
   lang/core/ir/transform/builtin_polyfill.cc
   lang/core/ir/transform/builtin_polyfill.h
+  lang/core/ir/transform/combine_access_instructions.cc
+  lang/core/ir/transform/combine_access_instructions.h
+  lang/core/ir/transform/conversion_polyfill.cc
+  lang/core/ir/transform/conversion_polyfill.h
   lang/core/ir/transform/demote_to_helper.cc
   lang/core/ir/transform/demote_to_helper.h
   lang/core/ir/transform/direct_variable_access.cc
   lang/core/ir/transform/direct_variable_access.h
   lang/core/ir/transform/multiplanar_external_texture.cc
   lang/core/ir/transform/multiplanar_external_texture.h
+  lang/core/ir/transform/preserve_padding.cc
+  lang/core/ir/transform/preserve_padding.h
   lang/core/ir/transform/robustness.cc
   lang/core/ir/transform/robustness.h
   lang/core/ir/transform/shader_io.cc
@@ -88,10 +94,13 @@
   lang/core/ir/transform/binding_remapper_test.cc
   lang/core/ir/transform/block_decorated_structs_test.cc
   lang/core/ir/transform/builtin_polyfill_test.cc
+  lang/core/ir/transform/combine_access_instructions_test.cc
+  lang/core/ir/transform/conversion_polyfill_test.cc
   lang/core/ir/transform/demote_to_helper_test.cc
   lang/core/ir/transform/direct_variable_access_test.cc
   lang/core/ir/transform/helper_test.h
   lang/core/ir/transform/multiplanar_external_texture_test.cc
+  lang/core/ir/transform/preserve_padding_test.cc
   lang/core/ir/transform/robustness_test.cc
   lang/core/ir/transform/std140_test.cc
   lang/core/ir/transform/zero_init_workgroup_memory_test.cc
@@ -109,10 +118,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_lang_wgsl_writer_ir_to_program
   tint_utils_containers
   tint_utils_diagnostic
@@ -133,8 +139,21 @@
   "gtest"
 )
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_core_ir_transform_test test
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
   tint_target_add_sources(tint_lang_core_ir_transform_test test
     "lang/core/ir/transform/direct_variable_access_wgsl_test.cc"
   )
 endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_core_ir_transform_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index 9ee4ae0..4185b72 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -43,12 +43,18 @@
     "block_decorated_structs.h",
     "builtin_polyfill.cc",
     "builtin_polyfill.h",
+    "combine_access_instructions.cc",
+    "combine_access_instructions.h",
+    "conversion_polyfill.cc",
+    "conversion_polyfill.h",
     "demote_to_helper.cc",
     "demote_to_helper.h",
     "direct_variable_access.cc",
     "direct_variable_access.h",
     "multiplanar_external_texture.cc",
     "multiplanar_external_texture.h",
+    "preserve_padding.cc",
+    "preserve_padding.h",
     "robustness.cc",
     "robustness.h",
     "shader_io.cc",
@@ -83,7 +89,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "add_empty_entry_point_test.cc",
       "bgra8unorm_polyfill_test.cc",
@@ -91,10 +96,13 @@
       "binding_remapper_test.cc",
       "block_decorated_structs_test.cc",
       "builtin_polyfill_test.cc",
+      "combine_access_instructions_test.cc",
+      "conversion_polyfill_test.cc",
       "demote_to_helper_test.cc",
       "direct_variable_access_test.cc",
       "helper_test.h",
       "multiplanar_external_texture_test.cc",
+      "preserve_padding_test.cc",
       "robustness_test.cc",
       "std140_test.cc",
       "zero_init_workgroup_memory_test.cc",
@@ -112,10 +120,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
       "${tint_src_dir}/lang/wgsl/sem",
-      "${tint_src_dir}/lang/wgsl/writer",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -132,8 +137,19 @@
       "${tint_src_dir}/utils/traits",
     ]
 
+    if (tint_build_wgsl_reader) {
+      deps += [
+        "${tint_src_dir}/lang/wgsl/reader",
+        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+      ]
+    }
+
     if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
       sources += [ "direct_variable_access_wgsl_test.cc" ]
     }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+    }
   }
 }
diff --git a/src/tint/lang/core/ir/transform/binding_remapper.cc b/src/tint/lang/core/ir/transform/binding_remapper.cc
index 47a591f..b927fd1 100644
--- a/src/tint/lang/core/ir/transform/binding_remapper.cc
+++ b/src/tint/lang/core/ir/transform/binding_remapper.cc
@@ -28,8 +28,9 @@
 
 namespace {
 
-Result<SuccessType> Run(ir::Module& ir, const BindingRemapperOptions& options) {
-    if (options.binding_points.empty()) {
+Result<SuccessType> Run(ir::Module& ir,
+                        const std::unordered_map<BindingPoint, BindingPoint>& binding_points) {
+    if (binding_points.empty()) {
         return Success;
     }
     if (ir.root_block->IsEmpty()) {
@@ -49,8 +50,8 @@
         }
 
         // Replace group and binding index if requested.
-        auto to = options.binding_points.find(bp.value());
-        if (to != options.binding_points.end()) {
+        auto to = binding_points.find(bp.value());
+        if (to != binding_points.end()) {
             var->SetBindingPoint(to->second.group, to->second.binding);
         }
     }
@@ -60,13 +61,15 @@
 
 }  // namespace
 
-Result<SuccessType> BindingRemapper(Module& ir, const BindingRemapperOptions& options) {
+Result<SuccessType> BindingRemapper(
+    Module& ir,
+    const std::unordered_map<BindingPoint, BindingPoint>& binding_points) {
     auto result = ValidateAndDumpIfNeeded(ir, "BindingRemapper transform");
     if (!result) {
         return result;
     }
 
-    return Run(ir, options);
+    return Run(ir, binding_points);
 }
 
 }  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/binding_remapper.h b/src/tint/lang/core/ir/transform/binding_remapper.h
index 5d7a2a0..51f4f14 100644
--- a/src/tint/lang/core/ir/transform/binding_remapper.h
+++ b/src/tint/lang/core/ir/transform/binding_remapper.h
@@ -16,8 +16,9 @@
 #define SRC_TINT_LANG_CORE_IR_TRANSFORM_BINDING_REMAPPER_H_
 
 #include <string>
+#include <unordered_map>
 
-#include "src/tint/api/options/binding_remapper.h"
+#include "src/tint/api/common/binding_point.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -29,9 +30,11 @@
 
 /// BindingRemapper is a transform that remaps binding point indices and access controls.
 /// @param module the module to transform
-/// @param options the remapping options
+/// @param binding_points the remapping data
 /// @returns success or failure
-Result<SuccessType> BindingRemapper(Module& module, const BindingRemapperOptions& options);
+Result<SuccessType> BindingRemapper(
+    Module& module,
+    const std::unordered_map<BindingPoint, BindingPoint>& binding_points);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/binding_remapper_test.cc b/src/tint/lang/core/ir/transform/binding_remapper_test.cc
index 81383c8..9f016fa 100644
--- a/src/tint/lang/core/ir/transform/binding_remapper_test.cc
+++ b/src/tint/lang/core/ir/transform/binding_remapper_test.cc
@@ -41,8 +41,8 @@
 
     auto* expect = src;
 
-    BindingRemapperOptions options;
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
@@ -62,9 +62,9 @@
 
     auto* expect = src;
 
-    BindingRemapperOptions options;
-    options.binding_points[tint::BindingPoint{0u, 1u}] = tint::BindingPoint{1u, 0u};
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    binding_points[tint::BindingPoint{0u, 1u}] = tint::BindingPoint{1u, 0u};
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
@@ -89,9 +89,9 @@
 
 )";
 
-    BindingRemapperOptions options;
-    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 2u};
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 2u};
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
@@ -116,9 +116,9 @@
 
 )";
 
-    BindingRemapperOptions options;
-    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{1u, 3u};
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{1u, 3u};
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
@@ -143,9 +143,9 @@
 
 )";
 
-    BindingRemapperOptions options;
-    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 4u};
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 4u};
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
@@ -175,10 +175,10 @@
 
 )";
 
-    BindingRemapperOptions options;
-    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 4u};
-    options.binding_points[tint::BindingPoint{3u, 4u}] = tint::BindingPoint{1u, 2u};
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{3u, 4u};
+    binding_points[tint::BindingPoint{3u, 4u}] = tint::BindingPoint{1u, 2u};
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
@@ -229,10 +229,10 @@
 }
 )";
 
-    BindingRemapperOptions options;
-    options.binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{0u, 1u};
-    options.binding_points[tint::BindingPoint{3u, 4u}] = tint::BindingPoint{0u, 1u};
-    Run(BindingRemapper, options);
+    std::unordered_map<tint::BindingPoint, tint::BindingPoint> binding_points;
+    binding_points[tint::BindingPoint{1u, 2u}] = tint::BindingPoint{0u, 1u};
+    binding_points[tint::BindingPoint{3u, 4u}] = tint::BindingPoint{0u, 1u};
+    Run(BindingRemapper, binding_points);
 
     EXPECT_EQ(expect, str());
 }
diff --git a/src/tint/lang/core/ir/transform/block_decorated_structs.cc b/src/tint/lang/core/ir/transform/block_decorated_structs.cc
index 55eada7..bb534b2 100644
--- a/src/tint/lang/core/ir/transform/block_decorated_structs.cc
+++ b/src/tint/lang/core/ir/transform/block_decorated_structs.cc
@@ -30,6 +30,7 @@
 
 void Run(Module& ir) {
     Builder builder{ir};
+    type::Manager& ty{ir.Types()};
 
     if (ir.root_block->IsEmpty()) {
         return;
@@ -54,58 +55,38 @@
         auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
         auto* store_ty = ptr->StoreType();
 
-        bool wrapped = false;
-        Vector<const core::type::StructMember*, 4> members;
-
-        // Build the member list for the block-decorated structure.
         if (auto* str = store_ty->As<core::type::Struct>(); str && !str->HasFixedFootprint()) {
             // We know the original struct will only ever be used as the store type of a buffer, so
-            // just redeclare it as a block-decorated struct.
-            for (auto* member : str->Members()) {
-                members.Push(member);
-            }
-        } else {
-            // The original struct might be used in other places, so create a new block-decorated
-            // struct that wraps the original struct.
-            members.Push(ir.Types().Get<core::type::StructMember>(
-                /* name */ ir.symbols.New(),
-                /* type */ store_ty,
-                /* index */ 0u,
-                /* offset */ 0u,
-                /* align */ store_ty->Align(),
-                /* size */ store_ty->Size(),
-                /* attributes */ core::type::StructMemberAttributes{}));
-            wrapped = true;
+            // just mark it as a block-decorated struct.
+            // TODO(crbug.com/tint/745): Remove the const_cast.
+            const_cast<type::Struct*>(str)->SetStructFlag(type::kBlock);
+            continue;
         }
 
-        // Create the block-decorated struct.
-        auto* block_struct = ir.Types().Get<core::type::Struct>(
-            /* name */ ir.symbols.New(),
-            /* members */ members,
-            /* align */ store_ty->Align(),
-            /* size */ tint::RoundUp(store_ty->Align(), store_ty->Size()),
-            /* size_no_padding */ store_ty->Size());
+        // The original struct might be used in other places, so create a new block-decorated
+        // struct that wraps the original struct.
+        auto inner_name = ir.symbols.New();
+        auto wrapper_name = ir.symbols.New();
+        auto* block_struct = ty.Struct(wrapper_name, {{inner_name, store_ty}});
         block_struct->SetStructFlag(core::type::StructFlag::kBlock);
 
         // Replace the old variable declaration with one that uses the block-decorated struct type.
-        auto* new_var =
-            builder.Var(ir.Types().ptr(ptr->AddressSpace(), block_struct, ptr->Access()));
+        auto* new_var = builder.Var(ty.ptr(ptr->AddressSpace(), block_struct, ptr->Access()));
         if (var->BindingPoint()) {
             new_var->SetBindingPoint(var->BindingPoint()->group, var->BindingPoint()->binding);
         }
         var->ReplaceWith(new_var);
 
         // Replace uses of the old variable.
+        // The structure has been wrapped, so replace all uses of the old variable with a member
+        // accessor on the new variable.
         var->Result()->ReplaceAllUsesWith([&](Usage use) -> Value* {
-            if (wrapped) {
-                // The structure has been wrapped, so replace all uses of the old variable with a
-                // member accessor on the new variable.
-                auto* access = builder.Access(var->Result()->Type(), new_var, 0_u);
-                access->InsertBefore(use.instruction);
-                return access->Result();
-            }
-            return new_var->Result();
+            auto* access = builder.Access(var->Result()->Type(), new_var, 0_u);
+            access->InsertBefore(use.instruction);
+            return access->Result();
         });
+
+        var->Destroy();
     }
 }
 
diff --git a/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc b/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc
index 500bfdd..4ad7380 100644
--- a/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc
+++ b/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc
@@ -205,18 +205,13 @@
     });
 
     auto* expect = R"(
-MyStruct = struct @align(4) {
-  i:i32 @offset(0)
-  arr:array<i32> @offset(4)
-}
-
-tint_symbol = struct @align(4), @block {
+MyStruct = struct @align(4), @block {
   i:i32 @offset(0)
   arr:array<i32> @offset(4)
 }
 
 %b1 = block {  # root
-  %1:ptr<storage, tint_symbol, read_write> = var @binding_point(0, 0)
+  %1:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
 }
 
 %foo = func():void -> %b2 {
@@ -235,6 +230,51 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_BlockDecoratedStructsTest, RuntimeArray_InStruct_ArrayLengthViaLets) {
+    auto* structure =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("i"), ty.i32()},
+                                                   {mod.symbols.New("arr"), ty.array<i32>()},
+                                               });
+
+    auto* buffer = b.Var(ty.ptr(storage, structure, core::Access::kReadWrite));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* let_root = b.Let("root", buffer->Result());
+        auto* let_arr = b.Let("arr", b.Access(ty.ptr(storage, ty.array<i32>()), let_root, 1_u));
+        auto* length = b.Call(ty.u32(), core::BuiltinFn::kArrayLength, let_arr);
+        b.Return(func, length);
+    });
+
+    auto* expect = R"(
+MyStruct = struct @align(4), @block {
+  i:i32 @offset(0)
+  arr:array<i32> @offset(4)
+}
+
+%b1 = block {  # root
+  %1:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():u32 -> %b2 {
+  %b2 = block {
+    %root:ptr<storage, MyStruct, read_write> = let %1
+    %4:ptr<storage, array<i32>, read_write> = access %root, 1u
+    %arr:ptr<storage, array<i32>, read_write> = let %4
+    %6:u32 = arrayLength %arr
+    ret %6
+  }
+}
+)";
+
+    Run(BlockDecoratedStructs);
+
+    EXPECT_EQ(expect, str());
+}
+
 TEST_F(IR_BlockDecoratedStructsTest, StructUsedElsewhere) {
     auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
                                                                  {mod.symbols.New("a"), ty.i32()},
@@ -296,11 +336,12 @@
     root->Append(buffer_c);
 
     auto* func = b.Function("foo", ty.void_());
-    auto* block = func->Block();
-    auto* load_b = block->Append(b.Load(buffer_b));
-    auto* load_c = block->Append(b.Load(buffer_c));
-    block->Append(b.Store(buffer_a, b.Add(ty.i32(), load_b, load_c)));
-    block->Append(b.Return(func));
+    b.Append(func->Block(), [&] {
+        auto* load_b = b.Load(buffer_b);
+        auto* load_c = b.Load(buffer_c);
+        b.Store(buffer_a, b.Add(ty.i32(), load_b, load_c));
+        b.Return(func);
+    });
 
     auto* expect = R"(
 tint_symbol_1 = struct @align(4), @block {
@@ -327,8 +368,9 @@
     %6:i32 = load %5
     %7:ptr<storage, i32, read_write> = access %3, 0u
     %8:i32 = load %7
-    %9:ptr<storage, i32, read_write> = access %1, 0u
-    store %9, %10
+    %9:i32 = add %6, %8
+    %10:ptr<storage, i32, read_write> = access %1, 0u
+    store %10, %9
     ret
   }
 }
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions.cc b/src/tint/lang/core/ir/transform/combine_access_instructions.cc
new file mode 100644
index 0000000..71b42c9
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions.cc
@@ -0,0 +1,80 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/combine_access_instructions.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"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    Module& ir;
+
+    /// Process the module.
+    void Process() {
+        // Loop over every instruction looking for access instructions.
+        for (auto* inst : ir.instructions.Objects()) {
+            if (auto* access = inst->As<ir::Access>(); access && access->Alive()) {
+                // Look for places where the result of this access instruction is used as a base
+                // pointer for another access instruction.
+                access->Result()->ForEachUse([&](Usage use) {
+                    auto* child = use.instruction->As<ir::Access>();
+                    if (child && use.operand_index == ir::Access::kObjectOperandOffset) {
+                        // Push the indices of the parent access instruction into the child.
+                        Vector<ir::Value*, 4> operands;
+                        operands.Push(access->Object());
+                        for (auto* idx : access->Indices()) {
+                            operands.Push(idx);
+                        }
+                        for (auto* idx : child->Indices()) {
+                            operands.Push(idx);
+                        }
+                        child->SetOperands(std::move(operands));
+                    }
+                });
+
+                // If there are no other uses of the access instruction, remove it.
+                if (access->Result()->Usages().IsEmpty()) {
+                    access->Destroy();
+                }
+            }
+        }
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> CombineAccessInstructions(Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "CombineAccessInstructions transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions.h b/src/tint/lang/core/ir/transform/combine_access_instructions.h
new file mode 100644
index 0000000..55005e2
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions.h
@@ -0,0 +1,34 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_COMBINE_ACCESS_INSTRUCTIONS_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_COMBINE_ACCESS_INSTRUCTIONS_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// CombineAccessInstructions is a transform that combines chains of access instructions.
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> CombineAccessInstructions(Module& module);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_COMBINE_ACCESS_INSTRUCTIONS_H_
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions_test.cc b/src/tint/lang/core/ir/transform/combine_access_instructions_test.cc
new file mode 100644
index 0000000..7c79865
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions_test.cc
@@ -0,0 +1,874 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/combine_access_instructions.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using IR_CombineAccessInstructionsTest = TransformTest;
+
+TEST_F(IR_CombineAccessInstructionsTest, NoModify_NoChaining) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_root = b.Access(ty.ptr(uniform, structure), buffer);
+        b.Load(access_root);
+
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        b.Load(access_arr);
+
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), buffer, 0_u, 1_u);
+        b.Load(access_mat);
+
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), buffer, 0_u, 1_u, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, MyStruct, read_write> = access %buffer
+    %4:MyStruct = load %3
+    %5:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %6:array<mat3x3<f32>, 4> = load %5
+    %7:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %8:mat3x3<f32> = load %7
+    %9:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %10:vec3<f32> = load %9
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, SimpleChain) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %6:vec3<f32> = load %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %4:vec3<f32> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, NoIndices_Root) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_root = b.Access(ty.ptr(uniform, structure), buffer);
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), access_root, 0_u);
+        b.Load(access_arr);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, MyStruct, read_write> = access %buffer
+    %4:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %3, 0u
+    %5:array<mat3x3<f32>, 4> = load %4
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:array<mat3x3<f32>, 4> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, NoIndices_Middle) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_copy = b.Access(ty.ptr(uniform, arr), access_arr);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_copy, 1_u);
+        b.Load(access_mat);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %3
+    %5:ptr<uniform, mat3x3<f32>, read_write> = access %4, 1u
+    %6:mat3x3<f32> = load %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %4:mat3x3<f32> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, NoIndices_End) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        auto* access_copy = b.Access(ty.ptr(uniform, mat), access_mat);
+        b.Load(access_copy);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, mat3x3<f32>, read_write> = access %4
+    %6:mat3x3<f32> = load %5
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %4:mat3x3<f32> = load %3
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, MutipleChains_FromRoot) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+
+        {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 2_u);
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 0_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 3_u);
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 1_u);
+            b.Load(access_vec);
+        }
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, mat3x3<f32>, read_write> = access %3, 2u
+    %8:ptr<uniform, vec3<f32>, read_write> = access %7, 0u
+    %9:vec3<f32> = load %8
+    %10:ptr<uniform, mat3x3<f32>, read_write> = access %3, 3u
+    %11:ptr<uniform, vec3<f32>, read_write> = access %10, 1u
+    %12:vec3<f32> = load %11
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %4:vec3<f32> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 2u, 0u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 3u, 1u
+    %8:vec3<f32> = load %7
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, MutipleChains_FromMiddle) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+
+        {
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 0_u);
+            b.Load(access_vec);
+        }
+        {
+            auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 1_u);
+            b.Load(access_vec);
+        }
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, vec3<f32>, read_write> = access %4, 0u
+    %8:vec3<f32> = load %7
+    %9:ptr<uniform, vec3<f32>, read_write> = access %4, 1u
+    %10:vec3<f32> = load %9
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %4:vec3<f32> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 0u
+    %6:vec3<f32> = load %5
+    %7:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 1u
+    %8:vec3<f32> = load %7
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, OtherUses_Root) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        b.Load(access_arr);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:array<mat3x3<f32>, 4> = load %3
+    %5:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %6:ptr<uniform, vec3<f32>, read_write> = access %5, 2u
+    %7:vec3<f32> = load %6
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:array<mat3x3<f32>, 4> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %6:vec3<f32> = load %5
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, OtherUses_Middle) {
+    auto* vec = ty.vec3<f32>();
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+        b.Load(access_mat);
+        auto* access_vec = b.Access(ty.ptr(uniform, vec), access_mat, 2_u);
+        b.Load(access_vec);
+
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+    %5:mat3x3<f32> = load %4
+    %6:ptr<uniform, vec3<f32>, read_write> = access %4, 2u
+    %7:vec3<f32> = load %6
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+    %4:mat3x3<f32> = load %3
+    %5:ptr<uniform, vec3<f32>, read_write> = access %buffer, 0u, 1u, 2u
+    %6:vec3<f32> = load %5
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, AccessResultUsesAsAccessIndex) {
+    auto* func = b.Function("foo", ty.f32());
+    auto* indices = b.FunctionParam("indices", ty.array<u32, 4>());
+    auto* values = b.FunctionParam("values", ty.array<f32, 4>());
+    b.Append(func->Block(), [&] {
+        auto* access_index = b.Access(ty.u32(), indices, 1_u);
+        auto* access_value = b.Access(ty.f32(), values, access_index);
+        b.Return(func, access_value);
+    });
+
+    auto* src = R"(
+%foo = func():f32 -> %b1 {
+  %b1 = block {
+    %2:u32 = access %indices, 1u
+    %4:f32 = access %values, %2
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, UseInDifferentBlock_If) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* ifelse = b.If(true);
+        b.Append(ifelse->True(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 1_u);
+            b.Load(access_mat);
+            b.ExitIf(ifelse);
+        });
+        b.Append(ifelse->False(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 2_u);
+            b.Load(access_mat);
+            b.ExitIf(ifelse);
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    if true [t: %b3, f: %b4] {  # if_1
+      %b3 = block {  # true
+        %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 1u
+        %5:mat3x3<f32> = load %4
+        exit_if  # if_1
+      }
+      %b4 = block {  # false
+        %6:ptr<uniform, mat3x3<f32>, read_write> = access %3, 2u
+        %7:mat3x3<f32> = load %6
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    if true [t: %b3, f: %b4] {  # if_1
+      %b3 = block {  # true
+        %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 1u
+        %4:mat3x3<f32> = load %3
+        exit_if  # if_1
+      }
+      %b4 = block {  # false
+        %5:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 2u
+        %6:mat3x3<f32> = load %5
+        exit_if  # if_1
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_CombineAccessInstructionsTest, UseInDifferentBlock_Loop) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), arr},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* access_arr = b.Access(ty.ptr(uniform, arr), buffer, 0_u);
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 2_u);
+            b.Load(access_mat);
+            b.Continue(loop);
+        });
+        b.Append(loop->Continuing(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat), access_arr, 3_u);
+            b.Load(access_mat);
+            b.BreakIf(loop, true);
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    %3:ptr<uniform, array<mat3x3<f32>, 4>, read_write> = access %buffer, 0u
+    loop [b: %b3, c: %b4] {  # loop_1
+      %b3 = block {  # body
+        %4:ptr<uniform, mat3x3<f32>, read_write> = access %3, 2u
+        %5:mat3x3<f32> = load %4
+        continue %b4
+      }
+      %b4 = block {  # continuing
+        %6:ptr<uniform, mat3x3<f32>, read_write> = access %3, 3u
+        %7:mat3x3<f32> = load %6
+        break_if true %b3
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:array<mat3x3<f32>, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    loop [b: %b3, c: %b4] {  # loop_1
+      %b3 = block {  # body
+        %3:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 2u
+        %4:mat3x3<f32> = load %3
+        continue %b4
+      }
+      %b4 = block {  # continuing
+        %5:ptr<uniform, mat3x3<f32>, read_write> = access %buffer, 0u, 3u
+        %6:mat3x3<f32> = load %5
+        break_if true %b3
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(CombineAccessInstructions);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/conversion_polyfill.cc b/src/tint/lang/core/ir/transform/conversion_polyfill.cc
new file mode 100644
index 0000000..dcae17e
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/conversion_polyfill.cc
@@ -0,0 +1,227 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/conversion_polyfill.h"
+
+#include <cmath>
+#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"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// PIMPL state for the transform.
+struct State {
+    /// The polyfill config.
+    const ConversionPolyfillConfig& config;
+
+    /// The IR module.
+    Module& ir;
+
+    /// The IR builder.
+    Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// The symbol table.
+    SymbolTable& sym{ir.symbols};
+
+    /// Map from integer type to its f32toi helper function.
+    Hashmap<const type::Type*, Function*, 4> f32toi_helpers{};
+
+    /// Map from integer type to its f16toi helper function.
+    Hashmap<const type::Type*, Function*, 4> f16toi_helpers{};
+
+    /// Process the module.
+    void Process() {
+        // Find the conversion instructions that need to be polyfilled.
+        Vector<ir::Convert*, 64> ftoi_worklist;
+        for (auto* inst : ir.instructions.Objects()) {
+            if (!inst->Alive()) {
+                continue;
+            }
+            if (auto* convert = inst->As<ir::Convert>()) {
+                auto* src_ty = convert->Args()[0]->Type();
+                auto* res_ty = convert->Result()->Type();
+                if (config.ftoi &&                          //
+                    src_ty->is_float_scalar_or_vector() &&  //
+                    res_ty->is_integer_scalar_or_vector()) {
+                    ftoi_worklist.Push(convert);
+                }
+            }
+        }
+
+        // Polyfill the conversion instructions that we found.
+        for (auto* convert : ftoi_worklist) {
+            auto* replacement = ftoi(convert);
+
+            // Replace the old conversion instruction result with the new value.
+            if (auto name = ir.NameOf(convert->Result())) {
+                ir.SetName(replacement, name);
+            }
+            convert->Result()->ReplaceAllUsesWith(replacement);
+            convert->Destroy();
+        }
+    }
+
+    /// Replace a conversion instruction with a call to helper function that manually clamps the
+    /// result to within the limit of the destination type.
+    /// @param convert the conversion instruction
+    /// @returns the replacement value
+    ir::Value* ftoi(ir::Convert* convert) {
+        auto* res_ty = convert->Result()->Type();
+        auto* src_ty = convert->Args()[0]->Type();
+        auto* src_el_ty = src_ty->DeepestElement();
+
+        auto& helpers = src_el_ty->Is<type::F32>() ? f32toi_helpers : f16toi_helpers;
+        auto* helper = helpers.GetOrCreate(res_ty, [&] {
+            // Generate a name for the helper function.
+            StringStream name;
+            name << "tint_";
+            if (auto* src_vec = src_ty->As<type::Vector>()) {
+                name << "v" << src_vec->Width() << src_vec->type()->FriendlyName();
+            } else {
+                name << src_ty->FriendlyName();
+            }
+            name << "_to_";
+            if (auto* res_vec = res_ty->As<type::Vector>()) {
+                name << "v" << res_vec->Width() << res_vec->type()->FriendlyName();
+            } else {
+                name << res_ty->FriendlyName();
+            }
+
+            // Generate constants for the limits.
+            struct {
+                ir::Constant* low_limit_f = nullptr;
+                ir::Constant* high_limit_f = nullptr;
+                ir::Constant* low_limit_i = nullptr;
+                ir::Constant* high_limit_i = nullptr;
+            } limits;
+
+            // Integer limits.
+            if (res_ty->is_signed_integer_scalar_or_vector()) {
+                limits.low_limit_i = MatchWidth(b.Constant(i32(INT32_MIN)), res_ty);
+                limits.high_limit_i = MatchWidth(b.Constant(i32(INT32_MAX)), res_ty);
+            } else {
+                limits.low_limit_i = MatchWidth(b.Constant(u32(0)), res_ty);
+                limits.high_limit_i = MatchWidth(b.Constant(u32(UINT32_MAX)), res_ty);
+            }
+
+            // Largest integers representable in the source floating point format.
+            if (src_el_ty->Is<type::F32>()) {
+                if (res_ty->is_signed_integer_scalar_or_vector()) {
+                    // INT32_MIN is -(2^31), which is exactly representable as an f32.
+                    // INT32_MAX is (2^31 - 1), which is not exactly representable as an f32, so we
+                    // instead use the next highest integer value in the f32 domain.
+                    const float kMaxI32AsF32 = std::nexttowardf(0x1p+31f, 0.0L);
+                    limits.low_limit_f = MatchWidth(b.Constant(f32(INT32_MIN)), res_ty);
+                    limits.high_limit_f = MatchWidth(b.Constant(f32(kMaxI32AsF32)), res_ty);
+                } else {
+                    // UINT32_MAX is (2^32 - 1), which is not exactly representable as an f32, so we
+                    // instead use the next highest integer value in the f32 domain.
+                    const float kMaxU32AsF32 = std::nexttowardf(0x1p+32f, 0.0L);
+                    limits.low_limit_f = MatchWidth(b.Constant(f32(0)), res_ty);
+                    limits.high_limit_f = MatchWidth(b.Constant(f32(kMaxU32AsF32)), res_ty);
+                }
+            } else if (src_el_ty->Is<type::F16>()) {
+                constexpr float MAX_F16 = 65504;
+                if (res_ty->is_signed_integer_scalar_or_vector()) {
+                    limits.low_limit_f = MatchWidth(b.Constant(f16(-MAX_F16)), res_ty);
+                    limits.high_limit_f = MatchWidth(b.Constant(f16(MAX_F16)), res_ty);
+                } else {
+                    limits.low_limit_f = MatchWidth(b.Constant(f16(0)), res_ty);
+                    limits.high_limit_f = MatchWidth(b.Constant(f16(MAX_F16)), res_ty);
+                }
+            } else {
+                TINT_UNIMPLEMENTED() << "unhandled floating-point type";
+            }
+
+            // Create the helper function.
+            auto* func = b.Function(name.str(), res_ty);
+            auto* value = b.FunctionParam("value", src_ty);
+            func->SetParams({value});
+            b.Append(func->Block(), [&] {
+                auto* bool_ty = MatchWidth(ty.bool_(), res_ty);
+
+                auto* converted = b.Convert(res_ty, value);
+
+                // low = select(low_limit_i, i32(value), value >= low_limit_f)
+                auto* low_cond = b.GreaterThanEqual(bool_ty, value, limits.low_limit_f);
+                auto* select_low = b.Call(res_ty, core::BuiltinFn::kSelect, limits.low_limit_i,
+                                          converted, low_cond);
+
+                // result = select(high_limit_i, low, value <= high_limit_f)
+                auto* high_cond = b.LessThanEqual(bool_ty, value, limits.high_limit_f);
+                auto* select_high = b.Call(res_ty, core::BuiltinFn::kSelect, limits.high_limit_i,
+                                           select_low, high_cond);
+
+                b.Return(func, select_high->Result());
+            });
+            return func;
+        });
+
+        // Call the helper function, splatting the arguments to match the target vector width.
+        auto* call = b.Call(res_ty, helper, convert->Args()[0]);
+        call->InsertBefore(convert);
+        return call->Result();
+    }
+
+    /// Return a type with element type @p type that has the same number of vector components as
+    /// @p match. If @p match is scalar just return @p type.
+    /// @param el_ty the type to extend
+    /// @param match the type to match the component count of
+    /// @returns a type with the same number of vector components as @p match
+    const core::type::Type* MatchWidth(const core::type::Type* el_ty,
+                                       const core::type::Type* match) {
+        if (auto* vec = match->As<core::type::Vector>()) {
+            return ty.vec(el_ty, vec->Width());
+        }
+        return el_ty;
+    }
+
+    /// Return a constant that has the same number of vector components as @p match, each with the
+    /// value @p element. If @p match is scalar just return @p element.
+    /// @param element the value to extend
+    /// @param match the type to match the component count of
+    /// @returns a value with the same number of vector components as @p match
+    ir::Constant* MatchWidth(ir::Constant* element, const core::type::Type* match) {
+        if (auto* vec = match->As<core::type::Vector>()) {
+            return b.Splat(MatchWidth(element->Type(), match), element, vec->Width());
+        }
+        return element;
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> ConversionPolyfill(Module& ir, const ConversionPolyfillConfig& config) {
+    auto result = ValidateAndDumpIfNeeded(ir, "ConversionPolyfill transform");
+    if (!result) {
+        return result;
+    }
+
+    State{config, ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/conversion_polyfill.h b/src/tint/lang/core/ir/transform/conversion_polyfill.h
new file mode 100644
index 0000000..0c51a67
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/conversion_polyfill.h
@@ -0,0 +1,44 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_CONVERSION_POLYFILL_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_CONVERSION_POLYFILL_H_
+
+#include <string>
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// The set of polyfills that should be applied.
+struct ConversionPolyfillConfig {
+    /// Should converting floating point values to integers be polyfilled?
+    bool ftoi = false;
+};
+
+/// ConversionPolyfill is a transform that modifies convert instructions to prepare them for raising
+/// to backend dialects that may have different semantics.
+/// @param module the module to transform
+/// @param config the polyfill configuration
+/// @returns success or failure
+Result<SuccessType> ConversionPolyfill(Module& module, const ConversionPolyfillConfig& config);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_CONVERSION_POLYFILL_H_
diff --git a/src/tint/lang/core/ir/transform/conversion_polyfill_test.cc b/src/tint/lang/core/ir/transform/conversion_polyfill_test.cc
new file mode 100644
index 0000000..f091dbb
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/conversion_polyfill_test.cc
@@ -0,0 +1,403 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/conversion_polyfill.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+class IR_ConversionPolyfillTest : public TransformTest {
+  protected:
+    /// Helper to build a function that executes a convert instruction.
+    /// @param src_ty the type of the source
+    /// @param res_ty the type of the result
+    void Build(const core::type::Type* src_ty, const core::type::Type* res_ty) {
+        auto* func = b.Function("foo", res_ty);
+        auto* src = b.FunctionParam("src", src_ty);
+        func->SetParams({src});
+        b.Append(func->Block(), [&] {
+            auto* result = b.Convert(res_ty, src);
+            b.Return(func, result);
+            mod.SetName(result, "result");
+        });
+    }
+};
+
+// No change expected in this direction.
+TEST_F(IR_ConversionPolyfillTest, I32_to_F32) {
+    Build(ty.i32(), ty.f32());
+    auto* src = R"(
+%foo = func(%src:i32):f32 -> %b1 {
+  %b1 = block {
+    %result:f32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = src;
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+// No change expected in this direction.
+TEST_F(IR_ConversionPolyfillTest, U32_to_F32) {
+    Build(ty.u32(), ty.f32());
+    auto* src = R"(
+%foo = func(%src:u32):f32 -> %b1 {
+  %b1 = block {
+    %result:f32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = src;
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F32_to_I32_NoPolyfill) {
+    Build(ty.f32(), ty.i32());
+    auto* src = R"(
+%foo = func(%src:f32):i32 -> %b1 {
+  %b1 = block {
+    %result:i32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = src;
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = false;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F32_to_I32) {
+    Build(ty.f32(), ty.i32());
+    auto* src = R"(
+%foo = func(%src:f32):i32 -> %b1 {
+  %b1 = block {
+    %result:i32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:f32):i32 -> %b1 {
+  %b1 = block {
+    %result:i32 = call %tint_f32_to_i32, %src
+    ret %result
+  }
+}
+%tint_f32_to_i32 = func(%value:f32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = convert %value
+    %7:bool = gte %value, -2147483648.0f
+    %8:i32 = select -2147483648i, %6, %7
+    %9:bool = lte %value, 2147483520.0f
+    %10:i32 = select 2147483647i, %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F32_to_U32) {
+    Build(ty.f32(), ty.u32());
+    auto* src = R"(
+%foo = func(%src:f32):u32 -> %b1 {
+  %b1 = block {
+    %result:u32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:f32):u32 -> %b1 {
+  %b1 = block {
+    %result:u32 = call %tint_f32_to_u32, %src
+    ret %result
+  }
+}
+%tint_f32_to_u32 = func(%value:f32):u32 -> %b2 {
+  %b2 = block {
+    %6:u32 = convert %value
+    %7:bool = gte %value, 0.0f
+    %8:u32 = select 0u, %6, %7
+    %9:bool = lte %value, 4294967040.0f
+    %10:u32 = select 4294967295u, %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F32_to_I32_Vec2) {
+    Build(ty.vec2<f32>(), ty.vec2<i32>());
+    auto* src = R"(
+%foo = func(%src:vec2<f32>):vec2<i32> -> %b1 {
+  %b1 = block {
+    %result:vec2<i32> = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:vec2<f32>):vec2<i32> -> %b1 {
+  %b1 = block {
+    %result:vec2<i32> = call %tint_v2f32_to_v2i32, %src
+    ret %result
+  }
+}
+%tint_v2f32_to_v2i32 = func(%value:vec2<f32>):vec2<i32> -> %b2 {
+  %b2 = block {
+    %6:vec2<i32> = convert %value
+    %7:vec2<bool> = gte %value, vec2<f32>(-2147483648.0f)
+    %8:vec2<i32> = select vec2<i32>(-2147483648i), %6, %7
+    %9:vec2<bool> = lte %value, vec2<f32>(2147483520.0f)
+    %10:vec2<i32> = select vec2<i32>(2147483647i), %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F32_to_U32_Vec3) {
+    Build(ty.vec2<f32>(), ty.vec2<u32>());
+    auto* src = R"(
+%foo = func(%src:vec2<f32>):vec2<u32> -> %b1 {
+  %b1 = block {
+    %result:vec2<u32> = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:vec2<f32>):vec2<u32> -> %b1 {
+  %b1 = block {
+    %result:vec2<u32> = call %tint_v2f32_to_v2u32, %src
+    ret %result
+  }
+}
+%tint_v2f32_to_v2u32 = func(%value:vec2<f32>):vec2<u32> -> %b2 {
+  %b2 = block {
+    %6:vec2<u32> = convert %value
+    %7:vec2<bool> = gte %value, vec2<f32>(0.0f)
+    %8:vec2<u32> = select vec2<u32>(0u), %6, %7
+    %9:vec2<bool> = lte %value, vec2<f32>(4294967040.0f)
+    %10:vec2<u32> = select vec2<u32>(4294967295u), %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F16_to_I32) {
+    Build(ty.f16(), ty.i32());
+    auto* src = R"(
+%foo = func(%src:f16):i32 -> %b1 {
+  %b1 = block {
+    %result:i32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:f16):i32 -> %b1 {
+  %b1 = block {
+    %result:i32 = call %tint_f16_to_i32, %src
+    ret %result
+  }
+}
+%tint_f16_to_i32 = func(%value:f16):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = convert %value
+    %7:bool = gte %value, -65504.0h
+    %8:i32 = select -2147483648i, %6, %7
+    %9:bool = lte %value, 65504.0h
+    %10:i32 = select 2147483647i, %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F16_to_U32) {
+    Build(ty.f16(), ty.u32());
+    auto* src = R"(
+%foo = func(%src:f16):u32 -> %b1 {
+  %b1 = block {
+    %result:u32 = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:f16):u32 -> %b1 {
+  %b1 = block {
+    %result:u32 = call %tint_f16_to_u32, %src
+    ret %result
+  }
+}
+%tint_f16_to_u32 = func(%value:f16):u32 -> %b2 {
+  %b2 = block {
+    %6:u32 = convert %value
+    %7:bool = gte %value, 0.0h
+    %8:u32 = select 0u, %6, %7
+    %9:bool = lte %value, 65504.0h
+    %10:u32 = select 4294967295u, %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F16_to_I32_Vec2) {
+    Build(ty.vec2<f16>(), ty.vec2<i32>());
+    auto* src = R"(
+%foo = func(%src:vec2<f16>):vec2<i32> -> %b1 {
+  %b1 = block {
+    %result:vec2<i32> = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:vec2<f16>):vec2<i32> -> %b1 {
+  %b1 = block {
+    %result:vec2<i32> = call %tint_v2f16_to_v2i32, %src
+    ret %result
+  }
+}
+%tint_v2f16_to_v2i32 = func(%value:vec2<f16>):vec2<i32> -> %b2 {
+  %b2 = block {
+    %6:vec2<i32> = convert %value
+    %7:vec2<bool> = gte %value, vec2<f16>(-65504.0h)
+    %8:vec2<i32> = select vec2<i32>(-2147483648i), %6, %7
+    %9:vec2<bool> = lte %value, vec2<f16>(65504.0h)
+    %10:vec2<i32> = select vec2<i32>(2147483647i), %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ConversionPolyfillTest, F16_to_U32_Vec3) {
+    Build(ty.vec2<f16>(), ty.vec2<u32>());
+    auto* src = R"(
+%foo = func(%src:vec2<f16>):vec2<u32> -> %b1 {
+  %b1 = block {
+    %result:vec2<u32> = convert %src
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%foo = func(%src:vec2<f16>):vec2<u32> -> %b1 {
+  %b1 = block {
+    %result:vec2<u32> = call %tint_v2f16_to_v2u32, %src
+    ret %result
+  }
+}
+%tint_v2f16_to_v2u32 = func(%value:vec2<f16>):vec2<u32> -> %b2 {
+  %b2 = block {
+    %6:vec2<u32> = convert %value
+    %7:vec2<bool> = gte %value, vec2<f16>(0.0h)
+    %8:vec2<u32> = select vec2<u32>(0u), %6, %7
+    %9:vec2<bool> = lte %value, vec2<f16>(65504.0h)
+    %10:vec2<u32> = select vec2<u32>(4294967295u), %8, %9
+    ret %10
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    ConversionPolyfillConfig config;
+    config.ftoi = true;
+    Run(ConversionPolyfill, config);
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/demote_to_helper.cc b/src/tint/lang/core/ir/transform/demote_to_helper.cc
index 8749807..b3c0431 100644
--- a/src/tint/lang/core/ir/transform/demote_to_helper.cc
+++ b/src/tint/lang/core/ir/transform/demote_to_helper.cc
@@ -50,15 +50,13 @@
 
     /// Process the module.
     void Process() {
-        // Check each fragment shader entry point for discard instruction, potentially inside
-        // functions called (transitively) by the entry point.
+        // Check each function for discard instructions, potentially inside other functions called
+        // (transitively) by the function.
         Vector<Function*, 4> to_process;
         for (auto* func : ir.functions) {
-            // If the function is a fragment shader that contains a discard, we need to process it.
-            if (func->Stage() == Function::PipelineStage::kFragment) {
-                if (HasDiscard(func)) {
-                    to_process.Push(func);
-                }
+            // If the function contains a discard (directly or indirectly), we need to process it.
+            if (HasDiscard(func)) {
+                to_process.Push(func);
             }
         }
         if (to_process.IsEmpty()) {
@@ -70,7 +68,7 @@
         continue_execution->SetInitializer(b.Constant(true));
         ir.root_block->Append(continue_execution);
 
-        // Process each entry point function that contains a discard.
+        // Process each function that directly or indirectly discards.
         for (auto* ep : to_process) {
             ProcessFunction(ep);
         }
diff --git a/src/tint/lang/core/ir/transform/demote_to_helper_test.cc b/src/tint/lang/core/ir/transform/demote_to_helper_test.cc
index 3efe9f4..9da0b70 100644
--- a/src/tint/lang/core/ir/transform/demote_to_helper_test.cc
+++ b/src/tint/lang/core/ir/transform/demote_to_helper_test.cc
@@ -860,5 +860,41 @@
     EXPECT_EQ(expect, str());
 }
 
+// Test that we transform unreachable functions that discard (see crbug.com/tint/2052).
+TEST_F(IR_DemoteToHelperTest, UnreachableHelperThatDiscards) {
+    auto* helper = b.Function("foo", ty.void_());
+    b.Append(helper->Block(), [&] {
+        b.Discard();
+        b.Return(helper);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    discard
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %continue_execution:ptr<private, bool, read_write> = var, true
+}
+
+%foo = func():void -> %b2 {
+  %b2 = block {
+    store %continue_execution, false
+    ret
+  }
+}
+)";
+
+    Run(DemoteToHelper);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access.cc b/src/tint/lang/core/ir/transform/direct_variable_access.cc
index 5939b82..b1079fa 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.cc
@@ -571,6 +571,7 @@
         ir.functions.Clear();
         for (auto* fn : input_fns) {
             if (auto info = need_forking.Get(fn)) {
+                fn->Destroy();
                 for (auto variant : (*info)->ordered_variants) {
                     ir.functions.Push(variant);
                 }
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access_test.cc b/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
index 5bccb80..4c1d25c 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
@@ -22,11 +22,6 @@
 #include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/core/type/struct.h"
 
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
-#include "src/tint/lang/wgsl/reader/reader.h"
-#include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
-#include "src/tint/lang/wgsl/writer/writer.h"
-
 namespace tint::core::ir::transform {
 namespace {
 
@@ -3912,20 +3907,19 @@
     EXPECT_EQ(expect, str());
 }
 
-TEST_F(IR_DirectVariableAccessTest_BuiltinFn, WorkgroupUniformLoad) {
+TEST_F(IR_DirectVariableAccessTest_BuiltinFn, AtomicLoad) {
     Var* W = nullptr;
     b.Append(b.ir.root_block,
              [&] {  //
-                 W = b.Var<workgroup, f32>("W");
+                 W = b.Var("W", ty.ptr<workgroup>(ty.atomic<i32>()));
              });
 
-    auto* fn_load = b.Function("load", ty.f32());
-    auto* fn_load_p = b.FunctionParam("p", ty.ptr<workgroup, f32>());
+    auto* fn_load = b.Function("load", ty.i32());
+    auto* fn_load_p = b.FunctionParam("p", ty.ptr<workgroup>(ty.atomic<i32>()));
     fn_load->SetParams({fn_load_p});
     b.Append(fn_load->Block(),
              [&] {  //
-                 b.Return(fn_load,
-                          b.Call(ty.f32(), core::BuiltinFn::kWorkgroupUniformLoad, fn_load_p));
+                 b.Return(fn_load, b.Call(ty.i32(), core::BuiltinFn::kAtomicLoad, fn_load_p));
              });
 
     auto* fn_f = b.Function("b", ty.void_());
@@ -3936,18 +3930,18 @@
 
     auto* src = R"(
 %b1 = block {  # root
-  %W:ptr<workgroup, f32, read_write> = var
+  %W:ptr<workgroup, atomic<i32>, read_write> = var
 }
 
-%load = func(%p:ptr<workgroup, f32, read_write>):f32 -> %b2 {
+%load = func(%p:ptr<workgroup, atomic<i32>, read_write>):i32 -> %b2 {
   %b2 = block {
-    %4:f32 = workgroupUniformLoad %p
+    %4:i32 = atomicLoad %p
     ret %4
   }
 }
 %b = func():void -> %b3 {
   %b3 = block {
-    %6:f32 = call %load, %W
+    %6:i32 = call %load, %W
     ret
   }
 }
@@ -3957,19 +3951,19 @@
 
     auto* expect = R"(
 %b1 = block {  # root
-  %W:ptr<workgroup, f32, read_write> = var
+  %W:ptr<workgroup, atomic<i32>, read_write> = var
 }
 
-%load_W = func():f32 -> %b2 {
+%load_W = func():i32 -> %b2 {
   %b2 = block {
-    %3:ptr<workgroup, f32, read_write> = access %W
-    %4:f32 = workgroupUniformLoad %3
+    %3:ptr<workgroup, atomic<i32>, read_write> = access %W
+    %4:i32 = atomicLoad %3
     ret %4
   }
 }
 %b = func():void -> %b3 {
   %b3 = block {
-    %6:f32 = call %load_W
+    %6:i32 = call %load_W
     ret
   }
 }
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc b/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
index a764250..adfcbd7 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
@@ -65,7 +65,7 @@
         auto transformed = wgsl::writer::IRToProgram(module.Get());
         if (!transformed.IsValid()) {
             return "wgsl::writer::IRToProgram() failed: \n" + transformed.Diagnostics().str() +
-                   "\n\nIR:\n" + ir::Disassembler(module.Get()).Disassemble() +  //
+                   "\n\nIR:\n" + ir::Disassemble(module.Get()) +  //
                    "\n\nAST:\n" + Program::printer(transformed);
         }
 
diff --git a/src/tint/lang/core/ir/transform/helper_test.h b/src/tint/lang/core/ir/transform/helper_test.h
index b188428..59142ab 100644
--- a/src/tint/lang/core/ir/transform/helper_test.h
+++ b/src/tint/lang/core/ir/transform/helper_test.h
@@ -49,10 +49,7 @@
     }
 
     /// @returns the transformed module as a disassembled string
-    std::string str() {
-        ir::Disassembler dis(mod);
-        return "\n" + dis.Disassemble();
-    }
+    std::string str() { return "\n" + ir::Disassemble(mod); }
 
   protected:
     /// The test IR module.
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
index db78182..96859cb 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
@@ -83,7 +83,8 @@
         }
 
         // Find function parameters that need to be replaced.
-        for (auto* func : ir.functions) {
+        auto functions = ir.functions;
+        for (auto* func : functions) {
             for (uint32_t index = 0; index < func->Params().Length(); index++) {
                 auto* param = func->Params()[index];
                 if (param->Type()->Is<core::type::ExternalTexture>()) {
diff --git a/src/tint/lang/core/ir/transform/preserve_padding.cc b/src/tint/lang/core/ir/transform/preserve_padding.cc
new file mode 100644
index 0000000..18f18f6
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/preserve_padding.cc
@@ -0,0 +1,173 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/preserve_padding.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"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    Module& ir;
+
+    /// The IR builder.
+    Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// The symbol table.
+    SymbolTable& sym{ir.symbols};
+
+    /// Map from a type to a helper function that will store a decomposed value.
+    Hashmap<const core::type::Type*, Function*, 4> helpers{};
+
+    /// Process the module.
+    void Process() {
+        // Find host-visible stores of types that contain padding bytes.
+        Vector<Store*, 8> worklist;
+        for (auto inst : ir.instructions.Objects()) {
+            if (auto* store = inst->As<Store>(); store && store->Alive()) {
+                auto* ptr = store->To()->Type()->As<core::type::Pointer>();
+                if (ptr->AddressSpace() == core::AddressSpace::kStorage &&
+                    ContainsPadding(ptr->StoreType())) {
+                    worklist.Push(store);
+                }
+            }
+        }
+
+        // Replace the stores we found with calls to helper functions that decompose the accesses.
+        for (auto* store : worklist) {
+            auto* replacement = MakeStore(store->To(), store->From());
+            store->ReplaceWith(replacement);
+            store->Destroy();
+        }
+    }
+
+    /// Check if a type contains padding bytes.
+    /// @param type the type to check
+    /// @returns true if the type contains padding bytes
+    bool ContainsPadding(const type::Type* type) {
+        return tint::Switch(
+            type,  //
+            [&](const type::Array* arr) {
+                auto* elem_ty = arr->ElemType();
+                if (arr->Stride() > elem_ty->Size()) {
+                    return true;
+                }
+                return ContainsPadding(elem_ty);
+            },
+            [&](const type::Matrix* mat) {
+                return mat->ColumnStride() > mat->ColumnType()->Size();
+            },
+            [&](const type::Struct* str) {
+                uint32_t current_offset = 0;
+                for (auto* member : str->Members()) {
+                    if (member->Offset() > current_offset) {
+                        return true;
+                    }
+                    if (ContainsPadding(member->Type())) {
+                        return true;
+                    }
+                    current_offset += member->Type()->Size();
+                }
+                return (current_offset < str->Size());
+            });
+    }
+
+    /// Create an instruction that stores a (possibly padded) type to memory, decomposing the access
+    /// into separate components to preserve padding if necessary.
+    /// @param to the pointer to store to
+    /// @param value the value to store
+    /// @returns the instruction that performs the store
+    Instruction* MakeStore(Value* to, Value* value) {
+        auto* store_type = value->Type();
+
+        // If there are no padding bytes in this type, just use a regular store instruction.
+        if (!ContainsPadding(store_type)) {
+            return b.Store(to, value);
+        }
+
+        // The type contains padding bytes, so call a helper function that decomposes the accesses.
+        auto* helper = helpers.GetOrCreate(store_type, [&] {
+            auto* func = b.Function("tint_store_and_preserve_padding", ty.void_());
+            auto* target = b.FunctionParam("target", ty.ptr(storage, store_type));
+            auto* value_param = b.FunctionParam("value_param", store_type);
+            func->SetParams({target, value_param});
+
+            b.Append(func->Block(), [&] {
+                tint::Switch(
+                    store_type,  //
+                    [&](const type::Array* arr) {
+                        b.LoopRange(
+                            ty, 0_u, u32(arr->ConstantCount().value()), 1_u, [&](Value* idx) {
+                                auto* el_ptr =
+                                    b.Access(ty.ptr(storage, arr->ElemType()), target, idx);
+                                auto* el_value = b.Access(arr->ElemType(), value_param, idx);
+                                MakeStore(el_ptr->Result(), el_value->Result());
+                            });
+                    },
+                    [&](const type::Matrix* mat) {
+                        for (uint32_t i = 0; i < mat->columns(); i++) {
+                            auto* col_ptr =
+                                b.Access(ty.ptr(storage, mat->ColumnType()), target, u32(i));
+                            auto* col_value = b.Access(mat->ColumnType(), value_param, u32(i));
+                            MakeStore(col_ptr->Result(), col_value->Result());
+                        }
+                    },
+                    [&](const type::Struct* str) {
+                        for (auto* member : str->Members()) {
+                            auto* sub_ptr = b.Access(ty.ptr(storage, member->Type()), target,
+                                                     u32(member->Index()));
+                            auto* sub_value =
+                                b.Access(member->Type(), value_param, u32(member->Index()));
+                            MakeStore(sub_ptr->Result(), sub_value->Result());
+                        }
+                    });
+
+                b.Return(func);
+            });
+
+            return func;
+        });
+
+        return b.Call(helper, to, value);
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> PreservePadding(Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "PreservePadding transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/preserve_padding.h b/src/tint/lang/core/ir/transform/preserve_padding.h
new file mode 100644
index 0000000..fb0082d
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/preserve_padding.h
@@ -0,0 +1,37 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_PRESERVE_PADDING_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_PRESERVE_PADDING_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// PreservePadding is a transform that decomposes stores of whole structure and array types to
+/// preserve padding bytes.
+///
+/// @note assumes that DirectVariableAccess will be run afterwards for backends that need it.
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> PreservePadding(Module& module);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_PRESERVE_PADDING_H_
diff --git a/src/tint/lang/core/ir/transform/preserve_padding_test.cc b/src/tint/lang/core/ir/transform/preserve_padding_test.cc
new file mode 100644
index 0000000..88fcebc
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/preserve_padding_test.cc
@@ -0,0 +1,1291 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/preserve_padding.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+#include "src/tint/lang/core/type/array.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/struct.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+class IR_PreservePaddingTest : public TransformTest {
+  protected:
+    const type::Struct* MakeStructWithoutPadding() {
+        auto* structure =
+            ty.Struct(mod.symbols.New("MyStruct"), {
+                                                       {mod.symbols.New("a"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("b"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("c"), ty.vec4<u32>()},
+                                                   });
+        return structure;
+    }
+
+    const type::Struct* MakeStructWithTrailingPadding() {
+        auto* structure =
+            ty.Struct(mod.symbols.New("MyStruct"), {
+                                                       {mod.symbols.New("a"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("b"), ty.u32()},
+                                                   });
+        return structure;
+    }
+
+    const type::Struct* MakeStructWithInternalPadding() {
+        auto* structure =
+            ty.Struct(mod.symbols.New("MyStruct"), {
+                                                       {mod.symbols.New("a"), ty.vec4<u32>()},
+                                                       {mod.symbols.New("b"), ty.u32()},
+                                                       {mod.symbols.New("c"), ty.vec4<u32>()},
+                                                   });
+        return structure;
+    }
+};
+
+TEST_F(IR_PreservePaddingTest, NoModify_Workgroup) {
+    auto* structure = MakeStructWithTrailingPadding();
+    auto* buffer = b.Var("buffer", ty.ptr(workgroup, structure));
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<workgroup, MyStruct, read_write> = var
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_Private) {
+    auto* structure = MakeStructWithTrailingPadding();
+    auto* buffer = b.Var("buffer", ty.ptr(private_, structure));
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<private, MyStruct, read_write> = var
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_Function) {
+    auto* structure = MakeStructWithTrailingPadding();
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        auto* buffer = b.Var("buffer", ty.ptr(function, structure));
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%foo = func(%value:MyStruct):void -> %b1 {
+  %b1 = block {
+    %buffer:ptr<function, MyStruct, read_write> = var
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_StructWithoutPadding) {
+    auto* structure = MakeStructWithoutPadding();
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:vec4<u32> @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_MatrixWithoutPadding) {
+    auto* mat = ty.mat4x4<f32>();
+    auto* buffer = b.Var("buffer", ty.ptr(storage, mat));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", mat);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, mat4x4<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat4x4<f32>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_ArrayWithoutPadding) {
+    auto* arr = ty.array<vec4<f32>>();
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<vec4<f32>>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<vec4<f32>>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_Vec3) {
+    auto* buffer = b.Var("buffer", ty.ptr(storage, ty.vec3<f32>()));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", ty.vec3<f32>());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, vec3<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:vec3<f32>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NoModify_LoadStructWithTrailingPadding) {
+    auto* structure = MakeStructWithTrailingPadding();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* func = b.Function("foo", structure);
+    b.Append(func->Block(), [&] {
+        auto* load = b.Load(buffer);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():MyStruct -> %b2 {
+  %b2 = block {
+    %3:MyStruct = load %buffer
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Struct_TrailingPadding) {
+    auto* structure = MakeStructWithTrailingPadding();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, MyStruct, read_write>, %value_param:MyStruct):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, vec4<u32>, read_write> = access %target, 0u
+    %9:vec4<u32> = access %value_param, 0u
+    store %8, %9
+    %10:ptr<storage, u32, read_write> = access %target, 1u
+    %11:u32 = access %value_param, 1u
+    store %10, %11
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Struct_InternalPadding) {
+    auto* structure = MakeStructWithInternalPadding();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, MyStruct, read_write>, %value_param:MyStruct):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, vec4<u32>, read_write> = access %target, 0u
+    %9:vec4<u32> = access %value_param, 0u
+    store %8, %9
+    %10:ptr<storage, u32, read_write> = access %target, 1u
+    %11:u32 = access %value_param, 1u
+    store %10, %11
+    %12:ptr<storage, vec4<u32>, read_write> = access %target, 2u
+    %13:vec4<u32> = access %value_param, 2u
+    store %12, %13
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, NestedStructWithPadding) {
+    auto* inner = MakeStructWithInternalPadding();
+    auto* outer = ty.Struct(mod.symbols.New("Outer"), {
+                                                          {mod.symbols.New("inner"), inner},
+                                                      });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, outer));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", outer);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+Outer = struct @align(16) {
+  inner:MyStruct @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, Outer, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:Outer):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+  c:vec4<u32> @offset(32)
+}
+
+Outer = struct @align(16) {
+  inner:MyStruct @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, Outer, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:Outer):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, Outer, read_write>, %value_param:Outer):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, MyStruct, read_write> = access %target, 0u
+    %9:MyStruct = access %value_param, 0u
+    %10:void = call %tint_store_and_preserve_padding_1, %8, %9
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, MyStruct, read_write>, %value_param_1:MyStruct):void -> %b4 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b4 = block {
+    %14:ptr<storage, vec4<u32>, read_write> = access %target_1, 0u
+    %15:vec4<u32> = access %value_param_1, 0u
+    store %14, %15
+    %16:ptr<storage, u32, read_write> = access %target_1, 1u
+    %17:u32 = access %value_param_1, 1u
+    store %16, %17
+    %18:ptr<storage, vec4<u32>, read_write> = access %target_1, 2u
+    %19:vec4<u32> = access %value_param_1, 2u
+    store %18, %19
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, StructWithPadding_InArray) {
+    auto* structure = MakeStructWithTrailingPadding();
+    auto* arr = ty.array(structure, 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<MyStruct, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<MyStruct, 4>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:vec4<u32> @offset(0)
+  b:u32 @offset(16)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<MyStruct, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<MyStruct, 4>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<MyStruct, 4>, read_write>, %value_param:array<MyStruct, 4>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 4u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, MyStruct, read_write> = access %target, %idx:u32
+        %11:MyStruct = access %value_param, %idx:u32
+        %12:void = call %tint_store_and_preserve_padding_1, %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %14:u32 = add %idx:u32, 1u
+        next_iteration %b5 %14
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, MyStruct, read_write>, %value_param_1:MyStruct):void -> %b8 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b8 = block {
+    %17:ptr<storage, vec4<u32>, read_write> = access %target_1, 0u
+    %18:vec4<u32> = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, u32, read_write> = access %target_1, 1u
+    %20:u32 = access %value_param_1, 1u
+    store %19, %20
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Mat3x3) {
+    auto* mat = ty.mat3x3<f32>();
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, mat));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", mat);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, mat3x3<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, mat3x3<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, mat3x3<f32>, read_write>, %value_param:mat3x3<f32>):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, vec3<f32>, read_write> = access %target, 0u
+    %9:vec3<f32> = access %value_param, 0u
+    store %8, %9
+    %10:ptr<storage, vec3<f32>, read_write> = access %target, 1u
+    %11:vec3<f32> = access %value_param, 1u
+    store %10, %11
+    %12:ptr<storage, vec3<f32>, read_write> = access %target, 2u
+    %13:vec3<f32> = access %value_param, 2u
+    store %12, %13
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Mat3x3_InStruct) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), mat},
+                                                                 {mod.symbols.New("b"), mat},
+                                                             });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", structure);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  a:mat3x3<f32> @offset(0)
+  b:mat3x3<f32> @offset(48)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  a:mat3x3<f32> @offset(0)
+  b:mat3x3<f32> @offset(48)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:MyStruct):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, MyStruct, read_write>, %value_param:MyStruct):void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, mat3x3<f32>, read_write> = access %target, 0u
+    %9:mat3x3<f32> = access %value_param, 0u
+    %10:void = call %tint_store_and_preserve_padding_1, %8, %9
+    %12:ptr<storage, mat3x3<f32>, read_write> = access %target, 1u
+    %13:mat3x3<f32> = access %value_param, 1u
+    %14:void = call %tint_store_and_preserve_padding_1, %12, %13
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, mat3x3<f32>, read_write>, %value_param_1:mat3x3<f32>):void -> %b4 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b4 = block {
+    %17:ptr<storage, vec3<f32>, read_write> = access %target_1, 0u
+    %18:vec3<f32> = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, vec3<f32>, read_write> = access %target_1, 1u
+    %20:vec3<f32> = access %value_param_1, 1u
+    store %19, %20
+    %21:ptr<storage, vec3<f32>, read_write> = access %target_1, 2u
+    %22:vec3<f32> = access %value_param_1, 2u
+    store %21, %22
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Mat3x3_Array) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<mat3x3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<mat3x3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<mat3x3<f32>, 4>, read_write>, %value_param:array<mat3x3<f32>, 4>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 4u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, mat3x3<f32>, read_write> = access %target, %idx:u32
+        %11:mat3x3<f32> = access %value_param, %idx:u32
+        %12:void = call %tint_store_and_preserve_padding_1, %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %14:u32 = add %idx:u32, 1u
+        next_iteration %b5 %14
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, mat3x3<f32>, read_write>, %value_param_1:mat3x3<f32>):void -> %b8 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b8 = block {
+    %17:ptr<storage, vec3<f32>, read_write> = access %target_1, 0u
+    %18:vec3<f32> = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, vec3<f32>, read_write> = access %target_1, 1u
+    %20:vec3<f32> = access %value_param_1, 1u
+    store %19, %20
+    %21:ptr<storage, vec3<f32>, read_write> = access %target_1, 2u
+    %22:vec3<f32> = access %value_param_1, 2u
+    store %21, %22
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, Vec3_Array) {
+    auto* arr = ty.array(ty.vec3<f32>(), 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", arr);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<vec3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<vec3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<vec3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<vec3<f32>, 4>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<vec3<f32>, 4>, read_write>, %value_param:array<vec3<f32>, 4>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 4u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, vec3<f32>, read_write> = access %target, %idx:u32
+        %11:vec3<f32> = access %value_param, %idx:u32
+        store %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %12:u32 = add %idx:u32, 1u
+        next_iteration %b5 %12
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, ComplexNesting) {
+    auto* inner =
+        ty.Struct(mod.symbols.New("Inner"), {
+                                                {mod.symbols.New("a"), ty.u32()},
+                                                {mod.symbols.New("b"), ty.array<vec3<f32>, 4>()},
+                                                {mod.symbols.New("c"), ty.mat3x3<f32>()},
+                                                {mod.symbols.New("d"), ty.u32()},
+                                            });
+
+    auto* outer =
+        ty.Struct(mod.symbols.New("Outer"), {
+                                                {mod.symbols.New("a"), ty.u32()},
+                                                {mod.symbols.New("inner"), inner},
+                                                {mod.symbols.New("inner_arr"), ty.array(inner, 4)},
+                                                {mod.symbols.New("b"), ty.u32()},
+                                            });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, ty.array(outer, 3)));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", ty.array(outer, 3));
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(buffer, value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+Inner = struct @align(16) {
+  a:u32 @offset(0)
+  b:array<vec3<f32>, 4> @offset(16)
+  c:mat3x3<f32> @offset(80)
+  d:u32 @offset(128)
+}
+
+Outer = struct @align(16) {
+  a_1:u32 @offset(0)
+  inner:Inner @offset(16)
+  inner_arr:array<Inner, 4> @offset(160)
+  b_1:u32 @offset(736)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<Outer, 3>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<Outer, 3>):void -> %b2 {
+  %b2 = block {
+    store %buffer, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Inner = struct @align(16) {
+  a:u32 @offset(0)
+  b:array<vec3<f32>, 4> @offset(16)
+  c:mat3x3<f32> @offset(80)
+  d:u32 @offset(128)
+}
+
+Outer = struct @align(16) {
+  a_1:u32 @offset(0)
+  inner:Inner @offset(16)
+  inner_arr:array<Inner, 4> @offset(160)
+  b_1:u32 @offset(736)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, array<Outer, 3>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:array<Outer, 3>):void -> %b2 {
+  %b2 = block {
+    %4:void = call %tint_store_and_preserve_padding, %buffer, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, array<Outer, 3>, read_write>, %value_param:array<Outer, 3>):void -> %b3 {
+  %b3 = block {
+    loop [i: %b4, b: %b5, c: %b6] {  # loop_1
+      %b4 = block {  # initializer
+        next_iteration %b5 0u
+      }
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 3u
+        if %9 [t: %b7] {  # if_1
+          %b7 = block {  # true
+            exit_loop  # loop_1
+          }
+        }
+        %10:ptr<storage, Outer, read_write> = access %target, %idx:u32
+        %11:Outer = access %value_param, %idx:u32
+        %12:void = call %tint_store_and_preserve_padding_1, %10, %11
+        continue %b6
+      }
+      %b6 = block {  # continuing
+        %14:u32 = add %idx:u32, 1u
+        next_iteration %b5 %14
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_1 = func(%target_1:ptr<storage, Outer, read_write>, %value_param_1:Outer):void -> %b8 {  # %tint_store_and_preserve_padding_1: 'tint_store_and_preserve_padding', %target_1: 'target', %value_param_1: 'value_param'
+  %b8 = block {
+    %17:ptr<storage, u32, read_write> = access %target_1, 0u
+    %18:u32 = access %value_param_1, 0u
+    store %17, %18
+    %19:ptr<storage, Inner, read_write> = access %target_1, 1u
+    %20:Inner = access %value_param_1, 1u
+    %21:void = call %tint_store_and_preserve_padding_2, %19, %20
+    %23:ptr<storage, array<Inner, 4>, read_write> = access %target_1, 2u
+    %24:array<Inner, 4> = access %value_param_1, 2u
+    %25:void = call %tint_store_and_preserve_padding_3, %23, %24
+    %27:ptr<storage, u32, read_write> = access %target_1, 3u
+    %28:u32 = access %value_param_1, 3u
+    store %27, %28
+    ret
+  }
+}
+%tint_store_and_preserve_padding_2 = func(%target_2:ptr<storage, Inner, read_write>, %value_param_2:Inner):void -> %b9 {  # %tint_store_and_preserve_padding_2: 'tint_store_and_preserve_padding', %target_2: 'target', %value_param_2: 'value_param'
+  %b9 = block {
+    %31:ptr<storage, u32, read_write> = access %target_2, 0u
+    %32:u32 = access %value_param_2, 0u
+    store %31, %32
+    %33:ptr<storage, array<vec3<f32>, 4>, read_write> = access %target_2, 1u
+    %34:array<vec3<f32>, 4> = access %value_param_2, 1u
+    %35:void = call %tint_store_and_preserve_padding_4, %33, %34
+    %37:ptr<storage, mat3x3<f32>, read_write> = access %target_2, 2u
+    %38:mat3x3<f32> = access %value_param_2, 2u
+    %39:void = call %tint_store_and_preserve_padding_5, %37, %38
+    %41:ptr<storage, u32, read_write> = access %target_2, 3u
+    %42:u32 = access %value_param_2, 3u
+    store %41, %42
+    ret
+  }
+}
+%tint_store_and_preserve_padding_4 = func(%target_3:ptr<storage, array<vec3<f32>, 4>, read_write>, %value_param_3:array<vec3<f32>, 4>):void -> %b10 {  # %tint_store_and_preserve_padding_4: 'tint_store_and_preserve_padding', %target_3: 'target', %value_param_3: 'value_param'
+  %b10 = block {
+    loop [i: %b11, b: %b12, c: %b13] {  # loop_2
+      %b11 = block {  # initializer
+        next_iteration %b12 0u
+      }
+      %b12 = block (%idx_1:u32) {  # body
+        %46:bool = gte %idx_1:u32, 4u
+        if %46 [t: %b14] {  # if_2
+          %b14 = block {  # true
+            exit_loop  # loop_2
+          }
+        }
+        %47:ptr<storage, vec3<f32>, read_write> = access %target_3, %idx_1:u32
+        %48:vec3<f32> = access %value_param_3, %idx_1:u32
+        store %47, %48
+        continue %b13
+      }
+      %b13 = block {  # continuing
+        %49:u32 = add %idx_1:u32, 1u
+        next_iteration %b12 %49
+      }
+    }
+    ret
+  }
+}
+%tint_store_and_preserve_padding_5 = func(%target_4:ptr<storage, mat3x3<f32>, read_write>, %value_param_4:mat3x3<f32>):void -> %b15 {  # %tint_store_and_preserve_padding_5: 'tint_store_and_preserve_padding', %target_4: 'target', %value_param_4: 'value_param'
+  %b15 = block {
+    %52:ptr<storage, vec3<f32>, read_write> = access %target_4, 0u
+    %53:vec3<f32> = access %value_param_4, 0u
+    store %52, %53
+    %54:ptr<storage, vec3<f32>, read_write> = access %target_4, 1u
+    %55:vec3<f32> = access %value_param_4, 1u
+    store %54, %55
+    %56:ptr<storage, vec3<f32>, read_write> = access %target_4, 2u
+    %57:vec3<f32> = access %value_param_4, 2u
+    store %56, %57
+    ret
+  }
+}
+%tint_store_and_preserve_padding_3 = func(%target_5:ptr<storage, array<Inner, 4>, read_write>, %value_param_5:array<Inner, 4>):void -> %b16 {  # %tint_store_and_preserve_padding_3: 'tint_store_and_preserve_padding', %target_5: 'target', %value_param_5: 'value_param'
+  %b16 = block {
+    loop [i: %b17, b: %b18, c: %b19] {  # loop_3
+      %b17 = block {  # initializer
+        next_iteration %b18 0u
+      }
+      %b18 = block (%idx_2:u32) {  # body
+        %61:bool = gte %idx_2:u32, 4u
+        if %61 [t: %b20] {  # if_3
+          %b20 = block {  # true
+            exit_loop  # loop_3
+          }
+        }
+        %62:ptr<storage, Inner, read_write> = access %target_5, %idx_2:u32
+        %63:Inner = access %value_param_5, %idx_2:u32
+        %64:void = call %tint_store_and_preserve_padding_2, %62, %63
+        continue %b19
+      }
+      %b19 = block {  # continuing
+        %65:u32 = add %idx_2:u32, 1u
+        next_iteration %b18 %65
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_PreservePaddingTest, MultipleStoresSameType) {
+    auto* mat = ty.mat3x3<f32>();
+    auto* arr = ty.array(mat, 4);
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, arr));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* value = b.FunctionParam("value", mat);
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({value});
+    b.Append(func->Block(), [&] {
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 0_u), value);
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 1_u), value);
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 2_u), value);
+        b.Store(b.Access(ty.ptr(storage, mat), buffer, 3_u), value);
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 0u
+    store %4, %value
+    %5:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 1u
+    store %5, %value
+    %6:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 2u
+    store %6, %value
+    %7:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 3u
+    store %7, %value
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %buffer:ptr<storage, array<mat3x3<f32>, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%value:mat3x3<f32>):void -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 0u
+    %5:void = call %tint_store_and_preserve_padding, %4, %value
+    %7:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 1u
+    %8:void = call %tint_store_and_preserve_padding, %7, %value
+    %9:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 2u
+    %10:void = call %tint_store_and_preserve_padding, %9, %value
+    %11:ptr<storage, mat3x3<f32>, read_write> = access %buffer, 3u
+    %12:void = call %tint_store_and_preserve_padding, %11, %value
+    ret
+  }
+}
+%tint_store_and_preserve_padding = func(%target:ptr<storage, mat3x3<f32>, read_write>, %value_param:mat3x3<f32>):void -> %b3 {
+  %b3 = block {
+    %15:ptr<storage, vec3<f32>, read_write> = access %target, 0u
+    %16:vec3<f32> = access %value_param, 0u
+    store %15, %16
+    %17:ptr<storage, vec3<f32>, read_write> = access %target, 1u
+    %18:vec3<f32> = access %value_param, 1u
+    store %17, %18
+    %19:ptr<storage, vec3<f32>, read_write> = access %target, 2u
+    %20:vec3<f32> = access %value_param, 2u
+    store %19, %20
+    ret
+  }
+}
+)";
+
+    Run(PreservePadding);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/shader_io.cc b/src/tint/lang/core/ir/transform/shader_io.cc
index df9987c..74fe767 100644
--- a/src/tint/lang/core/ir/transform/shader_io.cc
+++ b/src/tint/lang/core/ir/transform/shader_io.cc
@@ -263,7 +263,10 @@
 
 void RunShaderIOBase(Module& module, std::function<MakeBackendStateFunc> make_backend_state) {
     State state{module};
-    for (auto* func : module.functions) {
+
+    // Take a copy of the function list since the transform will add new functions to the module.
+    auto functions = module.functions;
+    for (auto* func : functions) {
         // Only process entry points.
         if (func->Stage() == Function::PipelineStage::kUndefined) {
             continue;
diff --git a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc
index 4e9c88b..7980ca7 100644
--- a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc
+++ b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc
@@ -156,7 +156,11 @@
                     });
                 } else {
                     // Use a loop for arrayed stores.
-                    GenerateZeroingLoop(local_index, count, wgsize, *element_stores);
+                    b.LoopRange(ty, local_index, u32(count), u32(wgsize), [&](Value* index) {
+                        for (auto& store : *element_stores) {
+                            GenerateStore(store, count, index);
+                        }
+                    });
                 }
             }
             b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
@@ -328,46 +332,6 @@
         }
     }
 
-    /// Generate a loop for a list of stores with the same iteration count.
-    /// @param local_index the local invocation index
-    /// @param total_count the number of iterations needed to store to all elements
-    /// @param wgsize the linearized workgroup size
-    /// @param stores the list of store descriptors
-    void GenerateZeroingLoop(Value* local_index,
-                             uint32_t total_count,
-                             uint32_t wgsize,
-                             const StoreList& stores) {
-        // The loop is equivalent to:
-        //   for (var idx = local_index; idx < linear_iteration_count; idx += wgsize) {
-        //     <store to elements at `idx`>
-        //   }
-        auto* loop = b.Loop();
-        auto* index = b.BlockParam(ty.u32());
-        loop->Body()->SetParams({index});
-        b.Append(loop->Initializer(), [&] {  //
-            b.NextIteration(loop, local_index);
-        });
-        b.Append(loop->Body(), [&] {
-            // Exit the loop when the iteration count has been exceeded.
-            auto* gt_max = b.GreaterThan(ty.bool_(), index, u32(total_count - 1u));
-            auto* ifelse = b.If(gt_max);
-            b.Append(ifelse->True(), [&] {  //
-                b.ExitLoop(loop);
-            });
-
-            // Insert all of the store instructions.
-            for (auto& store : stores) {
-                GenerateStore(store, total_count, index);
-            }
-
-            b.Continue(loop);
-        });
-        b.Append(loop->Continuing(), [&] {  //
-            // Increment the loop index by linearized workgroup size.
-            b.NextIteration(loop, b.Add(ty.u32(), index, u32(wgsize)));
-        });
-    }
-
     /// Check if a type can be efficiently zeroed with a single store. Returns `false` if there are
     /// any nested arrays or atomics.
     /// @param type the type to inspect
diff --git a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
index 026d5e3..955582c 100644
--- a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
+++ b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
@@ -466,19 +466,19 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 3u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 4u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:ptr<workgroup, i32, read_write> = access %wgvar, %4:u32
+        %6:ptr<workgroup, i32, read_write> = access %wgvar, %idx:u32
         store %6, 0i
         continue %b5
       }
       %b5 = block {  # continuing
-        %7:u32 = add %4:u32, 66u
+        %7:u32 = add %idx:u32, 66u
         next_iteration %b4 %7
       }
     }
@@ -528,21 +528,21 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 34u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 35u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:u32 = mod %4:u32, 5u
-        %7:u32 = div %4:u32, 5u
+        %6:u32 = mod %idx:u32, 5u
+        %7:u32 = div %idx:u32, 5u
         %8:ptr<workgroup, u32, read_write> = access %wgvar, %7, %6
         store %8, 0u
         continue %b5
       }
       %b5 = block {  # continuing
-        %9:u32 = add %4:u32, 66u
+        %9:u32 = add %idx:u32, 66u
         next_iteration %b4 %9
       }
     }
@@ -592,23 +592,23 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 104u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 105u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:u32 = mod %4:u32, 7u
-        %7:u32 = div %4:u32, 7u
+        %6:u32 = mod %idx:u32, 7u
+        %7:u32 = div %idx:u32, 7u
         %8:u32 = mod %7, 5u
-        %9:u32 = div %4:u32, 35u
+        %9:u32 = div %idx:u32, 35u
         %10:ptr<workgroup, i32, read_write> = access %wgvar, %9, %8, %6
         store %10, 0i
         continue %b5
       }
       %b5 = block {  # continuing
-        %11:u32 = add %4:u32, 1u
+        %11:u32 = add %idx:u32, 1u
         next_iteration %b4 %11
       }
     }
@@ -658,21 +658,21 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 14u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 15u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:u32 = mod %4:u32, 5u
-        %7:u32 = div %4:u32, 5u
+        %6:u32 = mod %idx:u32, 5u
+        %7:u32 = div %idx:u32, 5u
         %8:ptr<workgroup, i32, read_write> = access %wgvar, %7, %6, 0u
         store %8, 0i
         continue %b5
       }
       %b5 = block {  # continuing
-        %9:u32 = add %4:u32, 1u
+        %9:u32 = add %idx:u32, 1u
         next_iteration %b4 %9
       }
     }
@@ -722,21 +722,21 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 14u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 15u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:u32 = mod %4:u32, 3u
-        %7:u32 = div %4:u32, 3u
+        %6:u32 = mod %idx:u32, 3u
+        %7:u32 = div %idx:u32, 3u
         %8:ptr<workgroup, i32, read_write> = access %wgvar, %7, 0u, %6
         store %8, 0i
         continue %b5
       }
       %b5 = block {  # continuing
-        %9:u32 = add %4:u32, 1u
+        %9:u32 = add %idx:u32, 1u
         next_iteration %b4 %9
       }
     }
@@ -786,21 +786,21 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 14u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 15u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:u32 = mod %4:u32, 3u
-        %7:u32 = div %4:u32, 3u
+        %6:u32 = mod %idx:u32, 3u
+        %7:u32 = div %idx:u32, 3u
         %8:ptr<workgroup, i32, read_write> = access %wgvar, 0u, %7, %6
         store %8, 0i
         continue %b5
       }
       %b5 = block {  # continuing
-        %9:u32 = add %4:u32, 1u
+        %9:u32 = add %idx:u32, 1u
         next_iteration %b4 %9
       }
     }
@@ -1165,21 +1165,21 @@
       %b3 = block {  # initializer
         next_iteration %b4 %tint_local_index
       }
-      %b4 = block (%4:u32) {  # body
-        %5:bool = gt %4:u32, 6u
+      %b4 = block (%idx:u32) {  # body
+        %5:bool = gte %idx:u32, 7u
         if %5 [t: %b6] {  # if_1
           %b6 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %6:ptr<workgroup, f32, read_write> = access %wgvar, %4:u32, 0u
+        %6:ptr<workgroup, f32, read_write> = access %wgvar, %idx:u32, 0u
         store %6, 0.0f
-        %7:ptr<workgroup, bool, read_write> = access %wgvar, %4:u32, 2u
+        %7:ptr<workgroup, bool, read_write> = access %wgvar, %idx:u32, 2u
         store %7, false
         continue %b5
       }
       %b5 = block {  # continuing
-        %8:u32 = add %4:u32, 42u
+        %8:u32 = add %idx:u32, 42u
         next_iteration %b4 %8
       }
     }
@@ -1187,25 +1187,25 @@
       %b7 = block {  # initializer
         next_iteration %b8 %tint_local_index
       }
-      %b8 = block (%9:u32) {  # body
-        %10:bool = gt %9:u32, 90u
+      %b8 = block (%idx_1:u32) {  # body
+        %10:bool = gte %idx_1:u32, 91u
         if %10 [t: %b10] {  # if_2
           %b10 = block {  # true
             exit_loop  # loop_2
           }
         }
-        %11:u32 = mod %9:u32, 13u
-        %12:u32 = div %9:u32, 13u
+        %11:u32 = mod %idx_1:u32, 13u
+        %12:u32 = div %idx_1:u32, 13u
         %13:ptr<workgroup, i32, read_write> = access %wgvar, %12, 1u, %11, 0u
         store %13, 0i
-        %14:u32 = mod %9:u32, 13u
-        %15:u32 = div %9:u32, 13u
+        %14:u32 = mod %idx_1:u32, 13u
+        %15:u32 = div %idx_1:u32, 13u
         %16:ptr<workgroup, atomic<u32>, read_write> = access %wgvar, %15, 1u, %14, 1u
         %17:void = atomicStore %16, 0u
         continue %b9
       }
       %b9 = block {  # continuing
-        %18:u32 = add %9:u32, 42u
+        %18:u32 = add %idx_1:u32, 42u
         next_iteration %b8 %18
       }
     }
@@ -1272,19 +1272,19 @@
       %b4 = block {  # initializer
         next_iteration %b5 %tint_local_index
       }
-      %b5 = block (%7:u32) {  # body
-        %8:bool = gt %7:u32, 3u
+      %b5 = block (%idx:u32) {  # body
+        %8:bool = gte %idx:u32, 4u
         if %8 [t: %b7] {  # if_2
           %b7 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %9:ptr<workgroup, i32, read_write> = access %var_b, %7:u32
+        %9:ptr<workgroup, i32, read_write> = access %var_b, %idx:u32
         store %9, 0i
         continue %b6
       }
       %b6 = block {  # continuing
-        %10:u32 = add %7:u32, 66u
+        %10:u32 = add %idx:u32, 66u
         next_iteration %b5 %10
       }
     }
@@ -1292,21 +1292,21 @@
       %b8 = block {  # initializer
         next_iteration %b9 %tint_local_index
       }
-      %b9 = block (%11:u32) {  # body
-        %12:bool = gt %11:u32, 34u
+      %b9 = block (%idx_1:u32) {  # body
+        %12:bool = gte %idx_1:u32, 35u
         if %12 [t: %b11] {  # if_3
           %b11 = block {  # true
             exit_loop  # loop_2
           }
         }
-        %13:u32 = mod %11:u32, 5u
-        %14:u32 = div %11:u32, 5u
+        %13:u32 = mod %idx_1:u32, 5u
+        %14:u32 = div %idx_1:u32, 5u
         %15:ptr<workgroup, u32, read_write> = access %var_c, %14, %13
         store %15, 0u
         continue %b10
       }
       %b10 = block {  # continuing
-        %16:u32 = add %11:u32, 66u
+        %16:u32 = add %idx_1:u32, 66u
         next_iteration %b9 %16
       }
     }
@@ -1381,23 +1381,23 @@
       %b4 = block {  # initializer
         next_iteration %b5 %tint_local_index
       }
-      %b5 = block (%8:u32) {  # body
-        %9:bool = gt %8:u32, 41u
+      %b5 = block (%idx:u32) {  # body
+        %9:bool = gte %idx:u32, 42u
         if %9 [t: %b7] {  # if_2
           %b7 = block {  # true
             exit_loop  # loop_1
           }
         }
-        %10:ptr<workgroup, i32, read_write> = access %var_c, %8:u32
+        %10:ptr<workgroup, i32, read_write> = access %var_c, %idx:u32
         store %10, 0i
-        %11:u32 = mod %8:u32, 6u
-        %12:u32 = div %8:u32, 6u
+        %11:u32 = mod %idx:u32, 6u
+        %12:u32 = div %idx:u32, 6u
         %13:ptr<workgroup, u32, read_write> = access %var_d, %12, %11
         store %13, 0u
         continue %b6
       }
       %b6 = block {  # continuing
-        %14:u32 = add %8:u32, 66u
+        %14:u32 = add %idx:u32, 66u
         next_iteration %b5 %14
       }
     }
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index 8edf526..fbac2a8 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -271,6 +271,7 @@
     Disassembler dis_{mod_};
     Block* current_block_ = nullptr;
     Hashset<Function*, 4> all_functions_;
+    Hashset<Instruction*, 4> visited_instructions_;
     Vector<ControlInstruction*, 8> control_stack_;
 
     void DisassembleIfNeeded();
@@ -300,6 +301,15 @@
         CheckFunction(func);
     }
 
+    if (!diagnostics_.contains_errors()) {
+        // Check for orphaned instructions.
+        for (auto* inst : mod_.instructions.Objects()) {
+            if (inst->Alive() && !visited_instructions_.Contains(inst)) {
+                AddError("orphaned instruction: " + inst->FriendlyName());
+            }
+        }
+    }
+
     if (diagnostics_.contains_errors()) {
         DisassembleIfNeeded();
         diagnostics_.add_note(tint::diag::System::IR,
@@ -448,6 +458,7 @@
 }
 
 void Validator::CheckInstruction(Instruction* inst) {
+    visited_instructions_.Add(inst);
     if (!inst->Alive()) {
         AddError(inst, InstError(inst, "destroyed instruction found in instruction list"));
         return;
@@ -893,11 +904,10 @@
 Result<SuccessType> ValidateAndDumpIfNeeded([[maybe_unused]] Module& ir,
                                             [[maybe_unused]] const char* msg) {
 #if TINT_DUMP_IR_WHEN_VALIDATING
-    Disassembler disasm(ir);
     std::cout << "=========================================================" << std::endl;
     std::cout << "== IR dump before " << msg << ":" << std::endl;
     std::cout << "=========================================================" << std::endl;
-    std::cout << disasm.Disassemble();
+    std::cout << Disassemble(ir);
 #endif
 
 #ifndef NDEBUG
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 56ec9da..1521cf9 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -1183,6 +1183,29 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, Instruction_OrphanedInstruction) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.Append(f->Block());
+    auto* v = sb.Var(ty.ptr<function, f32>());
+    auto* load = sb.Load(v);
+    sb.Return(f);
+
+    load->Remove();
+
+    auto res = ir::Validate(mod);
+    ASSERT_FALSE(res);
+    EXPECT_EQ(res.Failure().reason.str(), R"(error: orphaned instruction: load
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    %2:ptr<function, f32, read_write> = var
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, Binary_LHS_Nullptr) {
     auto* f = b.Function("my_func", ty.void_());
 
diff --git a/src/tint/lang/core/ir/value.cc b/src/tint/lang/core/ir/value.cc
index ab886da..042bbad 100644
--- a/src/tint/lang/core/ir/value.cc
+++ b/src/tint/lang/core/ir/value.cc
@@ -28,7 +28,6 @@
 
 void Value::Destroy() {
     TINT_ASSERT(Alive());
-    TINT_ASSERT(Usages().Count() == 0);
     flags_.Add(Flag::kDead);
 }
 
diff --git a/src/tint/lang/core/ir/value_test.cc b/src/tint/lang/core/ir/value_test.cc
index 36fb415..3de50df 100644
--- a/src/tint/lang/core/ir/value_test.cc
+++ b/src/tint/lang/core/ir/value_test.cc
@@ -53,18 +53,6 @@
     EXPECT_FALSE(val->Alive());
 }
 
-TEST_F(IR_ValueTest, Destroy_HasUsage) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            auto* val = b.InstructionResult(mod.Types().i32());
-            b.Add(mod.Types().i32(), val, 1_i);
-            val->Destroy();
-        },
-        "");
-}
-
 TEST_F(IR_ValueTest, Destroy_HasSource) {
     EXPECT_FATAL_FAILURE(
         {
diff --git a/src/tint/lang/core/number.h b/src/tint/lang/core/number.h
index 3326ee8..b8e2b60 100644
--- a/src/tint/lang/core/number.h
+++ b/src/tint/lang/core/number.h
@@ -427,10 +427,6 @@
 #endif
 #endif
 
-/// Disables the false-positive maybe-uninitialized compiler warnings
-/// @see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80635
-TINT_BEGIN_DISABLE_WARNING(MAYBE_UNINITIALIZED);
-
 /// @param a the LHS number
 /// @param b the RHS number
 /// @returns a + b, or an empty optional if the resulting value overflowed the AInt
@@ -661,9 +657,6 @@
     return result;
 }
 
-/// Re-enables the maybe-uninitialized compiler warnings
-TINT_END_DISABLE_WARNING(MAYBE_UNINITIALIZED);
-
 }  // namespace tint::core
 
 namespace tint::core::number_suffixes {
diff --git a/src/tint/lang/core/type/BUILD.gn b/src/tint/lang/core/type/BUILD.gn
index c80abf5..79ad01f 100644
--- a/src/tint/lang/core/type/BUILD.gn
+++ b/src/tint/lang/core/type/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -119,7 +119,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "array_test.cc",
       "atomic_test.cc",
diff --git a/src/tint/lang/core/type/array.h b/src/tint/lang/core/type/array.h
index 0f97911..ea29674 100644
--- a/src/tint/lang/core/type/array.h
+++ b/src/tint/lang/core/type/array.h
@@ -28,7 +28,7 @@
 namespace tint::core::type {
 
 /// Array holds the type information for Array nodes.
-class Array final : public Castable<Array, Type> {
+class Array : public Castable<Array, Type> {
   public:
     /// An error message string stating that the array count was expected to be a constant
     /// expression. Used by multiple writers and transforms.
diff --git a/src/tint/lang/core/type/storage_texture.cc b/src/tint/lang/core/type/storage_texture.cc
index f1d7204..07ca8a0 100644
--- a/src/tint/lang/core/type/storage_texture.cc
+++ b/src/tint/lang/core/type/storage_texture.cc
@@ -28,7 +28,7 @@
 StorageTexture::StorageTexture(TextureDimension dim,
                                core::TexelFormat format,
                                core::Access access,
-                               Type* subtype)
+                               const Type* subtype)
     : Base(Hash(tint::TypeInfo::Of<StorageTexture>().full_hashcode, dim, format, access), dim),
       texel_format_(format),
       access_(access),
diff --git a/src/tint/lang/core/type/storage_texture.h b/src/tint/lang/core/type/storage_texture.h
index 491e7fa..7abcab1 100644
--- a/src/tint/lang/core/type/storage_texture.h
+++ b/src/tint/lang/core/type/storage_texture.h
@@ -40,7 +40,7 @@
     StorageTexture(TextureDimension dim,
                    core::TexelFormat format,
                    core::Access access,
-                   Type* subtype);
+                   const Type* subtype);
 
     /// Destructor
     ~StorageTexture() override;
@@ -50,7 +50,7 @@
     bool Equals(const UniqueNode& other) const override;
 
     /// @returns the storage subtype
-    Type* type() const { return subtype_; }
+    const Type* type() const { return subtype_; }
 
     /// @returns the texel format
     core::TexelFormat texel_format() const { return texel_format_; }
@@ -74,7 +74,7 @@
   private:
     core::TexelFormat const texel_format_;
     core::Access const access_;
-    Type* const subtype_;
+    const Type* const subtype_;
 };
 
 }  // namespace tint::core::type
diff --git a/src/tint/lang/glsl/BUILD.cmake b/src/tint/lang/glsl/BUILD.cmake
index d393f94..b067618d2 100644
--- a/src/tint/lang/glsl/BUILD.cmake
+++ b/src/tint/lang/glsl/BUILD.cmake
@@ -21,4 +21,5 @@
 #                       Do not modify this file directly
 ################################################################################
 
+include(lang/glsl/validate/BUILD.cmake)
 include(lang/glsl/writer/BUILD.cmake)
diff --git a/src/tint/lang/glsl/validate/BUILD.bazel b/src/tint/lang/glsl/validate/BUILD.bazel
new file mode 100644
index 0000000..30fa6ad
--- /dev/null
+++ b/src/tint/lang/glsl/validate/BUILD.bazel
@@ -0,0 +1,66 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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 = "validate",
+  srcs = [
+    "validate.cc",
+  ],
+  hdrs = [
+    "validate.h",
+  ],
+  deps = [
+    "//src/tint/lang/wgsl/ast",
+    "//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_glsl_writer": [
+      
+      
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_validator",
+  actual = "//src/tint:tint_build_glsl_validator_true",
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
diff --git a/src/tint/lang/glsl/validate/BUILD.cfg b/src/tint/lang/glsl/validate/BUILD.cfg
new file mode 100644
index 0000000..eb4a63d
--- /dev/null
+++ b/src/tint/lang/glsl/validate/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_glsl_validator"
+}
diff --git a/src/tint/lang/glsl/validate/BUILD.cmake b/src/tint/lang/glsl/validate/BUILD.cmake
new file mode 100644
index 0000000..7119d00
--- /dev/null
+++ b/src/tint/lang/glsl/validate/BUILD.cmake
@@ -0,0 +1,56 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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
+################################################################################
+
+if(TINT_BUILD_GLSL_VALIDATOR)
+################################################################################
+# Target:    tint_lang_glsl_validate
+# Kind:      lib
+# Condition: TINT_BUILD_GLSL_VALIDATOR
+################################################################################
+tint_add_target(tint_lang_glsl_validate lib
+  lang/glsl/validate/validate.cc
+  lang/glsl/validate/validate.h
+)
+
+tint_target_add_dependencies(tint_lang_glsl_validate lib
+  tint_lang_wgsl_ast
+  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_GLSL_WRITER)
+  tint_target_add_external_dependencies(tint_lang_glsl_validate lib
+    "glslang"
+    "glslang-res-limits"
+  )
+endif(TINT_BUILD_GLSL_WRITER)
+
+endif(TINT_BUILD_GLSL_VALIDATOR)
\ No newline at end of file
diff --git a/src/tint/lang/glsl/validate/BUILD.gn b/src/tint/lang/glsl/validate/BUILD.gn
new file mode 100644
index 0000000..944e029
--- /dev/null
+++ b/src/tint/lang/glsl/validate/BUILD.gn
@@ -0,0 +1,54 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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_glsl_validator) {
+  libtint_source_set("validate") {
+    sources = [
+      "validate.cc",
+      "validate.h",
+    ]
+    deps = [
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${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_glsl_writer) {
+      deps += [
+        "${tint_glslang_dir}:glslang_default_resource_limits_sources",
+        "${tint_glslang_dir}:glslang_lib_sources",
+      ]
+    }
+  }
+}
diff --git a/src/tint/lang/glsl/validate/validate.cc b/src/tint/lang/glsl/validate/validate.cc
new file mode 100644
index 0000000..8134c34
--- /dev/null
+++ b/src/tint/lang/glsl/validate/validate.cc
@@ -0,0 +1,68 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/glsl/validate/validate.h"
+
+#include <string>
+
+#include "glslang/Public/ResourceLimits.h"
+#include "glslang/Public/ShaderLang.h"
+#include "src/tint/utils/macros/static_init.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace tint::glsl::validate {
+
+namespace {
+
+EShLanguage PipelineStageToEshLanguage(tint::ast::PipelineStage stage) {
+    switch (stage) {
+        case tint::ast::PipelineStage::kFragment:
+            return EShLangFragment;
+        case tint::ast::PipelineStage::kVertex:
+            return EShLangVertex;
+        case tint::ast::PipelineStage::kCompute:
+            return EShLangCompute;
+        default:
+            TINT_UNREACHABLE();
+            return EShLangVertex;
+    }
+}
+
+}  // namespace
+
+Result<SuccessType> Validate(const std::string& source, const EntryPointList& entry_points) {
+    TINT_STATIC_INIT(glslang::InitializeProcess());
+
+    for (auto entry_pt : entry_points) {
+        EShLanguage lang = PipelineStageToEshLanguage(entry_pt.second);
+        glslang::TShader shader(lang);
+        const char* strings[1] = {source.c_str()};
+        int lengths[1] = {static_cast<int>(source.length())};
+        shader.setStringsWithLengths(strings, lengths, 1);
+        shader.setEntryPoint("main");
+        bool result =
+            shader.parse(GetDefaultResources(), 310, EEsProfile, false, false, EShMsgDefault);
+        if (!result) {
+            StringStream err;
+            err << "Error parsing GLSL shader:\n"
+                << shader.getInfoLog() << "\n"
+                << shader.getInfoDebugLog() << "\n";
+            return Failure{err.str()};
+        }
+    }
+
+    return Success;
+}
+
+}  // namespace tint::glsl::validate
diff --git a/src/tint/lang/glsl/validate/validate.h b/src/tint/lang/glsl/validate/validate.h
new file mode 100644
index 0000000..c30b23c
--- /dev/null
+++ b/src/tint/lang/glsl/validate/validate.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_GLSL_VALIDATE_VALIDATE_H_
+#define SRC_TINT_LANG_GLSL_VALIDATE_VALIDATE_H_
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/lang/wgsl/ast/pipeline_stage.h"
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations
+namespace tint {
+class Program;
+}  // namespace tint
+
+namespace tint::glsl::validate {
+
+using EntryPointList = std::vector<std::pair<std::string, ast::PipelineStage>>;
+
+/// Validate checks that the GLSL source passes validation.
+/// @param source the GLSL source
+/// @param entry_points the list of entry points to validate
+/// @return the result
+Result<SuccessType> Validate(const std::string& source, const EntryPointList& entry_points);
+
+}  // namespace tint::glsl::validate
+
+#endif  // SRC_TINT_LANG_GLSL_VALIDATE_VALIDATE_H_
diff --git a/src/tint/lang/glsl/writer/BUILD.bazel b/src/tint/lang/glsl/writer/BUILD.bazel
index a0e6bb2..19020bd 100644
--- a/src/tint/lang/glsl/writer/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/BUILD.bazel
@@ -70,13 +70,14 @@
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "writer_bench.cc",
   ],
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
-    "//src/tint/cmd/bench",
+    "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
@@ -97,6 +98,7 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
+    "@benchmark",
   ] + select({
     ":tint_build_glsl_writer": [
       "//src/tint/lang/glsl/writer",
diff --git a/src/tint/lang/glsl/writer/BUILD.cmake b/src/tint/lang/glsl/writer/BUILD.cmake
index 232ff0d..0afac03 100644
--- a/src/tint/lang/glsl/writer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/BUILD.cmake
@@ -86,7 +86,7 @@
 tint_target_add_dependencies(tint_lang_glsl_writer_bench bench
   tint_api_common
   tint_api_options
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -109,6 +109,10 @@
   tint_utils_traits
 )
 
+tint_target_add_external_dependencies(tint_lang_glsl_writer_bench bench
+  "google-benchmark"
+)
+
 if(TINT_BUILD_GLSL_WRITER)
   tint_target_add_dependencies(tint_lang_glsl_writer_bench bench
     tint_lang_glsl_writer
diff --git a/src/tint/lang/glsl/writer/BUILD.gn b/src/tint/lang/glsl/writer/BUILD.gn
index 96574b1..85dbaeb 100644
--- a/src/tint/lang/glsl/writer/BUILD.gn
+++ b/src/tint/lang/glsl/writer/BUILD.gn
@@ -24,6 +24,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_glsl_writer) {
   libtint_source_set("writer") {
     sources = [
@@ -67,3 +71,43 @@
     }
   }
 }
+if (tint_build_benchmarks) {
+  if (tint_build_glsl_writer) {
+    tint_unittests_source_set("bench") {
+      sources = [ "writer_bench.cc" ]
+      deps = [
+        "${tint_src_dir}:google_benchmark",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/api/options",
+        "${tint_src_dir}/cmd/bench:bench",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${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_glsl_writer) {
+        deps += [
+          "${tint_src_dir}/lang/glsl/writer",
+          "${tint_src_dir}/lang/glsl/writer/common",
+        ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
index 23ab99f..6afad1d 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
@@ -64,11 +64,6 @@
       "//src/tint/lang/glsl/writer/common",
     ],
     "//conditions:default": [],
-  }) + select({
-    ":tint_build_hlsl_writer": [
-      "//src/tint/lang/hlsl/writer/ast_raise",
-    ],
-    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -155,8 +150,3 @@
   actual = "//src/tint:tint_build_glsl_writer_true",
 )
 
-alias(
-  name = "tint_build_hlsl_writer",
-  actual = "//src/tint:tint_build_hlsl_writer_true",
-)
-
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
index 80b5a82..08de69c 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
@@ -68,12 +68,6 @@
   )
 endif(TINT_BUILD_GLSL_WRITER)
 
-if(TINT_BUILD_HLSL_WRITER)
-  tint_target_add_dependencies(tint_lang_glsl_writer_ast_printer lib
-    tint_lang_hlsl_writer_ast_raise
-  )
-endif(TINT_BUILD_HLSL_WRITER)
-
 endif(TINT_BUILD_GLSL_WRITER)
 if(TINT_BUILD_GLSL_WRITER)
 ################################################################################
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.gn b/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
index fc1c41e..cb02db6 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_glsl_writer) {
@@ -69,16 +69,11 @@
         "${tint_src_dir}/lang/glsl/writer/common",
       ]
     }
-
-    if (tint_build_hlsl_writer) {
-      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_raise" ]
-    }
   }
 }
 if (tint_build_unittests) {
   if (tint_build_glsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "array_accessor_test.cc",
         "assign_test.cc",
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index 6bf0f92..8263553 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -38,7 +38,6 @@
 #include "src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h"
 #include "src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h"
 #include "src/tint/lang/glsl/writer/common/options.h"
-#include "src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/id_attribute.h"
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
@@ -279,7 +278,7 @@
 ASTPrinter::~ASTPrinter() = default;
 
 bool ASTPrinter::Generate() {
-    if (!tint::writer::CheckSupportedExtensions(
+    if (!tint::wgsl::CheckSupportedExtensions(
             "GLSL", builder_.AST(), diagnostics_,
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
@@ -1535,7 +1534,7 @@
     if (auto* array_index = arg(Usage::kArrayIndex)) {
         // Array index needs to be appended to the coordinates.
         param_coords =
-            tint::writer::AppendVector(&builder_, param_coords, array_index)->Declaration();
+            tint::wgsl::AppendVector(&builder_, param_coords, array_index)->Declaration();
     }
 
     // GLSL requires Dref to be appended to the coordinates, *unless* it's
@@ -1552,8 +1551,7 @@
             // append zero here.
             depth_ref = CreateF32Zero(builder_.Sem().Get(param_coords)->Stmt());
         }
-        param_coords =
-            tint::writer::AppendVector(&builder_, param_coords, depth_ref)->Declaration();
+        param_coords = tint::wgsl::AppendVector(&builder_, param_coords, depth_ref)->Declaration();
     }
 
     emit_expr_as_signed(param_coords);
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
index e0b9f70..8926b55 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
@@ -85,11 +85,8 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
-    "//src/tint/lang/wgsl/ast/transform:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -109,6 +106,21 @@
       "//src/tint/lang/glsl/writer/ast_raise",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/ast/transform:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -119,3 +131,29 @@
   actual = "//src/tint:tint_build_glsl_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_glsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_glsl_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg b/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg
index 7459430..375c295 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg
@@ -1,3 +1,6 @@
 {
-    "condition": "tint_build_glsl_writer"
+    "condition": "tint_build_glsl_writer",
+    "test": {
+        "condition": "tint_build_wgsl_reader && tint_build_wgsl_writer",
+    }
 }
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
index 0666ae3..44ed836 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
@@ -66,11 +66,11 @@
 )
 
 endif(TINT_BUILD_GLSL_WRITER)
-if(TINT_BUILD_GLSL_WRITER)
+if(TINT_BUILD_GLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_glsl_writer_ast_raise_test
 # Kind:      test
-# Condition: TINT_BUILD_GLSL_WRITER
+# Condition: TINT_BUILD_GLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_glsl_writer_ast_raise_test test
   lang/glsl/writer/ast_raise/combine_samplers_test.cc
@@ -89,11 +89,8 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
-  tint_lang_wgsl_ast_transform_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -119,4 +116,22 @@
   )
 endif(TINT_BUILD_GLSL_WRITER)
 
-endif(TINT_BUILD_GLSL_WRITER)
\ No newline at end of file
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise_test test
+    tint_lang_wgsl_ast_transform_test
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_GLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.gn b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
index 7b7a0e0..6ee810f 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_glsl_writer) {
@@ -69,9 +69,9 @@
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_glsl_writer) {
+  if (tint_build_glsl_writer && tint_build_wgsl_reader &&
+      tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "combine_samplers_test.cc",
         "pad_structs_test.cc",
@@ -89,11 +89,8 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
-        "${tint_src_dir}/lang/wgsl/ast/transform:unittests",
         "${tint_src_dir}/lang/wgsl/program",
-        "${tint_src_dir}/lang/wgsl/reader",
         "${tint_src_dir}/lang/wgsl/sem",
-        "${tint_src_dir}/lang/wgsl/writer",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -112,6 +109,18 @@
       if (tint_build_glsl_writer) {
         deps += [ "${tint_src_dir}/lang/glsl/writer/ast_raise" ]
       }
+
+      if (tint_build_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+
+      if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/ast/transform:unittests" ]
+      }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
     }
   }
 }
diff --git a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
index c00f0fe3..3f5ad64 100644
--- a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
@@ -33,8 +33,8 @@
 namespace {
 
 bool IsGlobal(const tint::sem::VariablePair& pair) {
-    return pair.first->Is<tint::sem::GlobalVariable>() &&
-           (!pair.second || pair.second->Is<tint::sem::GlobalVariable>());
+    return (!pair.first || tint::Is<tint::sem::GlobalVariable>(pair.first)) &&
+           (!pair.second || tint::Is<tint::sem::GlobalVariable>(pair.second));
 }
 
 }  // namespace
@@ -104,7 +104,9 @@
                                               const sem::Variable* sampler_var,
                                               std::string name) {
         SamplerTexturePair bp_pair;
-        bp_pair.texture_binding_point = *texture_var->As<sem::GlobalVariable>()->BindingPoint();
+        bp_pair.texture_binding_point =
+            texture_var ? *texture_var->As<sem::GlobalVariable>()->BindingPoint()
+                        : binding_info->placeholder_binding_point;
         bp_pair.sampler_binding_point =
             sampler_var ? *sampler_var->As<sem::GlobalVariable>()->BindingPoint()
                         : binding_info->placeholder_binding_point;
@@ -132,16 +134,24 @@
     /// Creates Identifier for a given texture and sampler variable pair.
     /// Depth textures with no samplers are turned into the corresponding
     /// f32 texture (e.g., texture_depth_2d -> texture_2d<f32>).
+    /// Either texture or sampler could be nullptr, but cannot be nullptr at the same time.
+    /// The texture can only be nullptr, when the sampler is a dangling function parameter.
     /// @param texture the texture variable of interest
     /// @param sampler the texture variable of interest
     /// @returns the newly-created type
     ast::Type CreateCombinedASTTypeFor(const sem::Variable* texture, const sem::Variable* sampler) {
-        const core::type::Type* texture_type = texture->Type()->UnwrapRef();
-        const core::type::DepthTexture* depth = texture_type->As<core::type::DepthTexture>();
-        if (depth && !sampler) {
-            return ctx.dst->ty.sampled_texture(depth->dim(), ctx.dst->ty.f32());
+        if (texture) {
+            const core::type::Type* texture_type = texture->Type()->UnwrapRef();
+            const core::type::DepthTexture* depth = texture_type->As<core::type::DepthTexture>();
+            if (depth && !sampler) {
+                return ctx.dst->ty.sampled_texture(depth->dim(), ctx.dst->ty.f32());
+            } else {
+                return CreateASTTypeFor(ctx, texture_type);
+            }
         } else {
-            return CreateASTTypeFor(ctx, texture_type);
+            TINT_ASSERT(sampler != nullptr);
+            const core::type::Type* sampler_type = sampler->Type()->UnwrapRef();
+            return CreateASTTypeFor(ctx, sampler_type);
         }
     }
 
@@ -155,9 +165,15 @@
                     tint::Vector<const ast::Parameter*, 8>* params) {
         const sem::Variable* texture_var = pair.first;
         const sem::Variable* sampler_var = pair.second;
-        std::string name = texture_var->Declaration()->name->symbol.Name();
+        std::string name = "";
+        if (texture_var) {
+            name = texture_var->Declaration()->name->symbol.Name();
+        }
         if (sampler_var) {
-            name += "_" + sampler_var->Declaration()->name->symbol.Name();
+            if (!name.empty()) {
+                name += "_";
+            }
+            name += sampler_var->Declaration()->name->symbol.Name();
         }
         if (IsGlobal(pair)) {
             // Both texture and sampler are global; add a new global variable
diff --git a/src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc
index d408a8b..a324e24 100644
--- a/src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc
@@ -982,5 +982,180 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(CombineSamplersTest, UnusedTextureFunctionParameter) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+fn f(tex: texture_2d<f32>) -> u32 {
+  return 1u;
+}
+
+fn main() {
+  _ = f(t);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var t_1 : texture_2d<f32>;
+
+fn f() -> u32 {
+  return 1u;
+}
+
+fn main() {
+  _ = f();
+}
+)";
+
+    ast::transform::DataMap data;
+    data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
+    auto got = Run<CombineSamplers>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(CombineSamplersTest, UnusedSamplerFunctionParameter) {
+    auto* src = R"(
+@group(0) @binding(0) var s : sampler;
+
+fn f(sampler1: sampler) -> u32 {
+  return 1u;
+}
+
+fn main() {
+  _ = f(s);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var s_1 : sampler;
+
+fn f() -> u32 {
+  return 1u;
+}
+
+fn main() {
+  _ = f();
+}
+)";
+
+    ast::transform::DataMap data;
+    data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
+    auto got = Run<CombineSamplers>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(CombineSamplersTest, UnusedTextureAndSamplerFunctionParameter) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@group(0) @binding(1) var s : sampler;
+
+fn f(tex: texture_2d<f32>, sampler1: sampler) -> u32 {
+  return 1u;
+}
+
+fn main() {
+  _ = f(t, s);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var s_1 : sampler;
+
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var t_1 : texture_2d<f32>;
+
+fn f() -> u32 {
+  return 1u;
+}
+
+fn main() {
+  _ = f();
+}
+)";
+
+    ast::transform::DataMap data;
+    data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
+    auto got = Run<CombineSamplers>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(CombineSamplersTest, UnusedTextureFunctionParameter_Multiple) {
+    auto* src = R"(
+@group(0) @binding(0) var t1 : texture_2d<f32>;
+
+@group(0) @binding(1) var t2 : texture_2d_array<f32>;
+
+@group(0) @binding(2) var s : sampler;
+
+fn f(tex1: texture_2d<f32>, tex2: texture_2d<f32>, tex3: texture_2d_array<f32>, sampler1: sampler) -> u32 {
+  return 1u + textureNumLayers(tex3);
+}
+
+fn main() {
+  _ = f(t1, t1, t2, s);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var s_1 : sampler;
+
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var t1_1 : texture_2d<f32>;
+
+fn f(tex3_1 : texture_2d_array<f32>) -> u32 {
+  return (1u + textureNumLayers(tex3_1));
+}
+
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var t2_1 : texture_2d_array<f32>;
+
+fn main() {
+  _ = f(t2_1);
+}
+)";
+
+    ast::transform::DataMap data;
+    data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
+    auto got = Run<CombineSamplers>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(CombineSamplersTest, UnusedTextureFunctionParameter_Nested) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+fn f_nested(tex: texture_2d<f32>) -> u32 {
+  return 1u;
+}
+
+fn f(tex: texture_2d<f32>) -> u32 {
+  return f_nested(tex);
+}
+
+fn main() {
+  _ = f(t);
+}
+)";
+    auto* expect = R"(
+fn f_nested(tex_1 : texture_2d<f32>) -> u32 {
+  return 1u;
+}
+
+fn f(tex_2 : texture_2d<f32>) -> u32 {
+  return f_nested(tex_2);
+}
+
+@group(0) @binding(0) @internal(disable_validation__binding_point_collision) var t_1 : texture_2d<f32>;
+
+fn main() {
+  _ = f(t_1);
+}
+)";
+
+    ast::transform::DataMap data;
+    data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
+    auto got = Run<CombineSamplers>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace
 }  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc b/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
index 8057eee..73cc410 100644
--- a/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
@@ -145,13 +145,13 @@
 
         tint::Vector<const ast::Expression*, 8> new_args;
 
-        auto* arg = ast_call->args.begin();
+        auto arg = ast_call->args.begin();
         for (auto* member : new_struct->members) {
             if (padding_members.Contains(member)) {
                 new_args.Push(b.Expr(0_u));
             } else {
                 new_args.Push(ctx.Clone(*arg));
-                arg++;
+                ++arg;
             }
         }
         return b.Call(CreateASTTypeFor(ctx, str), new_args);
diff --git a/src/tint/lang/glsl/writer/writer_bench.cc b/src/tint/lang/glsl/writer/writer_bench.cc
index de9d3e6..904b4dc 100644
--- a/src/tint/lang/glsl/writer/writer_bench.cc
+++ b/src/tint/lang/glsl/writer/writer_bench.cc
@@ -24,11 +24,11 @@
 
 void GenerateGLSL(benchmark::State& state, std::string input_name) {
     auto res = bench::LoadProgram(input_name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        state.SkipWithError(err->msg.c_str());
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
         return;
     }
-    auto& program = std::get<bench::ProgramAndFile>(res).program;
+    auto& program = res->program;
     std::vector<std::string> entry_points;
     for (auto& fn : program.AST().Functions()) {
         if (fn->IsEntryPoint()) {
@@ -38,9 +38,9 @@
 
     for (auto _ : state) {
         for (auto& ep : entry_points) {
-            auto res = Generate(program, {}, ep);
-            if (!res) {
-                state.SkipWithError(res.Failure().reason.str());
+            auto gen_res = Generate(program, {}, ep);
+            if (!gen_res) {
+                state.SkipWithError(gen_res.Failure().reason.str());
             }
         }
     }
diff --git a/src/tint/lang/hlsl/writer/BUILD.bazel b/src/tint/lang/hlsl/writer/BUILD.bazel
index 323d038..58d4602 100644
--- a/src/tint/lang/hlsl/writer/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/BUILD.bazel
@@ -71,13 +71,14 @@
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "writer_bench.cc",
   ],
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
-    "//src/tint/cmd/bench",
+    "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
@@ -99,6 +100,7 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
+    "@benchmark",
   ] + select({
     ":tint_build_hlsl_writer": [
       "//src/tint/lang/hlsl/writer",
diff --git a/src/tint/lang/hlsl/writer/BUILD.cmake b/src/tint/lang/hlsl/writer/BUILD.cmake
index 887529b..3565fce 100644
--- a/src/tint/lang/hlsl/writer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/BUILD.cmake
@@ -87,7 +87,7 @@
 tint_target_add_dependencies(tint_lang_hlsl_writer_bench bench
   tint_api_common
   tint_api_options
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -111,6 +111,10 @@
   tint_utils_traits
 )
 
+tint_target_add_external_dependencies(tint_lang_hlsl_writer_bench bench
+  "google-benchmark"
+)
+
 if(TINT_BUILD_HLSL_WRITER)
   tint_target_add_dependencies(tint_lang_hlsl_writer_bench bench
     tint_lang_hlsl_writer
diff --git a/src/tint/lang/hlsl/writer/BUILD.gn b/src/tint/lang/hlsl/writer/BUILD.gn
index b270131..5cd6f64 100644
--- a/src/tint/lang/hlsl/writer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/BUILD.gn
@@ -24,6 +24,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_hlsl_writer) {
   libtint_source_set("writer") {
     sources = [
@@ -68,3 +72,41 @@
     }
   }
 }
+if (tint_build_benchmarks) {
+  if (tint_build_hlsl_writer) {
+    tint_unittests_source_set("bench") {
+      sources = [ "writer_bench.cc" ]
+      deps = [
+        "${tint_src_dir}:google_benchmark",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/api/options",
+        "${tint_src_dir}/cmd/bench:bench",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/hlsl/writer/common",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${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_hlsl_writer) {
+        deps += [ "${tint_src_dir}/lang/hlsl/writer" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn b/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
index 017911b..e44df18 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_hlsl_writer) {
@@ -72,7 +72,6 @@
 if (tint_build_unittests) {
   if (tint_build_hlsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "array_accessor_test.cc",
         "assign_test.cc",
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index f19e36d..94c372a 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -337,7 +337,7 @@
 ASTPrinter::~ASTPrinter() = default;
 
 bool ASTPrinter::Generate() {
-    if (!tint::writer::CheckSupportedExtensions(
+    if (!tint::wgsl::CheckSupportedExtensions(
             "HLSL", builder_.AST(), diagnostics_,
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
@@ -2842,13 +2842,13 @@
                                      zero, i32, core::EvaluationStage::kRuntime, stmt,
                                      /* constant_value */ nullptr,
                                      /* has_side_effects */ false));
-        auto* packed = tint::writer::AppendVector(&builder_, vector, zero);
+        auto* packed = tint::wgsl::AppendVector(&builder_, vector, zero);
         return EmitExpression(out, packed->Declaration());
     };
 
     auto emit_vector_appended_with_level = [&](const ast::Expression* vector) {
         if (auto* level = arg(Usage::kLevel)) {
-            auto* packed = tint::writer::AppendVector(&builder_, vector, level);
+            auto* packed = tint::wgsl::AppendVector(&builder_, vector, level);
             return EmitExpression(out, packed->Declaration());
         }
         return emit_vector_appended_with_i32_zero(vector);
@@ -2856,7 +2856,7 @@
 
     if (auto* array_index = arg(Usage::kArrayIndex)) {
         // Array index needs to be appended to the coordinates.
-        auto* packed = tint::writer::AppendVector(&builder_, param_coords, array_index);
+        auto* packed = tint::wgsl::AppendVector(&builder_, param_coords, array_index);
         if (pack_level_in_coords) {
             // Then mip level needs to be appended to the coordinates.
             if (!emit_vector_appended_with_level(packed->Declaration())) {
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
index fd49ad8..0741302 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
@@ -89,11 +89,8 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
-    "//src/tint/lang/wgsl/ast/transform:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -113,6 +110,21 @@
       "//src/tint/lang/hlsl/writer/ast_raise",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/ast/transform:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -123,3 +135,29 @@
   actual = "//src/tint:tint_build_hlsl_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_hlsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_hlsl_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg
index 31b4636..dd6ef8b 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg
@@ -1,3 +1,6 @@
 {
-    "condition": "tint_build_hlsl_writer"
+    "condition": "tint_build_hlsl_writer",
+    "test": {
+        "condition": "tint_build_wgsl_reader && tint_build_wgsl_writer",
+    }
 }
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
index 97916d3..d7240e6 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
@@ -69,11 +69,11 @@
 )
 
 endif(TINT_BUILD_HLSL_WRITER)
-if(TINT_BUILD_HLSL_WRITER)
+if(TINT_BUILD_HLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_hlsl_writer_ast_raise_test
 # Kind:      test
-# Condition: TINT_BUILD_HLSL_WRITER
+# Condition: TINT_BUILD_HLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_hlsl_writer_ast_raise_test test
   lang/hlsl/writer/ast_raise/calculate_array_length_test.cc
@@ -93,11 +93,8 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
-  tint_lang_wgsl_ast_transform_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -123,4 +120,22 @@
   )
 endif(TINT_BUILD_HLSL_WRITER)
 
-endif(TINT_BUILD_HLSL_WRITER)
\ No newline at end of file
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_hlsl_writer_ast_raise_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_hlsl_writer_ast_raise_test test
+    tint_lang_wgsl_ast_transform_test
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_hlsl_writer_ast_raise_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_HLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn b/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
index 24b8ca4..8fcad6b 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_hlsl_writer) {
@@ -72,9 +72,9 @@
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_hlsl_writer) {
+  if (tint_build_hlsl_writer && tint_build_wgsl_reader &&
+      tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "calculate_array_length_test.cc",
         "decompose_memory_access_test.cc",
@@ -93,11 +93,8 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
-        "${tint_src_dir}/lang/wgsl/ast/transform:unittests",
         "${tint_src_dir}/lang/wgsl/program",
-        "${tint_src_dir}/lang/wgsl/reader",
         "${tint_src_dir}/lang/wgsl/sem",
-        "${tint_src_dir}/lang/wgsl/writer",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -116,6 +113,18 @@
       if (tint_build_hlsl_writer) {
         deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_raise" ]
       }
+
+      if (tint_build_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+
+      if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/ast/transform:unittests" ]
+      }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
     }
   }
 }
diff --git a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
index d98deac..ba61303 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
@@ -107,8 +107,8 @@
         Config& operator=(const Config&);
 
         /// Indicate which interstage io locations are actually used by the later stage.
-        /// There can be at most 16 user defined interstage variables with locations.
-        std::bitset<16> interstage_locations;
+        /// There can be at most 30 user defined interstage variables with locations.
+        std::bitset<30> interstage_locations;
 
         /// Reflect the fields of this class so that it can be used by tint::ForeachField()
         TINT_REFLECT(interstage_variables);
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index bca3db6..10bc497 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -28,6 +28,10 @@
 
 namespace tint::hlsl::writer {
 
+/// kMaxInterStageLocations == D3D11_PS_INPUT_REGISTER_COUNT - 2
+/// D3D11_PS_INPUT_REGISTER_COUNT == D3D12_PS_INPUT_REGISTER_COUNT
+constexpr uint32_t kMaxInterStageLocations = 30;
+
 /// Configuration options used for generating HLSL.
 struct Options {
     /// Constructor
@@ -58,7 +62,7 @@
 
     /// Interstage locations actually used as inputs in the next stage of the pipeline.
     /// This is potentially used for truncating unused interstage outputs at current shader stage.
-    std::bitset<16> interstage_locations;
+    std::bitset<kMaxInterStageLocations> interstage_locations;
 
     /// The binding point to use for information passed via root constants.
     std::optional<BindingPoint> root_constant_binding_point;
diff --git a/src/tint/lang/hlsl/writer/writer_bench.cc b/src/tint/lang/hlsl/writer/writer_bench.cc
index 00c5d39..727e696 100644
--- a/src/tint/lang/hlsl/writer/writer_bench.cc
+++ b/src/tint/lang/hlsl/writer/writer_bench.cc
@@ -22,15 +22,14 @@
 
 void GenerateHLSL(benchmark::State& state, std::string input_name) {
     auto res = bench::LoadProgram(input_name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        state.SkipWithError(err->msg.c_str());
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
         return;
     }
-    auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(program, {});
-        if (!res) {
-            state.SkipWithError(res.Failure().reason.str());
+        auto gen_res = Generate(res->program, {});
+        if (!gen_res) {
+            state.SkipWithError(gen_res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/lang/msl/validate/val.h b/src/tint/lang/msl/validate/val.h
index e9f4037..58767f1 100644
--- a/src/tint/lang/msl/validate/val.h
+++ b/src/tint/lang/msl/validate/val.h
@@ -17,7 +17,6 @@
 
 #include <string>
 #include <utility>
-#include <vector>
 
 #include "src/tint/lang/wgsl/ast/pipeline_stage.h"
 
@@ -28,8 +27,6 @@
 
 namespace tint::msl::validate {
 
-using EntryPointList = std::vector<std::pair<std::string, ast::PipelineStage>>;
-
 /// The version of MSL to validate against.
 /// Note: these must kept be in ascending order
 enum class MslVersion {
diff --git a/src/tint/lang/msl/writer/BUILD.bazel b/src/tint/lang/msl/writer/BUILD.bazel
index 4348b90..098f03e 100644
--- a/src/tint/lang/msl/writer/BUILD.bazel
+++ b/src/tint/lang/msl/writer/BUILD.bazel
@@ -45,7 +45,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -68,19 +67,25 @@
       "//src/tint/lang/msl/writer/printer",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "writer_bench.cc",
   ],
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
-    "//src/tint/cmd/bench",
+    "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
@@ -101,6 +106,7 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
+    "@benchmark",
   ] + select({
     ":tint_build_msl_writer": [
       "//src/tint/lang/msl/writer",
@@ -117,3 +123,8 @@
   actual = "//src/tint:tint_build_msl_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/msl/writer/BUILD.cmake b/src/tint/lang/msl/writer/BUILD.cmake
index d3291bc..1e6b9f1 100644
--- a/src/tint/lang/msl/writer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/BUILD.cmake
@@ -52,7 +52,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -78,6 +77,12 @@
   )
 endif(TINT_BUILD_MSL_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_msl_writer lib
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 endif(TINT_BUILD_MSL_WRITER)
 if(TINT_BUILD_MSL_WRITER)
 ################################################################################
@@ -92,7 +97,7 @@
 tint_target_add_dependencies(tint_lang_msl_writer_bench bench
   tint_api_common
   tint_api_options
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -115,6 +120,10 @@
   tint_utils_traits
 )
 
+tint_target_add_external_dependencies(tint_lang_msl_writer_bench bench
+  "google-benchmark"
+)
+
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_lang_msl_writer_bench bench
     tint_lang_msl_writer
diff --git a/src/tint/lang/msl/writer/BUILD.gn b/src/tint/lang/msl/writer/BUILD.gn
index 9f13b86..b356e63 100644
--- a/src/tint/lang/msl/writer/BUILD.gn
+++ b/src/tint/lang/msl/writer/BUILD.gn
@@ -24,6 +24,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_msl_writer) {
   libtint_source_set("writer") {
     sources = [
@@ -44,7 +48,6 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/reader/lower",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -69,5 +72,49 @@
         "${tint_src_dir}/lang/msl/writer/printer",
       ]
     }
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader/program_to_ir" ]
+    }
+  }
+}
+if (tint_build_benchmarks) {
+  if (tint_build_msl_writer) {
+    tint_unittests_source_set("bench") {
+      sources = [ "writer_bench.cc" ]
+      deps = [
+        "${tint_src_dir}:google_benchmark",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/api/options",
+        "${tint_src_dir}/cmd/bench:bench",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${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_msl_writer) {
+        deps += [
+          "${tint_src_dir}/lang/msl/writer",
+          "${tint_src_dir}/lang/msl/writer/common",
+        ]
+      }
+    }
   }
 }
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.gn b/src/tint/lang/msl/writer/ast_printer/BUILD.gn
index 072b550..2402e43 100644
--- a/src/tint/lang/msl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_msl_writer) {
@@ -73,7 +73,6 @@
 if (tint_build_unittests) {
   if (tint_build_msl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "array_accessor_test.cc",
         "assign_test.cc",
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index 254e2b2..fde3e46 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -253,7 +253,7 @@
 ASTPrinter::~ASTPrinter() = default;
 
 bool ASTPrinter::Generate() {
-    if (!tint::writer::CheckSupportedExtensions(
+    if (!tint::wgsl::CheckSupportedExtensions(
             "MSL", builder_.AST(), diagnostics_,
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.bazel b/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
index 5a68d70..cd51db5 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
@@ -83,12 +83,9 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
-    "//src/tint/lang/wgsl/ast/transform:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -108,6 +105,21 @@
       "//src/tint/lang/msl/writer/ast_raise",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/ast/transform:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -118,3 +130,29 @@
   actual = "//src/tint:tint_build_msl_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_msl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_msl_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.cfg b/src/tint/lang/msl/writer/ast_raise/BUILD.cfg
index 1c7e256..98cff52 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.cfg
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.cfg
@@ -1,3 +1,6 @@
 {
-    "condition": "tint_build_msl_writer"
+    "condition": "tint_build_msl_writer",
+    "test": {
+        "condition": "tint_build_wgsl_reader && tint_build_wgsl_writer",
+    }
 }
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.cmake b/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
index 386768b..6af7310 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
@@ -65,11 +65,11 @@
 )
 
 endif(TINT_BUILD_MSL_WRITER)
-if(TINT_BUILD_MSL_WRITER)
+if(TINT_BUILD_MSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_msl_writer_ast_raise_test
 # Kind:      test
-# Condition: TINT_BUILD_MSL_WRITER
+# Condition: TINT_BUILD_MSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_msl_writer_ast_raise_test test
   lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param_test.cc
@@ -87,12 +87,9 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
-  tint_lang_wgsl_ast_transform_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -118,4 +115,22 @@
   )
 endif(TINT_BUILD_MSL_WRITER)
 
-endif(TINT_BUILD_MSL_WRITER)
\ No newline at end of file
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_msl_writer_ast_raise_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_msl_writer_ast_raise_test test
+    tint_lang_wgsl_ast_transform_test
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_msl_writer_ast_raise_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.gn b/src/tint/lang/msl/writer/ast_raise/BUILD.gn
index cf231b6..b7ebdb3 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_msl_writer) {
@@ -68,9 +68,9 @@
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_msl_writer) {
+  if (tint_build_msl_writer && tint_build_wgsl_reader &&
+      tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "module_scope_var_to_entry_point_param_test.cc",
         "packed_vec3_test.cc",
@@ -87,12 +87,9 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
-        "${tint_src_dir}/lang/wgsl/ast/transform:unittests",
         "${tint_src_dir}/lang/wgsl/program",
-        "${tint_src_dir}/lang/wgsl/reader",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
-        "${tint_src_dir}/lang/wgsl/writer",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -111,6 +108,18 @@
       if (tint_build_msl_writer) {
         deps += [ "${tint_src_dir}/lang/msl/writer/ast_raise" ]
       }
+
+      if (tint_build_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+
+      if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/ast/transform:unittests" ]
+      }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
     }
   }
 }
diff --git a/src/tint/lang/msl/writer/common/BUILD.gn b/src/tint/lang/msl/writer/common/BUILD.gn
index 8e834d8..7938be9 100644
--- a/src/tint/lang/msl/writer/common/BUILD.gn
+++ b/src/tint/lang/msl/writer/common/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_msl_writer) {
@@ -59,7 +59,6 @@
 if (tint_build_unittests) {
   if (tint_build_msl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [ "printer_support_test.cc" ]
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/lang/msl/writer/printer/BUILD.gn b/src/tint/lang/msl/writer/printer/BUILD.gn
index cf2af5a..0c07cdf 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/printer/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_msl_writer) {
@@ -64,7 +64,6 @@
 if (tint_build_unittests) {
   if (tint_build_msl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "binary_test.cc",
         "constant_test.cc",
diff --git a/src/tint/lang/msl/writer/writer.cc b/src/tint/lang/msl/writer/writer.cc
index 54fffe0..92fd019 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -21,7 +21,10 @@
 #include "src/tint/lang/msl/writer/printer/printer.h"
 #include "src/tint/lang/msl/writer/raise/raise.h"
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
+
+#if TINT_BUILD_WGSL_READER
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
+#endif
 
 namespace tint::msl::writer {
 
@@ -33,6 +36,7 @@
     Output output;
 
     if (options.use_tint_ir) {
+#if TINT_BUILD_WGSL_READER
         // Convert the AST program to an IR module.
         auto converted = wgsl::reader::ProgramToIR(program);
         if (!converted) {
@@ -58,6 +62,9 @@
             return result.Failure();
         }
         output.msl = impl->Result();
+#else
+        return Failure{"use_tint_ir requires building with TINT_BUILD_WGSL_READER"};
+#endif
     } else {
         // Sanitize the program.
         auto sanitized_result = Sanitize(program, options);
diff --git a/src/tint/lang/msl/writer/writer_bench.cc b/src/tint/lang/msl/writer/writer_bench.cc
index aa04473..712d272 100644
--- a/src/tint/lang/msl/writer/writer_bench.cc
+++ b/src/tint/lang/msl/writer/writer_bench.cc
@@ -24,11 +24,11 @@
 
 void GenerateMSL(benchmark::State& state, std::string input_name) {
     auto res = bench::LoadProgram(input_name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        state.SkipWithError(err->msg.c_str());
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
         return;
     }
-    auto& program = std::get<bench::ProgramAndFile>(res).program;
+    auto& program = res->program;
 
     tint::msl::writer::Options gen_options = {};
     gen_options.array_length_from_uniform.ubo_binding = tint::BindingPoint{0, 30};
@@ -61,9 +61,9 @@
         }
     }
     for (auto _ : state) {
-        auto res = Generate(program, gen_options);
-        if (!res) {
-            state.SkipWithError(res.Failure().reason.str());
+        auto gen_res = Generate(program, gen_options);
+        if (!gen_res) {
+            state.SkipWithError(gen_res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/lang/spirv/ir/BUILD.bazel b/src/tint/lang/spirv/ir/BUILD.bazel
index c6570fc..5e02b2f 100644
--- a/src/tint/lang/spirv/ir/BUILD.bazel
+++ b/src/tint/lang/spirv/ir/BUILD.bazel
@@ -27,9 +27,11 @@
   name = "ir",
   srcs = [
     "builtin_call.cc",
+    "literal_operand.cc",
   ],
   hdrs = [
     "builtin_call.h",
+    "literal_operand.h",
   ],
   deps = [
     "//src/tint/api/common",
diff --git a/src/tint/lang/spirv/ir/BUILD.cmake b/src/tint/lang/spirv/ir/BUILD.cmake
index e17a474..da5b68f 100644
--- a/src/tint/lang/spirv/ir/BUILD.cmake
+++ b/src/tint/lang/spirv/ir/BUILD.cmake
@@ -28,6 +28,8 @@
 tint_add_target(tint_lang_spirv_ir lib
   lang/spirv/ir/builtin_call.cc
   lang/spirv/ir/builtin_call.h
+  lang/spirv/ir/literal_operand.cc
+  lang/spirv/ir/literal_operand.h
 )
 
 tint_target_add_dependencies(tint_lang_spirv_ir lib
diff --git a/src/tint/lang/spirv/ir/BUILD.gn b/src/tint/lang/spirv/ir/BUILD.gn
index 2a81280..588ee98 100644
--- a/src/tint/lang/spirv/ir/BUILD.gn
+++ b/src/tint/lang/spirv/ir/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -33,6 +33,8 @@
   sources = [
     "builtin_call.cc",
     "builtin_call.h",
+    "literal_operand.cc",
+    "literal_operand.h",
   ]
   deps = [
     "${tint_src_dir}/api/common",
@@ -60,7 +62,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "builtin_call_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/cmd/bench/benchmark.cc b/src/tint/lang/spirv/ir/literal_operand.cc
similarity index 65%
copy from src/tint/cmd/bench/benchmark.cc
copy to src/tint/lang/spirv/ir/literal_operand.cc
index ad8658b..bc24878 100644
--- a/src/tint/cmd/bench/benchmark.cc
+++ b/src/tint/lang/spirv/ir/literal_operand.cc
@@ -12,9 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#if defined(__clang__)
-#pragma clang diagnostic ignored "-Wmissing-variable-declarations"
-#endif
+#include "src/tint/lang/spirv/ir/literal_operand.h"
 
-// A placeholder symbol used to emit a symbol for this lib target.
-int tint_cmd_bench_symbol = 1;
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::ir::LiteralOperand);
+
+namespace tint::spirv::ir {
+
+LiteralOperand::LiteralOperand(const core::constant::Value* value) : Base(value) {}
+
+LiteralOperand::~LiteralOperand() = default;
+
+}  // namespace tint::spirv::ir
diff --git a/src/tint/lang/spirv/ir/literal_operand.h b/src/tint/lang/spirv/ir/literal_operand.h
new file mode 100644
index 0000000..cbda1e6
--- /dev/null
+++ b/src/tint/lang/spirv/ir/literal_operand.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_SPIRV_IR_LITERAL_OPERAND_H_
+#define SRC_TINT_LANG_SPIRV_IR_LITERAL_OPERAND_H_
+
+#include "src/tint/lang/core/ir/constant.h"
+#include "src/tint/utils/rtti/castable.h"
+
+namespace tint::spirv::ir {
+
+/// LiteralOperand is a type of constant value that is intended to be emitted as a literal in
+/// the SPIR-V instruction stream.
+class LiteralOperand final : public Castable<LiteralOperand, core::ir::Constant> {
+  public:
+    /// Constructor
+    /// @param value the operand value
+    explicit LiteralOperand(const core::constant::Value* value);
+    /// Destructor
+    ~LiteralOperand() override;
+};
+
+}  // namespace tint::spirv::ir
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_LITERAL_OPERAND_H_
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
index 37651a6..aabcbe3 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
@@ -83,13 +83,9 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
-    "//src/tint/lang/wgsl/ast/transform:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
-    "//src/tint/lang/wgsl/reader/parser",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -109,6 +105,22 @@
       "//src/tint/lang/spirv/reader/ast_lower",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/parser",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/ast/transform:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -119,3 +131,29 @@
   actual = "//src/tint:tint_build_spv_reader_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_spv_reader",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg b/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg
index a460fd5..18f78f3 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg
@@ -1,3 +1,6 @@
 {
-    "condition": "tint_build_spv_reader"
+    "condition": "tint_build_spv_reader",
+    "test": {
+        "condition": "tint_build_wgsl_reader && tint_build_wgsl_writer",
+    }
 }
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
index f7a4893..3525002 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
@@ -65,11 +65,11 @@
 )
 
 endif(TINT_BUILD_SPV_READER)
-if(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_spirv_reader_ast_lower_test
 # Kind:      test
-# Condition: TINT_BUILD_SPV_READER
+# Condition: TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_reader_ast_lower_test test
   lang/spirv/reader/ast_lower/atomics_test.cc
@@ -87,13 +87,9 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
-  tint_lang_wgsl_ast_transform_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
-  tint_lang_wgsl_reader_parser
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -119,4 +115,23 @@
   )
 endif(TINT_BUILD_SPV_READER)
 
-endif(TINT_BUILD_SPV_READER)
\ No newline at end of file
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower_test test
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_parser
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower_test test
+    tint_lang_wgsl_ast_transform_test
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.gn b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
index 12924c2..19ed437 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_reader) {
@@ -68,9 +68,9 @@
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_spv_reader) {
+  if (tint_build_spv_reader && tint_build_wgsl_reader &&
+      tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "atomics_test.cc",
         "decompose_strided_array_test.cc",
@@ -87,13 +87,9 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
-        "${tint_src_dir}/lang/wgsl/ast/transform:unittests",
         "${tint_src_dir}/lang/wgsl/program",
-        "${tint_src_dir}/lang/wgsl/reader",
-        "${tint_src_dir}/lang/wgsl/reader/parser",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
-        "${tint_src_dir}/lang/wgsl/writer",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -112,6 +108,21 @@
       if (tint_build_spv_reader) {
         deps += [ "${tint_src_dir}/lang/spirv/reader/ast_lower" ]
       }
+
+      if (tint_build_wgsl_reader) {
+        deps += [
+          "${tint_src_dir}/lang/wgsl/reader",
+          "${tint_src_dir}/lang/wgsl/reader/parser",
+        ]
+      }
+
+      if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/ast/transform:unittests" ]
+      }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
     }
   }
 }
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
index 4a97ba1..70e4340 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
@@ -138,7 +138,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer/ast_printer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/generator",
@@ -166,6 +165,11 @@
       "@spirv_tools",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer/ast_printer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -181,6 +185,11 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
 selects.config_setting_group(
     name = "tint_build_spv_reader_or_tint_build_spv_writer",
     match_any = [
@@ -189,3 +198,11 @@
     ],
 )
 
+selects.config_setting_group(
+    name = "tint_build_spv_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_spv_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.cfg b/src/tint/lang/spirv/reader/ast_parser/BUILD.cfg
index a460fd5..c15bc20 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.cfg
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.cfg
@@ -1,3 +1,6 @@
 {
-    "condition": "tint_build_spv_reader"
+    "condition": "tint_build_spv_reader",
+    "test": {
+        "condition": "tint_build_wgsl_writer",
+    }
 }
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
index c8b5c9d..84c427f 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
@@ -91,11 +91,11 @@
 endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
 
 endif(TINT_BUILD_SPV_READER)
-if(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_spirv_reader_ast_parser_test
 # Kind:      test
-# Condition: TINT_BUILD_SPV_READER
+# Condition: TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_reader_ast_parser_test test
   lang/spirv/reader/ast_parser/ast_parser_test.cc
@@ -144,7 +144,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer_ast_printer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_generator
@@ -179,4 +178,10 @@
   )
 endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
 
-endif(TINT_BUILD_SPV_READER)
\ No newline at end of file
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_ast_parser_test test
+    tint_lang_wgsl_writer_ast_printer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
index 163e8be..d8d6256 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_reader) {
@@ -88,15 +88,15 @@
         "${tint_spirv_headers_dir}:spv_headers",
         "${tint_spirv_tools_dir}:spvtools",
         "${tint_spirv_tools_dir}:spvtools_opt",
+        "${tint_spirv_tools_dir}:spvtools_val",
       ]
     }
     public_configs = [ "${tint_spirv_tools_dir}/:spvtools_internal_config" ]
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_spv_reader) {
+  if (tint_build_spv_reader && tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "ast_parser_test.cc",
         "barrier_test.cc",
@@ -144,7 +144,6 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
-        "${tint_src_dir}/lang/wgsl/writer/ast_printer",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/generator",
@@ -172,8 +171,13 @@
           "${tint_spirv_tools_dir}:spvtools_headers",
           "${tint_spirv_tools_dir}:spvtools_opt",
           "${tint_spirv_tools_dir}:spvtools_val",
+          "${tint_spirv_tools_dir}:spvtools_val",
         ]
       }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer" ]
+      }
       public_configs = [ "${tint_spirv_tools_dir}/:spvtools_internal_config" ]
     }
   }
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
index eea7fce..af55917 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
@@ -377,7 +377,7 @@
         case spvtools::opt::analysis::Type::kArray:
             return ConvertType(type_id, spirv_type->AsArray());
         case spvtools::opt::analysis::Type::kStruct:
-            return ConvertType(type_id, spirv_type->AsStruct());
+            return ConvertStructType(type_id);
         case spvtools::opt::analysis::Type::kPointer:
             return ConvertType(type_id, ptr_as, spirv_type->AsPointer());
         case spvtools::opt::analysis::Type::kFunction:
@@ -1061,8 +1061,7 @@
     return true;
 }
 
-const Type* ASTParser::ConvertType(uint32_t type_id,
-                                   const spvtools::opt::analysis::Struct* struct_ty) {
+const Type* ASTParser::ConvertStructType(uint32_t type_id) {
     // Compute the struct decoration.
     auto struct_decorations = this->GetDecorationsFor(type_id);
     if (struct_decorations.size() == 1) {
@@ -1079,18 +1078,22 @@
         return nullptr;
     }
 
-    // Compute members
-    tint::Vector<const ast::StructMember*, 8> ast_members;
-    const auto members = struct_ty->element_types();
-    if (members.empty()) {
+    // The SPIR-V optimizer's types representation deduplicates types. We don't want that
+    // deduplication, so get the member types from the SPIR-V instruction directly.
+    const auto* inst = def_use_mgr_->GetDef(type_id);
+    auto num_members = inst->NumOperands() - 1;
+    if (num_members == 0) {
         Fail() << "WGSL does not support empty structures. can't convert type: "
                << def_use_mgr_->GetDef(type_id)->PrettyPrint();
         return nullptr;
     }
+
+    // Compute members
+    tint::Vector<const ast::StructMember*, 8> ast_members;
     TypeList ast_member_types;
     unsigned num_non_writable_members = 0;
-    for (uint32_t member_index = 0; member_index < members.size(); ++member_index) {
-        const auto member_type_id = type_mgr_->GetId(members[member_index]);
+    for (uint32_t member_index = 0; member_index < num_members; ++member_index) {
+        const auto member_type_id = inst->GetOperand(member_index + 1).AsId();
         auto* ast_member_ty = ConvertType(member_type_id);
         if (ast_member_ty == nullptr) {
             // Already emitted diagnostics.
@@ -1181,7 +1184,7 @@
     auto sym = builder_.Symbols().Register(name);
     auto* ast_struct =
         create<ast::Struct>(Source{}, builder_.Ident(sym), std::move(ast_members), tint::Empty);
-    if (num_non_writable_members == members.size()) {
+    if (num_non_writable_members == num_members) {
         read_only_struct_types_.insert(ast_struct->name->symbol);
     }
     AddTypeDecl(sym, ast_struct);
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.h b/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
index 69aa099..4ba25f5 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
@@ -774,8 +774,7 @@
     /// preserve member names, which are given by OpMemberName which is normally
     /// not significant to the optimizer's module representation.
     /// @param type_id the SPIR-V ID for the type.
-    /// @param struct_ty the Tint type
-    const Type* ConvertType(uint32_t type_id, const spvtools::opt::analysis::Struct* struct_ty);
+    const Type* ConvertStructType(uint32_t type_id);
     /// Converts a specific SPIR-V type to a Tint type. Pointer / Reference case
     /// The pointer to gl_PerVertex maps to nullptr, and instead is recorded
     /// in member #builtin_position_.
diff --git a/src/tint/lang/spirv/reader/ast_parser/named_types_test.cc b/src/tint/lang/spirv/reader/ast_parser/named_types_test.cc
index 86c44e1..f84d2d1 100644
--- a/src/tint/lang/spirv/reader/ast_parser/named_types_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/named_types_test.cc
@@ -143,6 +143,91 @@
     p->DeliberatelyInvalidSpirv();
 }
 
+// Make sure that we do not deduplicate nested structures, as this will break the names used for
+// chained accessors.
+TEST_F(SpirvASTParserTest, NamedTypes_NestedStructsDifferOnlyInMemberNames) {
+    auto p = parser(test::Assemble(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %main "main"
+
+               OpName %FooInner "FooInner"
+               OpName %FooOuter "FooOuter"
+               OpMemberName %FooInner 0 "foo_member"
+               OpMemberName %FooOuter 0 "foo_inner"
+
+               OpName %BarInner "BarInner"
+               OpName %BarOuter "BarOuter"
+               OpMemberName %BarInner 0 "bar_member"
+               OpMemberName %BarOuter 0 "bar_inner"
+
+       %void = OpTypeVoid
+          %3 = OpTypeFunction %void
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+
+   %FooInner = OpTypeStruct %int
+   %FooOuter = OpTypeStruct %FooInner
+
+   %BarInner = OpTypeStruct %int
+   %BarOuter = OpTypeStruct %BarInner
+
+     %FooPtr = OpTypePointer Private %FooOuter
+     %FooVar = OpVariable %FooPtr Private
+
+     %BarPtr = OpTypePointer Private %BarOuter
+     %BarVar = OpVariable %BarPtr Private
+
+    %ptr_int = OpTypePointer Private %int
+
+       %main = OpFunction %void None %3
+      %start = OpLabel
+ %access_foo = OpAccessChain %ptr_int %FooVar %int_0 %int_0
+ %access_bar = OpAccessChain %ptr_int %BarVar %int_0 %int_0
+     %fooval = OpLoad %int %access_foo
+     %barval = OpLoad %int %access_bar
+               OpReturn
+               OpFunctionEnd
+  )"));
+
+    EXPECT_TRUE(p->BuildAndParseInternalModule());
+
+    auto program = p->program();
+    EXPECT_EQ(test::ToString(program), R"(struct FooInner {
+  foo_member : i32,
+}
+
+struct FooOuter {
+  foo_inner : FooInner,
+}
+
+struct BarInner {
+  bar_member : i32,
+}
+
+struct BarOuter {
+  bar_inner : BarInner,
+}
+
+var<private> x_11 : FooOuter;
+
+var<private> x_13 : BarOuter;
+
+fn main_1() {
+  let x_18 = x_11.foo_inner.foo_member;
+  let x_19 = x_13.bar_inner.bar_member;
+  return;
+}
+
+@compute @workgroup_size(1i, 1i, 1i)
+fn main() {
+  main_1();
+}
+)");
+}
+
 // TODO(dneto): Handle arrays sized by a spec constant.
 // Blocked by crbug.com/tint/32
 
diff --git a/src/tint/lang/spirv/writer/BUILD.bazel b/src/tint/lang/spirv/writer/BUILD.bazel
index f8d6a11..f48468d 100644
--- a/src/tint/lang/spirv/writer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/BUILD.bazel
@@ -47,7 +47,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -75,6 +74,11 @@
       "//src/tint/lang/spirv/writer/raise",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -106,7 +110,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/intrinsic",
@@ -149,13 +152,13 @@
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "writer_bench.cc",
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
-    "//src/tint/cmd/bench",
+    "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
@@ -176,6 +179,7 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
+    "@benchmark",
   ] + select({
     ":tint_build_spv_writer": [
       "//src/tint/lang/spirv/writer",
@@ -197,6 +201,11 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
 selects.config_setting_group(
     name = "tint_build_spv_reader_or_tint_build_spv_writer",
     match_any = [
diff --git a/src/tint/lang/spirv/writer/BUILD.cmake b/src/tint/lang/spirv/writer/BUILD.cmake
index 0910cfb..5536c7c 100644
--- a/src/tint/lang/spirv/writer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/BUILD.cmake
@@ -24,6 +24,7 @@
 include(lang/spirv/writer/ast_printer/BUILD.cmake)
 include(lang/spirv/writer/ast_raise/BUILD.cmake)
 include(lang/spirv/writer/common/BUILD.cmake)
+include(lang/spirv/writer/helpers/BUILD.cmake)
 include(lang/spirv/writer/printer/BUILD.cmake)
 include(lang/spirv/writer/raise/BUILD.cmake)
 
@@ -54,7 +55,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -86,6 +86,12 @@
   )
 endif(TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_spirv_writer lib
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 endif(TINT_BUILD_SPV_WRITER)
 if(TINT_BUILD_SPV_WRITER)
 ################################################################################
@@ -118,7 +124,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
@@ -175,8 +180,7 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_bench bench
   tint_api_common
-  tint_api_options
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -199,6 +203,10 @@
   tint_utils_traits
 )
 
+tint_target_add_external_dependencies(tint_lang_spirv_writer_bench bench
+  "google-benchmark"
+)
+
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_lang_spirv_writer_bench bench
     tint_lang_spirv_writer
diff --git a/src/tint/lang/spirv/writer/BUILD.gn b/src/tint/lang/spirv/writer/BUILD.gn
index 38d4260..4587f07 100644
--- a/src/tint/lang/spirv/writer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_writer) {
@@ -50,7 +50,6 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/reader/lower",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -79,12 +78,15 @@
         "${tint_src_dir}/lang/spirv/writer/raise",
       ]
     }
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader/program_to_ir" ]
+    }
   }
 }
 if (tint_build_unittests) {
   if (tint_build_spv_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "access_test.cc",
         "atomic_builtin_test.cc",
@@ -110,7 +112,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/intrinsic",
@@ -153,3 +154,42 @@
     }
   }
 }
+if (tint_build_benchmarks) {
+  if (tint_build_spv_writer) {
+    tint_unittests_source_set("bench") {
+      sources = [ "writer_bench.cc" ]
+      deps = [
+        "${tint_src_dir}:google_benchmark",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/cmd/bench:bench",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${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_writer) {
+        deps += [
+          "${tint_src_dir}/lang/spirv/writer",
+          "${tint_src_dir}/lang/spirv/writer/common",
+        ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/spirv/writer/access_test.cc b/src/tint/lang/spirv/writer/access_test.cc
index ffdd185..967cdf5 100644
--- a/src/tint/lang/spirv/writer/access_test.cc
+++ b/src/tint/lang/spirv/writer/access_test.cc
@@ -22,11 +22,11 @@
 
 TEST_F(SpirvWriterTest, Access_Array_Value_ConstantIndex) {
     auto* arr_val = b.FunctionParam("arr", ty.array(ty.i32(), 4));
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({arr_val});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), arr_val, 1_u);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
@@ -35,11 +35,11 @@
 }
 
 TEST_F(SpirvWriterTest, Access_Array_Pointer_ConstantIndex) {
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     b.Append(func->Block(), [&] {
         auto* arr_var = b.Var("arr", ty.ptr<function, array<i32, 4>>());
         auto* result = b.Access(ty.ptr<function, i32>(), arr_var, 1_u);
-        b.Return(func);
+        b.Return(func, b.Load(result));
         mod.SetName(result, "result");
     });
 
@@ -49,31 +49,31 @@
 
 TEST_F(SpirvWriterTest, Access_Array_Pointer_DynamicIndex) {
     auto* idx = b.FunctionParam("idx", ty.i32());
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({idx});
     b.Append(func->Block(), [&] {
         auto* arr_var = b.Var("arr", ty.ptr<function, array<i32, 4>>());
         auto* result = b.Access(ty.ptr<function, i32>(), arr_var, idx);
-        b.Return(func);
+        b.Return(func, b.Load(result));
         mod.SetName(result, "result");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-         %13 = OpBitcast %uint %idx
-         %14 = OpExtInst %uint %15 UMin %13 %uint_3
-     %result = OpAccessChain %_ptr_Function_int %arr %14
+         %12 = OpBitcast %uint %idx
+         %13 = OpExtInst %uint %14 UMin %12 %uint_3
+     %result = OpAccessChain %_ptr_Function_int %arr %13
 )");
 }
 
 TEST_F(SpirvWriterTest, Access_Matrix_Value_ConstantIndex) {
     auto* mat_val = b.FunctionParam("mat", ty.mat2x2(ty.f32()));
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.vec2<f32>());
     func->SetParams({mat_val});
     b.Append(func->Block(), [&] {
         auto* result_vector = b.Access(ty.vec2(ty.f32()), mat_val, 1_u);
         auto* result_scalar = b.Access(ty.f32(), mat_val, 1_u, 0_u);
-        b.Return(func);
+        b.Return(func, b.Multiply(ty.vec2<f32>(), result_vector, result_scalar));
         mod.SetName(result_vector, "result_vector");
         mod.SetName(result_scalar, "result_scalar");
     });
@@ -127,11 +127,11 @@
 
 TEST_F(SpirvWriterTest, Access_Vector_Value_ConstantIndex) {
     auto* vec_val = b.FunctionParam("vec", ty.vec4(ty.i32()));
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({vec_val});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), vec_val, 1_u);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
@@ -142,82 +142,84 @@
 TEST_F(SpirvWriterTest, Access_Vector_Value_DynamicIndex) {
     auto* vec_val = b.FunctionParam("vec", ty.vec4(ty.i32()));
     auto* idx = b.FunctionParam("idx", ty.i32());
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({vec_val, idx});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), vec_val, idx);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-         %10 = OpBitcast %uint %idx
-         %11 = OpExtInst %uint %12 UMin %10 %uint_3
-     %result = OpVectorExtractDynamic %int %vec %11
+          %9 = OpBitcast %uint %idx
+         %10 = OpExtInst %uint %11 UMin %9 %uint_3
+     %result = OpVectorExtractDynamic %int %vec %10
 )");
 }
 
 TEST_F(SpirvWriterTest, Access_NestedVector_Value_DynamicIndex) {
     auto* val = b.FunctionParam("arr", ty.array(ty.array(ty.vec4(ty.i32()), 4), 4));
     auto* idx = b.FunctionParam("idx", ty.i32());
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({val, idx});
     b.Append(func->Block(), [&] {
         auto* result = b.Access(ty.i32(), val, 1_u, 2_u, idx);
-        b.Return(func);
+        b.Return(func, result);
         mod.SetName(result, "result");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST(R"(
-         %13 = OpBitcast %uint %idx
-         %14 = OpExtInst %uint %15 UMin %13 %uint_3
-         %18 = OpCompositeExtract %v4int %arr 1 2
-     %result = OpVectorExtractDynamic %int %18 %14
+         %12 = OpBitcast %uint %idx
+         %13 = OpExtInst %uint %14 UMin %12 %uint_3
+         %17 = OpCompositeExtract %v4int %arr 1 2
+     %result = OpVectorExtractDynamic %int %17 %13
 )");
 }
 
 TEST_F(SpirvWriterTest, Access_Struct_Value_ConstantIndex) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("a"), ty.i32()},
                                                    {mod.symbols.Register("b"), ty.vec4<i32>()},
                                                });
     auto* str_val = b.FunctionParam("str", str);
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.i32());
     func->SetParams({str_val});
     b.Append(func->Block(), [&] {
-        auto* result_a = b.Access(ty.f32(), str_val, 0_u);
+        auto* result_a = b.Access(ty.i32(), str_val, 0_u);
         auto* result_b = b.Access(ty.i32(), str_val, 1_u, 2_u);
-        b.Return(func);
+        b.Return(func, b.Add(ty.i32(), result_a, result_b));
         mod.SetName(result_a, "result_a");
         mod.SetName(result_b, "result_b");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result_a = OpCompositeExtract %float %str 0");
+    EXPECT_INST("%result_a = OpCompositeExtract %int %str 0");
     EXPECT_INST("%result_b = OpCompositeExtract %int %str 1 2");
 }
 
 TEST_F(SpirvWriterTest, Access_Struct_Pointer_ConstantIndex) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {
-                                                   {mod.symbols.Register("a"), ty.f32()},
+                                                   {mod.symbols.Register("a"), ty.i32()},
                                                    {mod.symbols.Register("b"), ty.vec4<i32>()},
                                                });
-    auto* func = b.Function("foo", ty.void_());
+    auto* func = b.Function("foo", ty.vec4<i32>());
     b.Append(func->Block(), [&] {
         auto* str_var = b.Var("str", ty.ptr(function, str, read_write));
-        auto* result_a = b.Access(ty.ptr<function, f32>(), str_var, 0_u);
+        auto* result_a = b.Access(ty.ptr<function, i32>(), str_var, 0_u);
         auto* result_b = b.Access(ty.ptr<function, vec4<i32>>(), str_var, 1_u);
-        b.Return(func);
+        auto* val_a = b.Load(result_a);
+        auto* val_b = b.Load(result_b);
+        b.Return(func, b.Add(ty.vec4<i32>(), val_a, val_b));
         mod.SetName(result_a, "result_a");
         mod.SetName(result_b, "result_b");
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result_a = OpAccessChain %_ptr_Function_float %str %uint_0");
+    EXPECT_INST("%result_a = OpAccessChain %_ptr_Function_int %str %uint_0");
     EXPECT_INST("%result_b = OpAccessChain %_ptr_Function_v4int %str %uint_1");
 }
 
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
index 99c7cd5..22c6d85 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
@@ -109,7 +109,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
index 46151c0..81b45cd 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
@@ -115,7 +115,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_ast_printer_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
index 4344f94..b855543 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_writer) {
@@ -79,7 +79,6 @@
 if (tint_build_unittests) {
   if (tint_build_spv_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "accessor_expression_test.cc",
         "assign_test.cc",
@@ -113,7 +112,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/type",
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index fa7d322..e8d1c45 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -24,6 +24,7 @@
 #include "src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h"
 #include "src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h"
 #include "src/tint/lang/spirv/writer/ast_raise/while_to_loop.h"
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
 #include "src/tint/lang/wgsl/ast/transform/add_block_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h"
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
@@ -93,16 +94,21 @@
         data.Add<ast::transform::Robustness::Config>(config);
     }
 
+    ExternalTextureOptions external_texture_options{};
+    RemapperData remapper_data{};
+    PopulateRemapperAndMultiplanarOptions(options, remapper_data, external_texture_options);
+
     // BindingRemapper must come before MultiplanarExternalTexture. Note, this is flipped to the
     // other generators which run Multiplanar first and then binding remapper.
     manager.Add<ast::transform::BindingRemapper>();
+
     data.Add<ast::transform::BindingRemapper::Remappings>(
-        options.binding_remapper_options.binding_points,
-        std::unordered_map<BindingPoint, core::Access>{}, /* allow_collisions */ false);
+        remapper_data, std::unordered_map<BindingPoint, core::Access>{},
+        /* allow_collisions */ false);
 
     // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
     data.Add<ast::transform::MultiplanarExternalTexture::NewBindingPoints>(
-        options.external_texture_options.bindings_map);
+        external_texture_options.bindings_map);
     manager.Add<ast::transform::MultiplanarExternalTexture>();
 
     {  // Builtin polyfills
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index 24fca39..0ebb922 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -260,7 +260,7 @@
 Builder::~Builder() = default;
 
 bool Builder::Build() {
-    if (!tint::writer::CheckSupportedExtensions(
+    if (!tint::wgsl::CheckSupportedExtensions(
             "SPIR-V", builder_.AST(), builder_.Diagnostics(),
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
@@ -1223,20 +1223,24 @@
     return 0;
 }
 
-bool Builder::IsConstructorConst(const ast::Expression* expr) {
+bool Builder::IsConstructorConst(const ast::CallExpression* expr) {
     bool is_const = true;
     ast::TraverseExpressions(expr, [&](const ast::Expression* e) {
+        auto* val = builder_.Sem().GetVal(e);
+        if (!val) {
+            return ast::TraverseAction::Descend;
+        }
+
         if (e->Is<ast::LiteralExpression>()) {
             return ast::TraverseAction::Descend;
         }
-        if (auto* ce = e->As<ast::CallExpression>()) {
-            auto* sem = builder_.Sem().Get(ce);
-            if (sem->Is<sem::Materialize>()) {
+        if (e->Is<ast::CallExpression>()) {
+            if (val->Is<sem::Materialize>()) {
                 // Materialize can only occur on compile time expressions, so this sub-tree must be
                 // constant.
                 return ast::TraverseAction::Skip;
             }
-            auto* call = sem->As<sem::Call>();
+            auto* call = val->As<sem::Call>();
             if (call->Target()->Is<sem::ValueConstructor>()) {
                 return ast::TraverseAction::Descend;
             }
@@ -1949,8 +1953,6 @@
     }
 
     // Create the result matrix from the added/subtracted column vectors
-    TINT_BEGIN_DISABLE_WARNING(MAYBE_UNINITIALIZED);  // GCC false-positive
-
     auto result_mat_id = result_op();
     ops.insert(ops.begin(), result_mat_id);
     ops.insert(ops.begin(), Operand(GenerateTypeIfNeeded(type)));
@@ -1958,8 +1960,6 @@
         return 0;
     }
 
-    TINT_END_DISABLE_WARNING(MAYBE_UNINITIALIZED);  // GCC false-positive
-
     return std::get<uint32_t>(result_mat_id);
 }
 
@@ -2754,8 +2754,8 @@
     auto append_coords_to_spirv_params = [&]() -> bool {
         if (auto* array_index = arg(Usage::kArrayIndex)) {
             // Array index needs to be appended to the coordinates.
-            auto* packed = tint::writer::AppendVector(&builder_, arg(Usage::kCoords)->Declaration(),
-                                                      array_index->Declaration());
+            auto* packed = tint::wgsl::AppendVector(&builder_, arg(Usage::kCoords)->Declaration(),
+                                                    array_index->Declaration());
             auto param = GenerateExpression(packed);
             if (param == 0) {
                 return false;
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.h b/src/tint/lang/spirv/writer/ast_printer/builder.h
index ae4b639..78214d1 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.h
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.h
@@ -465,7 +465,7 @@
     /// Determines if the given value constructor is created from constant values
     /// @param expr the expression to check
     /// @returns true if the constructor is constant
-    bool IsConstructorConst(const ast::Expression* expr);
+    bool IsConstructorConst(const ast::CallExpression* expr);
 
   private:
     /// @returns an Operand with a new result ID in it. Increments the next_id_
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
index b68aec9..836fdc5 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
@@ -89,11 +89,8 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
-    "//src/tint/lang/wgsl/ast/transform:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -113,6 +110,21 @@
       "//src/tint/lang/spirv/writer/ast_raise",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/ast/transform:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -123,3 +135,29 @@
   actual = "//src/tint:tint_build_spv_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_spv_writer",
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.cfg b/src/tint/lang/spirv/writer/ast_raise/BUILD.cfg
index 0a24987..54005ef 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.cfg
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.cfg
@@ -1,3 +1,6 @@
 {
-    "condition": "tint_build_spv_writer"
+    "condition": "tint_build_spv_writer",
+    "test": {
+        "condition": "tint_build_wgsl_reader && tint_build_wgsl_writer",
+    }
 }
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
index b36edc2..20a5f43 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
@@ -69,11 +69,11 @@
 )
 
 endif(TINT_BUILD_SPV_WRITER)
-if(TINT_BUILD_SPV_WRITER)
+if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_spirv_writer_ast_raise_test
 # Kind:      test
-# Condition: TINT_BUILD_SPV_WRITER
+# Condition: TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_ast_raise_test test
   lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
@@ -93,11 +93,8 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
-  tint_lang_wgsl_ast_transform_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -123,4 +120,22 @@
   )
 endif(TINT_BUILD_SPV_WRITER)
 
-endif(TINT_BUILD_SPV_WRITER)
\ No newline at end of file
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_ast_raise_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_ast_raise_test test
+    tint_lang_wgsl_ast_transform_test
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_ast_raise_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.gn b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
index 5ab53d5..735bb0b 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_writer) {
@@ -72,9 +72,9 @@
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_spv_writer) {
+  if (tint_build_spv_writer && tint_build_wgsl_reader &&
+      tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "clamp_frag_depth_test.cc",
         "for_loop_to_loop_test.cc",
@@ -93,11 +93,8 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
-        "${tint_src_dir}/lang/wgsl/ast/transform:unittests",
         "${tint_src_dir}/lang/wgsl/program",
-        "${tint_src_dir}/lang/wgsl/reader",
         "${tint_src_dir}/lang/wgsl/sem",
-        "${tint_src_dir}/lang/wgsl/writer",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -116,6 +113,18 @@
       if (tint_build_spv_writer) {
         deps += [ "${tint_src_dir}/lang/spirv/writer/ast_raise" ]
       }
+
+      if (tint_build_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+
+      if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/ast/transform:unittests" ]
+      }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
     }
   }
 }
diff --git a/src/tint/lang/spirv/writer/binary_test.cc b/src/tint/lang/spirv/writer/binary_test.cc
index 8e3fe2e..45eb416 100644
--- a/src/tint/lang/spirv/writer/binary_test.cc
+++ b/src/tint/lang/spirv/writer/binary_test.cc
@@ -408,6 +408,7 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%16 = OpConstantNull %v4int");
     EXPECT_INST(R"(
                ; Function foo
         %foo = OpFunction %v4int None %6
@@ -425,13 +426,13 @@
       %rhs_0 = OpFunctionParameter %v4int
          %14 = OpLabel
          %15 = OpIEqual %v4bool %rhs_0 %16
-         %20 = OpIEqual %v4bool %lhs_0 %21
-         %23 = OpIEqual %v4bool %rhs_0 %24
-         %26 = OpLogicalAnd %v4bool %20 %23
-         %27 = OpLogicalOr %v4bool %15 %26
-         %28 = OpSelect %v4int %27 %29 %rhs_0
-         %31 = OpSDiv %v4int %lhs_0 %28
-               OpReturnValue %31
+         %19 = OpIEqual %v4bool %lhs_0 %20
+         %22 = OpIEqual %v4bool %rhs_0 %23
+         %25 = OpLogicalAnd %v4bool %19 %22
+         %26 = OpLogicalOr %v4bool %15 %25
+         %27 = OpSelect %v4int %26 %28 %rhs_0
+         %30 = OpSDiv %v4int %lhs_0 %27
+               OpReturnValue %30
                OpFunctionEnd
 )");
 }
@@ -449,6 +450,7 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%16 = OpConstantNull %v4int");
     EXPECT_INST(R"(
                ; Function foo
         %foo = OpFunction %v4int None %6
@@ -466,13 +468,13 @@
       %rhs_0 = OpFunctionParameter %v4int
          %14 = OpLabel
          %15 = OpIEqual %v4bool %rhs_0 %16
-         %20 = OpIEqual %v4bool %lhs_0 %21
-         %23 = OpIEqual %v4bool %rhs_0 %24
-         %26 = OpLogicalAnd %v4bool %20 %23
-         %27 = OpLogicalOr %v4bool %15 %26
-         %28 = OpSelect %v4int %27 %29 %rhs_0
-         %31 = OpSDiv %v4int %lhs_0 %28
-               OpReturnValue %31
+         %19 = OpIEqual %v4bool %lhs_0 %20
+         %22 = OpIEqual %v4bool %rhs_0 %23
+         %25 = OpLogicalAnd %v4bool %19 %22
+         %26 = OpLogicalOr %v4bool %15 %25
+         %27 = OpSelect %v4int %26 %28 %rhs_0
+         %30 = OpSDiv %v4int %lhs_0 %27
+               OpReturnValue %30
                OpFunctionEnd
 )");
 }
@@ -570,6 +572,7 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%16 = OpConstantNull %v4int");
     EXPECT_INST(R"(
                ; Function foo
         %foo = OpFunction %v4int None %6
@@ -587,15 +590,15 @@
       %rhs_0 = OpFunctionParameter %v4int
          %14 = OpLabel
          %15 = OpIEqual %v4bool %rhs_0 %16
-         %20 = OpIEqual %v4bool %lhs_0 %21
-         %23 = OpIEqual %v4bool %rhs_0 %24
-         %26 = OpLogicalAnd %v4bool %20 %23
-         %27 = OpLogicalOr %v4bool %15 %26
-         %28 = OpSelect %v4int %27 %29 %rhs_0
-         %31 = OpSDiv %v4int %lhs_0 %28
-         %32 = OpIMul %v4int %31 %28
-         %33 = OpISub %v4int %lhs_0 %32
-               OpReturnValue %33
+         %19 = OpIEqual %v4bool %lhs_0 %20
+         %22 = OpIEqual %v4bool %rhs_0 %23
+         %25 = OpLogicalAnd %v4bool %19 %22
+         %26 = OpLogicalOr %v4bool %15 %25
+         %27 = OpSelect %v4int %26 %28 %rhs_0
+         %30 = OpSDiv %v4int %lhs_0 %27
+         %31 = OpIMul %v4int %30 %27
+         %32 = OpISub %v4int %lhs_0 %31
+               OpReturnValue %32
                OpFunctionEnd
 )");
 }
@@ -613,6 +616,7 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%16 = OpConstantNull %v4int");
     EXPECT_INST(R"(
                ; Function foo
         %foo = OpFunction %v4int None %6
@@ -630,15 +634,15 @@
       %rhs_0 = OpFunctionParameter %v4int
          %14 = OpLabel
          %15 = OpIEqual %v4bool %rhs_0 %16
-         %20 = OpIEqual %v4bool %lhs_0 %21
-         %23 = OpIEqual %v4bool %rhs_0 %24
-         %26 = OpLogicalAnd %v4bool %20 %23
-         %27 = OpLogicalOr %v4bool %15 %26
-         %28 = OpSelect %v4int %27 %29 %rhs_0
-         %31 = OpSDiv %v4int %lhs_0 %28
-         %32 = OpIMul %v4int %31 %28
-         %33 = OpISub %v4int %lhs_0 %32
-               OpReturnValue %33
+         %19 = OpIEqual %v4bool %lhs_0 %20
+         %22 = OpIEqual %v4bool %rhs_0 %23
+         %25 = OpLogicalAnd %v4bool %19 %22
+         %26 = OpLogicalOr %v4bool %15 %25
+         %27 = OpSelect %v4int %26 %28 %rhs_0
+         %30 = OpSDiv %v4int %lhs_0 %27
+         %31 = OpIMul %v4int %30 %27
+         %32 = OpISub %v4int %lhs_0 %31
+               OpReturnValue %32
                OpFunctionEnd
 )");
 }
diff --git a/src/tint/lang/spirv/writer/builtin_test.cc b/src/tint/lang/spirv/writer/builtin_test.cc
index ababde5..adce399 100644
--- a/src/tint/lang/spirv/writer/builtin_test.cc
+++ b/src/tint/lang/spirv/writer/builtin_test.cc
@@ -687,38 +687,38 @@
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST("%8 = OpConstantComposite %v2uint %uint_65535 %uint_65535");
     EXPECT_INST("%13 = OpConstantComposite %v2uint %uint_16 %uint_16");
-    EXPECT_INST("%15 = OpConstantComposite %v2uint %uint_0 %uint_0");
-    EXPECT_INST("%19 = OpConstantComposite %v2uint %uint_16777215 %uint_16777215");
-    EXPECT_INST("%22 = OpConstantComposite %v2uint %uint_8 %uint_8");
-    EXPECT_INST("%26 = OpConstantComposite %v2uint %uint_268435455 %uint_268435455");
-    EXPECT_INST("%29 = OpConstantComposite %v2uint %uint_4 %uint_4");
-    EXPECT_INST("%33 = OpConstantComposite %v2uint %uint_1073741823 %uint_1073741823");
-    EXPECT_INST("%36 = OpConstantComposite %v2uint %uint_2 %uint_2");
-    EXPECT_INST("%40 = OpConstantComposite %v2uint %uint_2147483647 %uint_2147483647");
-    EXPECT_INST("%43 = OpConstantComposite %v2uint %uint_1 %uint_1");
+    EXPECT_INST("%15 = OpConstantNull %v2uint");
+    EXPECT_INST("%18 = OpConstantComposite %v2uint %uint_16777215 %uint_16777215");
+    EXPECT_INST("%21 = OpConstantComposite %v2uint %uint_8 %uint_8");
+    EXPECT_INST("%25 = OpConstantComposite %v2uint %uint_268435455 %uint_268435455");
+    EXPECT_INST("%28 = OpConstantComposite %v2uint %uint_4 %uint_4");
+    EXPECT_INST("%32 = OpConstantComposite %v2uint %uint_1073741823 %uint_1073741823");
+    EXPECT_INST("%35 = OpConstantComposite %v2uint %uint_2 %uint_2");
+    EXPECT_INST("%39 = OpConstantComposite %v2uint %uint_2147483647 %uint_2147483647");
+    EXPECT_INST("%42 = OpConstantComposite %v2uint %uint_1 %uint_1");
     EXPECT_INST(R"(
           %7 = OpULessThanEqual %v2bool %arg %8
          %12 = OpSelect %v2uint %7 %13 %15
-         %17 = OpShiftLeftLogical %v2uint %arg %12
-         %18 = OpULessThanEqual %v2bool %17 %19
-         %21 = OpSelect %v2uint %18 %22 %15
-         %24 = OpShiftLeftLogical %v2uint %17 %21
-         %25 = OpULessThanEqual %v2bool %24 %26
-         %28 = OpSelect %v2uint %25 %29 %15
-         %31 = OpShiftLeftLogical %v2uint %24 %28
-         %32 = OpULessThanEqual %v2bool %31 %33
-         %35 = OpSelect %v2uint %32 %36 %15
-         %38 = OpShiftLeftLogical %v2uint %31 %35
-         %39 = OpULessThanEqual %v2bool %38 %40
-         %42 = OpSelect %v2uint %39 %43 %15
-         %45 = OpIEqual %v2bool %38 %15
-         %46 = OpSelect %v2uint %45 %43 %15
-         %47 = OpBitwiseOr %v2uint %42 %46
-         %48 = OpBitwiseOr %v2uint %35 %47
-         %49 = OpBitwiseOr %v2uint %28 %48
-         %50 = OpBitwiseOr %v2uint %21 %49
-         %51 = OpBitwiseOr %v2uint %12 %50
-     %result = OpIAdd %v2uint %51 %46
+         %16 = OpShiftLeftLogical %v2uint %arg %12
+         %17 = OpULessThanEqual %v2bool %16 %18
+         %20 = OpSelect %v2uint %17 %21 %15
+         %23 = OpShiftLeftLogical %v2uint %16 %20
+         %24 = OpULessThanEqual %v2bool %23 %25
+         %27 = OpSelect %v2uint %24 %28 %15
+         %30 = OpShiftLeftLogical %v2uint %23 %27
+         %31 = OpULessThanEqual %v2bool %30 %32
+         %34 = OpSelect %v2uint %31 %35 %15
+         %37 = OpShiftLeftLogical %v2uint %30 %34
+         %38 = OpULessThanEqual %v2bool %37 %39
+         %41 = OpSelect %v2uint %38 %42 %15
+         %44 = OpIEqual %v2bool %37 %15
+         %45 = OpSelect %v2uint %44 %42 %15
+         %46 = OpBitwiseOr %v2uint %41 %45
+         %47 = OpBitwiseOr %v2uint %34 %46
+         %48 = OpBitwiseOr %v2uint %27 %47
+         %49 = OpBitwiseOr %v2uint %20 %48
+         %50 = OpBitwiseOr %v2uint %12 %49
+     %result = OpIAdd %v2uint %50 %45
 )");
 }
 
@@ -818,42 +818,42 @@
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST("%8 = OpConstantComposite %v2uint %uint_65535 %uint_65535");
-    EXPECT_INST("%11 = OpConstantComposite %v2uint %uint_0 %uint_0");
-    EXPECT_INST("%16 = OpConstantComposite %v2uint %uint_16 %uint_16");
-    EXPECT_INST("%20 = OpConstantComposite %v2uint %uint_255 %uint_255");
-    EXPECT_INST("%24 = OpConstantComposite %v2uint %uint_8 %uint_8");
-    EXPECT_INST("%28 = OpConstantComposite %v2uint %uint_15 %uint_15");
-    EXPECT_INST("%32 = OpConstantComposite %v2uint %uint_4 %uint_4");
-    EXPECT_INST("%36 = OpConstantComposite %v2uint %uint_3 %uint_3");
-    EXPECT_INST("%40 = OpConstantComposite %v2uint %uint_2 %uint_2");
-    EXPECT_INST("%44 = OpConstantComposite %v2uint %uint_1 %uint_1");
+    EXPECT_INST("%11 = OpConstantNull %v2uint");
+    EXPECT_INST("%15 = OpConstantComposite %v2uint %uint_16 %uint_16");
+    EXPECT_INST("%19 = OpConstantComposite %v2uint %uint_255 %uint_255");
+    EXPECT_INST("%23 = OpConstantComposite %v2uint %uint_8 %uint_8");
+    EXPECT_INST("%27 = OpConstantComposite %v2uint %uint_15 %uint_15");
+    EXPECT_INST("%31 = OpConstantComposite %v2uint %uint_4 %uint_4");
+    EXPECT_INST("%35 = OpConstantComposite %v2uint %uint_3 %uint_3");
+    EXPECT_INST("%39 = OpConstantComposite %v2uint %uint_2 %uint_2");
+    EXPECT_INST("%43 = OpConstantComposite %v2uint %uint_1 %uint_1");
     EXPECT_INST(R"(
           %7 = OpBitwiseAnd %v2uint %arg %8
          %10 = OpIEqual %v2bool %7 %11
-         %15 = OpSelect %v2uint %10 %16 %11
-         %18 = OpShiftRightLogical %v2uint %arg %15
-         %19 = OpBitwiseAnd %v2uint %18 %20
-         %22 = OpIEqual %v2bool %19 %11
-         %23 = OpSelect %v2uint %22 %24 %11
-         %26 = OpShiftRightLogical %v2uint %18 %23
-         %27 = OpBitwiseAnd %v2uint %26 %28
-         %30 = OpIEqual %v2bool %27 %11
-         %31 = OpSelect %v2uint %30 %32 %11
-         %34 = OpShiftRightLogical %v2uint %26 %31
-         %35 = OpBitwiseAnd %v2uint %34 %36
-         %38 = OpIEqual %v2bool %35 %11
-         %39 = OpSelect %v2uint %38 %40 %11
-         %42 = OpShiftRightLogical %v2uint %34 %39
-         %43 = OpBitwiseAnd %v2uint %42 %44
-         %46 = OpIEqual %v2bool %43 %11
-         %47 = OpSelect %v2uint %46 %44 %11
-         %48 = OpIEqual %v2bool %42 %11
-         %49 = OpSelect %v2uint %48 %44 %11
-         %50 = OpBitwiseOr %v2uint %39 %47
-         %51 = OpBitwiseOr %v2uint %31 %50
-         %52 = OpBitwiseOr %v2uint %23 %51
-         %53 = OpBitwiseOr %v2uint %15 %52
-     %result = OpIAdd %v2uint %53 %49
+         %14 = OpSelect %v2uint %10 %15 %11
+         %17 = OpShiftRightLogical %v2uint %arg %14
+         %18 = OpBitwiseAnd %v2uint %17 %19
+         %21 = OpIEqual %v2bool %18 %11
+         %22 = OpSelect %v2uint %21 %23 %11
+         %25 = OpShiftRightLogical %v2uint %17 %22
+         %26 = OpBitwiseAnd %v2uint %25 %27
+         %29 = OpIEqual %v2bool %26 %11
+         %30 = OpSelect %v2uint %29 %31 %11
+         %33 = OpShiftRightLogical %v2uint %25 %30
+         %34 = OpBitwiseAnd %v2uint %33 %35
+         %37 = OpIEqual %v2bool %34 %11
+         %38 = OpSelect %v2uint %37 %39 %11
+         %41 = OpShiftRightLogical %v2uint %33 %38
+         %42 = OpBitwiseAnd %v2uint %41 %43
+         %45 = OpIEqual %v2bool %42 %11
+         %46 = OpSelect %v2uint %45 %43 %11
+         %47 = OpIEqual %v2bool %41 %11
+         %48 = OpSelect %v2uint %47 %43 %11
+         %49 = OpBitwiseOr %v2uint %38 %46
+         %50 = OpBitwiseOr %v2uint %30 %49
+         %51 = OpBitwiseOr %v2uint %22 %50
+         %52 = OpBitwiseOr %v2uint %14 %51
+     %result = OpIAdd %v2uint %52 %48
 )");
 }
 
@@ -954,42 +954,42 @@
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST("%8 = OpConstantComposite %v2uint %uint_4294901760 %uint_4294901760");
-    EXPECT_INST("%11 = OpConstantComposite %v2uint %uint_0 %uint_0");
-    EXPECT_INST("%16 = OpConstantComposite %v2uint %uint_16 %uint_16");
-    EXPECT_INST("%20 = OpConstantComposite %v2uint %uint_65280 %uint_65280");
-    EXPECT_INST("%24 = OpConstantComposite %v2uint %uint_8 %uint_8");
-    EXPECT_INST("%28 = OpConstantComposite %v2uint %uint_240 %uint_240");
-    EXPECT_INST("%32 = OpConstantComposite %v2uint %uint_4 %uint_4");
-    EXPECT_INST("%36 = OpConstantComposite %v2uint %uint_12 %uint_12");
-    EXPECT_INST("%40 = OpConstantComposite %v2uint %uint_2 %uint_2");
-    EXPECT_INST("%46 = OpConstantComposite %v2uint %uint_1 %uint_1");
-    EXPECT_INST("%54 = OpConstantComposite %v2uint %uint_4294967295 %uint_4294967295");
+    EXPECT_INST("%11 = OpConstantNull %v2uint");
+    EXPECT_INST("%15 = OpConstantComposite %v2uint %uint_16 %uint_16");
+    EXPECT_INST("%19 = OpConstantComposite %v2uint %uint_65280 %uint_65280");
+    EXPECT_INST("%23 = OpConstantComposite %v2uint %uint_8 %uint_8");
+    EXPECT_INST("%27 = OpConstantComposite %v2uint %uint_240 %uint_240");
+    EXPECT_INST("%31 = OpConstantComposite %v2uint %uint_4 %uint_4");
+    EXPECT_INST("%35 = OpConstantComposite %v2uint %uint_12 %uint_12");
+    EXPECT_INST("%39 = OpConstantComposite %v2uint %uint_2 %uint_2");
+    EXPECT_INST("%45 = OpConstantComposite %v2uint %uint_1 %uint_1");
+    EXPECT_INST("%53 = OpConstantComposite %v2uint %uint_4294967295 %uint_4294967295");
     EXPECT_INST(R"(
           %7 = OpBitwiseAnd %v2uint %arg %8
          %10 = OpIEqual %v2bool %7 %11
-         %15 = OpSelect %v2uint %10 %11 %16
-         %18 = OpShiftRightLogical %v2uint %arg %15
-         %19 = OpBitwiseAnd %v2uint %18 %20
-         %22 = OpIEqual %v2bool %19 %11
-         %23 = OpSelect %v2uint %22 %11 %24
-         %26 = OpShiftRightLogical %v2uint %18 %23
-         %27 = OpBitwiseAnd %v2uint %26 %28
-         %30 = OpIEqual %v2bool %27 %11
-         %31 = OpSelect %v2uint %30 %11 %32
-         %34 = OpShiftRightLogical %v2uint %26 %31
-         %35 = OpBitwiseAnd %v2uint %34 %36
-         %38 = OpIEqual %v2bool %35 %11
-         %39 = OpSelect %v2uint %38 %11 %40
-         %42 = OpShiftRightLogical %v2uint %34 %39
-         %43 = OpBitwiseAnd %v2uint %42 %40
-         %44 = OpIEqual %v2bool %43 %11
-         %45 = OpSelect %v2uint %44 %11 %46
-         %48 = OpBitwiseOr %v2uint %39 %45
-         %49 = OpBitwiseOr %v2uint %31 %48
-         %50 = OpBitwiseOr %v2uint %23 %49
-         %51 = OpBitwiseOr %v2uint %15 %50
-         %52 = OpIEqual %v2bool %42 %11
-     %result = OpSelect %v2uint %52 %54 %51
+         %14 = OpSelect %v2uint %10 %11 %15
+         %17 = OpShiftRightLogical %v2uint %arg %14
+         %18 = OpBitwiseAnd %v2uint %17 %19
+         %21 = OpIEqual %v2bool %18 %11
+         %22 = OpSelect %v2uint %21 %11 %23
+         %25 = OpShiftRightLogical %v2uint %17 %22
+         %26 = OpBitwiseAnd %v2uint %25 %27
+         %29 = OpIEqual %v2bool %26 %11
+         %30 = OpSelect %v2uint %29 %11 %31
+         %33 = OpShiftRightLogical %v2uint %25 %30
+         %34 = OpBitwiseAnd %v2uint %33 %35
+         %37 = OpIEqual %v2bool %34 %11
+         %38 = OpSelect %v2uint %37 %11 %39
+         %41 = OpShiftRightLogical %v2uint %33 %38
+         %42 = OpBitwiseAnd %v2uint %41 %39
+         %43 = OpIEqual %v2bool %42 %11
+         %44 = OpSelect %v2uint %43 %11 %45
+         %47 = OpBitwiseOr %v2uint %38 %44
+         %48 = OpBitwiseOr %v2uint %30 %47
+         %49 = OpBitwiseOr %v2uint %22 %48
+         %50 = OpBitwiseOr %v2uint %14 %49
+         %51 = OpIEqual %v2bool %41 %11
+     %result = OpSelect %v2uint %51 %53 %50
 )");
 }
 
@@ -1087,42 +1087,42 @@
 
     ASSERT_TRUE(Generate()) << Error() << output_;
     EXPECT_INST("%8 = OpConstantComposite %v2uint %uint_65535 %uint_65535");
-    EXPECT_INST("%11 = OpConstantComposite %v2uint %uint_0 %uint_0");
-    EXPECT_INST("%16 = OpConstantComposite %v2uint %uint_16 %uint_16");
-    EXPECT_INST("%20 = OpConstantComposite %v2uint %uint_255 %uint_255");
-    EXPECT_INST("%24 = OpConstantComposite %v2uint %uint_8 %uint_8");
-    EXPECT_INST("%28 = OpConstantComposite %v2uint %uint_15 %uint_15");
-    EXPECT_INST("%32 = OpConstantComposite %v2uint %uint_4 %uint_4");
-    EXPECT_INST("%36 = OpConstantComposite %v2uint %uint_3 %uint_3");
-    EXPECT_INST("%40 = OpConstantComposite %v2uint %uint_2 %uint_2");
-    EXPECT_INST("%44 = OpConstantComposite %v2uint %uint_1 %uint_1");
-    EXPECT_INST("%54 = OpConstantComposite %v2uint %uint_4294967295 %uint_4294967295");
+    EXPECT_INST("%11 = OpConstantNull %v2uint");
+    EXPECT_INST("%15 = OpConstantComposite %v2uint %uint_16 %uint_16");
+    EXPECT_INST("%19 = OpConstantComposite %v2uint %uint_255 %uint_255");
+    EXPECT_INST("%23 = OpConstantComposite %v2uint %uint_8 %uint_8");
+    EXPECT_INST("%27 = OpConstantComposite %v2uint %uint_15 %uint_15");
+    EXPECT_INST("%31 = OpConstantComposite %v2uint %uint_4 %uint_4");
+    EXPECT_INST("%35 = OpConstantComposite %v2uint %uint_3 %uint_3");
+    EXPECT_INST("%39 = OpConstantComposite %v2uint %uint_2 %uint_2");
+    EXPECT_INST("%43 = OpConstantComposite %v2uint %uint_1 %uint_1");
+    EXPECT_INST("%53 = OpConstantComposite %v2uint %uint_4294967295 %uint_4294967295");
     EXPECT_INST(R"(
           %7 = OpBitwiseAnd %v2uint %arg %8
          %10 = OpIEqual %v2bool %7 %11
-         %15 = OpSelect %v2uint %10 %16 %11
-         %18 = OpShiftRightLogical %v2uint %arg %15
-         %19 = OpBitwiseAnd %v2uint %18 %20
-         %22 = OpIEqual %v2bool %19 %11
-         %23 = OpSelect %v2uint %22 %24 %11
-         %26 = OpShiftRightLogical %v2uint %18 %23
-         %27 = OpBitwiseAnd %v2uint %26 %28
-         %30 = OpIEqual %v2bool %27 %11
-         %31 = OpSelect %v2uint %30 %32 %11
-         %34 = OpShiftRightLogical %v2uint %26 %31
-         %35 = OpBitwiseAnd %v2uint %34 %36
-         %38 = OpIEqual %v2bool %35 %11
-         %39 = OpSelect %v2uint %38 %40 %11
-         %42 = OpShiftRightLogical %v2uint %34 %39
-         %43 = OpBitwiseAnd %v2uint %42 %44
-         %46 = OpIEqual %v2bool %43 %11
-         %47 = OpSelect %v2uint %46 %44 %11
-         %48 = OpBitwiseOr %v2uint %39 %47
-         %49 = OpBitwiseOr %v2uint %31 %48
-         %50 = OpBitwiseOr %v2uint %23 %49
-         %51 = OpBitwiseOr %v2uint %15 %50
-         %52 = OpIEqual %v2bool %42 %11
-     %result = OpSelect %v2uint %52 %54 %51
+         %14 = OpSelect %v2uint %10 %15 %11
+         %17 = OpShiftRightLogical %v2uint %arg %14
+         %18 = OpBitwiseAnd %v2uint %17 %19
+         %21 = OpIEqual %v2bool %18 %11
+         %22 = OpSelect %v2uint %21 %23 %11
+         %25 = OpShiftRightLogical %v2uint %17 %22
+         %26 = OpBitwiseAnd %v2uint %25 %27
+         %29 = OpIEqual %v2bool %26 %11
+         %30 = OpSelect %v2uint %29 %31 %11
+         %33 = OpShiftRightLogical %v2uint %25 %30
+         %34 = OpBitwiseAnd %v2uint %33 %35
+         %37 = OpIEqual %v2bool %34 %11
+         %38 = OpSelect %v2uint %37 %39 %11
+         %41 = OpShiftRightLogical %v2uint %33 %38
+         %42 = OpBitwiseAnd %v2uint %41 %43
+         %45 = OpIEqual %v2bool %42 %11
+         %46 = OpSelect %v2uint %45 %43 %11
+         %47 = OpBitwiseOr %v2uint %38 %46
+         %48 = OpBitwiseOr %v2uint %30 %47
+         %49 = OpBitwiseOr %v2uint %22 %48
+         %50 = OpBitwiseOr %v2uint %14 %49
+         %51 = OpIEqual %v2bool %41 %11
+     %result = OpSelect %v2uint %51 %53 %50
 )");
 }
 
@@ -1151,11 +1151,10 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%9 = OpConstantNull %v4half");
     EXPECT_INST(
-        "%9 = OpConstantComposite %v4half %half_0x0p_0 %half_0x0p_0 %half_0x0p_0 %half_0x0p_0");
-    EXPECT_INST(
-        "%11 = OpConstantComposite %v4half %half_0x1p_0 %half_0x1p_0 %half_0x1p_0 %half_0x1p_0");
-    EXPECT_INST("%result = OpExtInst %v4half %8 NClamp %arg %9 %11");
+        "%10 = OpConstantComposite %v4half %half_0x1p_0 %half_0x1p_0 %half_0x1p_0 %half_0x1p_0");
+    EXPECT_INST("%result = OpExtInst %v4half %8 NClamp %arg %9 %10");
 }
 
 // Tests for builtins with the signature: T = func(T, T)
@@ -1981,8 +1980,8 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%1 = OpVariable %_ptr_StorageBuffer_tint_symbol StorageBuffer");
-    EXPECT_INST("%result = OpArrayLength %uint %1 2");
+    EXPECT_INST("%var = OpVariable %_ptr_StorageBuffer_Buffer StorageBuffer");
+    EXPECT_INST("%result = OpArrayLength %uint %var 2");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/lang/spirv/writer/common/BUILD.bazel b/src/tint/lang/spirv/writer/common/BUILD.bazel
index bf196f2..ecd94ab 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/common/BUILD.bazel
@@ -31,6 +31,7 @@
     "instruction.cc",
     "module.cc",
     "operand.cc",
+    "option_builder.cc",
   ],
   hdrs = [
     "binary_writer.h",
@@ -38,13 +39,14 @@
     "instruction.h",
     "module.h",
     "operand.h",
+    "option_builder.h",
     "options.h",
   ],
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
-    "//src/tint/lang/core",
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
@@ -76,7 +78,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/common/BUILD.cmake b/src/tint/lang/spirv/writer/common/BUILD.cmake
index 086cac1..dcb2f58 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/common/BUILD.cmake
@@ -38,14 +38,16 @@
   lang/spirv/writer/common/module.h
   lang/spirv/writer/common/operand.cc
   lang/spirv/writer/common/operand.h
+  lang/spirv/writer/common/option_builder.cc
+  lang/spirv/writer/common/option_builder.h
   lang/spirv/writer/common/options.h
 )
 
 tint_target_add_dependencies(tint_lang_spirv_writer_common lib
   tint_api_common
   tint_api_options
-  tint_lang_core
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
   tint_utils_math
@@ -81,7 +83,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_common_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
diff --git a/src/tint/lang/spirv/writer/common/BUILD.gn b/src/tint/lang/spirv/writer/common/BUILD.gn
index 5c12785..becae92 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.gn
+++ b/src/tint/lang/spirv/writer/common/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_writer) {
@@ -41,13 +41,15 @@
       "module.h",
       "operand.cc",
       "operand.h",
+      "option_builder.cc",
+      "option_builder.h",
       "options.h",
     ]
     deps = [
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/api/options",
-      "${tint_src_dir}/lang/core",
       "${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",
@@ -66,7 +68,6 @@
 if (tint_build_unittests) {
   if (tint_build_spv_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "binary_writer_test.cc",
         "helper_test.h",
@@ -79,7 +80,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/common/module.h b/src/tint/lang/spirv/writer/common/module.h
index df95adc..bd209cc 100644
--- a/src/tint/lang/spirv/writer/common/module.h
+++ b/src/tint/lang/spirv/writer/common/module.h
@@ -17,6 +17,7 @@
 
 #include <cstdint>
 #include <functional>
+#include <string>
 #include <vector>
 
 #include "src/tint/lang/spirv/writer/common/function.h"
@@ -154,7 +155,7 @@
     InstructionList annotations_;
     std::vector<Function> functions_;
     Hashset<uint32_t, 8> capability_set_;
-    Hashset<const char*, 8> extension_set_;
+    Hashset<std::string, 8> extension_set_;
 };
 
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/common/option_builder.cc b/src/tint/lang/spirv/writer/common/option_builder.cc
new file mode 100644
index 0000000..1bec1e9
--- /dev/null
+++ b/src/tint/lang/spirv/writer/common/option_builder.cc
@@ -0,0 +1,224 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
+
+#include "src/tint/utils/containers/hashset.h"
+
+namespace tint::spirv::writer {
+
+bool ValidateBindingOptions(const Options& options, diag::List& diagnostics) {
+    tint::Hashset<tint::BindingPoint, 8> seen_wgsl_bindings{};
+    tint::Hashset<binding::BindingInfo, 8> seen_spirv_bindings{};
+
+    auto wgsl_seen = [&diagnostics, &seen_wgsl_bindings](const tint::BindingPoint& info) -> bool {
+        if (seen_wgsl_bindings.Contains(info)) {
+            std::stringstream str;
+            str << "Found duplicate WGSL binding point: " << info;
+
+            diagnostics.add_error(diag::System::Writer, str.str());
+            return true;
+        }
+        seen_wgsl_bindings.Add(info);
+        return false;
+    };
+
+    auto spirv_seen = [&diagnostics,
+                       &seen_spirv_bindings](const binding::BindingInfo& info) -> bool {
+        if (seen_spirv_bindings.Contains(info)) {
+            std::stringstream str;
+            str << "Found duplicate SPIR-V binding point: [group: " << info.group
+                << ", binding: " << info.binding << "]";
+            diagnostics.add_error(diag::System::Writer, str.str());
+            return true;
+        }
+        seen_spirv_bindings.Add(info);
+        return false;
+    };
+
+    auto valid = [&wgsl_seen, &spirv_seen](const auto& hsh) -> bool {
+        for (const auto it : hsh) {
+            const auto& src_binding = it.first;
+            const auto& dst_binding = it.second;
+
+            if (wgsl_seen(src_binding)) {
+                return false;
+            }
+
+            if (spirv_seen(dst_binding)) {
+                return false;
+            }
+        }
+        return true;
+    };
+
+    if (!valid(options.bindings.uniform)) {
+        diagnostics.add_note(diag::System::Writer, "When processing uniform", {});
+        return false;
+    }
+    if (!valid(options.bindings.storage)) {
+        diagnostics.add_note(diag::System::Writer, "When processing storage", {});
+        return false;
+    }
+    if (!valid(options.bindings.texture)) {
+        diagnostics.add_note(diag::System::Writer, "When processing texture", {});
+        return false;
+    }
+    if (!valid(options.bindings.storage_texture)) {
+        diagnostics.add_note(diag::System::Writer, "When processing storage_texture", {});
+        return false;
+    }
+    if (!valid(options.bindings.sampler)) {
+        diagnostics.add_note(diag::System::Writer, "When processing sampler", {});
+        return false;
+    }
+
+    for (const auto it : options.bindings.external_texture) {
+        const auto& src_binding = it.first;
+        const auto& plane0 = it.second.plane0;
+        const auto& plane1 = it.second.plane1;
+        const auto& metadata = it.second.metadata;
+
+        // Validate with the actual source regardless of what the remapper will do
+        if (wgsl_seen(src_binding)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+
+        if (spirv_seen(plane0)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+        if (spirv_seen(plane1)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+        if (spirv_seen(metadata)) {
+            diagnostics.add_note(diag::System::Writer, "When processing external_texture", {});
+            return false;
+        }
+    }
+
+    return true;
+}
+
+// The remapped binding data and external texture data need to coordinate in order to put things in
+// the correct place when we're done.
+//
+// When the data comes in we have a list of all WGSL origin (group,binding) pairs to SPIR-V
+// (group,binding) pairs in the `uniform`, `storage`, `texture`, and `sampler` arrays.
+//
+// The `external_texture` array stores a WGSL origin (group,binding) pair for the external textures
+// which provide `plane0`, `plane1`, and `metadata` SPIR-V (group,binding) pairs.
+//
+// If the remapper is run first, then the `external_texture` will end up being moved from the WGSL
+// point, or the SPIR-V point (or the `plane0` value). There will also, possibly, have been bindings
+// moved aside in order to place the `external_texture` bindings.
+//
+// If multiplanar runs first, care needs to be taken that when the texture is split and we create
+// `plane1` and `metadata` that they do not collide with existing bindings. If they would collide
+// then we need to place them elsewhere and have the remapper place them in the correct locations.
+//
+// # Example
+// WGSL:
+//   @group(0) @binding(0) var<uniform> u: Uniforms;
+//   @group(0) @binding(1) var s: sampler;
+//   @group(0) @binding(2) var t: texture_external;
+//
+// Given that program, Dawn may decide to do the remappings such that:
+//   * WGSL u (0, 0) -> SPIR-V (0, 1)
+//   * WGSL s (0, 1) -> SPIR-V (0, 2)
+//   * WGSL t (0, 2):
+//     * plane0 -> SPIR-V (0, 3)
+//     * plane1 -> SPIR-V (0, 4)
+//     * metadata -> SPIR-V (0, 0)
+//
+// In this case, if we run binding remapper first, then tell multiplanar to look for the texture at
+// (0, 3) instead of the original (0, 2).
+//
+// If multiplanar runs first, then metadata (0, 0) needs to be placed elsewhere and then remapped
+// back to (0, 0) by the remapper. (Otherwise, we'll have two `@group(0) @binding(0)` items in the
+// program.)
+//
+// # Status
+// The below method assumes we run binding remapper first. So it will setup the binding data and
+// switch the value used by the multiplanar.
+void PopulateRemapperAndMultiplanarOptions(const Options& options,
+                                           RemapperData& remapper_data,
+                                           ExternalTextureOptions& external_texture) {
+    auto create_remappings = [&remapper_data](const auto& hsh) {
+        for (const auto it : hsh) {
+            const BindingPoint& src_binding_point = it.first;
+            const binding::Uniform& dst_binding_point = it.second;
+
+            // Bindings which go to the same slot in SPIR-V do not need to be re-bound.
+            if (src_binding_point.group == dst_binding_point.group &&
+                src_binding_point.binding == dst_binding_point.binding) {
+                continue;
+            }
+
+            remapper_data.emplace(src_binding_point,
+                                  BindingPoint{dst_binding_point.group, dst_binding_point.binding});
+        }
+    };
+
+    create_remappings(options.bindings.uniform);
+    create_remappings(options.bindings.storage);
+    create_remappings(options.bindings.texture);
+    create_remappings(options.bindings.storage_texture);
+    create_remappings(options.bindings.sampler);
+
+    // External textures are re-bound to their plane0 location
+    for (const auto it : options.bindings.external_texture) {
+        const BindingPoint& src_binding_point = it.first;
+        const binding::BindingInfo& plane0 = it.second.plane0;
+        const binding::BindingInfo& plane1 = it.second.plane1;
+        const binding::BindingInfo& metadata = it.second.metadata;
+
+        BindingPoint plane0_binding_point{plane0.group, plane0.binding};
+        BindingPoint plane1_binding_point{plane1.group, plane1.binding};
+        BindingPoint metadata_binding_point{metadata.group, metadata.binding};
+
+        // Use the re-bound spir-v plane0 value for the lookup key.
+        external_texture.bindings_map.emplace(
+            plane0_binding_point,
+            ExternalTextureOptions::BindingPoints{plane1_binding_point, metadata_binding_point});
+
+        // Bindings which go to the same slot in SPIR-V do not need to be re-bound.
+        if (src_binding_point.group == plane0.group &&
+            src_binding_point.binding == plane0.binding) {
+            continue;
+        }
+
+        remapper_data.emplace(src_binding_point, BindingPoint{plane0.group, plane0.binding});
+    }
+}
+
+}  // namespace tint::spirv::writer
+
+namespace std {
+
+/// Custom std::hash specialization for tint::spirv::writer::binding::BindingInfo so
+/// they can be used as keys for std::unordered_map and std::unordered_set.
+template <>
+class hash<tint::spirv::writer::binding::BindingInfo> {
+  public:
+    /// @param info the binding to create a hash for
+    /// @return the hash value
+    inline std::size_t operator()(const tint::spirv::writer::binding::BindingInfo& info) const {
+        return tint::Hash(info.group, info.binding);
+    }
+};
+
+}  // namespace std
diff --git a/src/tint/lang/spirv/writer/common/option_builder.h b/src/tint/lang/spirv/writer/common/option_builder.h
new file mode 100644
index 0000000..0187b48
--- /dev/null
+++ b/src/tint/lang/spirv/writer/common/option_builder.h
@@ -0,0 +1,44 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTION_BUILDER_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTION_BUILDER_H_
+
+#include <unordered_map>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/api/options/external_texture.h"
+#include "src/tint/lang/spirv/writer/common/options.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
+
+namespace tint::spirv::writer {
+
+using RemapperData = std::unordered_map<BindingPoint, BindingPoint>;
+
+/// @param options the options
+/// @returns true if the binding points are valid
+bool ValidateBindingOptions(const Options& options, diag::List& diagnostics);
+
+/// Populates data from the writer options for the remapper and external texture.
+/// @param options the writer options
+/// @param remapper_data where to put the remapper data
+/// @param external_texture where to store the external texture options
+/// Note, these are populated together because there are dependencies between the two types of data.
+void PopulateRemapperAndMultiplanarOptions(const Options& options,
+                                           RemapperData& remapper_data,
+                                           ExternalTextureOptions& external_texture);
+
+}  // namespace tint::spirv::writer
+
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTION_BUILDER_H_
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index 57e81e8..b18ae0b 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -15,11 +15,83 @@
 #ifndef SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTIONS_H_
 #define SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTIONS_H_
 
-#include "src/tint/api/options/binding_remapper.h"
-#include "src/tint/api/options/external_texture.h"
+#include <unordered_map>
+
+#include "src/tint/api/common/binding_point.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::spirv::writer {
+namespace binding {
+
+/// Generic binding point
+struct BindingInfo {
+    /// The group
+    uint32_t group = 0;
+    /// The binding
+    uint32_t binding = 0;
+
+    /// Equality operator
+    /// @param rhs the BindingInfo to compare against
+    /// @returns true if this BindingInfo is equal to `rhs`
+    inline bool operator==(const BindingInfo& rhs) const {
+        return group == rhs.group && binding == rhs.binding;
+    }
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(group, binding);
+};
+using Uniform = BindingInfo;
+using Storage = BindingInfo;
+using Texture = BindingInfo;
+using StorageTexture = BindingInfo;
+using Sampler = BindingInfo;
+
+/// An external texture
+struct ExternalTexture {
+    /// Metadata
+    BindingInfo metadata{};
+    /// Plane0 binding data
+    BindingInfo plane0{};
+    /// Plane1 binding data
+    BindingInfo plane1{};
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(metadata, plane0, plane1);
+};
+
+}  // namespace binding
+
+// Maps the WGSL binding point to the SPIR-V group,binding for uniforms
+using UniformBindings = std::unordered_map<BindingPoint, binding::Uniform>;
+// Maps the WGSL binding point to the SPIR-V group,binding for storage
+using StorageBindings = std::unordered_map<BindingPoint, binding::Storage>;
+// Maps the WGSL binding point to the SPIR-V group,binding for textures
+using TextureBindings = std::unordered_map<BindingPoint, binding::Texture>;
+// Maps the WGSL binding point to the SPIR-V group,binding for storage textures
+using StorageTextureBindings = std::unordered_map<BindingPoint, binding::StorageTexture>;
+// Maps the WGSL binding point to the SPIR-V group,binding for samplers
+using SamplerBindings = std::unordered_map<BindingPoint, binding::Sampler>;
+// Maps the WGSL binding point to the plane0, plane1, and metadata information for external textures
+using ExternalTextureBindings = std::unordered_map<BindingPoint, binding::ExternalTexture>;
+
+/// Binding information
+struct Bindings {
+    /// Uniform bindings
+    UniformBindings uniform{};
+    /// Storage bindings
+    StorageBindings storage{};
+    /// Texture bindings
+    TextureBindings texture{};
+    /// Storage texture bindings
+    StorageTextureBindings storage_texture{};
+    /// Sampler bindings
+    SamplerBindings sampler{};
+    /// External bindings
+    ExternalTextureBindings external_texture{};
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(uniform, storage, texture, sampler, external_texture);
+};
 
 /// Configuration options used for generating SPIR-V.
 struct Options {
@@ -46,20 +118,20 @@
     /// Set to `true` to clamp frag depth
     bool clamp_frag_depth = false;
 
+    /// Set to `true` to always pass matrices to user functions by pointer instead of by value.
+    bool pass_matrix_by_pointer = false;
+
     /// Set to `true` to generate SPIR-V via the Tint IR instead of from the AST.
     bool use_tint_ir = false;
 
-    /// Options used in the binding mappings for external textures
-    ExternalTextureOptions external_texture_options = {};
-
-    /// Options used in the bindings remapper
-    BindingRemapperOptions binding_remapper_options = {};
-
     /// Set to `true` to require `SPV_KHR_subgroup_uniform_control_flow` extension and
     /// `SubgroupUniformControlFlowKHR` execution mode for compute stage entry points in generated
     /// SPIRV module. Issue: dawn:464
     bool experimental_require_subgroup_uniform_control_flow = false;
 
+    /// The bindings
+    Bindings bindings;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
                  disable_image_robustness,
@@ -69,9 +141,8 @@
                  emit_vertex_point_size,
                  clamp_frag_depth,
                  use_tint_ir,
-                 external_texture_options,
-                 binding_remapper_options,
-                 experimental_require_subgroup_uniform_control_flow);
+                 experimental_require_subgroup_uniform_control_flow,
+                 bindings);
 };
 
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/constant_test.cc b/src/tint/lang/spirv/writer/constant_test.cc
index e54f8ef..1241487 100644
--- a/src/tint/lang/spirv/writer/constant_test.cc
+++ b/src/tint/lang/spirv/writer/constant_test.cc
@@ -148,6 +148,12 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, Constant_Array_LargeAllZero) {
+    writer_.Constant(b.Zero(ty.array<i32, 65535>()));
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%1 = OpConstantNull %_arr_int_uint_65535");
+}
+
 TEST_F(SpirvWriterTest, Constant_Struct) {
     auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
                                                               {mod.symbols.New("a"), ty.i32()},
diff --git a/src/tint/lang/spirv/writer/convert_test.cc b/src/tint/lang/spirv/writer/convert_test.cc
index 10fb71a..6775c9d 100644
--- a/src/tint/lang/spirv/writer/convert_test.cc
+++ b/src/tint/lang/spirv/writer/convert_test.cc
@@ -80,14 +80,12 @@
                              ConvertCase{kBool, kF16, "OpSelect", "half"},
 
                              // To i32.
-                             ConvertCase{kF32, kI32, "OpConvertFToS", "int"},
-                             ConvertCase{kF16, kI32, "OpConvertFToS", "int"},
+                             // Note: ftoi cases are polyfilled and tested separately.
                              ConvertCase{kU32, kI32, "OpBitcast", "int"},
                              ConvertCase{kBool, kI32, "OpSelect", "int"},
 
                              // To u32.
-                             ConvertCase{kF32, kU32, "OpConvertFToU", "uint"},
-                             ConvertCase{kF16, kU32, "OpConvertFToU", "uint"},
+                             // Note: ftoi cases are polyfilled and tested separately.
                              ConvertCase{kI32, kU32, "OpBitcast", "uint"},
                              ConvertCase{kBool, kU32, "OpSelect", "uint"},
 
@@ -140,5 +138,309 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, Convert_F32_to_I32) {
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({b.FunctionParam("arg", ty.f32())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.i32(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               ; Function foo
+        %foo = OpFunction %int None %5
+        %arg = OpFunctionParameter %float
+          %6 = OpLabel
+     %result = OpFunctionCall %int %tint_f32_to_i32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_f32_to_i32
+%tint_f32_to_i32 = OpFunction %int None %5
+      %value = OpFunctionParameter %float
+         %10 = OpLabel
+         %11 = OpConvertFToS %int %value
+         %12 = OpFOrdGreaterThanEqual %bool %value %float_n2_14748365e_09
+         %15 = OpSelect %int %12 %11 %int_n2147483648
+         %17 = OpFOrdLessThanEqual %bool %value %float_2_14748352e_09
+         %19 = OpSelect %int %17 %15 %int_2147483647
+               OpReturnValue %19
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F32_to_U32) {
+    auto* func = b.Function("foo", ty.u32());
+    func->SetParams({b.FunctionParam("arg", ty.f32())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.u32(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               ; Function foo
+        %foo = OpFunction %uint None %5
+        %arg = OpFunctionParameter %float
+          %6 = OpLabel
+     %result = OpFunctionCall %uint %tint_f32_to_u32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_f32_to_u32
+%tint_f32_to_u32 = OpFunction %uint None %5
+      %value = OpFunctionParameter %float
+         %10 = OpLabel
+         %11 = OpConvertFToU %uint %value
+         %12 = OpFOrdGreaterThanEqual %bool %value %float_0
+         %15 = OpSelect %uint %12 %11 %uint_0
+         %17 = OpFOrdLessThanEqual %bool %value %float_4_29496704e_09
+         %19 = OpSelect %uint %17 %15 %uint_4294967295
+               OpReturnValue %19
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F16_to_I32) {
+    auto* func = b.Function("foo", ty.i32());
+    func->SetParams({b.FunctionParam("arg", ty.f16())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.i32(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               ; Function foo
+        %foo = OpFunction %int None %5
+        %arg = OpFunctionParameter %half
+          %6 = OpLabel
+     %result = OpFunctionCall %int %tint_f16_to_i32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_f16_to_i32
+%tint_f16_to_i32 = OpFunction %int None %5
+      %value = OpFunctionParameter %half
+         %10 = OpLabel
+         %11 = OpConvertFToS %int %value
+         %12 = OpFOrdGreaterThanEqual %bool %value %half_n0x1_ffcp_15
+         %15 = OpSelect %int %12 %11 %int_n2147483648
+         %17 = OpFOrdLessThanEqual %bool %value %half_0x1_ffcp_15
+         %19 = OpSelect %int %17 %15 %int_2147483647
+               OpReturnValue %19
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F16_to_U32) {
+    auto* func = b.Function("foo", ty.u32());
+    func->SetParams({b.FunctionParam("arg", ty.f16())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.u32(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               ; Function foo
+        %foo = OpFunction %uint None %5
+        %arg = OpFunctionParameter %half
+          %6 = OpLabel
+     %result = OpFunctionCall %uint %tint_f16_to_u32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_f16_to_u32
+%tint_f16_to_u32 = OpFunction %uint None %5
+      %value = OpFunctionParameter %half
+         %10 = OpLabel
+         %11 = OpConvertFToU %uint %value
+         %12 = OpFOrdGreaterThanEqual %bool %value %half_0x0p_0
+         %15 = OpSelect %uint %12 %11 %uint_0
+         %17 = OpFOrdLessThanEqual %bool %value %half_0x1_ffcp_15
+         %19 = OpSelect %uint %17 %15 %uint_4294967295
+               OpReturnValue %19
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F32_to_I32_Vec2) {
+    auto* func = b.Function("foo", ty.vec2<i32>());
+    func->SetParams({b.FunctionParam("arg", ty.vec2<f32>())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.vec2<i32>(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+%float_n2_14748365e_09 = OpConstant %float -2.14748365e+09
+         %15 = OpConstantComposite %v2float %float_n2_14748365e_09 %float_n2_14748365e_09
+       %bool = OpTypeBool
+     %v2bool = OpTypeVector %bool 2
+%int_n2147483648 = OpConstant %int -2147483648
+         %20 = OpConstantComposite %v2int %int_n2147483648 %int_n2147483648
+%float_2_14748352e_09 = OpConstant %float 2.14748352e+09
+         %23 = OpConstantComposite %v2float %float_2_14748352e_09 %float_2_14748352e_09
+%int_2147483647 = OpConstant %int 2147483647
+         %26 = OpConstantComposite %v2int %int_2147483647 %int_2147483647
+       %void = OpTypeVoid
+         %30 = OpTypeFunction %void
+
+               ; Function foo
+        %foo = OpFunction %v2int None %7
+        %arg = OpFunctionParameter %v2float
+          %8 = OpLabel
+     %result = OpFunctionCall %v2int %tint_v2f32_to_v2i32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_v2f32_to_v2i32
+%tint_v2f32_to_v2i32 = OpFunction %v2int None %7
+      %value = OpFunctionParameter %v2float
+         %12 = OpLabel
+         %13 = OpConvertFToS %v2int %value
+         %14 = OpFOrdGreaterThanEqual %v2bool %value %15
+         %19 = OpSelect %v2int %14 %13 %20
+         %22 = OpFOrdLessThanEqual %v2bool %value %23
+         %25 = OpSelect %v2int %22 %19 %26
+               OpReturnValue %25
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F32_to_U32_Vec3) {
+    auto* func = b.Function("foo", ty.vec3<u32>());
+    func->SetParams({b.FunctionParam("arg", ty.vec3<f32>())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.vec3<u32>(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+%float_4_29496704e_09 = OpConstant %float 4.29496704e+09
+         %21 = OpConstantComposite %v3float %float_4_29496704e_09 %float_4_29496704e_09 %float_4_29496704e_09
+%uint_4294967295 = OpConstant %uint 4294967295
+         %24 = OpConstantComposite %v3uint %uint_4294967295 %uint_4294967295 %uint_4294967295
+       %void = OpTypeVoid
+         %28 = OpTypeFunction %void
+
+               ; Function foo
+        %foo = OpFunction %v3uint None %7
+        %arg = OpFunctionParameter %v3float
+          %8 = OpLabel
+     %result = OpFunctionCall %v3uint %tint_v3f32_to_v3u32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_v3f32_to_v3u32
+%tint_v3f32_to_v3u32 = OpFunction %v3uint None %7
+      %value = OpFunctionParameter %v3float
+         %12 = OpLabel
+         %13 = OpConvertFToU %v3uint %value
+         %14 = OpFOrdGreaterThanEqual %v3bool %value %15
+         %18 = OpSelect %v3uint %14 %13 %19
+         %20 = OpFOrdLessThanEqual %v3bool %value %21
+         %23 = OpSelect %v3uint %20 %18 %24
+               OpReturnValue %23
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F16_to_I32_Vec2) {
+    auto* func = b.Function("foo", ty.vec2<i32>());
+    func->SetParams({b.FunctionParam("arg", ty.vec2<f16>())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.vec2<i32>(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+%half_n0x1_ffcp_15 = OpConstant %half -0x1.ffcp+15
+         %15 = OpConstantComposite %v2half %half_n0x1_ffcp_15 %half_n0x1_ffcp_15
+       %bool = OpTypeBool
+     %v2bool = OpTypeVector %bool 2
+%int_n2147483648 = OpConstant %int -2147483648
+         %20 = OpConstantComposite %v2int %int_n2147483648 %int_n2147483648
+%half_0x1_ffcp_15 = OpConstant %half 0x1.ffcp+15
+         %23 = OpConstantComposite %v2half %half_0x1_ffcp_15 %half_0x1_ffcp_15
+%int_2147483647 = OpConstant %int 2147483647
+         %26 = OpConstantComposite %v2int %int_2147483647 %int_2147483647
+       %void = OpTypeVoid
+         %30 = OpTypeFunction %void
+
+               ; Function foo
+        %foo = OpFunction %v2int None %7
+        %arg = OpFunctionParameter %v2half
+          %8 = OpLabel
+     %result = OpFunctionCall %v2int %tint_v2f16_to_v2i32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_v2f16_to_v2i32
+%tint_v2f16_to_v2i32 = OpFunction %v2int None %7
+      %value = OpFunctionParameter %v2half
+         %12 = OpLabel
+         %13 = OpConvertFToS %v2int %value
+         %14 = OpFOrdGreaterThanEqual %v2bool %value %15
+         %19 = OpSelect %v2int %14 %13 %20
+         %22 = OpFOrdLessThanEqual %v2bool %value %23
+         %25 = OpSelect %v2int %22 %19 %26
+               OpReturnValue %25
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Convert_F16_to_U32_Vec4) {
+    auto* func = b.Function("foo", ty.vec4<u32>());
+    func->SetParams({b.FunctionParam("arg", ty.vec4<f16>())});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Convert(ty.vec4<u32>(), func->Params()[0]);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+%half_0x1_ffcp_15 = OpConstant %half 0x1.ffcp+15
+         %21 = OpConstantComposite %v4half %half_0x1_ffcp_15 %half_0x1_ffcp_15 %half_0x1_ffcp_15 %half_0x1_ffcp_15
+%uint_4294967295 = OpConstant %uint 4294967295
+         %24 = OpConstantComposite %v4uint %uint_4294967295 %uint_4294967295 %uint_4294967295 %uint_4294967295
+       %void = OpTypeVoid
+         %28 = OpTypeFunction %void
+
+               ; Function foo
+        %foo = OpFunction %v4uint None %7
+        %arg = OpFunctionParameter %v4half
+          %8 = OpLabel
+     %result = OpFunctionCall %v4uint %tint_v4f16_to_v4u32 %arg
+               OpReturnValue %result
+               OpFunctionEnd
+
+               ; Function tint_v4f16_to_v4u32
+%tint_v4f16_to_v4u32 = OpFunction %v4uint None %7
+      %value = OpFunctionParameter %v4half
+         %12 = OpLabel
+         %13 = OpConvertFToU %v4uint %value
+         %14 = OpFOrdGreaterThanEqual %v4bool %value %15
+         %18 = OpSelect %v4uint %14 %13 %19
+         %20 = OpFOrdLessThanEqual %v4bool %value %21
+         %23 = OpSelect %v4uint %20 %18 %24
+               OpReturnValue %23
+               OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/function_test.cc b/src/tint/lang/spirv/writer/function_test.cc
index dcbb23c..ab34b60 100644
--- a/src/tint/lang/spirv/writer/function_test.cc
+++ b/src/tint/lang/spirv/writer/function_test.cc
@@ -346,29 +346,98 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST(R"(OpEntryPoint Fragment %main "main" %main_loc0_Output %main_loc0_Output_0)");
+    EXPECT_INST(
+        R"(OpEntryPoint Fragment %main "main" %main_loc0_idx0_Output %main_loc0_idx1_Output)");
     EXPECT_INST(R"(
-               OpDecorate %main_loc0_Output Location 0
-               OpDecorate %main_loc0_Output Index 0
-               OpDecorate %main_loc0_Output_0 Location 0
-               OpDecorate %main_loc0_Output_0 Index 1
+               OpDecorate %main_loc0_idx0_Output Location 0
+               OpDecorate %main_loc0_idx0_Output Index 0
+               OpDecorate %main_loc0_idx1_Output Location 0
+               OpDecorate %main_loc0_idx1_Output Index 1
     )");
     EXPECT_INST(R"(
-%main_loc0_Output = OpVariable %_ptr_Output_float Output
-%main_loc0_Output_0 = OpVariable %_ptr_Output_float Output
+%main_loc0_idx0_Output = OpVariable %_ptr_Output_float Output
+%main_loc0_idx1_Output = OpVariable %_ptr_Output_float Output
     )");
     EXPECT_INST(R"(
        %main = OpFunction %void None %14
          %15 = OpLabel
          %16 = OpFunctionCall %Outputs %main_inner
          %17 = OpCompositeExtract %float %16 0
-               OpStore %main_loc0_Output %17
+               OpStore %main_loc0_idx0_Output %17
          %18 = OpCompositeExtract %float %16 1
-               OpStore %main_loc0_Output_0 %18
+               OpStore %main_loc0_idx1_Output %18
                OpReturn
                OpFunctionEnd
 )");
 }
 
+TEST_F(SpirvWriterTest, Function_PassMatrixByPointer) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* arr = mod.root_block->Append(b.Var("var", ty.ptr(private_, ty.array(mat_ty, 4))));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value_a = b.FunctionParam("value_a", mat_ty);
+    auto* scalar = b.FunctionParam("scalar", ty.f32());
+    auto* value_b = b.FunctionParam("value_b", mat_ty);
+    target->SetParams({value_a, scalar, value_b});
+    b.Append(target->Block(), [&] {
+        auto* scale = b.Multiply(mat_ty, value_a, scalar);
+        auto* sum = b.Add(mat_ty, scale, value_b);
+        b.Return(target, sum);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* mat_ptr = ty.ptr(private_, mat_ty);
+        auto* ma = b.Load(b.Access(mat_ptr, arr, 0_u));
+        auto* mb = b.Load(b.Access(mat_ptr, arr, 1_u));
+        auto* result = b.Call(mat_ty, target, ma, b.Constant(2_f), mb);
+        b.Return(caller, result);
+    });
+
+    Options options;
+    options.pass_matrix_by_pointer = true;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+
+    EXPECT_INST(R"(
+               ; Function target
+     %target = OpFunction %mat3v3float None %15
+         %12 = OpFunctionParameter %_ptr_Function_mat3v3float
+     %scalar = OpFunctionParameter %float
+         %14 = OpFunctionParameter %_ptr_Function_mat3v3float
+         %16 = OpLabel
+         %17 = OpLoad %mat3v3float %14
+         %18 = OpLoad %mat3v3float %12
+         %19 = OpMatrixTimesScalar %mat3v3float %18 %scalar
+         %20 = OpCompositeExtract %v3float %19 0
+         %21 = OpCompositeExtract %v3float %17 0
+         %22 = OpFAdd %v3float %20 %21
+         %23 = OpCompositeExtract %v3float %19 1
+         %24 = OpCompositeExtract %v3float %17 1
+         %25 = OpFAdd %v3float %23 %24
+         %26 = OpCompositeExtract %v3float %19 2
+         %27 = OpCompositeExtract %v3float %17 2
+         %28 = OpFAdd %v3float %26 %27
+         %29 = OpCompositeConstruct %mat3v3float %22 %25 %28
+               OpReturnValue %29
+               OpFunctionEnd
+
+               ; Function caller
+     %caller = OpFunction %mat3v3float None %31
+         %32 = OpLabel
+         %40 = OpVariable %_ptr_Function_mat3v3float Function
+         %41 = OpVariable %_ptr_Function_mat3v3float Function
+         %33 = OpAccessChain %_ptr_Private_mat3v3float %var %uint_0
+         %36 = OpLoad %mat3v3float %33
+         %37 = OpAccessChain %_ptr_Private_mat3v3float %var %uint_1
+         %39 = OpLoad %mat3v3float %37
+               OpStore %40 %36
+               OpStore %41 %39
+         %42 = OpFunctionCall %mat3v3float %target %40 %float_2 %41
+               OpReturnValue %42
+               OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.bazel b/src/tint/lang/spirv/writer/helpers/BUILD.bazel
new file mode 100644
index 0000000..190110b
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.bazel
@@ -0,0 +1,70 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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 = "helpers",
+  srcs = [
+    "generate_bindings.cc",
+  ],
+  hdrs = [
+    "generate_bindings.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+  ] + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.cfg b/src/tint/lang/spirv/writer/helpers/BUILD.cfg
new file mode 100644
index 0000000..0a24987
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_spv_writer"
+}
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.cmake b/src/tint/lang/spirv/writer/helpers/BUILD.cmake
new file mode 100644
index 0000000..e5ff89c
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.cmake
@@ -0,0 +1,65 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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
+################################################################################
+
+if(TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_writer_helpers
+# Kind:      lib
+# Condition: TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_writer_helpers lib
+  lang/spirv/writer/helpers/generate_bindings.cc
+  lang/spirv/writer/helpers/generate_bindings.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_writer_helpers lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  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
+)
+
+if(TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_helpers lib
+    tint_lang_spirv_writer_common
+  )
+endif(TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.gn b/src/tint/lang/spirv/writer/helpers/BUILD.gn
new file mode 100644
index 0000000..f7e62b3
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.gn
@@ -0,0 +1,61 @@
+# Copyright 2023 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+################################################################################
+# 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_spv_writer) {
+  libtint_source_set("helpers") {
+    sources = [
+      "generate_bindings.cc",
+      "generate_bindings.h",
+    ]
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${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_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/writer/common" ]
+    }
+  }
+}
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
new file mode 100644
index 0000000..0285b0b
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
@@ -0,0 +1,119 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
+
+#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/lang/core/type/external_texture.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::spirv::writer {
+
+Bindings GenerateBindings(const Program& program) {
+    // TODO(tint:1491): Use Inspector once we can get binding info for all
+    // variables, not just those referenced by entry points.
+
+    Bindings bindings{};
+
+    std::unordered_set<tint::BindingPoint> seen_binding_points;
+
+    // Collect next valid binding number per group
+    std::unordered_map<uint32_t, uint32_t> group_to_next_binding_number;
+    std::vector<tint::BindingPoint> ext_tex_bps;
+    for (auto* var : program.AST().GlobalVariables()) {
+        if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
+            if (auto bp = sem_var->BindingPoint()) {
+                // This is a bit of a hack. The binding points must be unique over all the `binding`
+                // entries. But, this is looking at _all_ entry points where bindings can overlap.
+                // In the case where both entry points used the same type (uniform, sampler, etc)
+                // then it woudl be fine as it just overwrites with itself. But, if one entry point
+                // has a uniform and the other a sampler at the same (group,binding) pair then we'll
+                // get a validation error due to duplicate WGSL bindings.
+                //
+                // For generate bindings we don't really care as we always map to itself, so if it
+                // exists anywhere, we just pretend that's the only one.
+                if (seen_binding_points.find(*bp) != seen_binding_points.end()) {
+                    continue;
+                }
+                seen_binding_points.emplace(*bp);
+
+                auto& n = group_to_next_binding_number[bp->group];
+                n = std::max(n, bp->binding + 1);
+
+                // Store up the external textures, we'll add them in the next step
+                if (sem_var->Type()->UnwrapRef()->Is<core::type::ExternalTexture>()) {
+                    ext_tex_bps.emplace_back(*bp);
+                    continue;
+                }
+
+                binding::BindingInfo info{bp->group, bp->binding};
+                switch (sem_var->AddressSpace()) {
+                    case core::AddressSpace::kHandle:
+                        Switch(
+                            sem_var->Type()->UnwrapRef(),  //
+                            [&](const core::type::Sampler*) {
+                                bindings.sampler.emplace(*bp, info);
+                            },
+                            [&](const core::type::StorageTexture*) {
+                                bindings.storage_texture.emplace(*bp, info);
+                            },
+                            [&](const core::type::Texture*) {
+                                bindings.texture.emplace(*bp, info);
+                            });
+                        break;
+                    case core::AddressSpace::kStorage:
+                        bindings.storage.emplace(*bp, info);
+                        break;
+                    case core::AddressSpace::kUniform:
+                        bindings.uniform.emplace(*bp, info);
+                        break;
+
+                    case core::AddressSpace::kUndefined:
+                    case core::AddressSpace::kPixelLocal:
+                    case core::AddressSpace::kPrivate:
+                    case core::AddressSpace::kPushConstant:
+                    case core::AddressSpace::kIn:
+                    case core::AddressSpace::kOut:
+                    case core::AddressSpace::kFunction:
+                    case core::AddressSpace::kWorkgroup:
+                        break;
+                }
+            }
+        }
+    }
+
+    for (auto bp : ext_tex_bps) {
+        uint32_t g = bp.group;
+        uint32_t& next_num = group_to_next_binding_number[g];
+
+        binding::BindingInfo plane0{bp.group, bp.binding};
+        binding::BindingInfo plane1{g, next_num++};
+        binding::BindingInfo metadata{g, next_num++};
+
+        bindings.external_texture.emplace(bp, binding::ExternalTexture{metadata, plane0, plane1});
+    }
+
+    return bindings;
+}
+
+}  // namespace tint::spirv::writer
diff --git a/src/tint/fuzzers/apply_substitute_overrides.h b/src/tint/lang/spirv/writer/helpers/generate_bindings.h
similarity index 62%
rename from src/tint/fuzzers/apply_substitute_overrides.h
rename to src/tint/lang/spirv/writer/helpers/generate_bindings.h
index 31f0263..d97120f 100644
--- a/src/tint/fuzzers/apply_substitute_overrides.h
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.h
@@ -12,20 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_FUZZERS_APPLY_SUBSTITUTE_OVERRIDES_H_
-#define SRC_TINT_FUZZERS_APPLY_SUBSTITUTE_OVERRIDES_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_HELPERS_GENERATE_BINDINGS_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_HELPERS_GENERATE_BINDINGS_H_
+
+#include "src/tint/lang/spirv/writer/common/options.h"
 
 // Forward declarations
 namespace tint {
 class Program;
 }
 
-namespace tint::fuzzers {
+namespace tint::spirv::writer {
 
-/// @returns a new program with all overrides subsituted with const variables
-/// @param program the input program
-Program ApplySubstituteOverrides(Program&& program);
+Bindings GenerateBindings(const Program& program);
 
-}  // namespace tint::fuzzers
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_FUZZERS_APPLY_SUBSTITUTE_OVERRIDES_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_HELPERS_GENERATE_BINDINGS_H_
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.bazel b/src/tint/lang/spirv/writer/printer/BUILD.bazel
index f595547..21bb414 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/printer/BUILD.bazel
@@ -33,7 +33,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.cmake b/src/tint/lang/spirv/writer/printer/BUILD.cmake
index 81c1626..cdadaec 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/printer/BUILD.cmake
@@ -34,7 +34,6 @@
 
 tint_target_add_dependencies(tint_lang_spirv_writer_printer lib
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.gn b/src/tint/lang/spirv/writer/printer/BUILD.gn
index 476271a..e57f56c 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/printer/BUILD.gn
@@ -32,7 +32,6 @@
     ]
     deps = [
       "${tint_src_dir}/api/common",
-      "${tint_src_dir}/api/options",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/intrinsic",
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index 43ed467..7c6b4da 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -73,6 +73,7 @@
 #include "src/tint/lang/core/type/u32.h"
 #include "src/tint/lang/core/type/vector.h"
 #include "src/tint/lang/core/type/void.h"
+#include "src/tint/lang/spirv/ir/literal_operand.h"
 #include "src/tint/lang/spirv/type/sampled_image.h"
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
 #include "src/tint/lang/spirv/writer/common/module.h"
@@ -159,14 +160,10 @@
         return valid.Failure();
     }
 
-    // TODO(crbug.com/tint/1906): Check supported extensions.
-
     module_.PushCapability(SpvCapabilityShader);
     module_.PushMemoryModel(spv::Op::OpMemoryModel, {U32Operand(SpvAddressingModelLogical),
                                                      U32Operand(SpvMemoryModelGLSL450)});
 
-    // TODO(crbug.com/tint/1906): Emit extensions.
-
     // Emit module-scope declarations.
     EmitRootBlock(ir_.root_block);
 
@@ -231,7 +228,7 @@
 
 uint32_t Printer::Constant(core::ir::Constant* constant) {
     // If it is a literal operand, just return the value.
-    if (auto* literal = constant->As<raise::LiteralOperand>()) {
+    if (auto* literal = constant->As<spirv::ir::LiteralOperand>()) {
         return literal->Value()->ValueAs<uint32_t>();
     }
 
@@ -247,8 +244,14 @@
 
 uint32_t Printer::Constant(const core::constant::Value* constant) {
     return constants_.GetOrCreate(constant, [&] {
-        auto id = module_.NextId();
         auto* ty = constant->Type();
+
+        // Use OpConstantNull for zero-valued composite constants.
+        if (!ty->Is<core::type::Scalar>() && constant->AllZero()) {
+            return ConstantNull(ty);
+        }
+
+        auto id = module_.NextId();
         Switch(
             ty,  //
             [&](const core::type::Bool*) {
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.bazel b/src/tint/lang/spirv/writer/raise/BUILD.bazel
index f233c85..c89a53a 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/raise/BUILD.bazel
@@ -30,6 +30,7 @@
     "expand_implicit_splats.cc",
     "handle_matrix_arithmetic.cc",
     "merge_return.cc",
+    "pass_matrix_by_pointer.cc",
     "raise.cc",
     "shader_io.cc",
     "var_for_dynamic_index.cc",
@@ -39,6 +40,7 @@
     "expand_implicit_splats.h",
     "handle_matrix_arithmetic.h",
     "merge_return.h",
+    "pass_matrix_by_pointer.h",
     "raise.h",
     "shader_io.h",
     "var_for_dynamic_index.h",
@@ -91,6 +93,7 @@
     "expand_implicit_splats_test.cc",
     "handle_matrix_arithmetic_test.cc",
     "merge_return_test.cc",
+    "pass_matrix_by_pointer_test.cc",
     "shader_io_test.cc",
     "var_for_dynamic_index_test.cc",
   ],
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.cmake b/src/tint/lang/spirv/writer/raise/BUILD.cmake
index 8385c91..6b69a52 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/raise/BUILD.cmake
@@ -36,6 +36,8 @@
   lang/spirv/writer/raise/handle_matrix_arithmetic.h
   lang/spirv/writer/raise/merge_return.cc
   lang/spirv/writer/raise/merge_return.h
+  lang/spirv/writer/raise/pass_matrix_by_pointer.cc
+  lang/spirv/writer/raise/pass_matrix_by_pointer.h
   lang/spirv/writer/raise/raise.cc
   lang/spirv/writer/raise/raise.h
   lang/spirv/writer/raise/shader_io.cc
@@ -96,6 +98,7 @@
   lang/spirv/writer/raise/expand_implicit_splats_test.cc
   lang/spirv/writer/raise/handle_matrix_arithmetic_test.cc
   lang/spirv/writer/raise/merge_return_test.cc
+  lang/spirv/writer/raise/pass_matrix_by_pointer_test.cc
   lang/spirv/writer/raise/shader_io_test.cc
   lang/spirv/writer/raise/var_for_dynamic_index_test.cc
 )
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.gn b/src/tint/lang/spirv/writer/raise/BUILD.gn
index 6d187d6..d908228 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.gn
+++ b/src/tint/lang/spirv/writer/raise/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 if (tint_build_spv_writer) {
@@ -39,6 +39,8 @@
       "handle_matrix_arithmetic.h",
       "merge_return.cc",
       "merge_return.h",
+      "pass_matrix_by_pointer.cc",
+      "pass_matrix_by_pointer.h",
       "raise.cc",
       "raise.h",
       "shader_io.cc",
@@ -86,12 +88,12 @@
 if (tint_build_unittests) {
   if (tint_build_spv_writer) {
     tint_unittests_source_set("unittests") {
-      testonly = true
       sources = [
         "builtin_polyfill_test.cc",
         "expand_implicit_splats_test.cc",
         "handle_matrix_arithmetic_test.cc",
         "merge_return_test.cc",
+        "pass_matrix_by_pointer_test.cc",
         "shader_io_test.cc",
         "var_for_dynamic_index_test.cc",
       ]
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
index dc5ff8b..feef5fb 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
@@ -29,11 +29,10 @@
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture.h"
 #include "src/tint/lang/spirv/ir/builtin_call.h"
+#include "src/tint/lang/spirv/ir/literal_operand.h"
 #include "src/tint/lang/spirv/type/sampled_image.h"
 #include "src/tint/utils/ice/ice.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::raise::LiteralOperand);
-
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
 
@@ -177,8 +176,8 @@
     /// Create a literal operand.
     /// @param value the literal value
     /// @returns the literal operand
-    LiteralOperand* Literal(u32 value) {
-        return ir.values.Create<LiteralOperand>(b.ConstantValue(value));
+    spirv::ir::LiteralOperand* Literal(u32 value) {
+        return ir.values.Create<spirv::ir::LiteralOperand>(b.ConstantValue(value));
     }
 
     /// Handle an `arrayLength()` builtin.
@@ -885,8 +884,4 @@
     return Success;
 }
 
-LiteralOperand::LiteralOperand(const core::constant::Value* value) : Base(value) {}
-
-LiteralOperand::~LiteralOperand() = default;
-
 }  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.h b/src/tint/lang/spirv/writer/raise/builtin_polyfill.h
index 469d6a4..1a1fe62 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.h
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.h
@@ -36,18 +36,6 @@
 /// @returns success or failure
 Result<SuccessType> BuiltinPolyfill(core::ir::Module& module);
 
-/// LiteralOperand is a type of constant value that is intended to be emitted as a literal in
-/// the SPIR-V instruction stream.
-/// TODO(jrprice): Move this to lang/spirv.
-class LiteralOperand final : public Castable<LiteralOperand, core::ir::Constant> {
-  public:
-    /// Constructor
-    /// @param value the operand value
-    explicit LiteralOperand(const core::constant::Value* value);
-    /// Destructor
-    ~LiteralOperand() override;
-};
-
 }  // namespace tint::spirv::writer::raise
 
 #endif  // SRC_TINT_LANG_SPIRV_WRITER_RAISE_BUILTIN_POLYFILL_H_
diff --git a/src/tint/lang/spirv/writer/raise/merge_return.cc b/src/tint/lang/spirv/writer/raise/merge_return.cc
index 75e40e6..e172165 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return.cc
+++ b/src/tint/lang/spirv/writer/raise/merge_return.cc
@@ -132,7 +132,7 @@
                 // Unreachable can become reachable once returns are turned into exits.
                 // As this is the terminator for the block, simply stop processing the
                 // instructions. A appropriate terminator will be created for this block below.
-                inst->Remove();
+                inst->Destroy();
                 break;
             }
 
diff --git a/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc
new file mode 100644
index 0000000..ff54237
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc
@@ -0,0 +1,128 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/spirv/writer/raise/pass_matrix_by_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"
+
+using namespace tint::core::number_suffixes;  // NOLINT
+using namespace tint::core::fluent_types;     // NOLINT
+
+namespace tint::spirv::writer::raise {
+
+namespace {
+
+/// 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()};
+
+    /// Process the module.
+    void Process() {
+        // Find user-declared functions that have value arguments containing matrices.
+        for (auto* func : ir.functions) {
+            for (auto* param : func->Params()) {
+                if (ContainsMatrix(param->Type())) {
+                    TransformFunction(func);
+                    break;
+                }
+            }
+        }
+    }
+
+    /// Checks if a type contains a matrix.
+    /// @param type the type to check
+    /// @returns true if the type contains a matrix, otherwise false
+    bool ContainsMatrix(const core::type::Type* type) {
+        return tint::Switch(
+            type,  //
+            [&](const core::type::Matrix*) { return true; },
+            [&](const core::type::Array* arr) { return ContainsMatrix(arr->ElemType()); },
+            [&](const core::type::Struct* str) {
+                for (auto* member : str->Members()) {
+                    if (ContainsMatrix(member->Type())) {
+                        return true;
+                    }
+                }
+                return false;
+            },
+            [&](Default) { return false; });
+    }
+
+    /// Transform a function that has value parameters containing matrices.
+    /// @param func the function to transform
+    void TransformFunction(core::ir::Function* func) {
+        Vector<core::ir::FunctionParam*, 4> replacement_params;
+        for (auto* param : func->Params()) {
+            if (ContainsMatrix(param->Type())) {
+                // Replace the value parameter with a pointer.
+                auto* new_param = b.FunctionParam(ty.ptr(function, param->Type()));
+
+                // Load from the pointer to get the value.
+                auto* load = b.Load(new_param);
+                func->Block()->Prepend(load);
+                param->ReplaceAllUsesWith(load->Result());
+
+                // Modify all of the callsites.
+                func->ForEachUse([&](core::ir::Usage use) {
+                    if (auto* call = use.instruction->As<core::ir::UserCall>()) {
+                        ReplaceCallArgument(call, replacement_params.Length());
+                    }
+                });
+
+                replacement_params.Push(new_param);
+            } else {
+                // No matrices, so just copy the parameter as is.
+                replacement_params.Push(param);
+            }
+        }
+        func->SetParams(std::move(replacement_params));
+    }
+
+    /// Replace a function call argument with an equivalent passed by pointer.
+    void ReplaceCallArgument(core::ir::UserCall* call, size_t arg_index) {
+        // Copy the argument to a locally declared variable.
+        auto* arg = call->Args()[arg_index];
+        auto* local_var = b.Var(ty.ptr(function, arg->Type()));
+        local_var->SetInitializer(arg);
+        local_var->InsertBefore(call);
+
+        call->SetOperand(core::ir::UserCall::kArgsOperandOffset + arg_index, local_var->Result());
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> PassMatrixByPointer(core::ir::Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "PassMatrixByPointer transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.h b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.h
new file mode 100644
index 0000000..c22be02
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_RAISE_PASS_MATRIX_BY_POINTER_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_RAISE_PASS_MATRIX_BY_POINTER_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::spirv::writer::raise {
+
+/// PassMatrixByPointer is a transform that modifies function calls that pass values that contain
+/// matrices to pass them by pointer instead.
+/// This is used to workaround bugs in some Qualcomm drivers (see crbug.com/tint/2045).
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> PassMatrixByPointer(core::ir::Module& module);
+
+}  // namespace tint::spirv::writer::raise
+
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_RAISE_PASS_MATRIX_BY_POINTER_H_
diff --git a/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer_test.cc b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer_test.cc
new file mode 100644
index 0000000..560dda5
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer_test.cc
@@ -0,0 +1,733 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::spirv::writer::raise {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using SpirvWriter_PassMatrixByPointerTest = core::ir::transform::TransformTest;
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, NoModify_ArrayValue) {
+    auto* arr_ty = ty.array<f32, 4u>();
+    auto* arr = mod.root_block->Append(b.Var("var", ty.ptr(private_, arr_ty)));
+
+    auto* target = b.Function("target", ty.f32());
+    auto* value = b.FunctionParam("value", arr_ty);
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* access = b.Access(ty.f32(), value, 1_i);
+        b.Return(target, access);
+    });
+
+    auto* caller = b.Function("caller", ty.f32());
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(ty.f32(), target, b.Load(arr));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, array<f32, 4>, read_write> = var
+}
+
+%target = func(%value:array<f32, 4>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = access %value, 1i
+    ret %4
+  }
+}
+%caller = func():f32 -> %b3 {
+  %b3 = block {
+    %6:array<f32, 4> = load %var
+    %7:f32 = call %target, %6
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, NoModify_MatrixPointer) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* mat = mod.root_block->Append(b.Var("var", ty.ptr(private_, mat_ty)));
+
+    auto* target = b.Function("target", ty.vec3<f32>());
+    auto* value = b.FunctionParam("value", ty.ptr(private_, mat_ty));
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* access = b.Access(ty.ptr<private_, vec3<f32>>(), value, 1_i);
+        b.Return(target, b.Load(access));
+    });
+
+    auto* caller = b.Function("caller", ty.vec3<f32>());
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(ty.vec3<f32>(), target, mat);
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%value:ptr<private, mat3x3<f32>, read_write>):vec3<f32> -> %b2 {
+  %b2 = block {
+    %4:ptr<private, vec3<f32>, read_write> = access %value, 1i
+    %5:vec3<f32> = load %4
+    ret %5
+  }
+}
+%caller = func():vec3<f32> -> %b3 {
+  %b3 = block {
+    %7:vec3<f32> = call %target, %var
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MatrixValuePassedToBuiltin) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* mat = mod.root_block->Append(b.Var("var", ty.ptr(private_, mat_ty)));
+
+    auto* caller = b.Function("caller", ty.f32());
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(ty.f32(), core::BuiltinFn::kDeterminant, b.Load(mat));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%caller = func():f32 -> %b2 {
+  %b2 = block {
+    %3:mat3x3<f32> = load %var
+    %4:f32 = determinant %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, SingleMatrixValue) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* mat = mod.root_block->Append(b.Var("var", ty.ptr(private_, mat_ty)));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value = b.FunctionParam("value", mat_ty);
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* scale = b.Multiply(mat_ty, value, b.Constant(2_f));
+        b.Return(target, scale);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(mat));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%value:mat3x3<f32>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = mul %value, 2.0f
+    ret %4
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %6:mat3x3<f32> = load %var
+    %7:mat3x3<f32> = call %target, %6
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%3:ptr<function, mat3x3<f32>, read_write>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = load %3
+    %5:mat3x3<f32> = mul %4, 2.0f
+    ret %5
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %7:mat3x3<f32> = load %var
+    %8:ptr<function, mat3x3<f32>, read_write> = var, %7
+    %9:mat3x3<f32> = call %target, %8
+    ret %9
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MultipleMatrixValues) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* arr = mod.root_block->Append(b.Var("var", ty.ptr(private_, ty.array(mat_ty, 4))));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value_a = b.FunctionParam("value_a", mat_ty);
+    auto* scalar = b.FunctionParam("scalar", ty.f32());
+    auto* value_b = b.FunctionParam("value_b", mat_ty);
+    target->SetParams({value_a, scalar, value_b});
+    b.Append(target->Block(), [&] {
+        auto* scale = b.Multiply(mat_ty, value_a, scalar);
+        auto* sum = b.Add(mat_ty, scale, value_b);
+        b.Return(target, sum);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* mat_ptr = ty.ptr(private_, mat_ty);
+        auto* ma = b.Load(b.Access(mat_ptr, arr, 0_u));
+        auto* mb = b.Load(b.Access(mat_ptr, arr, 1_u));
+        auto* result = b.Call(mat_ty, target, ma, b.Constant(2_f), mb);
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, array<mat3x3<f32>, 4>, read_write> = var
+}
+
+%target = func(%value_a:mat3x3<f32>, %scalar:f32, %value_b:mat3x3<f32>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %6:mat3x3<f32> = mul %value_a, %scalar
+    %7:mat3x3<f32> = add %6, %value_b
+    ret %7
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %9:ptr<private, mat3x3<f32>, read_write> = access %var, 0u
+    %10:mat3x3<f32> = load %9
+    %11:ptr<private, mat3x3<f32>, read_write> = access %var, 1u
+    %12:mat3x3<f32> = load %11
+    %13:mat3x3<f32> = call %target, %10, 2.0f, %12
+    ret %13
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %var:ptr<private, array<mat3x3<f32>, 4>, read_write> = var
+}
+
+%target = func(%3:ptr<function, mat3x3<f32>, read_write>, %scalar:f32, %5:ptr<function, mat3x3<f32>, read_write>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %6:mat3x3<f32> = load %5
+    %7:mat3x3<f32> = load %3
+    %8:mat3x3<f32> = mul %7, %scalar
+    %9:mat3x3<f32> = add %8, %6
+    ret %9
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %11:ptr<private, mat3x3<f32>, read_write> = access %var, 0u
+    %12:mat3x3<f32> = load %11
+    %13:ptr<private, mat3x3<f32>, read_write> = access %var, 1u
+    %14:mat3x3<f32> = load %13
+    %15:ptr<function, mat3x3<f32>, read_write> = var, %12
+    %16:ptr<function, mat3x3<f32>, read_write> = var, %14
+    %17:mat3x3<f32> = call %target, %15, 2.0f, %16
+    ret %17
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MultipleParamUses) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* mat = mod.root_block->Append(b.Var("var", ty.ptr(private_, mat_ty)));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value = b.FunctionParam("value", mat_ty);
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* add = b.Add(mat_ty, value, value);
+        auto* mul = b.Multiply(mat_ty, add, value);
+        b.Return(target, mul);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(mat));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%value:mat3x3<f32>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = add %value, %value
+    %5:mat3x3<f32> = mul %4, %value
+    ret %5
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %7:mat3x3<f32> = load %var
+    %8:mat3x3<f32> = call %target, %7
+    ret %8
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%3:ptr<function, mat3x3<f32>, read_write>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = load %3
+    %5:mat3x3<f32> = add %4, %4
+    %6:mat3x3<f32> = mul %5, %4
+    ret %6
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %8:mat3x3<f32> = load %var
+    %9:ptr<function, mat3x3<f32>, read_write> = var, %8
+    %10:mat3x3<f32> = call %target, %9
+    ret %10
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MultipleCallsites) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* mat = mod.root_block->Append(b.Var("var", ty.ptr(private_, mat_ty)));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value = b.FunctionParam("value", mat_ty);
+    auto* scalar = b.FunctionParam("scalar", ty.f32());
+    target->SetParams({value, scalar});
+    b.Append(target->Block(), [&] {
+        auto* scale = b.Multiply(mat_ty, value, scalar);
+        b.Return(target, scale);
+    });
+
+    auto* caller_a = b.Function("caller_a", mat_ty);
+    b.Append(caller_a->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(mat), b.Constant(2_f));
+        b.Return(caller_a, result);
+    });
+
+    auto* caller_b = b.Function("caller_b", mat_ty);
+    b.Append(caller_b->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(mat), b.Constant(3_f));
+        b.Return(caller_b, result);
+    });
+
+    auto* caller_c = b.Function("caller_c", mat_ty);
+    b.Append(caller_c->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(mat), b.Constant(4_f));
+        b.Return(caller_c, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%value:mat3x3<f32>, %scalar:f32):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %5:mat3x3<f32> = mul %value, %scalar
+    ret %5
+  }
+}
+%caller_a = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %7:mat3x3<f32> = load %var
+    %8:mat3x3<f32> = call %target, %7, 2.0f
+    ret %8
+  }
+}
+%caller_b = func():mat3x3<f32> -> %b4 {
+  %b4 = block {
+    %10:mat3x3<f32> = load %var
+    %11:mat3x3<f32> = call %target, %10, 3.0f
+    ret %11
+  }
+}
+%caller_c = func():mat3x3<f32> -> %b5 {
+  %b5 = block {
+    %13:mat3x3<f32> = load %var
+    %14:mat3x3<f32> = call %target, %13, 4.0f
+    ret %14
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %var:ptr<private, mat3x3<f32>, read_write> = var
+}
+
+%target = func(%3:ptr<function, mat3x3<f32>, read_write>, %scalar:f32):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %5:mat3x3<f32> = load %3
+    %6:mat3x3<f32> = mul %5, %scalar
+    ret %6
+  }
+}
+%caller_a = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %8:mat3x3<f32> = load %var
+    %9:ptr<function, mat3x3<f32>, read_write> = var, %8
+    %10:mat3x3<f32> = call %target, %9, 2.0f
+    ret %10
+  }
+}
+%caller_b = func():mat3x3<f32> -> %b4 {
+  %b4 = block {
+    %12:mat3x3<f32> = load %var
+    %13:ptr<function, mat3x3<f32>, read_write> = var, %12
+    %14:mat3x3<f32> = call %target, %13, 3.0f
+    ret %14
+  }
+}
+%caller_c = func():mat3x3<f32> -> %b5 {
+  %b5 = block {
+    %16:mat3x3<f32> = load %var
+    %17:ptr<function, mat3x3<f32>, read_write> = var, %16
+    %18:mat3x3<f32> = call %target, %17, 4.0f
+    ret %18
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MatrixInArray) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* arr_ty = ty.array(mat_ty, 2);
+    auto* arr = mod.root_block->Append(b.Var("var", ty.ptr(private_, arr_ty)));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value = b.FunctionParam("value", arr_ty);
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* ma = b.Access(mat_ty, value, 0_u);
+        auto* mb = b.Access(mat_ty, value, 1_u);
+        auto* add = b.Add(mat_ty, ma, mb);
+        b.Return(target, add);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(arr));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %var:ptr<private, array<mat3x3<f32>, 2>, read_write> = var
+}
+
+%target = func(%value:array<mat3x3<f32>, 2>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = access %value, 0u
+    %5:mat3x3<f32> = access %value, 1u
+    %6:mat3x3<f32> = add %4, %5
+    ret %6
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %8:array<mat3x3<f32>, 2> = load %var
+    %9:mat3x3<f32> = call %target, %8
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %var:ptr<private, array<mat3x3<f32>, 2>, read_write> = var
+}
+
+%target = func(%3:ptr<function, array<mat3x3<f32>, 2>, read_write>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:array<mat3x3<f32>, 2> = load %3
+    %5:mat3x3<f32> = access %4, 0u
+    %6:mat3x3<f32> = access %4, 1u
+    %7:mat3x3<f32> = add %5, %6
+    ret %7
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %9:array<mat3x3<f32>, 2> = load %var
+    %10:ptr<function, array<mat3x3<f32>, 2>, read_write> = var, %9
+    %11:mat3x3<f32> = call %target, %10
+    ret %11
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MatrixInStruct) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* str_ty = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                              {mod.symbols.New("m"), mat_ty},
+                                                              {mod.symbols.New("s"), ty.f32()},
+                                                          });
+    auto* structure = mod.root_block->Append(b.Var("var", ty.ptr(private_, str_ty)));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value = b.FunctionParam("value", str_ty);
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* m = b.Access(mat_ty, value, 0_u);
+        auto* s = b.Access(ty.f32(), value, 1_u);
+        auto* mul = b.Multiply(mat_ty, m, s);
+        b.Return(target, mul);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(structure));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  m:mat3x3<f32> @offset(0)
+  s:f32 @offset(48)
+}
+
+%b1 = block {  # root
+  %var:ptr<private, MyStruct, read_write> = var
+}
+
+%target = func(%value:MyStruct):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = access %value, 0u
+    %5:f32 = access %value, 1u
+    %6:mat3x3<f32> = mul %4, %5
+    ret %6
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %8:MyStruct = load %var
+    %9:mat3x3<f32> = call %target, %8
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  m:mat3x3<f32> @offset(0)
+  s:f32 @offset(48)
+}
+
+%b1 = block {  # root
+  %var:ptr<private, MyStruct, read_write> = var
+}
+
+%target = func(%3:ptr<function, MyStruct, read_write>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:MyStruct = load %3
+    %5:mat3x3<f32> = access %4, 0u
+    %6:f32 = access %4, 1u
+    %7:mat3x3<f32> = mul %5, %6
+    ret %7
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %9:MyStruct = load %var
+    %10:ptr<function, MyStruct, read_write> = var, %9
+    %11:mat3x3<f32> = call %target, %10
+    ret %11
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_PassMatrixByPointerTest, MatrixArrayOfStructOfArray) {
+    auto* mat_ty = ty.mat3x3<f32>();
+    auto* str_ty =
+        ty.Struct(mod.symbols.New("MyStruct"), {
+                                                   {mod.symbols.New("m"), ty.array(mat_ty, 2)},
+                                                   {mod.symbols.New("s"), ty.f32()},
+                                               });
+    auto* arr_ty = ty.array(str_ty, 4);
+    auto* var = mod.root_block->Append(b.Var("var", ty.ptr(private_, arr_ty)));
+
+    auto* target = b.Function("target", mat_ty);
+    auto* value = b.FunctionParam("value", arr_ty);
+    target->SetParams({value});
+    b.Append(target->Block(), [&] {
+        auto* ma = b.Access(mat_ty, value, 2_u, 0_u, 0_u);
+        auto* mb = b.Access(mat_ty, value, 2_u, 0_u, 1_u);
+        auto* s = b.Access(ty.f32(), value, 2_u, 1_u);
+        auto* add = b.Add(mat_ty, ma, mb);
+        auto* mul = b.Multiply(mat_ty, add, s);
+        b.Return(target, mul);
+    });
+
+    auto* caller = b.Function("caller", mat_ty);
+    b.Append(caller->Block(), [&] {
+        auto* result = b.Call(mat_ty, target, b.Load(var));
+        b.Return(caller, result);
+    });
+
+    auto* src = R"(
+MyStruct = struct @align(16) {
+  m:array<mat3x3<f32>, 2> @offset(0)
+  s:f32 @offset(96)
+}
+
+%b1 = block {  # root
+  %var:ptr<private, array<MyStruct, 4>, read_write> = var
+}
+
+%target = func(%value:array<MyStruct, 4>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:mat3x3<f32> = access %value, 2u, 0u, 0u
+    %5:mat3x3<f32> = access %value, 2u, 0u, 1u
+    %6:f32 = access %value, 2u, 1u
+    %7:mat3x3<f32> = add %4, %5
+    %8:mat3x3<f32> = mul %7, %6
+    ret %8
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %10:array<MyStruct, 4> = load %var
+    %11:mat3x3<f32> = call %target, %10
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16) {
+  m:array<mat3x3<f32>, 2> @offset(0)
+  s:f32 @offset(96)
+}
+
+%b1 = block {  # root
+  %var:ptr<private, array<MyStruct, 4>, read_write> = var
+}
+
+%target = func(%3:ptr<function, array<MyStruct, 4>, read_write>):mat3x3<f32> -> %b2 {
+  %b2 = block {
+    %4:array<MyStruct, 4> = load %3
+    %5:mat3x3<f32> = access %4, 2u, 0u, 0u
+    %6:mat3x3<f32> = access %4, 2u, 0u, 1u
+    %7:f32 = access %4, 2u, 1u
+    %8:mat3x3<f32> = add %5, %6
+    %9:mat3x3<f32> = mul %8, %7
+    ret %9
+  }
+}
+%caller = func():mat3x3<f32> -> %b3 {
+  %b3 = block {
+    %11:array<MyStruct, 4> = load %var
+    %12:ptr<function, array<MyStruct, 4>, read_write> = var, %11
+    %13:mat3x3<f32> = call %target, %12
+    ret %13
+  }
+}
+)";
+
+    Run(PassMatrixByPointer);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 6045987..de1eb99 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -22,15 +22,21 @@
 #include "src/tint/lang/core/ir/transform/binding_remapper.h"
 #include "src/tint/lang/core/ir/transform/block_decorated_structs.h"
 #include "src/tint/lang/core/ir/transform/builtin_polyfill.h"
+#include "src/tint/lang/core/ir/transform/combine_access_instructions.h"
+#include "src/tint/lang/core/ir/transform/conversion_polyfill.h"
 #include "src/tint/lang/core/ir/transform/demote_to_helper.h"
+#include "src/tint/lang/core/ir/transform/direct_variable_access.h"
 #include "src/tint/lang/core/ir/transform/multiplanar_external_texture.h"
+#include "src/tint/lang/core/ir/transform/preserve_padding.h"
 #include "src/tint/lang/core/ir/transform/robustness.h"
 #include "src/tint/lang/core/ir/transform/std140.h"
 #include "src/tint/lang/core/ir/transform/zero_init_workgroup_memory.h"
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
 #include "src/tint/lang/spirv/writer/raise/builtin_polyfill.h"
 #include "src/tint/lang/spirv/writer/raise/expand_implicit_splats.h"
 #include "src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h"
 #include "src/tint/lang/spirv/writer/raise/merge_return.h"
+#include "src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.h"
 #include "src/tint/lang/spirv/writer/raise/shader_io.h"
 #include "src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h"
 
@@ -45,7 +51,11 @@
         }                                \
     } while (false)
 
-    RUN_TRANSFORM(core::ir::transform::BindingRemapper, module, options.binding_remapper_options);
+    ExternalTextureOptions external_texture_options{};
+    RemapperData remapper_data{};
+    PopulateRemapperAndMultiplanarOptions(options, remapper_data, external_texture_options);
+
+    RUN_TRANSFORM(core::ir::transform::BindingRemapper, module, remapper_data);
 
     core::ir::transform::BinaryPolyfillConfig binary_polyfills;
     binary_polyfills.bitshift_modulo = true;
@@ -64,6 +74,10 @@
     core_polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
     RUN_TRANSFORM(core::ir::transform::BuiltinPolyfill, module, core_polyfills);
 
+    core::ir::transform::ConversionPolyfillConfig conversion_polyfills;
+    conversion_polyfills.ftoi = true;
+    RUN_TRANSFORM(core::ir::transform::ConversionPolyfill, module, conversion_polyfills);
+
     if (!options.disable_robustness) {
         core::ir::transform::RobustnessConfig config;
         if (options.disable_image_robustness) {
@@ -75,16 +89,35 @@
     }
 
     RUN_TRANSFORM(core::ir::transform::MultiplanarExternalTexture, module,
-                  options.external_texture_options);
+                  external_texture_options);
 
     if (!options.disable_workgroup_init &&
         !options.use_zero_initialize_workgroup_memory_extension) {
         RUN_TRANSFORM(core::ir::transform::ZeroInitWorkgroupMemory, module);
     }
 
+    // PreservePadding must come before DirectVariableAccess.
+    RUN_TRANSFORM(core::ir::transform::PreservePadding, module);
+
+    core::ir::transform::DirectVariableAccessOptions dva_options;
+    dva_options.transform_function = true;
+    dva_options.transform_private = true;
+    RUN_TRANSFORM(core::ir::transform::DirectVariableAccess, module, dva_options);
+
+    if (options.pass_matrix_by_pointer) {
+        // PassMatrixByPointer must come after PreservePadding+DirectVariableAccess.
+        RUN_TRANSFORM(PassMatrixByPointer, module);
+    }
+
     RUN_TRANSFORM(core::ir::transform::AddEmptyEntryPoint, module);
     RUN_TRANSFORM(core::ir::transform::Bgra8UnormPolyfill, module);
     RUN_TRANSFORM(core::ir::transform::BlockDecoratedStructs, module);
+
+    // CombineAccessInstructions must come after DirectVariableAccess and BlockDecoratedStructs.
+    // We run this transform as some Qualcomm drivers struggle with partial access chains that
+    // produce pointers to matrices.
+    RUN_TRANSFORM(core::ir::transform::CombineAccessInstructions, module);
+
     RUN_TRANSFORM(BuiltinPolyfill, module);
     RUN_TRANSFORM(core::ir::transform::DemoteToHelper, module);
     RUN_TRANSFORM(ExpandImplicitSplats, module);
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.cc b/src/tint/lang/spirv/writer/raise/shader_io.cc
index f807c31..2aa195e 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -83,6 +83,9 @@
                 }
             } else {
                 name << "_loc" << io.attributes.location.value();
+                if (io.attributes.index.has_value()) {
+                    name << "_idx" << io.attributes.index.value();
+                }
             }
             name << name_suffix;
 
diff --git a/src/tint/lang/spirv/writer/raise/shader_io_test.cc b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
index 343f778..c37f9fb 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -547,6 +547,78 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_ShaderIOTest, ReturnValue_DualSourceBlending) {
+    auto* str_ty = ty.Struct(mod.symbols.New("Output"), {
+                                                            {
+                                                                mod.symbols.New("color1"),
+                                                                ty.f32(),
+                                                                {0u, 0u, {}, {}, false},
+                                                            },
+                                                            {
+                                                                mod.symbols.New("color2"),
+                                                                ty.f32(),
+                                                                {0u, 1u, {}, {}, false},
+                                                            },
+                                                        });
+
+    auto* ep = b.Function("foo", str_ty);
+    ep->SetStage(core::ir::Function::PipelineStage::kFragment);
+
+    b.Append(ep->Block(), [&] {  //
+        b.Return(ep, b.Construct(str_ty, 0.25_f, 0.75_f));
+    });
+
+    auto* src = R"(
+Output = struct @align(4) {
+  color1:f32 @offset(0), @location(0)
+  color2:f32 @offset(4), @location(0)
+}
+
+%foo = @fragment func():Output -> %b1 {
+  %b1 = block {
+    %2:Output = construct 0.25f, 0.75f
+    ret %2
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Output = struct @align(4) {
+  color1:f32 @offset(0)
+  color2:f32 @offset(4)
+}
+
+%b1 = block {  # root
+  %foo_loc0_idx0_Output:ptr<__out, f32, write> = var @location(0) @index(0)
+  %foo_loc0_idx1_Output:ptr<__out, f32, write> = var @location(0) @index(1)
+}
+
+%foo_inner = func():Output -> %b2 {
+  %b2 = block {
+    %4:Output = construct 0.25f, 0.75f
+    ret %4
+  }
+}
+%foo = @fragment func():void -> %b3 {
+  %b3 = block {
+    %6:Output = call %foo_inner
+    %7:f32 = access %6, 0u
+    store %foo_loc0_idx0_Output, %7
+    %8:f32 = access %6, 1u
+    store %foo_loc0_idx1_Output, %8
+    ret
+  }
+}
+)";
+
+    ShaderIOConfig config;
+    config.clamp_frag_depth = false;
+    Run(ShaderIO, config);
+
+    EXPECT_EQ(expect, str());
+}
+
 TEST_F(SpirvWriter_ShaderIOTest, Struct_SharedByVertexAndFragment) {
     auto* vec4f = ty.vec4<f32>();
     auto* str_ty = ty.Struct(mod.symbols.New("Interface"),
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
index 88d564f..6880fba 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -117,7 +117,7 @@
     // Find the access instructions that need replacing.
     Vector<AccessToReplace, 4> worklist;
     for (auto* inst : ir.instructions.Objects()) {
-        if (auto* access = inst->As<core::ir::Access>()) {
+        if (auto* access = inst->As<core::ir::Access>(); access && access->Alive()) {
             if (auto to_replace = ShouldReplace(access)) {
                 worklist.Push(to_replace.value());
             }
@@ -184,6 +184,7 @@
         // Replace all uses of the old access instruction with the loaded result.
         access->Result()->ReplaceAllUsesWith(load->Result());
         access->ReplaceWith(load);
+        access->Destroy();
     }
 }
 
diff --git a/src/tint/lang/spirv/writer/type_test.cc b/src/tint/lang/spirv/writer/type_test.cc
index cb21390..ff1c4e1 100644
--- a/src/tint/lang/spirv/writer/type_test.cc
+++ b/src/tint/lang/spirv/writer/type_test.cc
@@ -368,9 +368,6 @@
                                                 Dim::k3d, Format::kR32Float},
 
                              // Test all the formats with 2D.
-                             // TODO(jrprice): Enable this format when we polyfill it.
-                             // StorageTextureCase{"%1 = OpTypeImage %float 2D 0 0 0 2 Bgra8Unorm",
-                             //                    Dim::k2d, Format::kBgra8Unorm},
                              StorageTextureCase{"%1 = OpTypeImage %int 2D 0 0 0 2 R32i",  //
                                                 Dim::k2d, Format::kR32Sint},
                              StorageTextureCase{"%1 = OpTypeImage %uint 2D 0 0 0 2 R32u",  //
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index 1a47183..f1a6763 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -18,10 +18,14 @@
 #include <utility>
 
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
+#include "src/tint/lang/spirv/writer/common/option_builder.h"
 #include "src/tint/lang/spirv/writer/printer/printer.h"
 #include "src/tint/lang/spirv/writer/raise/raise.h"
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
+
+#if TINT_BUILD_WGSL_READER
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
+#endif
 
 // Included by 'ast_printer.h', included again here for './tools/run gen' track the dependency.
 #include "spirv/unified1/spirv.h"
@@ -40,9 +44,17 @@
     bool zero_initialize_workgroup_memory =
         !options.disable_workgroup_init && options.use_zero_initialize_workgroup_memory_extension;
 
+    {
+        diag::List validation_diagnostics;
+        if (!ValidateBindingOptions(options, validation_diagnostics)) {
+            return Failure{validation_diagnostics};
+        }
+    }
+
     Output output;
 
     if (options.use_tint_ir) {
+#if TINT_BUILD_WGSL_READER
         // Convert the AST program to an IR module.
         auto converted = wgsl::reader::ProgramToIR(program);
         if (!converted) {
@@ -68,6 +80,9 @@
             return std::move(spirv.Failure());
         }
         output.spirv = std::move(spirv.Get());
+#else
+        return Failure{"use_tint_ir requires building with TINT_BUILD_WGSL_READER"};
+#endif
     } else {
         // Sanitize the program.
         auto sanitized_result = Sanitize(program, options);
diff --git a/src/tint/lang/spirv/writer/writer_bench.cc b/src/tint/lang/spirv/writer/writer_bench.cc
index adef3df..c5bccce 100644
--- a/src/tint/lang/spirv/writer/writer_bench.cc
+++ b/src/tint/lang/spirv/writer/writer_bench.cc
@@ -22,15 +22,14 @@
 
 void RunBenchmark(benchmark::State& state, std::string input_name, Options options) {
     auto res = bench::LoadProgram(input_name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        state.SkipWithError(err->msg.c_str());
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
         return;
     }
-    auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(program, options);
-        if (!res) {
-            state.SkipWithError(res.Failure().reason.str());
+        auto gen_res = Generate(res->program, options);
+        if (!gen_res) {
+            state.SkipWithError(gen_res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/lang/wgsl/BUILD.bazel b/src/tint/lang/wgsl/BUILD.bazel
index 83e4df2..e415e16 100644
--- a/src/tint/lang/wgsl/BUILD.bazel
+++ b/src/tint/lang/wgsl/BUILD.bazel
@@ -60,10 +60,14 @@
     "extension_test.cc",
     "wgsl_test.cc",
   ] + select({
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
       "ir_roundtrip_test.cc",
     ],
     "//conditions:default": [],
+  }) + select({
+    "//conditions:default": [],
   }),
   deps = [
     "//src/tint/api/common",
@@ -75,12 +79,9 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/helpers:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/reader/lower",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/lang/wgsl/writer/ir_to_program",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -96,12 +97,26 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "diagnostic_rule_bench.cc",
     "diagnostic_severity_bench.cc",
@@ -118,6 +133,7 @@
     "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
+    "@benchmark",
   ],
   copts = COPTS,
   visibility = ["//visibility:public"],
diff --git a/src/tint/lang/wgsl/BUILD.cmake b/src/tint/lang/wgsl/BUILD.cmake
index c72aa5e..36b6d78 100644
--- a/src/tint/lang/wgsl/BUILD.cmake
+++ b/src/tint/lang/wgsl/BUILD.cmake
@@ -80,12 +80,9 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_reader_lower
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_lang_wgsl_writer_ir_to_program
   tint_utils_containers
   tint_utils_diagnostic
@@ -106,12 +103,25 @@
   "gtest"
 )
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_test test
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
   tint_target_add_sources(tint_lang_wgsl_test test
     "lang/wgsl/ir_roundtrip_test.cc"
   )
 endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
 ################################################################################
 # Target:    tint_lang_wgsl_bench
 # Kind:      bench
@@ -134,3 +144,7 @@
   tint_utils_text
   tint_utils_traits
 )
+
+tint_target_add_external_dependencies(tint_lang_wgsl_bench bench
+  "google-benchmark"
+)
diff --git a/src/tint/lang/wgsl/BUILD.gn b/src/tint/lang/wgsl/BUILD.gn
index 4ddc480..4a8a3bc 100644
--- a/src/tint/lang/wgsl/BUILD.gn
+++ b/src/tint/lang/wgsl/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -54,7 +54,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "diagnostic_rule_test.cc",
       "diagnostic_severity_test.cc",
@@ -72,12 +71,9 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/helpers:unittests",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
       "${tint_src_dir}/lang/wgsl/reader/lower",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
-      "${tint_src_dir}/lang/wgsl/writer",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -94,8 +90,41 @@
       "${tint_src_dir}/utils/traits",
     ]
 
+    if (tint_build_wgsl_reader) {
+      deps += [
+        "${tint_src_dir}/lang/wgsl/reader",
+        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+      ]
+    }
+
     if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
       sources += [ "ir_roundtrip_test.cc" ]
     }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+    }
+  }
+}
+if (tint_build_benchmarks) {
+  tint_unittests_source_set("bench") {
+    sources = [
+      "diagnostic_rule_bench.cc",
+      "diagnostic_severity_bench.cc",
+      "extension_bench.cc",
+    ]
+    deps = [
+      "${tint_src_dir}:google_benchmark",
+      "${tint_src_dir}/lang/wgsl",
+      "${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/rtti",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
   }
 }
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
index 3bed5ae..4ca45b5 100644
--- a/src/tint/lang/wgsl/ast/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -256,7 +256,6 @@
     "location_attribute_test.cc",
     "loop_statement_test.cc",
     "member_accessor_expression_test.cc",
-    "module_clone_test.cc",
     "module_test.cc",
     "phony_expression_test.cc",
     "return_statement_test.cc",
@@ -275,7 +274,16 @@
     "variable_test.cc",
     "while_statement_test.cc",
     "workgroup_attribute_test.cc",
-  ],
+  ] + select({
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "module_clone_test.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    "//conditions:default": [],
+  }),
   deps = [
     "//src/tint/api/common",
     "//src/tint/lang/core",
@@ -286,10 +294,8 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -304,8 +310,38 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/wgsl/ast/BUILD.cmake b/src/tint/lang/wgsl/ast/BUILD.cmake
index f9293f9..c060fc8 100644
--- a/src/tint/lang/wgsl/ast/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/BUILD.cmake
@@ -256,7 +256,6 @@
   lang/wgsl/ast/location_attribute_test.cc
   lang/wgsl/ast/loop_statement_test.cc
   lang/wgsl/ast/member_accessor_expression_test.cc
-  lang/wgsl/ast/module_clone_test.cc
   lang/wgsl/ast/module_test.cc
   lang/wgsl/ast/phony_expression_test.cc
   lang/wgsl/ast/return_statement_test.cc
@@ -287,10 +286,8 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -309,3 +306,21 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_ast_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_ast_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_sources(tint_lang_wgsl_ast_test test
+    "lang/wgsl/ast/module_clone_test.cc"
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_ast_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/wgsl/ast/BUILD.gn b/src/tint/lang/wgsl/ast/BUILD.gn
index 2cc22fe..b8f68a9 100644
--- a/src/tint/lang/wgsl/ast/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -211,7 +211,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "alias_test.cc",
       "assignment_statement_test.cc",
@@ -257,7 +256,6 @@
       "location_attribute_test.cc",
       "loop_statement_test.cc",
       "member_accessor_expression_test.cc",
-      "module_clone_test.cc",
       "module_test.cc",
       "phony_expression_test.cc",
       "return_statement_test.cc",
@@ -288,10 +286,8 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
-      "${tint_src_dir}/lang/wgsl/writer",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
@@ -306,5 +302,17 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_wgsl_reader) {
+      deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+    }
+
+    if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+      sources += [ "module_clone_test.cc" ]
+    }
+
+    if (tint_build_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+    }
   }
 }
diff --git a/src/tint/lang/wgsl/ast/module_clone_test.cc b/src/tint/lang/wgsl/ast/module_clone_test.cc
index a683b0a..035a565 100644
--- a/src/tint/lang/wgsl/ast/module_clone_test.cc
+++ b/src/tint/lang/wgsl/ast/module_clone_test.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader && tint_build_wgsl_writer)
+
 #include <unordered_set>
 
 #include "gtest/gtest.h"
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.bazel b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
index 03b8500..cf13cce 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
@@ -173,10 +173,8 @@
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -191,8 +189,36 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cfg b/src/tint/lang/wgsl/ast/transform/BUILD.cfg
new file mode 100644
index 0000000..f31a82c
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cfg
@@ -0,0 +1,5 @@
+{
+    "test": {
+        "condition": "tint_build_wgsl_reader && tint_build_wgsl_writer"
+    }
+}
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index 1f7ecf6..bc523fc 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -118,9 +118,11 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_wgsl_ast_transform_test
 # Kind:      test
+# Condition: TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_wgsl_ast_transform_test test
   lang/wgsl/ast/transform/add_block_attribute_test.cc
@@ -172,10 +174,8 @@
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -194,3 +194,17 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_ast_transform_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_ast_transform_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_ast_transform_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 958683e..5012415 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -123,75 +123,82 @@
   ]
 }
 if (tint_build_unittests) {
-  tint_unittests_source_set("unittests") {
-    testonly = true
-    sources = [
-      "add_block_attribute_test.cc",
-      "add_empty_entry_point_test.cc",
-      "array_length_from_uniform_test.cc",
-      "binding_remapper_test.cc",
-      "builtin_polyfill_test.cc",
-      "canonicalize_entry_point_io_test.cc",
-      "demote_to_helper_test.cc",
-      "direct_variable_access_test.cc",
-      "disable_uniformity_analysis_test.cc",
-      "expand_compound_assignment_test.cc",
-      "first_index_offset_test.cc",
-      "get_insertion_point_test.cc",
-      "helper_test.h",
-      "hoist_to_decl_before_test.cc",
-      "manager_test.cc",
-      "multiplanar_external_texture_test.cc",
-      "preserve_padding_test.cc",
-      "promote_initializers_to_let_test.cc",
-      "promote_side_effects_to_decl_test.cc",
-      "remove_phonies_test.cc",
-      "remove_unreachable_statements_test.cc",
-      "renamer_test.cc",
-      "robustness_test.cc",
-      "simplify_pointers_test.cc",
-      "single_entry_point_test.cc",
-      "std140_exhaustive_test.cc",
-      "std140_f16_test.cc",
-      "std140_f32_test.cc",
-      "std140_test.cc",
-      "substitute_override_test.cc",
-      "transform_test.cc",
-      "unshadow_test.cc",
-      "vectorize_scalar_matrix_initializers_test.cc",
-      "vertex_pulling_test.cc",
-      "zero_init_workgroup_memory_test.cc",
-    ]
-    deps = [
-      "${tint_src_dir}:gmock_and_gtest",
-      "${tint_src_dir}/api/common",
-      "${tint_src_dir}/api/options",
-      "${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/ast:unittests",
-      "${tint_src_dir}/lang/wgsl/ast/transform",
-      "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
-      "${tint_src_dir}/lang/wgsl/resolver",
-      "${tint_src_dir}/lang/wgsl/sem",
-      "${tint_src_dir}/lang/wgsl/writer",
-      "${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_wgsl_reader && tint_build_wgsl_writer) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "add_block_attribute_test.cc",
+        "add_empty_entry_point_test.cc",
+        "array_length_from_uniform_test.cc",
+        "binding_remapper_test.cc",
+        "builtin_polyfill_test.cc",
+        "canonicalize_entry_point_io_test.cc",
+        "demote_to_helper_test.cc",
+        "direct_variable_access_test.cc",
+        "disable_uniformity_analysis_test.cc",
+        "expand_compound_assignment_test.cc",
+        "first_index_offset_test.cc",
+        "get_insertion_point_test.cc",
+        "helper_test.h",
+        "hoist_to_decl_before_test.cc",
+        "manager_test.cc",
+        "multiplanar_external_texture_test.cc",
+        "preserve_padding_test.cc",
+        "promote_initializers_to_let_test.cc",
+        "promote_side_effects_to_decl_test.cc",
+        "remove_phonies_test.cc",
+        "remove_unreachable_statements_test.cc",
+        "renamer_test.cc",
+        "robustness_test.cc",
+        "simplify_pointers_test.cc",
+        "single_entry_point_test.cc",
+        "std140_exhaustive_test.cc",
+        "std140_f16_test.cc",
+        "std140_f32_test.cc",
+        "std140_test.cc",
+        "substitute_override_test.cc",
+        "transform_test.cc",
+        "unshadow_test.cc",
+        "vectorize_scalar_matrix_initializers_test.cc",
+        "vertex_pulling_test.cc",
+        "zero_init_workgroup_memory_test.cc",
+      ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/api/options",
+        "${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/ast:unittests",
+        "${tint_src_dir}/lang/wgsl/ast/transform",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/resolver",
+        "${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_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+
+      if (tint_build_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
+    }
   }
 }
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
index c61798d..8daf8d1 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -20,6 +20,7 @@
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/rtti/switch.h"
@@ -73,13 +74,10 @@
             decl,  //
             [&](const TypeDecl* ty) {
                 // Strip aliases that reference unused override declarations.
-                if (auto* arr = sem.Get(ty)->As<core::type::Array>()) {
-                    auto* refs = sem.TransitivelyReferencedOverrides(arr);
-                    if (refs) {
-                        for (auto* o : *refs) {
-                            if (!referenced_vars.Contains(o)) {
-                                return;
-                            }
+                if (auto* arr = sem.Get(ty)->As<sem::Array>()) {
+                    for (auto* o : arr->TransitivelyReferencedOverrides()) {
+                        if (!referenced_vars.Contains(o)) {
+                            return;
                         }
                     }
                 }
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index cc747fb..944ecf4 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -126,6 +126,8 @@
             return out << "sint32x3";
         case VertexFormat::kSint32x4:
             return out << "sint32x4";
+        case VertexFormat::kUnorm10_10_10_2:
+            return out << "unorm10-10-10-2";
     }
     return out << "<unknown>";
 }
@@ -223,6 +225,7 @@
         case VertexFormat::kSnorm16x4:
         case VertexFormat::kFloat16x4:
         case VertexFormat::kFloat32x4:
+        case VertexFormat::kUnorm10_10_10_2:
             return {VertexDataType::kFloat, 4};
     }
     return {VertexDataType::kInvalid, 0};
@@ -667,6 +670,15 @@
             case VertexFormat::kFloat16x4:
                 return b.Call<vec4<f32>>(b.Call("unpack2x16float", load_u32()),
                                          b.Call("unpack2x16float", load_next_u32()));
+            case VertexFormat::kUnorm10_10_10_2:
+                auto* u32s = b.Call<vec4<u32>>(load_u32());
+                // shr = u32s >> vec4u(0, 10, 20, 30);
+                auto* shr = b.Shr(u32s, b.Call<vec4<u32>>(0_u, 10_u, 20_u, 30_u));
+                // mask = shr & vec4u(0x3FF, 0x3FF, 0x3FF, 0x3);
+                auto* mask = b.And(shr, b.Call<vec4<u32>>(0x3FF_u, 0x3FF_u, 0x3FF_u, 0x3_u));
+                // return vec4f(mask) / vec4f(1023, 1023, 1023, 3);
+                return b.Div(b.Call<vec4<f32>>(mask),
+                             b.Call<vec4<f32>>(1023_f, 1023_f, 1023_f, 3_f));
         }
 
         TINT_UNREACHABLE() << "format " << static_cast<int>(format);
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.h b/src/tint/lang/wgsl/ast/transform/vertex_pulling.h
index deaed5a..e5084ab 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.h
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.h
@@ -27,36 +27,37 @@
 
 /// Describes the format of data in a vertex buffer
 enum class VertexFormat {
-    kUint8x2,    // uint8x2
-    kUint8x4,    // uint8x4
-    kSint8x2,    // sint8x2
-    kSint8x4,    // sint8x4
-    kUnorm8x2,   // unorm8x2
-    kUnorm8x4,   // unorm8x4
-    kSnorm8x2,   // snorm8x2
-    kSnorm8x4,   // snorm8x4
-    kUint16x2,   // uint16x2
-    kUint16x4,   // uint16x4
-    kSint16x2,   // sint16x2
-    kSint16x4,   // sint16x4
-    kUnorm16x2,  // unorm16x2
-    kUnorm16x4,  // unorm16x4
-    kSnorm16x2,  // snorm16x2
-    kSnorm16x4,  // snorm16x4
-    kFloat16x2,  // float16x2
-    kFloat16x4,  // float16x4
-    kFloat32,    // float32
-    kFloat32x2,  // float32x2
-    kFloat32x3,  // float32x3
-    kFloat32x4,  // float32x4
-    kUint32,     // uint32
-    kUint32x2,   // uint32x2
-    kUint32x3,   // uint32x3
-    kUint32x4,   // uint32x4
-    kSint32,     // sint32
-    kSint32x2,   // sint32x2
-    kSint32x3,   // sint32x3
-    kSint32x4,   // sint32x4
+    kUint8x2,          // uint8x2
+    kUint8x4,          // uint8x4
+    kSint8x2,          // sint8x2
+    kSint8x4,          // sint8x4
+    kUnorm8x2,         // unorm8x2
+    kUnorm8x4,         // unorm8x4
+    kSnorm8x2,         // snorm8x2
+    kSnorm8x4,         // snorm8x4
+    kUint16x2,         // uint16x2
+    kUint16x4,         // uint16x4
+    kSint16x2,         // sint16x2
+    kSint16x4,         // sint16x4
+    kUnorm16x2,        // unorm16x2
+    kUnorm16x4,        // unorm16x4
+    kSnorm16x2,        // snorm16x2
+    kSnorm16x4,        // snorm16x4
+    kFloat16x2,        // float16x2
+    kFloat16x4,        // float16x4
+    kFloat32,          // float32
+    kFloat32x2,        // float32x2
+    kFloat32x3,        // float32x3
+    kFloat32x4,        // float32x4
+    kUint32,           // uint32
+    kUint32x2,         // uint32x2
+    kUint32x3,         // uint32x3
+    kUint32x4,         // uint32x4
+    kSint32,           // sint32
+    kSint32x2,         // sint32x2
+    kSint32x3,         // sint32x3
+    kSint32x4,         // sint32x4
+    kUnorm10_10_10_2,  // unorm10-10-10-2
 
     kLastEntry = kSint32x4,
 };
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling_test.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling_test.cc
index 5bb7c59..df3f1c8 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling_test.cc
@@ -1000,6 +1000,7 @@
     @location(11) float32x2 : vec2<f32>,
     @location(12) float32x3 : vec3<f32>,
     @location(13) float32x4 : vec4<f32>,
+    @location(14) unorm10_10_10_2 : vec4<f32>,
   ) -> @builtin(position) vec4<f32> {
   return vec4<f32>(0.0, 0.0, 0.0, 1.0);
 }
@@ -1028,6 +1029,7 @@
   var float32x2 : vec2<f32>;
   var float32x3 : vec3<f32>;
   var float32x4 : vec4<f32>;
+  var unorm10_10_10_2 : vec4<f32>;
   {
     let buffer_array_base_0 = (tint_pulling_vertex_index * 64u);
     unorm8x2 = unpack4x8unorm((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] & 65535u)).xy;
@@ -1044,6 +1046,7 @@
     float32x2 = vec2<f32>(bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)]));
     float32x3 = vec3<f32>(bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)]));
     float32x4 = vec4<f32>(bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 19u)]));
+    unorm10_10_10_2 = (vec4<f32>(((vec4<u32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]) >> vec4<u32>(0u, 10u, 20u, 30u)) & vec4<u32>(1023u, 1023u, 1023u, 3u))) / vec4<f32>(1023.0f, 1023.0f, 1023.0f, 3.0f));
   }
   return vec4<f32>(0.0, 0.0, 0.0, 1.0);
 }
@@ -1067,6 +1070,7 @@
                               {VertexFormat::kFloat32x2, 64, 11},
                               {VertexFormat::kFloat32x3, 64, 12},
                               {VertexFormat::kFloat32x4, 64, 13},
+                              {VertexFormat::kUnorm10_10_10_2, 64, 14},
                           }}}};
 
     DataMap data;
@@ -1332,6 +1336,7 @@
     @location(11) float32x2 : vec2<f32>,
     @location(12) float32x3 : vec3<f32>,
     @location(13) float32x4 : vec4<f32>,
+    @location(14) unorm10_10_10_2 : vec4<f32>,
   ) -> @builtin(position) vec4<f32> {
   return vec4<f32>(0.0, 0.0, 0.0, 1.0);
 }
@@ -1360,6 +1365,7 @@
   var float32x2 : vec2<f32>;
   var float32x3 : vec3<f32>;
   var float32x4 : vec4<f32>;
+  var unorm10_10_10_2 : vec4<f32>;
   {
     let buffer_array_base_0 = (tint_pulling_vertex_index * 64u);
     unorm8x2 = unpack4x8unorm((((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] << 8u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 15u)] >> 24u)) & 65535u)).xy;
@@ -1376,6 +1382,7 @@
     float32x2 = vec2<f32>(bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 15u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] << 8u))), bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)] << 8u))));
     float32x3 = vec3<f32>(bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 15u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] << 8u))), bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)] << 8u))), bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)] << 8u))));
     float32x4 = vec4<f32>(bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 15u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] << 8u))), bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)] << 8u))), bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)] << 8u))), bitcast<f32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 19u)] << 8u))));
+    unorm10_10_10_2 = (vec4<f32>(((vec4<u32>(((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 15u)] >> 24u) | (tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] << 8u))) >> vec4<u32>(0u, 10u, 20u, 30u)) & vec4<u32>(1023u, 1023u, 1023u, 3u))) / vec4<f32>(1023.0f, 1023.0f, 1023.0f, 3.0f));
   }
   return vec4<f32>(0.0, 0.0, 0.0, 1.0);
 }
@@ -1399,6 +1406,7 @@
                               {VertexFormat::kFloat32x2, 63, 11},
                               {VertexFormat::kFloat32x3, 63, 12},
                               {VertexFormat::kFloat32x4, 63, 13},
+                              {VertexFormat::kUnorm10_10_10_2, 63, 14},
                           }}}};
 
     DataMap data;
@@ -2088,6 +2096,9 @@
     @location(23) sclr_float32x4 :      f32 ,
     @location(24) vec2_float32x4 : vec2<f32>,
     @location(25) vec3_float32x4 : vec3<f32>,
+    @location(26) sclr_unorm10_10_10_2   :      f32 ,
+    @location(27) vec2_unorm10_10_10_2   : vec2<f32>,
+    @location(28) vec3_unorm10_10_10_2   : vec3<f32>,
   ) -> @builtin(position) vec4<f32> {
   return vec4<f32>(0.0, 0.0, 0.0, 1.0);
 }
@@ -2128,6 +2139,9 @@
   var sclr_float32x4 : f32;
   var vec2_float32x4 : vec2<f32>;
   var vec3_float32x4 : vec3<f32>;
+  var sclr_unorm10_10_10_2 : f32;
+  var vec2_unorm10_10_10_2 : vec2<f32>;
+  var vec3_unorm10_10_10_2 : vec3<f32>;
   {
     let buffer_array_base_0 = (tint_pulling_vertex_index * 64u);
     sclr_unorm8x2 = unpack4x8unorm((tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)] & 65535u)).xy.x;
@@ -2156,6 +2170,9 @@
     sclr_float32x4 = vec4<f32>(bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 19u)])).x;
     vec2_float32x4 = vec4<f32>(bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 19u)])).xy;
     vec3_float32x4 = vec4<f32>(bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 17u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 18u)]), bitcast<f32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 19u)])).xyz;
+    sclr_unorm10_10_10_2 = ((vec4<f32>(((vec4<u32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]) >> vec4<u32>(0u, 10u, 20u, 30u)) & vec4<u32>(1023u, 1023u, 1023u, 3u))) / vec4<f32>(1023.0f, 1023.0f, 1023.0f, 3.0f))).x;
+    vec2_unorm10_10_10_2 = ((vec4<f32>(((vec4<u32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]) >> vec4<u32>(0u, 10u, 20u, 30u)) & vec4<u32>(1023u, 1023u, 1023u, 3u))) / vec4<f32>(1023.0f, 1023.0f, 1023.0f, 3.0f))).xy;
+    vec3_unorm10_10_10_2 = ((vec4<f32>(((vec4<u32>(tint_pulling_vertex_buffer_0.tint_vertex_data[(buffer_array_base_0 + 16u)]) >> vec4<u32>(0u, 10u, 20u, 30u)) & vec4<u32>(1023u, 1023u, 1023u, 3u))) / vec4<f32>(1023.0f, 1023.0f, 1023.0f, 3.0f))).xyz;
   }
   return vec4<f32>(0.0, 0.0, 0.0, 1.0);
 }
@@ -2166,19 +2183,21 @@
         {{256,
           VertexStepMode::kVertex,
           {
-              {VertexFormat::kUnorm8x2, 64, 0},   {VertexFormat::kUnorm8x4, 64, 1},
-              {VertexFormat::kUnorm8x4, 64, 2},   {VertexFormat::kUnorm8x4, 64, 3},
-              {VertexFormat::kSnorm8x2, 64, 4},   {VertexFormat::kSnorm8x4, 64, 5},
-              {VertexFormat::kSnorm8x4, 64, 6},   {VertexFormat::kSnorm8x4, 64, 7},
-              {VertexFormat::kUnorm16x2, 64, 8},  {VertexFormat::kUnorm16x4, 64, 9},
-              {VertexFormat::kUnorm16x4, 64, 10}, {VertexFormat::kUnorm16x4, 64, 11},
-              {VertexFormat::kSnorm16x2, 64, 12}, {VertexFormat::kSnorm16x4, 64, 13},
-              {VertexFormat::kSnorm16x4, 64, 14}, {VertexFormat::kSnorm16x4, 64, 15},
-              {VertexFormat::kFloat16x2, 64, 16}, {VertexFormat::kFloat16x4, 64, 17},
-              {VertexFormat::kFloat16x4, 64, 18}, {VertexFormat::kFloat16x4, 64, 19},
-              {VertexFormat::kFloat32x2, 64, 20}, {VertexFormat::kFloat32x3, 64, 21},
-              {VertexFormat::kFloat32x3, 64, 22}, {VertexFormat::kFloat32x4, 64, 23},
-              {VertexFormat::kFloat32x4, 64, 24}, {VertexFormat::kFloat32x4, 64, 25},
+              {VertexFormat::kUnorm8x2, 64, 0},         {VertexFormat::kUnorm8x4, 64, 1},
+              {VertexFormat::kUnorm8x4, 64, 2},         {VertexFormat::kUnorm8x4, 64, 3},
+              {VertexFormat::kSnorm8x2, 64, 4},         {VertexFormat::kSnorm8x4, 64, 5},
+              {VertexFormat::kSnorm8x4, 64, 6},         {VertexFormat::kSnorm8x4, 64, 7},
+              {VertexFormat::kUnorm16x2, 64, 8},        {VertexFormat::kUnorm16x4, 64, 9},
+              {VertexFormat::kUnorm16x4, 64, 10},       {VertexFormat::kUnorm16x4, 64, 11},
+              {VertexFormat::kSnorm16x2, 64, 12},       {VertexFormat::kSnorm16x4, 64, 13},
+              {VertexFormat::kSnorm16x4, 64, 14},       {VertexFormat::kSnorm16x4, 64, 15},
+              {VertexFormat::kFloat16x2, 64, 16},       {VertexFormat::kFloat16x4, 64, 17},
+              {VertexFormat::kFloat16x4, 64, 18},       {VertexFormat::kFloat16x4, 64, 19},
+              {VertexFormat::kFloat32x2, 64, 20},       {VertexFormat::kFloat32x3, 64, 21},
+              {VertexFormat::kFloat32x3, 64, 22},       {VertexFormat::kFloat32x4, 64, 23},
+              {VertexFormat::kFloat32x4, 64, 24},       {VertexFormat::kFloat32x4, 64, 25},
+              {VertexFormat::kUnorm10_10_10_2, 64, 26}, {VertexFormat::kUnorm10_10_10_2, 64, 27},
+              {VertexFormat::kUnorm10_10_10_2, 64, 28},
           }}}};
 
     DataMap data;
diff --git a/src/tint/lang/wgsl/ast/traverse_expressions.h b/src/tint/lang/wgsl/ast/traverse_expressions.h
index 17688c2..7319a78 100644
--- a/src/tint/lang/wgsl/ast/traverse_expressions.h
+++ b/src/tint/lang/wgsl/ast/traverse_expressions.h
@@ -24,6 +24,7 @@
 #include "src/tint/lang/wgsl/ast/literal_expression.h"
 #include "src/tint/lang/wgsl/ast/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/ast/phony_expression.h"
+#include "src/tint/lang/wgsl/ast/templated_identifier.h"
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
 #include "src/tint/utils/containers/reverse.h"
 #include "src/tint/utils/containers/vector.h"
@@ -73,7 +74,7 @@
 
     auto push_single = [&](const Expression* expr, size_t depth) { to_visit.Push({expr, depth}); };
     auto push_pair = [&](const Expression* left, const Expression* right, size_t depth) {
-        if (ORDER == TraverseOrder::LeftToRight) {
+        if constexpr (ORDER == TraverseOrder::LeftToRight) {
             to_visit.Push({right, depth});
             to_visit.Push({left, depth});
         } else {
@@ -82,7 +83,7 @@
         }
     };
     auto push_list = [&](VectorRef<const Expression*> exprs, size_t depth) {
-        if (ORDER == TraverseOrder::LeftToRight) {
+        if constexpr (ORDER == TraverseOrder::LeftToRight) {
             for (auto* expr : tint::Reverse(exprs)) {
                 to_visit.Push({expr, depth});
             }
@@ -117,6 +118,12 @@
 
         bool ok = Switch(
             expr,
+            [&](const IdentifierExpression* ident) {
+                if (auto* tmpl = ident->identifier->As<TemplatedIdentifier>()) {
+                    push_list(tmpl->arguments, p.depth + 1);
+                }
+                return true;
+            },
             [&](const IndexAccessorExpression* idx) {
                 push_pair(idx->object, idx->index, p.depth + 1);
                 return true;
@@ -130,7 +137,13 @@
                 return true;
             },
             [&](const CallExpression* call) {
-                push_list(call->args, p.depth + 1);
+                if constexpr (ORDER == TraverseOrder::LeftToRight) {
+                    push_list(call->args, p.depth + 1);
+                    push_single(call->target, p.depth + 1);
+                } else {
+                    push_single(call->target, p.depth + 1);
+                    push_list(call->args, p.depth + 1);
+                }
                 return true;
             },
             [&](const MemberAccessorExpression* member) {
@@ -142,8 +155,7 @@
                 return true;
             },
             [&](Default) {
-                if (TINT_LIKELY((expr->IsAnyOf<LiteralExpression, IdentifierExpression,
-                                               PhonyExpression>()))) {
+                if (TINT_LIKELY((expr->IsAnyOf<LiteralExpression, PhonyExpression>()))) {
                     return true;  // Leaf expression
                 }
                 TINT_ICE() << "unhandled expression type: "
diff --git a/src/tint/lang/wgsl/ast/traverse_expressions_test.cc b/src/tint/lang/wgsl/ast/traverse_expressions_test.cc
index c477ac0..85da66c 100644
--- a/src/tint/lang/wgsl/ast/traverse_expressions_test.cc
+++ b/src/tint/lang/wgsl/ast/traverse_expressions_test.cc
@@ -27,22 +27,44 @@
 
 using TraverseExpressionsTest = TestHelper;
 
+TEST_F(TraverseExpressionsTest, DescendTemplatedIdentifier) {
+    tint::Vector e{Expr(1_i), Expr(2_i), Expr(1_i), Expr(1_i)};
+    tint::Vector c{Expr(Ident("a", e[0], e[1])), Expr(Ident("b", e[2], e[3]))};
+    auto* root = Expr(Ident("c", c[0], c[1]));
+    {
+        Vector<const Expression*, 8> l2r;
+        TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
+            l2r.Push(expr);
+            return TraverseAction::Descend;
+        });
+        EXPECT_THAT(l2r, ElementsAre(root, c[0], e[0], e[1], c[1], e[2], e[3]));
+    }
+    {
+        Vector<const Expression*, 8> r2l;
+        TraverseExpressions<TraverseOrder::RightToLeft>(root, [&](const Expression* expr) {
+            r2l.Push(expr);
+            return TraverseAction::Descend;
+        });
+        EXPECT_THAT(r2l, ElementsAre(root, c[1], e[3], e[2], c[0], e[1], e[0]));
+    }
+}
+
 TEST_F(TraverseExpressionsTest, DescendIndexAccessor) {
-    std::vector<const Expression*> e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
-    std::vector<const Expression*> i = {IndexAccessor(e[0], e[1]), IndexAccessor(e[2], e[3])};
+    Vector e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
+    Vector i = {IndexAccessor(e[0], e[1]), IndexAccessor(e[2], e[3])};
     auto* root = IndexAccessor(i[0], i[1]);
     {
-        std::vector<const Expression*> l2r;
+        Vector<const ast::Expression*, 8> l2r;
         TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-            l2r.push_back(expr);
+            l2r.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(l2r, ElementsAre(root, i[0], e[0], e[1], i[1], e[2], e[3]));
     }
     {
-        std::vector<const Expression*> r2l;
+        Vector<const ast::Expression*, 8> r2l;
         TraverseExpressions<TraverseOrder::RightToLeft>(root, [&](const Expression* expr) {
-            r2l.push_back(expr);
+            r2l.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(r2l, ElementsAre(root, i[1], e[3], e[2], i[0], e[1], e[0]));
@@ -50,21 +72,21 @@
 }
 
 TEST_F(TraverseExpressionsTest, DescendBinaryExpression) {
-    std::vector<const Expression*> e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
-    std::vector<const Expression*> i = {Add(e[0], e[1]), Sub(e[2], e[3])};
+    Vector e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
+    Vector i = {Add(e[0], e[1]), Sub(e[2], e[3])};
     auto* root = Mul(i[0], i[1]);
     {
-        std::vector<const Expression*> l2r;
+        Vector<const ast::Expression*, 8> l2r;
         TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-            l2r.push_back(expr);
+            l2r.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(l2r, ElementsAre(root, i[0], e[0], e[1], i[1], e[2], e[3]));
     }
     {
-        std::vector<const Expression*> r2l;
+        Vector<const ast::Expression*, 8> r2l;
         TraverseExpressions<TraverseOrder::RightToLeft>(root, [&](const Expression* expr) {
-            r2l.push_back(expr);
+            r2l.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(r2l, ElementsAre(root, i[1], e[3], e[2], i[0], e[1], e[0]));
@@ -72,8 +94,8 @@
 }
 
 TEST_F(TraverseExpressionsTest, Depth) {
-    std::vector<const Expression*> e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
-    std::vector<const Expression*> i = {Add(e[0], e[1]), Sub(e[2], e[3])};
+    Vector e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
+    Vector i = {Add(e[0], e[1]), Sub(e[2], e[3])};
     auto* root = Mul(i[0], i[1]);
 
     size_t j = 0;
@@ -113,16 +135,17 @@
 }
 
 TEST_F(TraverseExpressionsTest, DescendCallExpression) {
-    tint::Vector e{Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
-    tint::Vector c{Call("a", e[0], e[1]), Call("b", e[2], e[3])};
-    auto* root = Call("c", c[0], c[1]);
+    tint::Vector i{Expr("a"), Expr("b"), Expr("c")};
+    tint::Vector e{Expr(1_i), Expr(2_i), Expr(1_i), Expr(1_i)};
+    tint::Vector c{Call(i[0], e[0], e[1]), Call(i[1], e[2], e[3])};
+    auto* root = Call(i[2], c[0], c[1]);
     {
         Vector<const Expression*, 8> l2r;
         TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
             l2r.Push(expr);
             return TraverseAction::Descend;
         });
-        EXPECT_THAT(l2r, ElementsAre(root, c[0], e[0], e[1], c[1], e[2], e[3]));
+        EXPECT_THAT(l2r, ElementsAre(root, i[2], c[0], i[0], e[0], e[1], c[1], i[1], e[2], e[3]));
     }
     {
         Vector<const Expression*, 8> r2l;
@@ -130,7 +153,7 @@
             r2l.Push(expr);
             return TraverseAction::Descend;
         });
-        EXPECT_THAT(r2l, ElementsAre(root, c[1], e[3], e[2], c[0], e[1], e[0]));
+        EXPECT_THAT(r2l, ElementsAre(root, c[1], e[3], e[2], i[1], c[0], e[1], e[0], i[0], i[2]));
     }
 }
 
@@ -139,17 +162,17 @@
     auto* m = MemberAccessor(e, "a");
     auto* root = MemberAccessor(m, "b");
     {
-        std::vector<const Expression*> l2r;
+        Vector<const ast::Expression*, 8> l2r;
         TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-            l2r.push_back(expr);
+            l2r.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(l2r, ElementsAre(root, m, e));
     }
     {
-        std::vector<const Expression*> r2l;
+        Vector<const ast::Expression*, 8> r2l;
         TraverseExpressions<TraverseOrder::RightToLeft>(root, [&](const Expression* expr) {
-            r2l.push_back(expr);
+            r2l.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(r2l, ElementsAre(root, m, e));
@@ -165,17 +188,17 @@
     auto* f = IndexAccessor(d, e);
     auto* root = IndexAccessor(c, f);
     {
-        std::vector<const Expression*> l2r;
+        Vector<const ast::Expression*, 8> l2r;
         TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-            l2r.push_back(expr);
+            l2r.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(l2r, ElementsAre(root, c, a, b, f, d, e));
     }
     {
-        std::vector<const Expression*> r2l;
+        Vector<const ast::Expression*, 8> r2l;
         TraverseExpressions<TraverseOrder::RightToLeft>(root, [&](const Expression* expr) {
-            r2l.push_back(expr);
+            r2l.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(r2l, ElementsAre(root, f, e, d, c, b, a));
@@ -189,17 +212,17 @@
     auto* u2 = AddressOf(u1);
     auto* root = Deref(u2);
     {
-        std::vector<const Expression*> l2r;
+        Vector<const ast::Expression*, 8> l2r;
         TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-            l2r.push_back(expr);
+            l2r.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(l2r, ElementsAre(root, u2, u1, u0, e));
     }
     {
-        std::vector<const Expression*> r2l;
+        Vector<const ast::Expression*, 8> r2l;
         TraverseExpressions<TraverseOrder::RightToLeft>(root, [&](const Expression* expr) {
-            r2l.push_back(expr);
+            r2l.Push(expr);
             return TraverseAction::Descend;
         });
         EXPECT_THAT(r2l, ElementsAre(root, u2, u1, u0, e));
@@ -207,24 +230,24 @@
 }
 
 TEST_F(TraverseExpressionsTest, Skip) {
-    std::vector<const Expression*> e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
-    std::vector<const Expression*> i = {IndexAccessor(e[0], e[1]), IndexAccessor(e[2], e[3])};
+    Vector e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
+    Vector i = {IndexAccessor(e[0], e[1]), IndexAccessor(e[2], e[3])};
     auto* root = IndexAccessor(i[0], i[1]);
-    std::vector<const Expression*> order;
+    Vector<const ast::Expression*, 8> order;
     TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-        order.push_back(expr);
+        order.Push(expr);
         return expr == i[0] ? TraverseAction::Skip : TraverseAction::Descend;
     });
     EXPECT_THAT(order, ElementsAre(root, i[0], i[1], e[2], e[3]));
 }
 
 TEST_F(TraverseExpressionsTest, Stop) {
-    std::vector<const Expression*> e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
-    std::vector<const Expression*> i = {IndexAccessor(e[0], e[1]), IndexAccessor(e[2], e[3])};
+    Vector e = {Expr(1_i), Expr(1_i), Expr(1_i), Expr(1_i)};
+    Vector i = {IndexAccessor(e[0], e[1]), IndexAccessor(e[2], e[3])};
     auto* root = IndexAccessor(i[0], i[1]);
-    std::vector<const Expression*> order;
+    Vector<const ast::Expression*, 8> order;
     TraverseExpressions<TraverseOrder::LeftToRight>(root, [&](const Expression* expr) {
-        order.push_back(expr);
+        order.Push(expr);
         return expr == i[0] ? TraverseAction::Stop : TraverseAction::Descend;
     });
     EXPECT_THAT(order, ElementsAre(root, i[0]));
diff --git a/src/tint/lang/wgsl/helpers/BUILD.bazel b/src/tint/lang/wgsl/helpers/BUILD.bazel
index 16067dd..4afa836 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.bazel
+++ b/src/tint/lang/wgsl/helpers/BUILD.bazel
@@ -27,11 +27,13 @@
   name = "helpers",
   srcs = [
     "append_vector.cc",
+    "apply_substitute_overrides.cc",
     "check_supported_extensions.cc",
     "flatten_bindings.cc",
   ],
   hdrs = [
     "append_vector.h",
+    "apply_substitute_overrides.h",
     "check_supported_extensions.h",
     "flatten_bindings.h",
   ],
@@ -70,8 +72,12 @@
     "append_vector_test.cc",
     "check_supported_extensions_test.cc",
     "flatten_bindings_test.cc",
-    "ir_program_test.h",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "ir_program_test.h",
+    ],
+    "//conditions:default": [],
+  }),
   deps = [
     "//src/tint/api/common",
     "//src/tint/lang/core",
@@ -85,9 +91,7 @@
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/reader/lower",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -104,8 +108,19 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/wgsl/helpers/BUILD.cmake b/src/tint/lang/wgsl/helpers/BUILD.cmake
index 6eb49fb..9a77250 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.cmake
+++ b/src/tint/lang/wgsl/helpers/BUILD.cmake
@@ -28,6 +28,8 @@
 tint_add_target(tint_lang_wgsl_helpers lib
   lang/wgsl/helpers/append_vector.cc
   lang/wgsl/helpers/append_vector.h
+  lang/wgsl/helpers/apply_substitute_overrides.cc
+  lang/wgsl/helpers/apply_substitute_overrides.h
   lang/wgsl/helpers/check_supported_extensions.cc
   lang/wgsl/helpers/check_supported_extensions.h
   lang/wgsl/helpers/flatten_bindings.cc
@@ -68,7 +70,6 @@
   lang/wgsl/helpers/append_vector_test.cc
   lang/wgsl/helpers/check_supported_extensions_test.cc
   lang/wgsl/helpers/flatten_bindings_test.cc
-  lang/wgsl/helpers/ir_program_test.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_helpers_test test
@@ -84,9 +85,7 @@
   tint_lang_wgsl_helpers
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_reader_lower
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -107,3 +106,13 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_helpers_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_wgsl_helpers_test test
+    "lang/wgsl/helpers/ir_program_test.h"
+  )
+  tint_target_add_dependencies(tint_lang_wgsl_helpers_test test
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
diff --git a/src/tint/lang/wgsl/helpers/BUILD.gn b/src/tint/lang/wgsl/helpers/BUILD.gn
index 25ea790..42370f6 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.gn
+++ b/src/tint/lang/wgsl/helpers/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -33,6 +33,8 @@
   sources = [
     "append_vector.cc",
     "append_vector.h",
+    "apply_substitute_overrides.cc",
+    "apply_substitute_overrides.h",
     "check_supported_extensions.cc",
     "check_supported_extensions.h",
     "flatten_bindings.cc",
@@ -66,12 +68,10 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "append_vector_test.cc",
       "check_supported_extensions_test.cc",
       "flatten_bindings_test.cc",
-      "ir_program_test.h",
     ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
@@ -87,9 +87,7 @@
       "${tint_src_dir}/lang/wgsl/helpers",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
       "${tint_src_dir}/lang/wgsl/reader/lower",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
@@ -106,5 +104,13 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "ir_program_test.h" ]
+      deps += [
+        "${tint_src_dir}/lang/wgsl/reader",
+        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+      ]
+    }
   }
 }
diff --git a/src/tint/lang/wgsl/helpers/append_vector.cc b/src/tint/lang/wgsl/helpers/append_vector.cc
index 0f0da17..6affec6 100644
--- a/src/tint/lang/wgsl/helpers/append_vector.cc
+++ b/src/tint/lang/wgsl/helpers/append_vector.cc
@@ -26,7 +26,7 @@
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
-namespace tint::writer {
+namespace tint::wgsl {
 namespace {
 
 struct VectorConstructorInfo {
@@ -134,11 +134,9 @@
     if (packed_el_sem_ty != scalar_sem->Type()->UnwrapRef()) {
         // Cast scalar to the vector element type
         auto* scalar_cast_ast = b->Call(packed_el_ast_ty, scalar_ast);
-        auto* scalar_cast_target = b->create<sem::ValueConversion>(
-            packed_el_sem_ty,
-            b->create<sem::Parameter>(nullptr, 0u, scalar_sem->Type()->UnwrapRef(),
-                                      core::AddressSpace::kUndefined, core::Access::kUndefined),
-            core::EvaluationStage::kRuntime);
+        auto* param = b->create<sem::Parameter>(nullptr, 0u, scalar_sem->Type()->UnwrapRef());
+        auto* scalar_cast_target = b->create<sem::ValueConversion>(packed_el_sem_ty, param,
+                                                                   core::EvaluationStage::kRuntime);
         auto* scalar_cast_sem = b->create<sem::Call>(
             scalar_cast_ast, scalar_cast_target, core::EvaluationStage::kRuntime,
             Vector<const sem::ValueExpression*, 1>{scalar_sem}, statement,
@@ -157,9 +155,8 @@
         packed_sem_ty,
         tint::Transform(packed,
                         [&](const tint::sem::ValueExpression* arg, size_t i) {
-                            return b->create<sem::Parameter>(
-                                nullptr, static_cast<uint32_t>(i), arg->Type()->UnwrapRef(),
-                                core::AddressSpace::kUndefined, core::Access::kUndefined);
+                            return b->create<sem::Parameter>(nullptr, static_cast<uint32_t>(i),
+                                                             arg->Type()->UnwrapRef());
                         }),
         core::EvaluationStage::kRuntime);
     auto* ctor_sem = b->create<sem::Call>(ctor_ast, ctor_target, core::EvaluationStage::kRuntime,
@@ -170,4 +167,4 @@
     return ctor_sem;
 }
 
-}  // namespace tint::writer
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/helpers/append_vector.h b/src/tint/lang/wgsl/helpers/append_vector.h
index 3a73fc2..cb51a6a 100644
--- a/src/tint/lang/wgsl/helpers/append_vector.h
+++ b/src/tint/lang/wgsl/helpers/append_vector.h
@@ -26,7 +26,7 @@
 class Call;
 }  // namespace tint::sem
 
-namespace tint::writer {
+namespace tint::wgsl {
 
 /// A helper function used to append a vector with an additional scalar.
 /// If the scalar's type does not match the target vector element type,
@@ -42,6 +42,6 @@
                               const ast::Expression* vector,
                               const ast::Expression* scalar);
 
-}  // namespace tint::writer
+}  // namespace tint::wgsl
 
 #endif  // SRC_TINT_LANG_WGSL_HELPERS_APPEND_VECTOR_H_
diff --git a/src/tint/lang/wgsl/helpers/append_vector_test.cc b/src/tint/lang/wgsl/helpers/append_vector_test.cc
index 8829706..03201c5 100644
--- a/src/tint/lang/wgsl/helpers/append_vector_test.cc
+++ b/src/tint/lang/wgsl/helpers/append_vector_test.cc
@@ -20,7 +20,7 @@
 
 #include "gmock/gmock.h"
 
-namespace tint::writer {
+namespace tint::wgsl {
 namespace {
 
 using namespace tint::core::fluent_types;     // NOLINT
@@ -498,4 +498,4 @@
 }
 
 }  // namespace
-}  // namespace tint::writer
+}  // namespace tint::wgsl
diff --git a/src/tint/fuzzers/apply_substitute_overrides.cc b/src/tint/lang/wgsl/helpers/apply_substitute_overrides.cc
similarity index 88%
rename from src/tint/fuzzers/apply_substitute_overrides.cc
rename to src/tint/lang/wgsl/helpers/apply_substitute_overrides.cc
index e5ef24d..0065689 100644
--- a/src/tint/fuzzers/apply_substitute_overrides.cc
+++ b/src/tint/lang/wgsl/helpers/apply_substitute_overrides.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/fuzzers/apply_substitute_overrides.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
 
 #include <memory>
 #include <utility>
@@ -22,9 +22,9 @@
 #include "src/tint/lang/wgsl/inspector/inspector.h"
 #include "src/tint/lang/wgsl/program/program.h"
 
-namespace tint::fuzzers {
+namespace tint::wgsl {
 
-Program ApplySubstituteOverrides(Program&& program) {
+std::optional<Program> ApplySubstituteOverrides(const Program& program) {
     ast::transform::SubstituteOverride::Config cfg;
     inspector::Inspector inspector(program);
     auto default_values = inspector.GetOverrideDefaultValues();
@@ -37,7 +37,7 @@
     }
 
     if (default_values.empty()) {
-        return std::move(program);
+        return std::nullopt;
     }
 
     ast::transform::DataMap override_data;
@@ -50,4 +50,4 @@
     return mgr.Run(program, override_data, outputs);
 }
 
-}  // namespace tint::fuzzers
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/helpers/apply_substitute_overrides.h b/src/tint/lang/wgsl/helpers/apply_substitute_overrides.h
new file mode 100644
index 0000000..dd6da44
--- /dev/null
+++ b/src/tint/lang/wgsl/helpers/apply_substitute_overrides.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_WGSL_HELPERS_APPLY_SUBSTITUTE_OVERRIDES_H_
+#define SRC_TINT_LANG_WGSL_HELPERS_APPLY_SUBSTITUTE_OVERRIDES_H_
+
+#include <optional>
+
+// Forward declarations
+namespace tint {
+class Program;
+}
+
+namespace tint::wgsl {
+
+/// If needed, returns a new program with all `override` declarations substituted with `const`
+/// variables.
+/// @param program A valid program
+/// @return A new program with `override`s substituted, or std::nullopt if the program has no
+/// `override`s.
+std::optional<Program> ApplySubstituteOverrides(const Program& program);
+
+}  // namespace tint::wgsl
+
+#endif  // SRC_TINT_LANG_WGSL_HELPERS_APPLY_SUBSTITUTE_OVERRIDES_H_
diff --git a/src/tint/lang/wgsl/helpers/check_supported_extensions.cc b/src/tint/lang/wgsl/helpers/check_supported_extensions.cc
index ece9b37..79520a7 100644
--- a/src/tint/lang/wgsl/helpers/check_supported_extensions.cc
+++ b/src/tint/lang/wgsl/helpers/check_supported_extensions.cc
@@ -21,7 +21,7 @@
 #include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/text/string.h"
 
-namespace tint::writer {
+namespace tint::wgsl {
 
 bool CheckSupportedExtensions(std::string_view writer_name,
                               const ast::Module& module,
@@ -46,4 +46,4 @@
     return true;
 }
 
-}  // namespace tint::writer
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/helpers/check_supported_extensions.h b/src/tint/lang/wgsl/helpers/check_supported_extensions.h
index b2f09f3..c732a0f 100644
--- a/src/tint/lang/wgsl/helpers/check_supported_extensions.h
+++ b/src/tint/lang/wgsl/helpers/check_supported_extensions.h
@@ -25,7 +25,7 @@
 class List;
 }  // namespace tint::diag
 
-namespace tint::writer {
+namespace tint::wgsl {
 
 /// Checks that all the extensions enabled in @p module are found in @p supported, raising an error
 /// diagnostic if an enabled extension is not supported.
@@ -38,6 +38,6 @@
                               diag::List& diags,
                               VectorRef<wgsl::Extension> supported);
 
-}  // namespace tint::writer
+}  // namespace tint::wgsl
 
 #endif  // SRC_TINT_LANG_WGSL_HELPERS_CHECK_SUPPORTED_EXTENSIONS_H_
diff --git a/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc b/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc
index 0d90851..27b6b06 100644
--- a/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc
+++ b/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc
@@ -18,7 +18,7 @@
 
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
-namespace tint::writer {
+namespace tint::wgsl {
 namespace {
 
 class CheckSupportedExtensionsTest : public ::testing::Test, public ProgramBuilder {};
@@ -44,4 +44,4 @@
 }
 
 }  // namespace
-}  // namespace tint::writer
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings.cc b/src/tint/lang/wgsl/helpers/flatten_bindings.cc
index 3400dd9..ebee730 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings.cc
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings.cc
@@ -21,7 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/inspector/inspector.h"
 
-namespace tint::writer {
+namespace tint::wgsl {
 
 std::optional<Program> FlattenBindings(const Program& program) {
     // TODO(crbug.com/tint/1101): Make this more robust for multiple entry points.
@@ -79,4 +79,4 @@
     return {};
 }
 
-}  // namespace tint::writer
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings.h b/src/tint/lang/wgsl/helpers/flatten_bindings.h
index be2ca44..2124622 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings.h
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings.h
@@ -18,7 +18,7 @@
 #include <optional>
 #include "src/tint/lang/wgsl/program/program.h"
 
-namespace tint::writer {
+namespace tint::wgsl {
 
 /// If needed, remaps resource numbers of `program` to a flat namespace: all in
 /// group 0 within unique binding numbers.
@@ -26,6 +26,6 @@
 /// @return A new program with bindings remapped if needed
 std::optional<Program> FlattenBindings(const Program& program);
 
-}  // namespace tint::writer
+}  // namespace tint::wgsl
 
 #endif  // SRC_TINT_LANG_WGSL_HELPERS_FLATTEN_BINDINGS_H_
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
index ed04a59..e193bdc 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
@@ -22,7 +22,7 @@
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 
-namespace tint::writer {
+namespace tint::wgsl {
 namespace {
 
 using namespace tint::core::number_suffixes;  // NOLINT
@@ -34,7 +34,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics();
 
-    auto flattened = tint::writer::FlattenBindings(program);
+    auto flattened = FlattenBindings(program);
     EXPECT_FALSE(flattened);
 }
 
@@ -47,7 +47,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics();
 
-    auto flattened = tint::writer::FlattenBindings(program);
+    auto flattened = FlattenBindings(program);
     EXPECT_FALSE(flattened);
 }
 
@@ -61,7 +61,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics();
 
-    auto flattened = tint::writer::FlattenBindings(program);
+    auto flattened = FlattenBindings(program);
     EXPECT_TRUE(flattened);
 
     auto& vars = flattened->AST().GlobalVariables();
@@ -123,7 +123,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics();
 
-    auto flattened = tint::writer::FlattenBindings(program);
+    auto flattened = FlattenBindings(program);
     EXPECT_TRUE(flattened);
 
     auto& vars = flattened->AST().GlobalVariables();
@@ -149,4 +149,4 @@
 }
 
 }  // namespace
-}  // namespace tint::writer
+}  // namespace tint::wgsl
diff --git a/src/tint/lang/wgsl/helpers/ir_program_test.h b/src/tint/lang/wgsl/helpers/ir_program_test.h
index fb5fd01..87fa102 100644
--- a/src/tint/lang/wgsl/helpers/ir_program_test.h
+++ b/src/tint/lang/wgsl/helpers/ir_program_test.h
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
+
 #ifndef SRC_TINT_LANG_WGSL_HELPERS_IR_PROGRAM_TEST_H_
 #define SRC_TINT_LANG_WGSL_HELPERS_IR_PROGRAM_TEST_H_
 
@@ -67,7 +69,6 @@
     /// @param wgsl the WGSL to convert to IR
     /// @returns the generated module
     Result<core::ir::Module> Build(std::string wgsl) {
-#if TINT_BUILD_WGSL_READER
         Source::File file("test.wgsl", std::move(wgsl));
         auto result = wgsl::reader::WgslToIR(&file);
         if (result) {
@@ -77,17 +78,6 @@
             }
         }
         return result;
-#else
-        (void)wgsl;
-        return Failure{"error: Tint not built with the WGSL reader"};
-#endif
-    }
-
-    /// @param mod the module
-    /// @returns the disassembly string of the module
-    std::string Disassemble(core::ir::Module& mod) {
-        core::ir::Disassembler d(mod);
-        return d.Disassemble();
     }
 };
 
diff --git a/src/tint/lang/wgsl/inspector/BUILD.bazel b/src/tint/lang/wgsl/inspector/BUILD.bazel
index edbb595..323a326 100644
--- a/src/tint/lang/wgsl/inspector/BUILD.bazel
+++ b/src/tint/lang/wgsl/inspector/BUILD.bazel
@@ -83,7 +83,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -100,8 +99,18 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/wgsl/inspector/BUILD.cfg b/src/tint/lang/wgsl/inspector/BUILD.cfg
new file mode 100644
index 0000000..22818cc
--- /dev/null
+++ b/src/tint/lang/wgsl/inspector/BUILD.cfg
@@ -0,0 +1,5 @@
+{
+    "test": {
+        "condition": "tint_build_wgsl_reader",
+    }
+}
diff --git a/src/tint/lang/wgsl/inspector/BUILD.cmake b/src/tint/lang/wgsl/inspector/BUILD.cmake
index 8cfe979..623914b 100644
--- a/src/tint/lang/wgsl/inspector/BUILD.cmake
+++ b/src/tint/lang/wgsl/inspector/BUILD.cmake
@@ -60,9 +60,11 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_inspector_test
 # Kind:      test
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_inspector_test test
   lang/wgsl/inspector/inspector_builder_test.cc
@@ -82,7 +84,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -103,3 +104,11 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_inspector_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_inspector_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/inspector/BUILD.gn b/src/tint/lang/wgsl/inspector/BUILD.gn
index 566230b..fe581cd 100644
--- a/src/tint/lang/wgsl/inspector/BUILD.gn
+++ b/src/tint/lang/wgsl/inspector/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -65,42 +65,46 @@
   ]
 }
 if (tint_build_unittests) {
-  tint_unittests_source_set("unittests") {
-    testonly = true
-    sources = [
-      "inspector_builder_test.cc",
-      "inspector_builder_test.h",
-      "inspector_runner_test.cc",
-      "inspector_runner_test.h",
-      "inspector_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/inspector",
-      "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
-      "${tint_src_dir}/lang/wgsl/resolver",
-      "${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_wgsl_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "inspector_builder_test.cc",
+        "inspector_builder_test.h",
+        "inspector_runner_test.cc",
+        "inspector_runner_test.h",
+        "inspector_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/inspector",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/resolver",
+        "${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_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+    }
   }
 }
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index 3585594..a3f4584 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -34,8 +34,7 @@
         auto ir_module = wgsl::reader::WgslToIR(&file);
         ASSERT_TRUE(ir_module) << ir_module;
 
-        tint::core::ir::Disassembler d{ir_module.Get()};
-        auto disassembly = d.Disassemble();
+        auto disassembly = tint::core::ir::Disassemble(ir_module.Get());
 
         auto output = wgsl::writer::WgslFromIR(ir_module.Get());
         if (!output) {
@@ -313,10 +312,10 @@
 
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_PtrArg) {
     Test(R"(
-var<workgroup> v : bool;
+@group(0) @binding(0) var<storage, read> v : array<u32>;
 
-fn foo() -> bool {
-  return workgroupUniformLoad(&(v));
+fn foo() -> u32 {
+  return arrayLength(&(v));
 }
 )");
 }
diff --git a/src/tint/lang/wgsl/program/BUILD.gn b/src/tint/lang/wgsl/program/BUILD.gn
index ea3bd1b..7186f92 100644
--- a/src/tint/lang/wgsl/program/BUILD.gn
+++ b/src/tint/lang/wgsl/program/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -63,7 +63,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "clone_context_test.cc",
       "program_builder_test.cc",
diff --git a/src/tint/lang/wgsl/reader/BUILD.bazel b/src/tint/lang/wgsl/reader/BUILD.bazel
index 116874d..a6a57a1 100644
--- a/src/tint/lang/wgsl/reader/BUILD.bazel
+++ b/src/tint/lang/wgsl/reader/BUILD.bazel
@@ -41,8 +41,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
-    "//src/tint/lang/wgsl/reader/parser",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -58,18 +56,25 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader/parser",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "reader_bench.cc",
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/cmd/bench",
+    "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/ir",
@@ -77,7 +82,6 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -92,8 +96,19 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
-  ],
+    "@benchmark",
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/wgsl/reader/BUILD.cfg b/src/tint/lang/wgsl/reader/BUILD.cfg
new file mode 100644
index 0000000..e85bddd
--- /dev/null
+++ b/src/tint/lang/wgsl/reader/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_wgsl_reader",
+}
diff --git a/src/tint/lang/wgsl/reader/BUILD.cmake b/src/tint/lang/wgsl/reader/BUILD.cmake
index 2d94eb5..a424d38 100644
--- a/src/tint/lang/wgsl/reader/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/BUILD.cmake
@@ -25,9 +25,11 @@
 include(lang/wgsl/reader/parser/BUILD.cmake)
 include(lang/wgsl/reader/program_to_ir/BUILD.cmake)
 
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_reader
 # Kind:      lib
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader lib
   lang/wgsl/reader/reader.cc
@@ -44,8 +46,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
-  tint_lang_wgsl_reader_parser
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -63,9 +63,19 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_reader lib
+    tint_lang_wgsl_reader_parser
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_WGSL_READER)
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_bench
 # Kind:      bench
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_bench bench
   lang/wgsl/reader/reader_bench.cc
@@ -73,7 +83,7 @@
 
 tint_target_add_dependencies(tint_lang_wgsl_reader_bench bench
   tint_api_common
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_ir
@@ -81,7 +91,6 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -97,3 +106,15 @@
   tint_utils_text
   tint_utils_traits
 )
+
+tint_target_add_external_dependencies(tint_lang_wgsl_reader_bench bench
+  "google-benchmark"
+)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_reader_bench bench
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/reader/BUILD.gn b/src/tint/lang/wgsl/reader/BUILD.gn
index 7962250..a1502c1 100644
--- a/src/tint/lang/wgsl/reader/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/BUILD.gn
@@ -25,37 +25,84 @@
 
 import("${tint_src_dir}/tint.gni")
 
-libtint_source_set("reader") {
-  sources = [
-    "reader.cc",
-    "reader.h",
-  ]
-  deps = [
-    "${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/program",
-    "${tint_src_dir}/lang/wgsl/reader/lower",
-    "${tint_src_dir}/lang/wgsl/reader/parser",
-    "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-    "${tint_src_dir}/lang/wgsl/resolver",
-    "${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_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_wgsl_reader) {
+  libtint_source_set("reader") {
+    sources = [
+      "reader.cc",
+      "reader.h",
+    ]
+    deps = [
+      "${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/program",
+      "${tint_src_dir}/lang/wgsl/reader/lower",
+      "${tint_src_dir}/lang/wgsl/resolver",
+      "${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_wgsl_reader) {
+      deps += [
+        "${tint_src_dir}/lang/wgsl/reader/parser",
+        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+      ]
+    }
+  }
+}
+if (tint_build_benchmarks) {
+  if (tint_build_wgsl_reader) {
+    tint_unittests_source_set("bench") {
+      sources = [ "reader_bench.cc" ]
+      deps = [
+        "${tint_src_dir}:google_benchmark",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/cmd/bench:bench",
+        "${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/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_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+    }
+  }
 }
diff --git a/src/tint/lang/wgsl/reader/lower/BUILD.gn b/src/tint/lang/wgsl/reader/lower/BUILD.gn
index 674130d..35f7ff0 100644
--- a/src/tint/lang/wgsl/reader/lower/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/lower/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -61,7 +61,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "lower_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/lang/wgsl/reader/lower/lower.cc b/src/tint/lang/wgsl/reader/lower/lower.cc
index 64bef4b..9331b21 100644
--- a/src/tint/lang/wgsl/reader/lower/lower.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower.cc
@@ -17,7 +17,9 @@
 #include <utility>
 
 #include "src/tint/lang/core/builtin_fn.h"
+#include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/wgsl/builtin_fn.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 
@@ -116,7 +118,6 @@
         CASE(kUnpack4X8Snorm)
         CASE(kUnpack4X8Unorm)
         CASE(kWorkgroupBarrier)
-        CASE(kWorkgroupUniformLoad)
         CASE(kTextureBarrier)
         CASE(kTextureDimensions)
         CASE(kTextureGather)
@@ -155,12 +156,40 @@
 }  // namespace
 
 Result<SuccessType> Lower(core::ir::Module& mod) {
+    if (auto res = core::ir::ValidateAndDumpIfNeeded(mod, "lowering from WGSL"); !res) {
+        return res.Failure();
+    }
+
+    core::ir::Builder b{mod};
+    core::type::Manager& ty{mod.Types()};
     for (auto* inst : mod.instructions.Objects()) {
         if (auto* call = inst->As<wgsl::ir::BuiltinCall>()) {
-            Vector<core::ir::Value*, 8> args(call->Args());
-            auto* replacement = mod.instructions.Create<core::ir::CoreBuiltinCall>(
-                call->Result(), Convert(call->Func()), std::move(args));
-            call->ReplaceWith(replacement);
+            switch (call->Func()) {
+                case BuiltinFn::kWorkgroupUniformLoad: {
+                    // Replace:
+                    //    %value = call workgroupUniformLoad %ptr
+                    // With:
+                    //    call workgroupBarrier
+                    //    %value = load &ptr
+                    //    call workgroupBarrier
+                    b.InsertBefore(call, [&] {
+                        b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
+                        auto* load = b.Load(call->Args()[0]);
+                        call->Result()->ReplaceAllUsesWith(load->Result());
+                        b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
+                    });
+                    break;
+                }
+                default: {
+                    Vector<core::ir::Value*, 8> args(call->Args());
+                    auto* replacement = mod.instructions.Create<core::ir::CoreBuiltinCall>(
+                        call->Result(), Convert(call->Func()), std::move(args));
+                    call->ReplaceWith(replacement);
+                    call->ClearResults();
+                    break;
+                }
+            }
+            call->Destroy();
         }
     }
     return Success;
diff --git a/src/tint/lang/wgsl/reader/lower/lower_test.cc b/src/tint/lang/wgsl/reader/lower/lower_test.cc
index 589a8c4..0e4f43f 100644
--- a/src/tint/lang/wgsl/reader/lower/lower_test.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower_test.cc
@@ -64,5 +64,51 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(Wgslreader_LowerTest, WorkgroupUniformLoad) {
+    auto* wgvar = b.Var("wgvar", ty.ptr<workgroup, i32>());
+    mod.root_block->Append(wgvar);
+
+    auto* f = b.Function("f", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        auto* result = b.InstructionResult(ty.i32());
+        b.Append(b.ir.instructions.Create<wgsl::ir::BuiltinCall>(
+            result, wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{wgvar->Result()}));
+        b.Return(f, result);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %wgvar:ptr<workgroup, i32, read_write> = var
+}
+
+%f = func():i32 -> %b2 {
+  %b2 = block {
+    %3:i32 = wgsl.workgroupUniformLoad %wgvar
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %wgvar:ptr<workgroup, i32, read_write> = var
+}
+
+%f = func():i32 -> %b2 {
+  %b2 = block {
+    %3:void = workgroupBarrier
+    %4:i32 = load %wgvar
+    %5:void = workgroupBarrier
+    ret %4
+  }
+}
+)";
+
+    Run(Lower);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::wgsl::reader::lower
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.bazel b/src/tint/lang/wgsl/reader/parser/BUILD.bazel
index d1d9b22..c6d9f5d 100644
--- a/src/tint/lang/wgsl/reader/parser/BUILD.bazel
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.bazel
@@ -147,7 +147,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader/parser",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -164,8 +163,18 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader/parser",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.cfg b/src/tint/lang/wgsl/reader/parser/BUILD.cfg
new file mode 100644
index 0000000..e85bddd
--- /dev/null
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_wgsl_reader",
+}
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.cmake b/src/tint/lang/wgsl/reader/parser/BUILD.cmake
index 453af11..37231fc 100644
--- a/src/tint/lang/wgsl/reader/parser/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.cmake
@@ -21,9 +21,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_parser
 # Kind:      lib
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_parser lib
   lang/wgsl/reader/parser/classify_template_args.cc
@@ -63,9 +65,12 @@
   tint_utils_traits
 )
 
+endif(TINT_BUILD_WGSL_READER)
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_parser_test
 # Kind:      test
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_parser_test test
   lang/wgsl/reader/parser/additive_expression_test.cc
@@ -146,7 +151,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader_parser
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -167,3 +171,11 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_reader_parser_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_reader_parser_test test
+    tint_lang_wgsl_reader_parser
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.gn b/src/tint/lang/wgsl/reader/parser/BUILD.gn
index f765960..ee54683 100644
--- a/src/tint/lang/wgsl/reader/parser/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.gn
@@ -25,131 +25,30 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
-
-libtint_source_set("parser") {
-  sources = [
-    "classify_template_args.cc",
-    "classify_template_args.h",
-    "detail.h",
-    "lexer.cc",
-    "lexer.h",
-    "parser.cc",
-    "parser.h",
-    "token.cc",
-    "token.h",
-  ]
-  deps = [
-    "${tint_src_dir}/api/common",
-    "${tint_src_dir}/lang/core",
-    "${tint_src_dir}/lang/core/constant",
-    "${tint_src_dir}/lang/core/type",
-    "${tint_src_dir}/lang/wgsl",
-    "${tint_src_dir}/lang/wgsl/ast",
-    "${tint_src_dir}/lang/wgsl/program",
-    "${tint_src_dir}/lang/wgsl/resolver",
-    "${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/strconv",
-    "${tint_src_dir}/utils/symbol",
-    "${tint_src_dir}/utils/text",
-    "${tint_src_dir}/utils/traits",
-  ]
-}
-if (tint_build_unittests) {
-  tint_unittests_source_set("unittests") {
-    testonly = true
+if (tint_build_wgsl_reader) {
+  libtint_source_set("parser") {
     sources = [
-      "additive_expression_test.cc",
-      "argument_expression_list_test.cc",
-      "assignment_stmt_test.cc",
-      "bitwise_expression_test.cc",
-      "break_stmt_test.cc",
-      "bug_cases_test.cc",
-      "call_stmt_test.cc",
-      "classify_template_args_test.cc",
-      "compound_stmt_test.cc",
-      "const_literal_test.cc",
-      "continue_stmt_test.cc",
-      "continuing_stmt_test.cc",
-      "core_lhs_expression_test.cc",
-      "diagnostic_attribute_test.cc",
-      "diagnostic_control_test.cc",
-      "diagnostic_directive_test.cc",
-      "enable_directive_test.cc",
-      "error_msg_test.cc",
-      "error_resync_test.cc",
-      "expression_test.cc",
-      "for_stmt_test.cc",
-      "function_attribute_list_test.cc",
-      "function_attribute_test.cc",
-      "function_decl_test.cc",
-      "function_header_test.cc",
-      "global_constant_decl_test.cc",
-      "global_decl_test.cc",
-      "global_variable_decl_test.cc",
-      "helper_test.cc",
-      "helper_test.h",
-      "if_stmt_test.cc",
-      "increment_decrement_stmt_test.cc",
-      "lexer_test.cc",
-      "lhs_expression_test.cc",
-      "loop_stmt_test.cc",
-      "math_expression_test.cc",
-      "multiplicative_expression_test.cc",
-      "param_list_test.cc",
-      "paren_expression_test.cc",
-      "parser_test.cc",
-      "primary_expression_test.cc",
-      "relational_expression_test.cc",
-      "require_directive_test.cc",
-      "reserved_keyword_test.cc",
-      "shift_expression_test.cc",
-      "singular_expression_test.cc",
-      "statement_test.cc",
-      "statements_test.cc",
-      "struct_attribute_decl_test.cc",
-      "struct_body_decl_test.cc",
-      "struct_decl_test.cc",
-      "struct_member_attribute_decl_test.cc",
-      "struct_member_attribute_test.cc",
-      "struct_member_test.cc",
-      "switch_body_test.cc",
-      "switch_stmt_test.cc",
-      "token_test.cc",
-      "type_alias_test.cc",
-      "type_decl_test.cc",
-      "unary_expression_test.cc",
-      "variable_attribute_list_test.cc",
-      "variable_attribute_test.cc",
-      "variable_decl_test.cc",
-      "variable_ident_decl_test.cc",
-      "variable_qualifier_test.cc",
-      "variable_stmt_test.cc",
-      "while_stmt_test.cc",
+      "classify_template_args.cc",
+      "classify_template_args.h",
+      "detail.h",
+      "lexer.cc",
+      "lexer.h",
+      "parser.cc",
+      "parser.h",
+      "token.cc",
+      "token.h",
     ]
     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/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
-      "${tint_src_dir}/lang/wgsl/ast:unittests",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader/parser",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
@@ -162,9 +61,115 @@
       "${tint_src_dir}/utils/reflection",
       "${tint_src_dir}/utils/result",
       "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/strconv",
       "${tint_src_dir}/utils/symbol",
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
   }
 }
+if (tint_build_unittests) {
+  if (tint_build_wgsl_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "additive_expression_test.cc",
+        "argument_expression_list_test.cc",
+        "assignment_stmt_test.cc",
+        "bitwise_expression_test.cc",
+        "break_stmt_test.cc",
+        "bug_cases_test.cc",
+        "call_stmt_test.cc",
+        "classify_template_args_test.cc",
+        "compound_stmt_test.cc",
+        "const_literal_test.cc",
+        "continue_stmt_test.cc",
+        "continuing_stmt_test.cc",
+        "core_lhs_expression_test.cc",
+        "diagnostic_attribute_test.cc",
+        "diagnostic_control_test.cc",
+        "diagnostic_directive_test.cc",
+        "enable_directive_test.cc",
+        "error_msg_test.cc",
+        "error_resync_test.cc",
+        "expression_test.cc",
+        "for_stmt_test.cc",
+        "function_attribute_list_test.cc",
+        "function_attribute_test.cc",
+        "function_decl_test.cc",
+        "function_header_test.cc",
+        "global_constant_decl_test.cc",
+        "global_decl_test.cc",
+        "global_variable_decl_test.cc",
+        "helper_test.cc",
+        "helper_test.h",
+        "if_stmt_test.cc",
+        "increment_decrement_stmt_test.cc",
+        "lexer_test.cc",
+        "lhs_expression_test.cc",
+        "loop_stmt_test.cc",
+        "math_expression_test.cc",
+        "multiplicative_expression_test.cc",
+        "param_list_test.cc",
+        "paren_expression_test.cc",
+        "parser_test.cc",
+        "primary_expression_test.cc",
+        "relational_expression_test.cc",
+        "require_directive_test.cc",
+        "reserved_keyword_test.cc",
+        "shift_expression_test.cc",
+        "singular_expression_test.cc",
+        "statement_test.cc",
+        "statements_test.cc",
+        "struct_attribute_decl_test.cc",
+        "struct_body_decl_test.cc",
+        "struct_decl_test.cc",
+        "struct_member_attribute_decl_test.cc",
+        "struct_member_attribute_test.cc",
+        "struct_member_test.cc",
+        "switch_body_test.cc",
+        "switch_stmt_test.cc",
+        "token_test.cc",
+        "type_alias_test.cc",
+        "type_decl_test.cc",
+        "unary_expression_test.cc",
+        "variable_attribute_list_test.cc",
+        "variable_attribute_test.cc",
+        "variable_decl_test.cc",
+        "variable_ident_decl_test.cc",
+        "variable_qualifier_test.cc",
+        "variable_stmt_test.cc",
+        "while_stmt_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/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/ast:unittests",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/resolver",
+        "${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_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader/parser" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
index e964d93..6a6cb75 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
@@ -89,9 +89,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/helpers:test",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/reader/lower",
-    "//src/tint/lang/wgsl/reader/program_to_ir",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -108,8 +106,19 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+      "//src/tint/lang/wgsl/reader/program_to_ir",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cfg b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cfg
new file mode 100644
index 0000000..e85bddd
--- /dev/null
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_wgsl_reader",
+}
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
index 7cff404..454341d 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
@@ -21,9 +21,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_program_to_ir
 # Kind:      lib
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_program_to_ir lib
   lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -58,9 +60,12 @@
   tint_utils_traits
 )
 
+endif(TINT_BUILD_WGSL_READER)
+if(TINT_BUILD_WGSL_READER)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_program_to_ir_test
 # Kind:      test
+# Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_program_to_ir_test test
   lang/wgsl/reader/program_to_ir/accessor_test.cc
@@ -88,9 +93,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_reader_lower
-  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -111,3 +114,12 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_reader_program_to_ir_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_wgsl_reader_program_to_ir_test test
+    tint_lang_wgsl_reader
+    tint_lang_wgsl_reader_program_to_ir
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_WGSL_READER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
index 87f764b..57cfd95 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
@@ -25,76 +25,27 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
-
-libtint_source_set("program_to_ir") {
-  sources = [
-    "program_to_ir.cc",
-    "program_to_ir.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}/lang/wgsl",
-    "${tint_src_dir}/lang/wgsl/ast",
-    "${tint_src_dir}/lang/wgsl/intrinsic",
-    "${tint_src_dir}/lang/wgsl/ir",
-    "${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_unittests) {
-  tint_unittests_source_set("unittests") {
-    testonly = true
+if (tint_build_wgsl_reader) {
+  libtint_source_set("program_to_ir") {
     sources = [
-      "accessor_test.cc",
-      "binary_test.cc",
-      "builtin_test.cc",
-      "call_test.cc",
-      "function_test.cc",
-      "let_test.cc",
-      "literal_test.cc",
-      "materialize_test.cc",
-      "program_to_ir_test.cc",
-      "shadowing_test.cc",
-      "store_test.cc",
-      "unary_test.cc",
-      "var_test.cc",
+      "program_to_ir.cc",
+      "program_to_ir.h",
     ]
     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/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
-      "${tint_src_dir}/lang/wgsl/helpers:unittests",
+      "${tint_src_dir}/lang/wgsl/intrinsic",
+      "${tint_src_dir}/lang/wgsl/ir",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
-      "${tint_src_dir}/lang/wgsl/reader/lower",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-      "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -112,3 +63,59 @@
     ]
   }
 }
+if (tint_build_unittests) {
+  if (tint_build_wgsl_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "accessor_test.cc",
+        "binary_test.cc",
+        "builtin_test.cc",
+        "call_test.cc",
+        "function_test.cc",
+        "let_test.cc",
+        "literal_test.cc",
+        "materialize_test.cc",
+        "program_to_ir_test.cc",
+        "shadowing_test.cc",
+        "store_test.cc",
+        "unary_test.cc",
+        "var_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/helpers:unittests",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/reader/lower",
+        "${tint_src_dir}/lang/wgsl/resolver",
+        "${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_wgsl_reader) {
+        deps += [
+          "${tint_src_dir}/lang/wgsl/reader",
+          "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+        ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/reader/reader_bench.cc b/src/tint/lang/wgsl/reader/reader_bench.cc
index b5f736a..be3898c 100644
--- a/src/tint/lang/wgsl/reader/reader_bench.cc
+++ b/src/tint/lang/wgsl/reader/reader_bench.cc
@@ -22,15 +22,14 @@
 
 void ParseWGSL(benchmark::State& state, std::string input_name) {
     auto res = bench::LoadInputFile(input_name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        state.SkipWithError(err->msg.c_str());
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
         return;
     }
-    auto& file = std::get<Source::File>(res);
     for (auto _ : state) {
-        auto res = Parse(&file);
-        if (res.Diagnostics().contains_errors()) {
-            state.SkipWithError(res.Diagnostics().str().c_str());
+        auto program = Parse(&res.Get());
+        if (program.Diagnostics().contains_errors()) {
+            state.SkipWithError(program.Diagnostics().str());
         }
     }
 }
diff --git a/src/tint/lang/wgsl/resolver/BUILD.bazel b/src/tint/lang/wgsl/resolver/BUILD.bazel
index 5e4635f..8369cba 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.bazel
+++ b/src/tint/lang/wgsl/resolver/BUILD.bazel
@@ -27,18 +27,22 @@
   name = "resolver",
   srcs = [
     "dependency_graph.cc",
+    "incomplete_type.cc",
     "resolve.cc",
     "resolver.cc",
     "sem_helper.cc",
     "uniformity.cc",
+    "unresolved_identifier.cc",
     "validator.cc",
   ],
   hdrs = [
     "dependency_graph.h",
+    "incomplete_type.h",
     "resolve.h",
     "resolver.h",
     "sem_helper.h",
     "uniformity.h",
+    "unresolved_identifier.h",
     "validator.h",
   ],
   deps = [
@@ -123,14 +127,18 @@
     "struct_pipeline_stage_use_test.cc",
     "subgroups_extension_test.cc",
     "type_validation_test.cc",
-    "uniformity_test.cc",
     "unresolved_identifier_test.cc",
     "validation_test.cc",
     "validator_is_storeable_test.cc",
     "value_constructor_validation_test.cc",
     "variable_test.cc",
     "variable_validation_test.cc",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "uniformity_test.cc",
+    ],
+    "//conditions:default": [],
+  }),
   deps = [
     "//src/tint/api/common",
     "//src/tint/lang/core",
@@ -145,7 +153,6 @@
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/lang/wgsl/sem:test",
@@ -163,8 +170,18 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/wgsl/resolver/BUILD.cmake b/src/tint/lang/wgsl/resolver/BUILD.cmake
index fd17863..ff882f1 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.cmake
+++ b/src/tint/lang/wgsl/resolver/BUILD.cmake
@@ -28,6 +28,8 @@
 tint_add_target(tint_lang_wgsl_resolver lib
   lang/wgsl/resolver/dependency_graph.cc
   lang/wgsl/resolver/dependency_graph.h
+  lang/wgsl/resolver/incomplete_type.cc
+  lang/wgsl/resolver/incomplete_type.h
   lang/wgsl/resolver/resolve.cc
   lang/wgsl/resolver/resolve.h
   lang/wgsl/resolver/resolver.cc
@@ -36,6 +38,8 @@
   lang/wgsl/resolver/sem_helper.h
   lang/wgsl/resolver/uniformity.cc
   lang/wgsl/resolver/uniformity.h
+  lang/wgsl/resolver/unresolved_identifier.cc
+  lang/wgsl/resolver/unresolved_identifier.h
   lang/wgsl/resolver/validator.cc
   lang/wgsl/resolver/validator.h
 )
@@ -121,7 +125,6 @@
   lang/wgsl/resolver/struct_pipeline_stage_use_test.cc
   lang/wgsl/resolver/subgroups_extension_test.cc
   lang/wgsl/resolver/type_validation_test.cc
-  lang/wgsl/resolver/uniformity_test.cc
   lang/wgsl/resolver/unresolved_identifier_test.cc
   lang/wgsl/resolver/validation_test.cc
   lang/wgsl/resolver/validator_is_storeable_test.cc
@@ -144,7 +147,6 @@
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_lang_wgsl_sem_test
@@ -166,3 +168,12 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_resolver_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_wgsl_resolver_test test
+    "lang/wgsl/resolver/uniformity_test.cc"
+  )
+  tint_target_add_dependencies(tint_lang_wgsl_resolver_test test
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
diff --git a/src/tint/lang/wgsl/resolver/BUILD.gn b/src/tint/lang/wgsl/resolver/BUILD.gn
index c10075f..49c8be2 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.gn
+++ b/src/tint/lang/wgsl/resolver/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -33,6 +33,8 @@
   sources = [
     "dependency_graph.cc",
     "dependency_graph.h",
+    "incomplete_type.cc",
+    "incomplete_type.h",
     "resolve.cc",
     "resolve.h",
     "resolver.cc",
@@ -41,6 +43,8 @@
     "sem_helper.h",
     "uniformity.cc",
     "uniformity.h",
+    "unresolved_identifier.cc",
+    "unresolved_identifier.h",
     "validator.cc",
     "validator.h",
   ]
@@ -72,7 +76,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "address_space_layout_validation_test.cc",
       "address_space_validation_test.cc",
@@ -124,7 +127,6 @@
       "struct_pipeline_stage_use_test.cc",
       "subgroups_extension_test.cc",
       "type_validation_test.cc",
-      "uniformity_test.cc",
       "unresolved_identifier_test.cc",
       "validation_test.cc",
       "validator_is_storeable_test.cc",
@@ -147,7 +149,6 @@
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/lang/wgsl/sem:unittests",
@@ -165,5 +166,10 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "uniformity_test.cc" ]
+      deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+    }
   }
 }
diff --git a/src/tint/lang/wgsl/resolver/address_space_validation_test.cc b/src/tint/lang/wgsl/resolver/address_space_validation_test.cc
index 1d4f390..ecb5107 100644
--- a/src/tint/lang/wgsl/resolver/address_space_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/address_space_validation_test.cc
@@ -216,18 +216,6 @@
 56:78 note: while instantiating 'var' g)");
 }
 
-TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_Storage_Pointer) {
-    // type t = ptr<storage, ptr<private, f32>>;
-    Alias("t", ty.ptr<storage>(Source{{56, 78}}, ty.ptr<private_, f32>(Source{{12, 34}})));
-
-    ASSERT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(
-        r()->error(),
-        R"(12:34 error: Type 'ptr<private, f32, read_write>' cannot be used in address space 'storage' as it is non-host-shareable
-56:78 note: while instantiating ptr<storage, ptr<private, f32, read_write>, read>)");
-}
-
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_Storage_IntScalar) {
     // var<storage> g : i32;
     GlobalVar("g", ty.i32(), core::AddressSpace::kStorage, Binding(0_a), Group(0_a));
@@ -631,18 +619,6 @@
 56:78 note: while instantiating 'var' g)");
 }
 
-TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_UniformPointer) {
-    // type t = ptr<uniform, ptr<private, f32>>;
-    Alias("t", ty.ptr<uniform>(Source{{56, 78}}, ty.ptr<private_, f32>(Source{{12, 34}})));
-
-    ASSERT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(
-        r()->error(),
-        R"(12:34 error: Type 'ptr<private, f32, read_write>' cannot be used in address space 'uniform' as it is non-host-shareable
-56:78 note: while instantiating ptr<uniform, ptr<private, f32, read_write>, read>)");
-}
-
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_UniformBufferIntScalar) {
     // var<uniform> g : i32;
     GlobalVar(Source{{56, 78}}, "g", ty.i32(), core::AddressSpace::kUniform, Binding(0_a),
@@ -920,19 +896,6 @@
 56:78 note: while instantiating 'var' g)");
 }
 
-TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_PushConstantPointer) {
-    // enable chromium_experimental_push_constant;
-    // type t = ptr<push_constant, ptr<private, f32>>;
-    Enable(wgsl::Extension::kChromiumExperimentalPushConstant);
-    Alias(Source{{56, 78}}, "t", ty.ptr<push_constant>(ty.ptr<private_, f32>(Source{{12, 34}})));
-
-    ASSERT_FALSE(r()->Resolve());
-    EXPECT_EQ(
-        r()->error(),
-        R"(12:34 error: Type 'ptr<private, f32, read_write>' cannot be used in address space 'push_constant' as it is non-host-shareable
-note: while instantiating ptr<push_constant, ptr<private, f32, read_write>, read_write>)");
-}
-
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_PushConstantIntScalar) {
     // enable chromium_experimental_push_constant;
     // var<push_constant> g : i32;
diff --git a/src/tint/lang/wgsl/resolver/builtin_enum_test.cc b/src/tint/lang/wgsl/resolver/builtin_enum_test.cc
index 3f73a5d..1ad9ea7 100644
--- a/src/tint/lang/wgsl/resolver/builtin_enum_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtin_enum_test.cc
@@ -36,8 +36,8 @@
 using ResolverAccessUsedWithTemplateArgs = ResolverTestWithParam<const char*>;
 
 TEST_P(ResolverAccessUsedWithTemplateArgs, Test) {
-    // @group(0) @binding(0) var t : texture_storage_2d<rgba8unorm, ACCESS<T>>;
-    auto* tmpl = Ident(Source{{12, 34}}, GetParam(), "T");
+    // @group(0) @binding(0) var t : texture_storage_2d<rgba8unorm, ACCESS<i32>>;
+    auto* tmpl = Ident(Source{{12, 34}}, GetParam(), "i32");
     GlobalVar("v", ty("texture_storage_2d", "rgba8unorm", tmpl), Group(0_u), Binding(0_u));
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/lang/wgsl/resolver/builtin_validation_test.cc b/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
index 86e4f4c..d9d475f 100644
--- a/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
@@ -35,7 +35,8 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: builtin 'workgroupBarrier' does not return a value");
+    EXPECT_EQ(r()->error(),
+              "12:34 error: builtin function 'workgroupBarrier' does not return a value");
 }
 
 TEST_F(ResolverBuiltinValidationTest, InvalidPipelineStageDirect) {
@@ -118,7 +119,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(56:78 error: cannot use function 'mix' as value
-12:34 note: function 'mix' declared here)");
+12:34 note: function 'mix' declared here
+56:78 note: are you missing '()'?)");
 }
 
 TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalConstUsedAsVariable) {
diff --git a/src/tint/lang/wgsl/resolver/call_validation_test.cc b/src/tint/lang/wgsl/resolver/call_validation_test.cc
index b665d0d..0018bba 100644
--- a/src/tint/lang/wgsl/resolver/call_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/call_validation_test.cc
@@ -501,7 +501,8 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: builtin 'min' does not take template arguments)");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: builtin function 'min' does not take template arguments)");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.cc b/src/tint/lang/wgsl/resolver/dependency_graph.cc
index e2f25d8..8d9fcd4 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.cc
@@ -349,13 +349,7 @@
                     expr,
                     [&](const ast::IdentifierExpression* e) {
                         AddDependency(e->identifier, e->identifier->symbol);
-                        if (auto* tmpl_ident = e->identifier->As<ast::TemplatedIdentifier>()) {
-                            for (auto* arg : tmpl_ident->arguments) {
-                                pending.Push(arg);
-                            }
-                        }
                     },
-                    [&](const ast::CallExpression* call) { TraverseExpression(call->target); },
                     [&](const ast::BitcastExpression* cast) { TraverseExpression(cast->type); });
                 return ast::TraverseAction::Descend;
             });
@@ -533,7 +527,8 @@
             auto builtin_info = GetBuiltinInfo(to);
             switch (builtin_info.type) {
                 case BuiltinType::kNone:
-                    graph_.resolved_identifiers.Add(from, UnresolvedIdentifier{to.Name()});
+                    graph_.resolved_identifiers.Add(
+                        from, ResolvedIdentifier::UnresolvedIdentifier{to.Name()});
                     break;
                 case BuiltinType::kFunction:
                     graph_.resolved_identifiers.Add(
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.h b/src/tint/lang/wgsl/resolver/dependency_graph.h
index 85a91e2..b9a1440 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.h
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.h
@@ -31,12 +31,6 @@
 
 namespace tint::resolver {
 
-/// UnresolvedIdentifier is the variant value used by ResolvedIdentifier
-struct UnresolvedIdentifier {
-    /// Name of the unresolved identifier
-    std::string name;
-};
-
 /// ResolvedIdentifier holds the resolution of an ast::Identifier.
 /// Can hold one of:
 /// - UnresolvedIdentifier
@@ -53,6 +47,12 @@
 /// - core::TexelFormat
 class ResolvedIdentifier {
   public:
+    /// UnresolvedIdentifier is the variant value used to represent an unresolved identifier
+    struct UnresolvedIdentifier {
+        /// Name of the unresolved identifier
+        std::string name;
+    };
+
     /// Constructor
     /// @param value the resolved identifier value
     template <typename T>
diff --git a/src/tint/lang/wgsl/resolver/expression_kind_test.cc b/src/tint/lang/wgsl/resolver/expression_kind_test.cc
index aad7671..2bd1c61 100644
--- a/src/tint/lang/wgsl/resolver/expression_kind_test.cc
+++ b/src/tint/lang/wgsl/resolver/expression_kind_test.cc
@@ -171,7 +171,12 @@
         }
         case Def::kBuiltinFunction: {
             sym = Sym("workgroupBarrier");
-            check_expr = [](const sem::Expression* expr) { EXPECT_EQ(expr, nullptr); };
+            check_expr = [](const sem::Expression* expr) {
+                ASSERT_NE(expr, nullptr);
+                auto* fn_expr = expr->As<sem::BuiltinEnumExpression<wgsl::BuiltinFn>>();
+                ASSERT_NE(fn_expr, nullptr);
+                EXPECT_EQ(fn_expr->Value(), wgsl::BuiltinFn::kWorkgroupBarrier);
+            };
             break;
         }
         case Def::kBuiltinType: {
@@ -415,37 +420,40 @@
          R"(5:6 error: cannot use address space 'workgroup' as value)"},
 
         {Def::kBuiltinFunction, Use::kAccess,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as access)"},
         {Def::kBuiltinFunction, Use::kAddressSpace,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as address space)"},
         {Def::kBuiltinFunction, Use::kBinaryOp,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as value
+7:8 note: are you missing '()'?)"},
         {Def::kBuiltinFunction, Use::kBuiltinValue,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as builtin value)"},
         {Def::kBuiltinFunction, Use::kCallStmt, kPass},
         {Def::kBuiltinFunction, Use::kFunctionReturnType,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
         {Def::kBuiltinFunction, Use::kInterpolationSampling,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as interpolation sampling)"},
         {Def::kBuiltinFunction, Use::kInterpolationType,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as interpolation type)"},
         {Def::kBuiltinFunction, Use::kMemberType,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
         {Def::kBuiltinFunction, Use::kTexelFormat,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as texel format)"},
         {Def::kBuiltinFunction, Use::kValueExpression,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as value
+7:8 note: are you missing '()'?)"},
         {Def::kBuiltinFunction, Use::kVariableType,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
         {Def::kBuiltinFunction, Use::kUnaryOp,
-         R"(7:8 error: missing '(' for builtin function call)"},
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as value
+7:8 note: are you missing '()'?)"},
 
         {Def::kBuiltinType, Use::kAccess, R"(5:6 error: cannot use type 'vec4<f32>' as access)"},
         {Def::kBuiltinType, Use::kAddressSpace,
          R"(5:6 error: cannot use type 'vec4<f32>' as address space)"},
         {Def::kBuiltinType, Use::kBinaryOp,
          R"(5:6 error: cannot use type 'vec4<f32>' as value
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
         {Def::kBuiltinType, Use::kBuiltinValue,
          R"(5:6 error: cannot use type 'vec4<f32>' as builtin value)"},
         {Def::kBuiltinType, Use::kCallExpr, kPass},
@@ -459,11 +467,11 @@
          R"(5:6 error: cannot use type 'vec4<f32>' as texel format)"},
         {Def::kBuiltinType, Use::kValueExpression,
          R"(5:6 error: cannot use type 'vec4<f32>' as value
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
         {Def::kBuiltinType, Use::kVariableType, kPass},
         {Def::kBuiltinType, Use::kUnaryOp,
          R"(5:6 error: cannot use type 'vec4<f32>' as value
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
 
         {Def::kBuiltinValue, Use::kAccess,
          R"(5:6 error: cannot use builtin value 'position' as access)"},
@@ -499,7 +507,8 @@
          R"(5:6 error: cannot use function 'FUNCTION' as address space
 1:2 note: function 'FUNCTION' declared here)"},
         {Def::kFunction, Use::kBinaryOp, R"(5:6 error: cannot use function 'FUNCTION' as value
-1:2 note: function 'FUNCTION' declared here)"},
+1:2 note: function 'FUNCTION' declared here
+7:8 note: are you missing '()'?)"},
         {Def::kFunction, Use::kBuiltinValue,
          R"(5:6 error: cannot use function 'FUNCTION' as builtin value
 1:2 note: function 'FUNCTION' declared here)"},
@@ -522,12 +531,14 @@
 1:2 note: function 'FUNCTION' declared here)"},
         {Def::kFunction, Use::kValueExpression,
          R"(5:6 error: cannot use function 'FUNCTION' as value
-1:2 note: function 'FUNCTION' declared here)"},
+1:2 note: function 'FUNCTION' declared here
+7:8 note: are you missing '()'?)"},
         {Def::kFunction, Use::kVariableType,
          R"(5:6 error: cannot use function 'FUNCTION' as type
 1:2 note: function 'FUNCTION' declared here)"},
         {Def::kFunction, Use::kUnaryOp, R"(5:6 error: cannot use function 'FUNCTION' as value
-1:2 note: function 'FUNCTION' declared here)"},
+1:2 note: function 'FUNCTION' declared here
+7:8 note: are you missing '()'?)"},
 
         {Def::kInterpolationSampling, Use::kAccess,
          R"(5:6 error: cannot use interpolation sampling 'center' as access)"},
@@ -605,7 +616,7 @@
 1:2 note: struct 'STRUCT' declared here)"},
         {Def::kStruct, Use::kBinaryOp, R"(5:6 error: cannot use type 'STRUCT' as value
 1:2 note: struct 'STRUCT' declared here
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
         {Def::kStruct, Use::kBuiltinValue,
          R"(5:6 error: cannot use type 'STRUCT' as builtin value
 1:2 note: struct 'STRUCT' declared here)"},
@@ -622,12 +633,12 @@
         {Def::kStruct, Use::kValueExpression,
          R"(5:6 error: cannot use type 'STRUCT' as value
 1:2 note: struct 'STRUCT' declared here
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
         {Def::kStruct, Use::kVariableType, kPass},
         {Def::kStruct, Use::kUnaryOp,
          R"(5:6 error: cannot use type 'STRUCT' as value
 1:2 note: struct 'STRUCT' declared here
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
 
         {Def::kTexelFormat, Use::kAccess,
          R"(5:6 error: cannot use texel format 'rgba8unorm' as access)"},
@@ -662,7 +673,7 @@
          R"(5:6 error: cannot use type 'i32' as address space)"},
         {Def::kTypeAlias, Use::kBinaryOp,
          R"(5:6 error: cannot use type 'i32' as value
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
         {Def::kTypeAlias, Use::kBuiltinValue,
          R"(5:6 error: cannot use type 'i32' as builtin value)"},
         {Def::kTypeAlias, Use::kCallExpr, kPass},
@@ -675,11 +686,11 @@
         {Def::kTypeAlias, Use::kTexelFormat, R"(5:6 error: cannot use type 'i32' as texel format)"},
         {Def::kTypeAlias, Use::kValueExpression,
          R"(5:6 error: cannot use type 'i32' as value
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
         {Def::kTypeAlias, Use::kVariableType, kPass},
         {Def::kTypeAlias, Use::kUnaryOp,
          R"(5:6 error: cannot use type 'i32' as value
-7:8 note: are you missing '()' for value constructor?)"},
+7:8 note: are you missing '()'?)"},
 
         {Def::kVariable, Use::kAccess, R"(5:6 error: cannot use const 'VARIABLE' as access
 1:2 note: const 'VARIABLE' declared here)"},
diff --git a/src/tint/lang/wgsl/resolver/incomplete_type.cc b/src/tint/lang/wgsl/resolver/incomplete_type.cc
new file mode 100644
index 0000000..ca4c555
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/incomplete_type.cc
@@ -0,0 +1,60 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/wgsl/resolver/incomplete_type.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::resolver::IncompleteType);
+
+namespace tint::resolver {
+
+IncompleteType::IncompleteType(core::BuiltinType b)
+    : Base(static_cast<size_t>(tint::TypeInfo::Of<IncompleteType>().full_hashcode),
+           core::type::Flags{}),
+      builtin(b) {}
+
+IncompleteType::~IncompleteType() = default;
+
+std::string IncompleteType::FriendlyName() const {
+    return "<incomplete-type>";
+}
+
+uint32_t IncompleteType::Size() const {
+    return 0;
+}
+
+uint32_t IncompleteType::Align() const {
+    return 0;
+}
+
+core::type::Type* IncompleteType::Clone(core::type::CloneContext&) const {
+    TINT_ICE() << "IncompleteType does not support cloning";
+    return nullptr;
+}
+
+core::type::TypeAndCount IncompleteType::Elements(const Type*, uint32_t) const {
+    return {};
+}
+
+const core::type::Type* IncompleteType::Element(uint32_t) const {
+    return nullptr;
+}
+
+bool IncompleteType::Equals(const core::type::UniqueNode& other) const {
+    if (auto* o = other.As<IncompleteType>()) {
+        return o->builtin == builtin;
+    }
+    return false;
+}
+
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/incomplete_type.h b/src/tint/lang/wgsl/resolver/incomplete_type.h
new file mode 100644
index 0000000..da02c59
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/incomplete_type.h
@@ -0,0 +1,70 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_WGSL_RESOLVER_INCOMPLETE_TYPE_H_
+#define SRC_TINT_LANG_WGSL_RESOLVER_INCOMPLETE_TYPE_H_
+
+#include <string>
+
+#include "src/tint/lang/core/builtin_type.h"
+#include "src/tint/lang/core/type/type.h"
+
+namespace tint::resolver {
+
+/// Represents a expression that resolved to the name of a type without explicit template arguments.
+/// This is a placeholder type that on successful resolving, is replaced with the type built using
+/// the inferred template arguments.
+/// For example, given the expression `vec3(1i, 2i, 3i)`:
+/// * The IdentifierExpression for `vec3` is resolved first, to an IncompleteType as it does not
+///   know the vector element type.
+/// * Next, the CallExpression replaces the IncompleteType with a core::type::Vector once it can
+///   infer the element type from the call's arguments.
+class IncompleteType : public Castable<IncompleteType, core::type::Type> {
+  public:
+    /// Constructor
+    /// @param b the incomplete builtin type
+    explicit IncompleteType(core::BuiltinType b);
+
+    /// Destructor
+    ~IncompleteType() override;
+
+    /// The incomplete builtin type
+    const core::BuiltinType builtin = core::BuiltinType::kUndefined;
+
+    /// @copydoc core::type::Type::FriendlyName
+    std::string FriendlyName() const override;
+
+    /// @copydoc core::type::Type::Size
+    uint32_t Size() const override;
+
+    /// @copydoc core::type::Type::Align
+    uint32_t Align() const override;
+
+    /// @copydoc core::type::Type::Clone
+    core::type::Type* Clone(core::type::CloneContext& ctx) const override;
+
+    /// @copydoc core::type::Type::Elements
+    core::type::TypeAndCount Elements(const Type* type_if_invalid = nullptr,
+                                      uint32_t count_if_invalid = 0) const override;
+
+    /// @copydoc core::type::Type::Element
+    const Type* Element(uint32_t index) const override;
+
+    /// @copydoc core::type::UniqueNode::Equals
+    bool Equals(const UniqueNode& other) const override;
+};
+
+}  // namespace tint::resolver
+
+#endif  // SRC_TINT_LANG_WGSL_RESOLVER_INCOMPLETE_TYPE_H_
diff --git a/src/tint/lang/wgsl/resolver/inferred_type_test.cc b/src/tint/lang/wgsl/resolver/inferred_type_test.cc
index 89212fe..ec9e59a 100644
--- a/src/tint/lang/wgsl/resolver/inferred_type_test.cc
+++ b/src/tint/lang/wgsl/resolver/inferred_type_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/lang/wgsl/resolver/resolver.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 
 #include "gmock/gmock.h"
 
@@ -124,9 +125,9 @@
 
 TEST_F(ResolverInferredTypeTest, InferArray_Pass) {
     auto type = ty.array<u32, 10>();
-    auto* expected_type = create<core::type::Array>(create<core::type::U32>(),
-                                                    create<core::type::ConstantArrayCount>(10u), 4u,
-                                                    4u * 10u, 4u, 4u);
+    auto* expected_type =
+        create<sem::Array>(create<core::type::U32>(), create<core::type::ConstantArrayCount>(10u),
+                           4u, 4u * 10u, 4u, 4u);
 
     auto* ctor_expr = Call(type);
     auto* var = Var("a", core::AddressSpace::kFunction, ctor_expr);
diff --git a/src/tint/lang/wgsl/resolver/is_host_shareable_test.cc b/src/tint/lang/wgsl/resolver/is_host_shareable_test.cc
index e23a9b5..b7b0aa6 100644
--- a/src/tint/lang/wgsl/resolver/is_host_shareable_test.cc
+++ b/src/tint/lang/wgsl/resolver/is_host_shareable_test.cc
@@ -17,6 +17,7 @@
 #include "gmock/gmock.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 
 namespace tint::resolver {
 namespace {
@@ -106,14 +107,14 @@
 }
 
 TEST_F(ResolverIsHostShareable, ArraySizedOfHostShareable) {
-    auto* arr = create<core::type::Array>(
-        create<core::type::I32>(), create<core::type::ConstantArrayCount>(5u), 4u, 20u, 4u, 4u);
+    auto* arr = create<sem::Array>(create<core::type::I32>(),
+                                   create<core::type::ConstantArrayCount>(5u), 4u, 20u, 4u, 4u);
     EXPECT_TRUE(r()->IsHostShareable(arr));
 }
 
 TEST_F(ResolverIsHostShareable, ArrayUnsizedOfHostShareable) {
-    auto* arr = create<core::type::Array>(create<core::type::I32>(),
-                                          create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
+    auto* arr = create<sem::Array>(create<core::type::I32>(),
+                                   create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
     EXPECT_TRUE(r()->IsHostShareable(arr));
 }
 
diff --git a/src/tint/lang/wgsl/resolver/is_storeable_test.cc b/src/tint/lang/wgsl/resolver/is_storeable_test.cc
index b040dce..e549151 100644
--- a/src/tint/lang/wgsl/resolver/is_storeable_test.cc
+++ b/src/tint/lang/wgsl/resolver/is_storeable_test.cc
@@ -17,6 +17,7 @@
 #include "gmock/gmock.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 
 using namespace tint::core::fluent_types;  // NOLINT
 
@@ -91,14 +92,14 @@
 }
 
 TEST_F(ResolverIsStorableTest, ArraySizedOfStorable) {
-    auto* arr = create<core::type::Array>(
-        create<core::type::I32>(), create<core::type::ConstantArrayCount>(5u), 4u, 20u, 4u, 4u);
+    auto* arr = create<sem::Array>(create<core::type::I32>(),
+                                   create<core::type::ConstantArrayCount>(5u), 4u, 20u, 4u, 4u);
     EXPECT_TRUE(r()->IsStorable(arr));
 }
 
 TEST_F(ResolverIsStorableTest, ArrayUnsizedOfStorable) {
-    auto* arr = create<core::type::Array>(create<core::type::I32>(),
-                                          create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
+    auto* arr = create<sem::Array>(create<core::type::I32>(),
+                                   create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
     EXPECT_TRUE(r()->IsStorable(arr));
 }
 
diff --git a/src/tint/lang/wgsl/resolver/materialize_test.cc b/src/tint/lang/wgsl/resolver/materialize_test.cc
index f4b0ab5..53a6bc1 100644
--- a/src/tint/lang/wgsl/resolver/materialize_test.cc
+++ b/src/tint/lang/wgsl/resolver/materialize_test.cc
@@ -17,6 +17,7 @@
 #include "src/tint/lang/core/type/helper_test.h"
 #include "src/tint/lang/wgsl/resolver/resolver.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/utils/rtti/switch.h"
 
 #include "gmock/gmock.h"
@@ -117,7 +118,7 @@
                     }
                 }
             },
-            [&](const core::type::Array* a) {
+            [&](const sem::Array* a) {
                 auto count = a->ConstantCount();
                 ASSERT_NE(count, 0u);
                 for (uint32_t i = 0; i < count; i++) {
diff --git a/src/tint/lang/wgsl/resolver/override_test.cc b/src/tint/lang/wgsl/resolver/override_test.cc
index f575a4f..575d58a 100644
--- a/src/tint/lang/wgsl/resolver/override_test.cc
+++ b/src/tint/lang/wgsl/resolver/override_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/lang/wgsl/resolver/resolver.h"
 
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
@@ -134,15 +135,13 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     {
-        auto* r = Sem().TransitivelyReferencedOverrides(Sem().Get(b));
-        ASSERT_NE(r, nullptr);
-        auto& refs = *r;
+        auto refs = Sem().Get(b)->TransitivelyReferencedOverrides();
         ASSERT_EQ(refs.Length(), 1u);
         EXPECT_EQ(refs[0], Sem().Get(a));
     }
 
     {
-        auto& refs = Sem().Get(func)->TransitivelyReferencedGlobals();
+        auto refs = Sem().Get(func)->TransitivelyReferencedGlobals();
         ASSERT_EQ(refs.Length(), 2u);
         EXPECT_EQ(refs[0], Sem().Get(b));
         EXPECT_EQ(refs[1], Sem().Get(a));
@@ -161,9 +160,7 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
     {
-        auto* r = Sem().TransitivelyReferencedOverrides(Sem().Get<sem::GlobalVariable>(b));
-        ASSERT_NE(r, nullptr);
-        auto& refs = *r;
+        auto refs = Sem().Get<sem::GlobalVariable>(b)->TransitivelyReferencedOverrides();
         ASSERT_EQ(refs.Length(), 1u);
         EXPECT_EQ(refs[0], Sem().Get(a));
     }
@@ -201,7 +198,6 @@
     auto* a = Override("a", ty.i32());
     auto* b = Override("b", ty.i32(), Mul(2_a, "a"));
     auto* arr = GlobalVar("arr", core::AddressSpace::kWorkgroup, ty.array(ty.i32(), Mul(2_a, "b")));
-    auto arr_ty = arr->type;
     Override("unused", ty.i32(), Expr(1_a));
     auto* func = Func("foo", tint::Empty, ty.void_(),
                       Vector{
@@ -210,26 +206,25 @@
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
+    auto* global = Sem().Get<sem::GlobalVariable>(arr);
+    ASSERT_NE(global, nullptr);
+    auto* arr_ty = global->Type()->UnwrapRef()->As<sem::Array>();
+    ASSERT_NE(arr_ty, nullptr);
+
     {
-        auto* r = Sem().TransitivelyReferencedOverrides(TypeOf(arr_ty));
-        ASSERT_NE(r, nullptr);
-        auto& refs = *r;
+        auto refs = global->TransitivelyReferencedOverrides();
         ASSERT_EQ(refs.Length(), 2u);
         EXPECT_EQ(refs[0], Sem().Get(b));
         EXPECT_EQ(refs[1], Sem().Get(a));
     }
-
     {
-        auto* r = Sem().TransitivelyReferencedOverrides(Sem().Get<sem::GlobalVariable>(arr));
-        ASSERT_NE(r, nullptr);
-        auto& refs = *r;
+        auto refs = arr_ty->TransitivelyReferencedOverrides();
         ASSERT_EQ(refs.Length(), 2u);
         EXPECT_EQ(refs[0], Sem().Get(b));
         EXPECT_EQ(refs[1], Sem().Get(a));
     }
-
     {
-        auto& refs = Sem().Get(func)->TransitivelyReferencedGlobals();
+        auto refs = Sem().Get(func)->TransitivelyReferencedGlobals();
         ASSERT_EQ(refs.Length(), 3u);
         EXPECT_EQ(refs[0], Sem().Get(arr));
         EXPECT_EQ(refs[1], Sem().Get(b));
@@ -242,7 +237,6 @@
     auto* b = Override("b", ty.i32(), Mul(2_a, "a"));
     Alias("arr_ty", ty.array(ty.i32(), Mul(2_a, "b")));
     auto* arr = GlobalVar("arr", core::AddressSpace::kWorkgroup, ty("arr_ty"));
-    auto arr_ty = arr->type;
     Override("unused", ty.i32(), Expr(1_a));
     auto* func = Func("foo", tint::Empty, ty.void_(),
                       Vector{
@@ -251,26 +245,25 @@
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 
+    auto* global = Sem().Get<sem::GlobalVariable>(arr);
+    ASSERT_NE(global, nullptr);
+    auto* arr_ty = global->Type()->UnwrapRef()->As<sem::Array>();
+    ASSERT_NE(arr_ty, nullptr);
+
     {
-        auto* r = Sem().TransitivelyReferencedOverrides(TypeOf(arr_ty));
-        ASSERT_NE(r, nullptr);
-        auto& refs = *r;
+        auto refs = global->TransitivelyReferencedOverrides();
         ASSERT_EQ(refs.Length(), 2u);
         EXPECT_EQ(refs[0], Sem().Get(b));
         EXPECT_EQ(refs[1], Sem().Get(a));
     }
-
     {
-        auto* r = Sem().TransitivelyReferencedOverrides(Sem().Get<sem::GlobalVariable>(arr));
-        ASSERT_NE(r, nullptr);
-        auto& refs = *r;
+        auto refs = arr_ty->TransitivelyReferencedOverrides();
         ASSERT_EQ(refs.Length(), 2u);
         EXPECT_EQ(refs[0], Sem().Get(b));
         EXPECT_EQ(refs[1], Sem().Get(a));
     }
-
     {
-        auto& refs = Sem().Get(func)->TransitivelyReferencedGlobals();
+        auto refs = Sem().Get(func)->TransitivelyReferencedGlobals();
         ASSERT_EQ(refs.Length(), 3u);
         EXPECT_EQ(refs[0], Sem().Get(arr));
         EXPECT_EQ(refs[1], Sem().Get(b));
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index bf0e9b3..9f6843f 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -61,7 +61,10 @@
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
 #include "src/tint/lang/wgsl/intrinsic/ctor_conv.h"
 #include "src/tint/lang/wgsl/intrinsic/dialect.h"
+#include "src/tint/lang/wgsl/resolver/incomplete_type.h"
 #include "src/tint/lang/wgsl/resolver/uniformity.h"
+#include "src/tint/lang/wgsl/resolver/unresolved_identifier.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/break_if_statement.h"
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -95,13 +98,6 @@
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::Access>);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::AddressSpace>);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::BuiltinValue>);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::InterpolationSampling>);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::InterpolationType>);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::TexelFormat>);
-
 namespace tint::resolver {
 namespace {
 
@@ -115,7 +111,7 @@
 }  // namespace
 
 Resolver::Resolver(ProgramBuilder* builder)
-    : builder_(builder),
+    : b(*builder),
       diagnostics_(builder->Diagnostics()),
       const_eval_(builder->constants, diagnostics_),
       intrinsic_table_{builder->Types(), builder->Symbols(), builder->Diagnostics()},
@@ -133,12 +129,12 @@
         return false;
     }
 
-    builder_->Sem().Reserve(builder_->LastAllocatedNodeID());
+    b.Sem().Reserve(b.LastAllocatedNodeID());
 
     // Pre-allocate the marked bitset with the total number of AST nodes.
-    marked_.Resize(builder_->ASTNodes().Count());
+    marked_.Resize(b.ASTNodes().Count());
 
-    if (!DependencyGraph::Build(builder_->AST(), diagnostics_, dependencies_)) {
+    if (!DependencyGraph::Build(b.AST(), diagnostics_, dependencies_)) {
         return false;
     }
 
@@ -150,15 +146,15 @@
     }
 
     // Create the semantic module. Don't be tempted to std::move() these, they're used below.
-    auto* mod = builder_->create<sem::Module>(dependencies_.ordered_globals, enabled_extensions_);
+    auto* mod = b.create<sem::Module>(dependencies_.ordered_globals, enabled_extensions_);
     ApplyDiagnosticSeverities(mod);
-    builder_->Sem().SetModule(mod);
+    b.Sem().SetModule(mod);
 
     const bool disable_uniformity_analysis =
         enabled_extensions_.Contains(wgsl::Extension::kChromiumDisableUniformityAnalysis);
     if (result && !disable_uniformity_analysis) {
         // Run the uniformity analysis, which requires a complete semantic module.
-        if (!AnalyzeUniformity(builder_, dependencies_)) {
+        if (!AnalyzeUniformity(b, dependencies_)) {
             return false;
         }
     }
@@ -167,7 +163,7 @@
 }
 
 bool Resolver::ResolveInternal() {
-    Mark(&builder_->AST());
+    Mark(&b.AST());
 
     // Process all module-scope declarations in dependency order.
     Vector<const ast::DiagnosticControl*, 4> diagnostic_controls;
@@ -211,7 +207,7 @@
     }
 
     bool result = true;
-    for (auto* node : builder_->ASTNodes().Objects()) {
+    for (auto* node : b.ASTNodes().Objects()) {
         if (TINT_UNLIKELY(!marked_[node->node_id.value])) {
             StringStream err;
             err << "AST node '" << node->TypeInfo().name << "' was not reached by the resolver\n"
@@ -230,7 +226,7 @@
     return Switch(
         v,  //
         [&](const ast::Var* var) { return Var(var, is_global); },
-        [&](const ast::Let* let) { return Let(let, is_global); },
+        [&](const ast::Let* let) { return Let(let); },
         [&](const ast::Override* override) { return Override(override); },
         [&](const ast::Const* const_) { return Const(const_, is_global); },
         [&](Default) {
@@ -242,15 +238,18 @@
         });
 }
 
-sem::Variable* Resolver::Let(const ast::Let* v, bool is_global) {
-    const core::type::Type* ty = nullptr;
+sem::Variable* Resolver::Let(const ast::Let* v) {
+    auto* sem = b.create<sem::LocalVariable>(v, current_statement_);
+    sem->SetStage(core::EvaluationStage::kRuntime);
+    b.Sem().Add(v, sem);
 
     // If the variable has a declared type, resolve it.
     if (v->type) {
-        ty = Type(v->type);
-        if (!ty) {
+        auto* ty = Type(v->type);
+        if (TINT_UNLIKELY(!ty)) {
             return nullptr;
         }
+        sem->SetType(ty);
     }
 
     for (auto* attribute : v->attributes) {
@@ -267,53 +266,49 @@
         }
     }
 
-    if (!v->initializer) {
+    if (TINT_UNLIKELY(!v->initializer)) {
         AddError("'let' declaration must have an initializer", v->source);
         return nullptr;
     }
 
-    auto* rhs = Load(Materialize(ValueExpression(v->initializer), ty));
-    if (!rhs) {
+    auto* rhs = Load(Materialize(ValueExpression(v->initializer), sem->Type()));
+    if (TINT_UNLIKELY(!rhs)) {
         return nullptr;
     }
+    sem->SetInitializer(rhs);
 
     // If the variable has no declared type, infer it from the RHS
-    if (!ty) {
-        ty = rhs->Type()->UnwrapRef();  // Implicit load of RHS
+    if (!sem->Type()) {
+        sem->SetType(rhs->Type()->UnwrapRef());  // Implicit load of RHS
     }
 
-    if (rhs && !validator_.VariableInitializer(v, ty, rhs)) {
+    if (TINT_UNLIKELY(rhs && !validator_.VariableInitializer(v, sem->Type(), rhs))) {
         return nullptr;
     }
 
     if (!ApplyAddressSpaceUsageToType(core::AddressSpace::kUndefined,
-                                      const_cast<core::type::Type*>(ty), v->source)) {
+                                      const_cast<core::type::Type*>(sem->Type()), v->source)) {
         AddNote("while instantiating 'let' " + v->name->symbol.Name(), v->source);
         return nullptr;
     }
 
-    sem::Variable* sem = nullptr;
-    if (is_global) {
-        sem = builder_->create<sem::GlobalVariable>(
-            v, ty, core::EvaluationStage::kRuntime, core::AddressSpace::kUndefined,
-            core::Access::kUndefined,
-            /* constant_value */ nullptr, std::nullopt, std::nullopt);
-    } else {
-        sem = builder_->create<sem::LocalVariable>(v, ty, core::EvaluationStage::kRuntime,
-                                                   core::AddressSpace::kUndefined,
-                                                   core::Access::kUndefined, current_statement_,
-                                                   /* constant_value */ nullptr);
-    }
-
-    sem->SetInitializer(rhs);
-    builder_->Sem().Add(v, sem);
     return sem;
 }
 
 sem::Variable* Resolver::Override(const ast::Override* v) {
-    const core::type::Type* ty = nullptr;
+    auto* sem = b.create<sem::GlobalVariable>(v);
+    b.Sem().Add(v, sem);
+    sem->SetStage(core::EvaluationStage::kOverride);
+
+    on_transitively_reference_global_.Push([&](const sem::GlobalVariable* ref) {
+        if (ref->Declaration()->Is<ast::Override>()) {
+            sem->AddTransitivelyReferencedOverride(ref);
+        }
+    });
+    TINT_DEFER(on_transitively_reference_global_.Pop());
 
     // If the variable has a declared type, resolve it.
+    const core::type::Type* ty = nullptr;
     if (v->type) {
         ty = Type(v->type);
         if (!ty) {
@@ -321,31 +316,31 @@
         }
     }
 
-    const sem::ValueExpression* rhs = nullptr;
-
     // Does the variable have an initializer?
+    const sem::ValueExpression* init = nullptr;
     if (v->initializer) {
         // Note: RHS must be a const or override expression, which excludes references.
         // So there's no need to load or unwrap references here.
-
         ExprEvalStageConstraint constraint{core::EvaluationStage::kOverride,
                                            "override initializer"};
         TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-        rhs = Materialize(ValueExpression(v->initializer), ty);
-        if (!rhs) {
+        init = Materialize(ValueExpression(v->initializer), ty);
+        if (TINT_UNLIKELY(!init)) {
             return nullptr;
         }
+        sem->SetInitializer(init);
 
-        // If the variable has no declared type, infer it from the RHS
+        // If the variable has no declared type, infer it from the initializer
         if (!ty) {
-            ty = rhs->Type();
+            ty = init->Type();
         }
     } else if (!ty) {
         AddError("override declaration requires a type or initializer", v->source);
         return nullptr;
     }
+    sem->SetType(ty);
 
-    if (rhs && !validator_.VariableInitializer(v, ty, rhs)) {
+    if (init && !validator_.VariableInitializer(v, ty, init)) {
         return nullptr;
     }
 
@@ -355,12 +350,6 @@
         return nullptr;
     }
 
-    auto* sem = builder_->create<sem::GlobalVariable>(
-        v, ty, core::EvaluationStage::kOverride, core::AddressSpace::kUndefined,
-        core::Access::kUndefined,
-        /* constant_value */ nullptr, std::nullopt, std::nullopt);
-    sem->SetInitializer(rhs);
-
     for (auto* attribute : v->attributes) {
         Mark(attribute);
         bool ok = Switch(
@@ -408,25 +397,19 @@
         }
     }
 
-    builder_->Sem().Add(v, sem);
     return sem;
 }
 
 sem::Variable* Resolver::Const(const ast::Const* c, bool is_global) {
-    const core::type::Type* ty = nullptr;
-
-    // If the variable has a declared type, resolve it.
-    if (c->type) {
-        ty = Type(c->type);
-        if (!ty) {
-            return nullptr;
-        }
+    sem::Variable* sem = nullptr;
+    sem::GlobalVariable* global = nullptr;
+    if (is_global) {
+        global = b.create<sem::GlobalVariable>(c);
+        sem = global;
+    } else {
+        sem = b.create<sem::LocalVariable>(c, current_statement_);
     }
-
-    if (!c->initializer) {
-        AddError("'const' declaration must have an initializer", c->source);
-        return nullptr;
-    }
+    b.Sem().Add(c, sem);
 
     for (auto* attribute : c->attributes) {
         Mark(attribute);
@@ -440,31 +423,47 @@
         }
     }
 
-    const sem::ValueExpression* rhs = nullptr;
-    {
-        ExprEvalStageConstraint constraint{core::EvaluationStage::kConstant, "const initializer"};
-        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-        rhs = ValueExpression(c->initializer);
-        if (!rhs) {
-            return nullptr;
-        }
+    if (TINT_UNLIKELY(!c->initializer)) {
+        AddError("'const' declaration must have an initializer", c->source);
+        return nullptr;
+    }
+
+    ExprEvalStageConstraint constraint{core::EvaluationStage::kConstant, "const initializer"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+    const auto* init = ValueExpression(c->initializer);
+    if (TINT_UNLIKELY(!init)) {
+        return nullptr;
     }
 
     // Note: RHS must be a const expression, which excludes references.
     // So there's no need to load or unwrap references here.
 
+    // If the variable has a declared type, resolve it.
+    const core::type::Type* ty = nullptr;
+    if (c->type) {
+        ty = Type(c->type);
+        if (TINT_UNLIKELY(!ty)) {
+            return nullptr;
+        }
+    }
+
     if (ty) {
         // If an explicit type was specified, materialize to that type
-        rhs = Materialize(rhs, ty);
-        if (!rhs) {
+        init = Materialize(init, ty);
+        if (TINT_UNLIKELY(!init)) {
             return nullptr;
         }
     } else {
         // If no type was specified, infer it from the RHS
-        ty = rhs->Type();
+        ty = init->Type();
     }
 
-    if (!validator_.VariableInitializer(c, ty, rhs)) {
+    sem->SetInitializer(init);
+    sem->SetStage(core::EvaluationStage::kConstant);
+    sem->SetConstantValue(init->ConstantValue());
+    sem->SetType(ty);
+
+    if (!validator_.VariableInitializer(c, ty, init)) {
         return nullptr;
     }
 
@@ -474,33 +473,43 @@
         return nullptr;
     }
 
-    const auto value = rhs->ConstantValue();
-    auto* sem = is_global
-                    ? static_cast<sem::Variable*>(builder_->create<sem::GlobalVariable>(
-                          c, ty, core::EvaluationStage::kConstant, core::AddressSpace::kUndefined,
-                          core::Access::kUndefined, value, std::nullopt, std::nullopt))
-                    : static_cast<sem::Variable*>(builder_->create<sem::LocalVariable>(
-                          c, ty, core::EvaluationStage::kConstant, core::AddressSpace::kUndefined,
-                          core::Access::kUndefined, current_statement_, value));
-
-    sem->SetInitializer(rhs);
-    builder_->Sem().Add(c, sem);
     return sem;
 }
 
 sem::Variable* Resolver::Var(const ast::Var* var, bool is_global) {
-    const core::type::Type* storage_ty = nullptr;
+    sem::Variable* sem = nullptr;
+    sem::GlobalVariable* global = nullptr;
+    if (is_global) {
+        global = b.create<sem::GlobalVariable>(var);
+        sem = global;
+    } else {
+        sem = b.create<sem::LocalVariable>(var, current_statement_);
+    }
+    sem->SetStage(core::EvaluationStage::kRuntime);
+    b.Sem().Add(var, sem);
+
+    if (is_global) {
+        on_transitively_reference_global_.Push([&](const sem::GlobalVariable* ref) {
+            if (ref->Declaration()->Is<ast::Override>()) {
+                global->AddTransitivelyReferencedOverride(ref);
+            }
+        });
+    }
+    TINT_DEFER({
+        if (is_global) {
+            on_transitively_reference_global_.Pop();
+        }
+    });
 
     // If the variable has a declared type, resolve it.
+    const core::type::Type* storage_ty = nullptr;
     if (auto ty = var->type) {
         storage_ty = Type(ty);
-        if (!storage_ty) {
+        if (TINT_UNLIKELY(!storage_ty)) {
             return nullptr;
         }
     }
 
-    const sem::ValueExpression* rhs = nullptr;
-
     // Does the variable have a initializer?
     if (var->initializer) {
         ExprEvalStageConstraint constraint{
@@ -509,13 +518,15 @@
         };
         TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-        rhs = Load(Materialize(ValueExpression(var->initializer), storage_ty));
-        if (!rhs) {
+        auto* init = Load(Materialize(ValueExpression(var->initializer), storage_ty));
+        if (TINT_UNLIKELY(!init)) {
             return nullptr;
         }
+        sem->SetInitializer(init);
+
         // If the variable has no declared type, infer it from the RHS
         if (!storage_ty) {
-            storage_ty = rhs->Type();
+            storage_ty = init->Type();
         }
     }
 
@@ -524,62 +535,61 @@
         return nullptr;
     }
 
-    auto address_space = core::AddressSpace::kUndefined;
     if (var->declared_address_space) {
-        auto expr = AddressSpaceExpression(var->declared_address_space);
-        if (TINT_UNLIKELY(!expr)) {
+        auto space = AddressSpaceExpression(var->declared_address_space);
+        if (TINT_UNLIKELY(!space)) {
             return nullptr;
         }
-        address_space = expr->Value();
+        sem->SetAddressSpace(space->Value());
     } else {
         // No declared address space. Infer from usage / type.
         if (!is_global) {
-            address_space = core::AddressSpace::kFunction;
+            sem->SetAddressSpace(core::AddressSpace::kFunction);
         } else if (storage_ty->UnwrapRef()->is_handle()) {
             // https://gpuweb.github.io/gpuweb/wgsl/#module-scope-variables
             // If the store type is a texture type or a sampler type, then the
             // variable declaration must not have a address space attribute. The
             // address space will always be handle.
-            address_space = core::AddressSpace::kHandle;
+            sem->SetAddressSpace(core::AddressSpace::kHandle);
         }
     }
 
-    if (!is_global && address_space != core::AddressSpace::kFunction &&
+    if (!is_global && sem->AddressSpace() != core::AddressSpace::kFunction &&
         validator_.IsValidationEnabled(var->attributes,
                                        ast::DisabledValidation::kIgnoreAddressSpace)) {
         AddError("function-scope 'var' declaration must use 'function' address space", var->source);
         return nullptr;
     }
 
-    auto access = core::Access::kUndefined;
     if (var->declared_access) {
         auto expr = AccessExpression(var->declared_access);
         if (!expr) {
             return nullptr;
         }
-        access = expr->Value();
+        sem->SetAccess(expr->Value());
     } else {
-        access = DefaultAccessForAddressSpace(address_space);
+        sem->SetAccess(DefaultAccessForAddressSpace(sem->AddressSpace()));
     }
 
-    if (rhs && !validator_.VariableInitializer(var, storage_ty, rhs)) {
+    sem->SetType(b.create<core::type::Reference>(sem->AddressSpace(), storage_ty, sem->Access()));
+
+    if (sem->Initializer() &&
+        !validator_.VariableInitializer(var, storage_ty, sem->Initializer())) {
         return nullptr;
     }
 
-    auto* var_ty = builder_->create<core::type::Reference>(address_space, storage_ty, access);
-
-    if (!ApplyAddressSpaceUsageToType(address_space, var_ty,
+    if (!ApplyAddressSpaceUsageToType(sem->AddressSpace(),
+                                      const_cast<core::type::Type*>(sem->Type()),
                                       var->type ? var->type->source : var->source)) {
         AddNote("while instantiating 'var' " + var->name->symbol.Name(), var->source);
         return nullptr;
     }
 
-    sem::Variable* sem = nullptr;
     if (is_global) {
-        bool has_io_address_space =
-            address_space == core::AddressSpace::kIn || address_space == core::AddressSpace::kOut;
+        bool has_io_address_space = sem->AddressSpace() == core::AddressSpace::kIn ||
+                                    sem->AddressSpace() == core::AddressSpace::kOut;
 
-        std::optional<uint32_t> group, binding, location, index;
+        std::optional<uint32_t> group, binding;
         for (auto* attribute : var->attributes) {
             Mark(attribute);
             enum Status { kSuccess, kErrored, kInvalid };
@@ -609,7 +619,7 @@
                     if (!value) {
                         return kErrored;
                     }
-                    location = value.Get();
+                    global->SetLocation(value.Get());
                     return kSuccess;
                 },
                 [&](const ast::IndexAttribute* attr) {
@@ -620,7 +630,7 @@
                     if (!value) {
                         return kErrored;
                     }
-                    index = value.Get();
+                    global->SetIndex(value.Get());
                     return kSuccess;
                 },
                 [&](const ast::BuiltinAttribute* attr) {
@@ -657,13 +667,9 @@
             }
         }
 
-        std::optional<BindingPoint> binding_point;
         if (group && binding) {
-            binding_point = BindingPoint{group.value(), binding.value()};
+            global->SetBindingPoint(BindingPoint{group.value(), binding.value()});
         }
-        sem = builder_->create<sem::GlobalVariable>(
-            var, var_ty, core::EvaluationStage::kRuntime, address_space, access,
-            /* constant_value */ nullptr, binding_point, location, index);
 
     } else {
         for (auto* attribute : var->attributes) {
@@ -679,13 +685,8 @@
                 return nullptr;
             }
         }
-        sem = builder_->create<sem::LocalVariable>(var, var_ty, core::EvaluationStage::kRuntime,
-                                                   address_space, access, current_statement_,
-                                                   /* constant_value */ nullptr);
     }
 
-    sem->SetInitializer(rhs);
-    builder_->Sem().Add(var, sem);
     return sem;
 }
 
@@ -694,23 +695,25 @@
                                     uint32_t index) {
     Mark(param->name);
 
+    auto* sem = b.create<sem::Parameter>(param, index);
+    b.Sem().Add(param, sem);
+
     auto add_note = [&] {
         AddNote("while instantiating parameter " + param->name->symbol.Name(), param->source);
     };
 
-    std::optional<uint32_t> location, group, binding;
-
     if (func->IsEntryPoint()) {
+        std::optional<uint32_t> group, binding;
         for (auto* attribute : param->attributes) {
             Mark(attribute);
             bool ok = Switch(
                 attribute,  //
                 [&](const ast::LocationAttribute* attr) {
                     auto value = LocationAttribute(attr);
-                    if (!value) {
+                    if (TINT_UNLIKELY(!value)) {
                         return false;
                     }
-                    location = value.Get();
+                    sem->SetLocation(value.Get());
                     return true;
                 },
                 [&](const ast::BuiltinAttribute* attr) -> bool { return BuiltinAttribute(attr); },
@@ -728,7 +731,7 @@
                         return false;
                     }
                     auto value = GroupAttribute(attr);
-                    if (!value) {
+                    if (TINT_UNLIKELY(!value)) {
                         return false;
                     }
                     group = value.Get();
@@ -741,7 +744,7 @@
                         return false;
                     }
                     auto value = BindingAttribute(attr);
-                    if (!value) {
+                    if (TINT_UNLIKELY(!value)) {
                         return false;
                     }
                     binding = value.Get();
@@ -755,6 +758,9 @@
                 return nullptr;
             }
         }
+        if (group && binding) {
+            sem->SetBindingPoint(BindingPoint{group.value(), binding.value()});
+        }
     } else {
         for (auto* attribute : param->attributes) {
             Mark(attribute);
@@ -781,9 +787,10 @@
     }
 
     core::type::Type* ty = Type(param->type);
-    if (!ty) {
+    if (TINT_UNLIKELY(!ty)) {
         return nullptr;
     }
+    sem->SetType(ty);
 
     if (!ApplyAddressSpaceUsageToType(core::AddressSpace::kUndefined, ty, param->type->source)) {
         add_note();
@@ -801,16 +808,6 @@
         }
     }
 
-    std::optional<BindingPoint> binding_point;
-    if (group && binding) {
-        binding_point = BindingPoint{group.value(), binding.value()};
-    }
-
-    auto* sem = builder_->create<sem::Parameter>(
-        param, index, ty, core::AddressSpace::kUndefined, core::Access::kUndefined,
-        core::ParameterUsage::kNone, binding_point, location);
-    builder_->Sem().Add(param, sem);
-
     if (!validator_.Parameter(sem)) {
         return nullptr;
     }
@@ -849,7 +846,7 @@
     // deterministic.
     // TODO(crbug.com/tint/1192): If a transform changes the order or removes an
     // unused constant, the allocation may change on the next Resolver pass.
-    for (auto* decl : builder_->AST().GlobalDeclarations()) {
+    for (auto* decl : b.AST().GlobalDeclarations()) {
         auto* override = decl->As<ast::Override>();
         if (!override) {
             continue;
@@ -882,8 +879,8 @@
 
 void Resolver::SetShadows() {
     for (auto it : dependencies_.shadows) {
-        CastableBase* b = sem_.Get(it.value);
-        if (TINT_UNLIKELY(!b)) {
+        CastableBase* shadowed = sem_.Get(it.value);
+        if (TINT_UNLIKELY(!shadowed)) {
             StringStream err;
             err << "AST node '" << it.value->TypeInfo().name << "' had no semantic info\n"
                 << "Pointer: " << it.value;
@@ -892,15 +889,12 @@
 
         Switch(
             sem_.Get(it.key),  //
-            [&](sem::LocalVariable* local) { local->SetShadows(b); },
-            [&](sem::Parameter* param) { param->SetShadows(b); });
+            [&](sem::LocalVariable* local) { local->SetShadows(shadowed); },
+            [&](sem::Parameter* param) { param->SetShadows(shadowed); });
     }
 }
 
 sem::GlobalVariable* Resolver::GlobalVariable(const ast::Variable* v) {
-    UniqueVector<const sem::GlobalVariable*, 4> transitively_referenced_overrides;
-    TINT_SCOPED_ASSIGNMENT(resolved_overrides_, &transitively_referenced_overrides);
-
     auto* sem = As<sem::GlobalVariable>(Variable(v, /* is_global */ true));
     if (!sem) {
         return nullptr;
@@ -914,20 +908,6 @@
         return nullptr;
     }
 
-    // Track the pipeline-overridable constants that are transitively referenced by this
-    // variable.
-    for (auto* var : transitively_referenced_overrides) {
-        builder_->Sem().AddTransitivelyReferencedOverride(sem, var);
-    }
-    if (auto* arr = sem->Type()->UnwrapRef()->As<core::type::Array>()) {
-        auto* refs = builder_->Sem().TransitivelyReferencedOverrides(arr);
-        if (refs) {
-            for (auto* var : *refs) {
-                builder_->Sem().AddTransitivelyReferencedOverride(sem, var);
-            }
-        }
-    }
-
     return sem;
 }
 
@@ -948,19 +928,23 @@
         AddError("const assertion failed", assertion->source);
         return nullptr;
     }
-    auto* sem =
-        builder_->create<sem::Statement>(assertion, current_compound_statement_, current_function_);
-    builder_->Sem().Add(assertion, sem);
+    auto* sem = b.create<sem::Statement>(assertion, current_compound_statement_, current_function_);
+    b.Sem().Add(assertion, sem);
     return sem;
 }
 
 sem::Function* Resolver::Function(const ast::Function* decl) {
     Mark(decl->name);
 
-    auto* func = builder_->create<sem::Function>(decl);
-    builder_->Sem().Add(decl, func);
+    auto* func = b.create<sem::Function>(decl);
+    b.Sem().Add(decl, func);
     TINT_SCOPED_ASSIGNMENT(current_function_, func);
 
+    on_transitively_reference_global_.Push([&](const sem::GlobalVariable* ref) {  //
+        func->AddDirectlyReferencedGlobal(ref);
+    });
+    TINT_DEFER(on_transitively_reference_global_.Pop());
+
     validator_.DiagnosticFilters().Push();
     TINT_DEFER(validator_.DiagnosticFilters().Pop());
 
@@ -1040,7 +1024,7 @@
             return nullptr;
         }
     } else {
-        return_type = builder_->create<core::type::Void>();
+        return_type = b.create<core::type::Void>();
     }
     func->SetReturnType(return_type);
 
@@ -1158,7 +1142,7 @@
             AddICE(err.str(), decl->body->source);
             return nullptr;
         }
-        auto* body = StatementScope(decl->body, builder_->create<sem::FunctionBlockStatement>(func),
+        auto* body = StatementScope(decl->body, b.create<sem::FunctionBlockStatement>(func),
                                     [&] { return Statements(decl->body->statements); });
         if (!body) {
             return nullptr;
@@ -1227,25 +1211,25 @@
         stmt,
         // Compound statements. These create their own sem::CompoundStatement
         // bindings.
-        [&](const ast::BlockStatement* b) { return BlockStatement(b); },
-        [&](const ast::ForLoopStatement* l) { return ForLoopStatement(l); },
-        [&](const ast::LoopStatement* l) { return LoopStatement(l); },
-        [&](const ast::WhileStatement* w) { return WhileStatement(w); },
-        [&](const ast::IfStatement* i) { return IfStatement(i); },
+        [&](const ast::BlockStatement* s) { return BlockStatement(s); },
+        [&](const ast::ForLoopStatement* s) { return ForLoopStatement(s); },
+        [&](const ast::LoopStatement* s) { return LoopStatement(s); },
+        [&](const ast::WhileStatement* s) { return WhileStatement(s); },
+        [&](const ast::IfStatement* s) { return IfStatement(s); },
         [&](const ast::SwitchStatement* s) { return SwitchStatement(s); },
 
         // Non-Compound statements
-        [&](const ast::AssignmentStatement* a) { return AssignmentStatement(a); },
-        [&](const ast::BreakStatement* b) { return BreakStatement(b); },
-        [&](const ast::BreakIfStatement* b) { return BreakIfStatement(b); },
-        [&](const ast::CallStatement* c) { return CallStatement(c); },
-        [&](const ast::CompoundAssignmentStatement* c) { return CompoundAssignmentStatement(c); },
-        [&](const ast::ContinueStatement* c) { return ContinueStatement(c); },
-        [&](const ast::DiscardStatement* d) { return DiscardStatement(d); },
-        [&](const ast::IncrementDecrementStatement* i) { return IncrementDecrementStatement(i); },
-        [&](const ast::ReturnStatement* r) { return ReturnStatement(r); },
-        [&](const ast::VariableDeclStatement* v) { return VariableDeclStatement(v); },
-        [&](const ast::ConstAssert* sa) { return ConstAssert(sa); },
+        [&](const ast::AssignmentStatement* s) { return AssignmentStatement(s); },
+        [&](const ast::BreakStatement* s) { return BreakStatement(s); },
+        [&](const ast::BreakIfStatement* s) { return BreakIfStatement(s); },
+        [&](const ast::CallStatement* s) { return CallStatement(s); },
+        [&](const ast::CompoundAssignmentStatement* s) { return CompoundAssignmentStatement(s); },
+        [&](const ast::ContinueStatement* s) { return ContinueStatement(s); },
+        [&](const ast::DiscardStatement* s) { return DiscardStatement(s); },
+        [&](const ast::IncrementDecrementStatement* s) { return IncrementDecrementStatement(s); },
+        [&](const ast::ReturnStatement* s) { return ReturnStatement(s); },
+        [&](const ast::VariableDeclStatement* s) { return VariableDeclStatement(s); },
+        [&](const ast::ConstAssert* s) { return ConstAssert(s); },
 
         // Error cases
         [&](const ast::CaseStatement*) {
@@ -1260,8 +1244,7 @@
 
 sem::CaseStatement* Resolver::CaseStatement(const ast::CaseStatement* stmt,
                                             const core::type::Type* ty) {
-    auto* sem =
-        builder_->create<sem::CaseStatement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::CaseStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         sem->Selectors().reserve(stmt->selectors.Length());
         for (auto* sel : stmt->selectors) {
@@ -1289,7 +1272,7 @@
                 }
             }
 
-            sem->Selectors().emplace_back(builder_->create<sem::CaseSelector>(sel, const_value));
+            sem->Selectors().emplace_back(b.create<sem::CaseSelector>(sel, const_value));
         }
 
         Mark(stmt->body);
@@ -1304,8 +1287,7 @@
 }
 
 sem::IfStatement* Resolver::IfStatement(const ast::IfStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::IfStatement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::IfStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto* cond = Load(ValueExpression(stmt->condition));
         if (!cond) {
@@ -1316,8 +1298,8 @@
         sem->Behaviors().Remove(sem::Behavior::kNext);
 
         Mark(stmt->body);
-        auto* body = builder_->create<sem::BlockStatement>(stmt->body, current_compound_statement_,
-                                                           current_function_);
+        auto* body = b.create<sem::BlockStatement>(stmt->body, current_compound_statement_,
+                                                   current_function_);
         if (!StatementScope(stmt->body, body, [&] { return Statements(stmt->body->statements); })) {
             return false;
         }
@@ -1342,19 +1324,18 @@
 }
 
 sem::BlockStatement* Resolver::BlockStatement(const ast::BlockStatement* stmt) {
-    auto* sem = builder_->create<sem::BlockStatement>(
-        stmt->As<ast::BlockStatement>(), current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::BlockStatement>(stmt->As<ast::BlockStatement>(),
+                                              current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] { return Statements(stmt->statements); });
 }
 
 sem::LoopStatement* Resolver::LoopStatement(const ast::LoopStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::LoopStatement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::LoopStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         Mark(stmt->body);
 
-        auto* body = builder_->create<sem::LoopBlockStatement>(
-            stmt->body, current_compound_statement_, current_function_);
+        auto* body = b.create<sem::LoopBlockStatement>(stmt->body, current_compound_statement_,
+                                                       current_function_);
         return StatementScope(stmt->body, body, [&] {
             if (!Statements(stmt->body->statements)) {
                 return false;
@@ -1366,7 +1347,7 @@
                 Mark(stmt->continuing);
                 auto* continuing = StatementScope(
                     stmt->continuing,
-                    builder_->create<sem::LoopContinuingBlockStatement>(
+                    b.create<sem::LoopContinuingBlockStatement>(
                         stmt->continuing, current_compound_statement_, current_function_),
                     [&] { return Statements(stmt->continuing->statements); });
                 if (!continuing) {
@@ -1388,8 +1369,8 @@
 }
 
 sem::ForLoopStatement* Resolver::ForLoopStatement(const ast::ForLoopStatement* stmt) {
-    auto* sem = builder_->create<sem::ForLoopStatement>(stmt, current_compound_statement_,
-                                                        current_function_);
+    auto* sem =
+        b.create<sem::ForLoopStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto& behaviors = sem->Behaviors();
         if (auto* initializer = stmt->initializer) {
@@ -1421,8 +1402,8 @@
 
         Mark(stmt->body);
 
-        auto* body = builder_->create<sem::LoopBlockStatement>(
-            stmt->body, current_compound_statement_, current_function_);
+        auto* body = b.create<sem::LoopBlockStatement>(stmt->body, current_compound_statement_,
+                                                       current_function_);
         if (!StatementScope(stmt->body, body, [&] { return Statements(stmt->body->statements); })) {
             return false;
         }
@@ -1440,8 +1421,7 @@
 }
 
 sem::WhileStatement* Resolver::WhileStatement(const ast::WhileStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::WhileStatement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::WhileStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto& behaviors = sem->Behaviors();
 
@@ -1454,8 +1434,8 @@
 
         Mark(stmt->body);
 
-        auto* body = builder_->create<sem::LoopBlockStatement>(
-            stmt->body, current_compound_statement_, current_function_);
+        auto* body = b.create<sem::LoopBlockStatement>(stmt->body, current_compound_statement_,
+                                                       current_function_);
         if (!StatementScope(stmt->body, body, [&] { return Statements(stmt->body->statements); })) {
             return false;
         }
@@ -1517,11 +1497,11 @@
             [&](const ast::MemberAccessorExpression* member) { return MemberAccessor(member); },
             [&](const ast::UnaryOpExpression* unary) { return UnaryOp(unary); },
             [&](const ast::PhonyExpression*) {
-                return builder_->create<sem::ValueExpression>(
-                    expr, builder_->create<core::type::Void>(), core::EvaluationStage::kRuntime,
-                    current_statement_,
-                    /* constant_value */ nullptr,
-                    /* has_side_effects */ false);
+                return b.create<sem::ValueExpression>(expr, b.create<core::type::Void>(),
+                                                      core::EvaluationStage::kRuntime,
+                                                      current_statement_,
+                                                      /* constant_value */ nullptr,
+                                                      /* has_side_effects */ false);
             },
             [&](Default) {
                 StringStream err;
@@ -1544,7 +1524,7 @@
             }
         }
 
-        builder_->Sem().Add(expr, sem_expr);
+        b.Sem().Add(expr, sem_expr);
         if (expr == root) {
             return sem_expr;
         }
@@ -1580,26 +1560,43 @@
 }
 
 sem::TypeExpression* Resolver::TypeExpression(const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "type"};
     return sem_.AsTypeExpression(Expression(expr));
 }
 
 sem::FunctionExpression* Resolver::FunctionExpression(const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "call target"};
     return sem_.AsFunctionExpression(Expression(expr));
 }
 
 core::type::Type* Resolver::Type(const ast::Expression* ast) {
+    Vector<const sem::GlobalVariable*, 4> referenced_overrides;
+    on_transitively_reference_global_.Push([&](const sem::GlobalVariable* ref) {
+        if (ref->Declaration()->Is<ast::Override>()) {
+            referenced_overrides.Push(ref);
+        }
+    });
+    TINT_DEFER(on_transitively_reference_global_.Pop());
+
     auto* type_expr = TypeExpression(ast);
-    if (!type_expr) {
+    if (TINT_UNLIKELY(!type_expr)) {
         return nullptr;
     }
-    return const_cast<core::type::Type*>(type_expr->Type());
+
+    auto* type = const_cast<core::type::Type*>(type_expr->Type());
+    if (TINT_UNLIKELY(!type)) {
+        return nullptr;
+    }
+
+    if (auto* arr = type->As<sem::Array>()) {
+        for (auto* ref : referenced_overrides) {
+            arr->AddTransitivelyReferencedOverride(ref);
+        }
+    }
+
+    return type;
 }
 
 sem::BuiltinEnumExpression<core::AddressSpace>* Resolver::AddressSpaceExpression(
     const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "address space", core::kAddressSpaceStrings};
     auto address_space_expr = sem_.AsAddressSpace(Expression(expr));
     if (TINT_UNLIKELY(!address_space_expr)) {
         return nullptr;
@@ -1618,31 +1615,25 @@
 
 sem::BuiltinEnumExpression<core::BuiltinValue>* Resolver::BuiltinValueExpression(
     const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "builtin value", core::kBuiltinValueStrings};
     return sem_.AsBuiltinValue(Expression(expr));
 }
 
 sem::BuiltinEnumExpression<core::TexelFormat>* Resolver::TexelFormatExpression(
     const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "texel format", core::kTexelFormatStrings};
     return sem_.AsTexelFormat(Expression(expr));
 }
 
 sem::BuiltinEnumExpression<core::Access>* Resolver::AccessExpression(const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "access", core::kAccessStrings};
     return sem_.AsAccess(Expression(expr));
 }
 
 sem::BuiltinEnumExpression<core::InterpolationSampling>* Resolver::InterpolationSampling(
     const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "interpolation sampling",
-                                core::kInterpolationSamplingStrings};
     return sem_.AsInterpolationSampling(Expression(expr));
 }
 
 sem::BuiltinEnumExpression<core::InterpolationType>* Resolver::InterpolationType(
     const ast::Expression* expr) {
-    identifier_resolve_hint_ = {expr, "interpolation type", core::kInterpolationTypeStrings};
     return sem_.AsInterpolationType(Expression(expr));
 }
 
@@ -1768,12 +1759,12 @@
 const core::type::Type* Resolver::ConcreteType(const core::type::Type* ty,
                                                const core::type::Type* target_ty,
                                                const Source& source) {
-    auto i32 = [&] { return builder_->create<core::type::I32>(); };
-    auto f32 = [&] { return builder_->create<core::type::F32>(); };
-    auto i32v = [&](uint32_t width) { return builder_->create<core::type::Vector>(i32(), width); };
-    auto f32v = [&](uint32_t width) { return builder_->create<core::type::Vector>(f32(), width); };
+    auto i32 = [&] { return b.create<core::type::I32>(); };
+    auto f32 = [&] { return b.create<core::type::F32>(); };
+    auto i32v = [&](uint32_t width) { return b.create<core::type::Vector>(i32(), width); };
+    auto f32v = [&](uint32_t width) { return b.create<core::type::Vector>(f32(), width); };
     auto f32m = [&](uint32_t columns, uint32_t rows) {
-        return builder_->create<core::type::Matrix>(f32v(rows), columns);
+        return b.create<core::type::Matrix>(f32v(rows), columns);
     };
 
     return Switch(
@@ -1796,9 +1787,9 @@
                               return target_ty ? target_ty : f32m(m->columns(), m->rows());
                           });
         },
-        [&](const core::type::Array* a) -> const core::type::Type* {
+        [&](const sem::Array* a) -> const core::type::Type* {
             const core::type::Type* target_el_ty = nullptr;
-            if (auto* target_arr_ty = As<core::type::Array>(target_ty)) {
+            if (auto* target_arr_ty = As<sem::Array>(target_ty)) {
                 target_el_ty = target_arr_ty->ElemType();
             }
             if (auto* el_ty = ConcreteType(a->ElemType(), target_el_ty, source)) {
@@ -1825,9 +1816,9 @@
         return expr;
     }
 
-    auto* load = builder_->create<sem::Load>(expr, current_statement_);
+    auto* load = b.create<sem::Load>(expr, current_statement_);
     load->Behaviors() = expr->Behaviors();
-    builder_->Sem().Replace(expr->Declaration(), load);
+    b.Sem().Replace(expr->Declaration(), load);
 
     // Track the load for the alias analysis.
     auto& alias_info = alias_analysis_infos_[current_function_];
@@ -1887,10 +1878,9 @@
         }
     }
 
-    auto* m =
-        builder_->create<sem::Materialize>(expr, current_statement_, concrete_ty, materialized_val);
+    auto* m = b.create<sem::Materialize>(expr, current_statement_, concrete_ty, materialized_val);
     m->Behaviors() = expr->Behaviors();
-    builder_->Sem().Replace(decl, m);
+    b.Sem().Replace(decl, m);
     return m;
 }
 
@@ -1966,10 +1956,10 @@
     auto* obj_ty = obj_raw_ty->UnwrapRef();
     auto* ty = Switch(
         obj_ty,  //
-        [&](const core::type::Array* arr) { return arr->ElemType(); },
+        [&](const sem::Array* arr) { return arr->ElemType(); },
         [&](const core::type::Vector* vec) { return vec->type(); },
         [&](const core::type::Matrix* mat) {
-            return builder_->create<core::type::Vector>(mat->type(), mat->rows());
+            return b.create<core::type::Vector>(mat->type(), mat->rows());
         },
         [&](Default) {
             AddError("cannot index type '" + sem_.TypeNameOf(obj_ty) + "'", expr->source);
@@ -1988,7 +1978,7 @@
 
     // If we're extracting from a reference, we return a reference.
     if (auto* ref = obj_raw_ty->As<core::type::Reference>()) {
-        ty = builder_->create<core::type::Reference>(ref->AddressSpace(), ty, ref->Access());
+        ty = b.create<core::type::Reference>(ref->AddressSpace(), ty, ref->Access());
     }
 
     const core::constant::Value* val = nullptr;
@@ -2006,9 +1996,9 @@
         }
     }
     bool has_side_effects = idx->HasSideEffects() || obj->HasSideEffects();
-    auto* sem = builder_->create<sem::IndexAccessorExpression>(
-        expr, ty, stage, obj, idx, current_statement_, std::move(val), has_side_effects,
-        obj->RootIdentifier());
+    auto* sem = b.create<sem::IndexAccessorExpression>(expr, ty, stage, obj, idx,
+                                                       current_statement_, std::move(val),
+                                                       has_side_effects, obj->RootIdentifier());
     sem->Behaviors() = idx->Behaviors() + obj->Behaviors();
     return sem;
 }
@@ -2040,8 +2030,8 @@
         }
     }
 
-    auto* sem = builder_->create<sem::ValueExpression>(expr, ty, stage, current_statement_,
-                                                       std::move(value), inner->HasSideEffects());
+    auto* sem = b.create<sem::ValueExpression>(expr, ty, stage, current_statement_,
+                                               std::move(value), inner->HasSideEffects());
     sem->Behaviors() = inner->Behaviors();
     return sem;
 }
@@ -2052,11 +2042,10 @@
     // * A builtin call.
     // * A value constructor.
     // * A value conversion.
-    auto* target = expr->target;
-    Mark(target);
-
-    auto* ident = target->identifier;
-    Mark(ident);
+    auto* target = sem_.Get(expr->target);
+    if (TINT_UNLIKELY(!target)) {
+        return nullptr;
+    }
 
     // Resolve all of the arguments, their types and the set of behaviors.
     Vector<const sem::ValueExpression*, 8> args;
@@ -2097,22 +2086,18 @@
         if (match->info->flags.Contains(OverloadFlag::kIsConstructor)) {
             // Type constructor
             auto params = Transform(match->parameters, [&](auto& p, size_t i) {
-                return builder_->create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type,
-                                                        core::AddressSpace::kUndefined,
-                                                        core::Access::kUndefined, p.usage);
+                return b.create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type, p.usage);
             });
             target_sem = constructors_.GetOrCreate(match.Get(), [&] {
-                return builder_->create<sem::ValueConstructor>(match->return_type,
-                                                               std::move(params), overload_stage);
+                return b.create<sem::ValueConstructor>(match->return_type, std::move(params),
+                                                       overload_stage);
             });
         } else {
             // Type conversion
             target_sem = converters_.GetOrCreate(match.Get(), [&] {
-                auto param = builder_->create<sem::Parameter>(
-                    nullptr, 0u, match->parameters[0].type, core::AddressSpace::kUndefined,
-                    core::Access::kUndefined, match->parameters[0].usage);
-                return builder_->create<sem::ValueConversion>(match->return_type, param,
-                                                              overload_stage);
+                auto* param = b.create<sem::Parameter>(nullptr, 0u, match->parameters[0].type,
+                                                       match->parameters[0].usage);
+                return b.create<sem::ValueConversion>(match->return_type, param, overload_stage);
             });
         }
 
@@ -2138,26 +2123,25 @@
                 return nullptr;
             }
         }
-        return builder_->create<sem::Call>(expr, target_sem, stage, std::move(args),
-                                           current_statement_, value, has_side_effects);
+        return b.create<sem::Call>(expr, target_sem, stage, std::move(args), current_statement_,
+                                   value, has_side_effects);
     };
 
     // arr_or_str_init is a helper for building a sem::ValueConstructor for an array or structure
     // constructor call target.
     auto arr_or_str_init = [&](const core::type::Type* ty,
                                const sem::CallTarget* call_target) -> sem::Call* {
-        if (!MaybeMaterializeAndLoadArguments(args, call_target)) {
-            return nullptr;
-        }
-
         auto stage = args_stage;                       // The evaluation stage of the call
         const core::constant::Value* value = nullptr;  // The constant value for the call
         if (stage == core::EvaluationStage::kConstant && skip_const_eval_.Contains(expr)) {
             stage = core::EvaluationStage::kNotEvaluated;
         }
         if (stage == core::EvaluationStage::kConstant) {
-            auto els = tint::Transform(args, [&](auto* arg) { return arg->ConstantValue(); });
-            if (auto r = const_eval_.ArrayOrStructCtor(ty, std::move(els))) {
+            auto const_args = ConvertArguments(args, call_target);
+            if (!const_args) {
+                return nullptr;
+            }
+            if (auto r = const_eval_.ArrayOrStructCtor(ty, std::move(const_args.Get()))) {
                 value = r.Get();
             } else {
                 return nullptr;
@@ -2172,8 +2156,8 @@
             }
         }
 
-        return builder_->create<sem::Call>(expr, call_target, stage, std::move(args),
-                                           current_statement_, value, has_side_effects);
+        return b.create<sem::Call>(expr, call_target, stage, std::move(args), current_statement_,
+                                   value, has_side_effects);
     };
 
     auto ty_init_or_conv = [&](const core::type::Type* type) {
@@ -2201,32 +2185,27 @@
                 return ctor_or_conv(wgsl::intrinsic::MatrixCtorConv(m->columns(), m->rows()),
                                     m->type());
             },
-            [&](const core::type::Array* arr) -> sem::Call* {
+            [&](const sem::Array* arr) -> sem::Call* {
                 auto* call_target = array_ctors_.GetOrCreate(
                     ArrayConstructorSig{{arr, args.Length(), args_stage}},
                     [&]() -> sem::ValueConstructor* {
                         auto params = tint::Transform(args, [&](auto, size_t i) {
-                            return builder_->create<sem::Parameter>(
-                                nullptr,                         // declaration
-                                static_cast<uint32_t>(i),        // index
-                                arr->ElemType(),                 // type
-                                core::AddressSpace::kUndefined,  // address_space
-                                core::Access::kUndefined);
+                            return b.create<sem::Parameter>(nullptr,  // declaration
+                                                            static_cast<uint32_t>(i),  // index
+                                                            arr->ElemType());
                         });
-                        return builder_->create<sem::ValueConstructor>(arr, std::move(params),
-                                                                       args_stage);
+                        return b.create<sem::ValueConstructor>(arr, std::move(params), args_stage);
                     });
 
-                auto* call = arr_or_str_init(arr, call_target);
-                if (!call) {
+                if (TINT_UNLIKELY(!MaybeMaterializeAndLoadArguments(args, call_target))) {
                     return nullptr;
                 }
 
-                // Validation must occur after argument materialization in arr_or_str_init().
-                if (!validator_.ArrayConstructor(expr, arr)) {
+                if (TINT_UNLIKELY(!validator_.ArrayConstructor(expr, arr))) {
                     return nullptr;
                 }
-                return call;
+
+                return arr_or_str_init(arr, call_target);
             },
             [&](const core::type::Struct* str) -> sem::Call* {
                 auto* call_target = struct_ctors_.GetOrCreate(
@@ -2235,27 +2214,23 @@
                         Vector<sem::Parameter*, 8> params;
                         params.Resize(std::min(args.Length(), str->Members().Length()));
                         for (size_t i = 0, n = params.Length(); i < n; i++) {
-                            params[i] = builder_->create<sem::Parameter>(
-                                nullptr,                         // declaration
-                                static_cast<uint32_t>(i),        // index
-                                str->Members()[i]->Type(),       // type
-                                core::AddressSpace::kUndefined,  // address_space
-                                core::Access::kUndefined);       // access
+                            params[i] =
+                                b.create<sem::Parameter>(nullptr,                     // declaration
+                                                         static_cast<uint32_t>(i),    // index
+                                                         str->Members()[i]->Type());  // type
                         }
-                        return builder_->create<sem::ValueConstructor>(str, std::move(params),
-                                                                       args_stage);
+                        return b.create<sem::ValueConstructor>(str, std::move(params), args_stage);
                     });
 
-                auto* call = arr_or_str_init(str, call_target);
-                if (!call) {
+                if (TINT_UNLIKELY(!MaybeMaterializeAndLoadArguments(args, call_target))) {
                     return nullptr;
                 }
 
-                // Validation must occur after argument materialization in arr_or_str_init().
-                if (!validator_.StructureInitializer(expr, str)) {
+                if (TINT_UNLIKELY(!validator_.StructureInitializer(expr, str))) {
                     return nullptr;
                 }
-                return call;
+
+                return arr_or_str_init(str, call_target);
             },
             [&](Default) {
                 AddError("type is not constructible", expr->source);
@@ -2263,137 +2238,99 @@
             });
     };
 
-    auto inferred_array = [&]() -> tint::sem::Call* {
-        auto el_count =
-            builder_->create<core::type::ConstantArrayCount>(static_cast<uint32_t>(args.Length()));
-        auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type()->UnwrapRef(); });
-        auto el_ty = core::type::Type::Common(arg_tys);
-        if (!el_ty) {
-            AddError("cannot infer common array element type from constructor arguments",
-                     expr->source);
-            Hashset<const core::type::Type*, 8> types;
-            for (size_t i = 0; i < args.Length(); i++) {
-                if (types.Add(args[i]->Type())) {
-                    AddNote("argument " + std::to_string(i) + " is of type '" +
-                                sem_.TypeNameOf(args[i]->Type()) + "'",
-                            args[i]->Declaration()->source);
+    auto incomplete_type = [&](const IncompleteType* t) -> sem::Call* {
+        // A type without template arguments.
+        // Examples: vec3(...), array(...)
+        switch (t->builtin) {
+            case core::BuiltinType::kVec2:
+                return ctor_or_conv(CtorConvIntrinsic::kVec2, nullptr);
+            case core::BuiltinType::kVec3:
+                return ctor_or_conv(CtorConvIntrinsic::kVec3, nullptr);
+            case core::BuiltinType::kVec4:
+                return ctor_or_conv(CtorConvIntrinsic::kVec4, nullptr);
+            case core::BuiltinType::kMat2X2:
+                return ctor_or_conv(CtorConvIntrinsic::kMat2x2, nullptr);
+            case core::BuiltinType::kMat2X3:
+                return ctor_or_conv(CtorConvIntrinsic::kMat2x3, nullptr);
+            case core::BuiltinType::kMat2X4:
+                return ctor_or_conv(CtorConvIntrinsic::kMat2x4, nullptr);
+            case core::BuiltinType::kMat3X2:
+                return ctor_or_conv(CtorConvIntrinsic::kMat3x2, nullptr);
+            case core::BuiltinType::kMat3X3:
+                return ctor_or_conv(CtorConvIntrinsic::kMat3x3, nullptr);
+            case core::BuiltinType::kMat3X4:
+                return ctor_or_conv(CtorConvIntrinsic::kMat3x4, nullptr);
+            case core::BuiltinType::kMat4X2:
+                return ctor_or_conv(CtorConvIntrinsic::kMat4x2, nullptr);
+            case core::BuiltinType::kMat4X3:
+                return ctor_or_conv(CtorConvIntrinsic::kMat4x3, nullptr);
+            case core::BuiltinType::kMat4X4:
+                return ctor_or_conv(CtorConvIntrinsic::kMat4x4, nullptr);
+            case core::BuiltinType::kArray: {
+                auto el_count =
+                    b.create<core::type::ConstantArrayCount>(static_cast<uint32_t>(args.Length()));
+                auto arg_tys =
+                    tint::Transform(args, [](auto* arg) { return arg->Type()->UnwrapRef(); });
+                auto el_ty = core::type::Type::Common(arg_tys);
+                if (TINT_UNLIKELY(!el_ty)) {
+                    AddError("cannot infer common array element type from constructor arguments",
+                             expr->source);
+                    Hashset<const core::type::Type*, 8> types;
+                    for (size_t i = 0; i < args.Length(); i++) {
+                        if (types.Add(args[i]->Type())) {
+                            AddNote("argument " + std::to_string(i) + " is of type '" +
+                                        sem_.TypeNameOf(args[i]->Type()) + "'",
+                                    args[i]->Declaration()->source);
+                        }
+                    }
+                    return nullptr;
                 }
+                auto* arr = Array(expr->source, expr->source, expr->source, el_ty, el_count,
+                                  /* explicit_stride */ 0);
+                if (TINT_UNLIKELY(!arr)) {
+                    return nullptr;
+                }
+                return ty_init_or_conv(arr);
             }
-            return nullptr;
+            default: {
+                TINT_ICE() << "unhandled IncompleteType builtin: " << t->builtin;
+                return nullptr;
+            }
         }
-        auto* arr = Array(expr->source, expr->source, expr->source, el_ty, el_count,
-                          /* explicit_stride */ 0);
-        if (!arr) {
-            return nullptr;
-        }
-        return ty_init_or_conv(arr);
     };
 
-    auto call = [&]() -> sem::Call* {
-        auto resolved = dependencies_.resolved_identifiers.Get(ident);
-        if (!resolved) {
-            StringStream err;
-            err << "identifier '" << ident->symbol.Name() << "' was not resolved";
-            AddICE(err.str(), ident->source);
-            return nullptr;
-        }
-
-        if (auto* ast_node = resolved->Node()) {
+    auto* call = Switch(
+        target,  //
+        [&](const sem::FunctionExpression* fn_expr) {
+            return FunctionCall(expr, const_cast<sem::Function*>(fn_expr->Function()),
+                                std::move(args), arg_behaviors);
+        },
+        [&](const sem::TypeExpression* ty_expr) {
             return Switch(
-                sem_.Get(ast_node),  //
-                [&](core::type::Type* t) -> tint::sem::Call* {
-                    // User declared types cannot be templated.
-                    if (!TINT_LIKELY(CheckNotTemplated("type", ident))) {
+                ty_expr->Type(),  //
+                [&](const IncompleteType* t) -> sem::Call* {
+                    auto* ctor = incomplete_type(t);
+                    if (TINT_UNLIKELY(!ctor)) {
                         return nullptr;
                     }
-                    return ty_init_or_conv(t);
+                    // Replace incomplete type with resolved type
+                    const_cast<sem::TypeExpression*>(ty_expr)->SetType(ctor->Type());
+                    return ctor;
                 },
-                [&](sem::Function* f) -> sem::Call* {
-                    if (!TINT_LIKELY(CheckNotTemplated("function", ident))) {
-                        return nullptr;
-                    }
-                    return FunctionCall(expr, f, args, arg_behaviors);
-                },
-                [&](sem::Expression* e) {
-                    sem_.ErrorUnexpectedExprKind(e, "call target");
-                    return nullptr;
-                },
-                [&](Default) {
-                    ErrorMismatchedResolvedIdentifier(ident->source, *resolved, "call target");
-                    return nullptr;
-                });
-        }
-
-        if (auto f = resolved->BuiltinFn(); f != wgsl::BuiltinFn::kNone) {
-            if (!TINT_LIKELY(CheckNotTemplated("builtin", ident))) {
-                return nullptr;
-            }
-            return BuiltinCall(expr, f, args);
-        }
-
-        if (auto b = resolved->BuiltinType(); b != core::BuiltinType::kUndefined) {
-            if (!ident->Is<ast::TemplatedIdentifier>()) {
-                // No template arguments provided.
-                // Check to see if this is an inferred-element-type call.
-                switch (b) {
-                    case core::BuiltinType::kArray:
-                        return inferred_array();
-                    case core::BuiltinType::kVec2:
-                        return ctor_or_conv(CtorConvIntrinsic::kVec2, nullptr);
-                    case core::BuiltinType::kVec3:
-                        return ctor_or_conv(CtorConvIntrinsic::kVec3, nullptr);
-                    case core::BuiltinType::kVec4:
-                        return ctor_or_conv(CtorConvIntrinsic::kVec4, nullptr);
-                    case core::BuiltinType::kMat2X2:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat2x2, nullptr);
-                    case core::BuiltinType::kMat2X3:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat2x3, nullptr);
-                    case core::BuiltinType::kMat2X4:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat2x4, nullptr);
-                    case core::BuiltinType::kMat3X2:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat3x2, nullptr);
-                    case core::BuiltinType::kMat3X3:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat3x3, nullptr);
-                    case core::BuiltinType::kMat3X4:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat3x4, nullptr);
-                    case core::BuiltinType::kMat4X2:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat4x2, nullptr);
-                    case core::BuiltinType::kMat4X3:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat4x3, nullptr);
-                    case core::BuiltinType::kMat4X4:
-                        return ctor_or_conv(CtorConvIntrinsic::kMat4x4, nullptr);
-                    default:
-                        break;
-                }
-            }
-            auto* ty = BuiltinType(b, ident);
-            if (TINT_UNLIKELY(!ty)) {
-                return nullptr;
-            }
-            return ty_init_or_conv(ty);
-        }
-
-        if (auto* unresolved = resolved->Unresolved()) {
-            AddError("unresolved call target '" + unresolved->name + "'", expr->source);
+                [&](Default) { return ty_init_or_conv(ty_expr->Type()); });
+        },
+        [&](const sem::BuiltinEnumExpression<wgsl::BuiltinFn>* fn_expr) {
+            return BuiltinCall(expr, fn_expr->Value(), args);
+        },
+        [&](Default) {
+            sem_.ErrorUnexpectedExprKind(target, "call target");
             return nullptr;
-        }
-
-        ErrorMismatchedResolvedIdentifier(ident->source, *resolved, "call target");
-        return nullptr;
-    }();
+        });
 
     if (!call) {
         return nullptr;
     }
 
-    if (call->Target()->IsAnyOf<sem::ValueConstructor, sem::ValueConversion>()) {
-        // The target of the call was a type.
-        // Associate the target identifier expression with the resolved type.
-        auto* ty_expr =
-            builder_->create<sem::TypeExpression>(target, current_statement_, call->Type());
-        builder_->Sem().Add(target, ty_expr);
-    }
-
     return validator_.Call(call, current_statement_) ? call : nullptr;
 }
 
@@ -2415,9 +2352,7 @@
     // De-duplicate builtins that are identical.
     auto* target = builtins_.GetOrCreate(std::make_pair(overload.Get(), fn), [&] {
         auto params = Transform(overload->parameters, [&](auto& p, size_t i) {
-            return builder_->create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type,
-                                                    core::AddressSpace::kUndefined,
-                                                    core::Access::kUndefined, p.usage);
+            return b.create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type, p.usage);
         });
         sem::PipelineStageSet supported_stages;
         auto flags = overload->info->flags;
@@ -2432,7 +2367,7 @@
         }
         auto eval_stage = overload->const_eval_fn ? core::EvaluationStage::kConstant
                                                   : core::EvaluationStage::kRuntime;
-        return builder_->create<sem::BuiltinFn>(
+        return b.create<sem::BuiltinFn>(
             fn, overload->return_type, std::move(params), eval_stage, supported_stages,
             flags.Contains(OverloadFlag::kIsDeprecated), flags.Contains(OverloadFlag::kMustUse));
     });
@@ -2477,8 +2412,8 @@
     bool has_side_effects =
         target->HasSideEffects() ||
         std::any_of(args.begin(), args.end(), [](auto* e) { return e->HasSideEffects(); });
-    auto* call = builder_->create<sem::Call>(expr, target, stage, std::move(args),
-                                             current_statement_, value, has_side_effects);
+    auto* call = b.create<sem::Call>(expr, target, stage, std::move(args), current_statement_,
+                                     value, has_side_effects);
 
     if (current_function_) {
         current_function_->AddDirectlyCalledBuiltin(target);
@@ -2517,491 +2452,210 @@
 
 core::type::Type* Resolver::BuiltinType(core::BuiltinType builtin_ty,
                                         const ast::Identifier* ident) {
-    auto& b = *builder_;
-
     auto check_no_tmpl_args = [&](core::type::Type* ty) -> core::type::Type* {
         return TINT_LIKELY(CheckNotTemplated("type", ident)) ? ty : nullptr;
     };
-    auto af = [&] { return b.create<core::type::AbstractFloat>(); };
-    auto f32 = [&] { return b.create<core::type::F32>(); };
-    auto i32 = [&] { return b.create<core::type::I32>(); };
-    auto u32 = [&] { return b.create<core::type::U32>(); };
-    auto f16 = [&] {
-        return validator_.CheckF16Enabled(ident->source) ? b.create<core::type::F16>() : nullptr;
-    };
-    auto templated_identifier =
-        [&](size_t min_args, size_t max_args = /* use min */ 0) -> const ast::TemplatedIdentifier* {
-        if (max_args == 0) {
-            max_args = min_args;
-        }
-        auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>();
-        if (!tmpl_ident) {
-            if (TINT_UNLIKELY(min_args != 0)) {
-                AddError("expected '<' for '" + ident->symbol.Name() + "'",
-                         Source{ident->source.range.end});
-            }
-            return nullptr;
-        }
-        if (min_args == max_args) {
-            if (TINT_UNLIKELY(tmpl_ident->arguments.Length() != min_args)) {
-                AddError("'" + ident->symbol.Name() + "' requires " + std::to_string(min_args) +
-                             " template arguments",
-                         ident->source);
-                return nullptr;
-            }
-        } else {
-            if (TINT_UNLIKELY(tmpl_ident->arguments.Length() < min_args)) {
-                AddError("'" + ident->symbol.Name() + "' requires at least " +
-                             std::to_string(min_args) + " template arguments",
-                         ident->source);
-                return nullptr;
-            }
-            if (TINT_UNLIKELY(tmpl_ident->arguments.Length() > max_args)) {
-                AddError("'" + ident->symbol.Name() + "' requires at most " +
-                             std::to_string(max_args) + " template arguments",
-                         ident->source);
-                return nullptr;
-            }
-        }
-        return tmpl_ident;
-    };
-    auto vec = [&](core::type::Type* el, uint32_t n) -> core::type::Vector* {
-        if (TINT_UNLIKELY(!el)) {
-            return nullptr;
-        }
-        if (TINT_UNLIKELY(!validator_.Vector(el, ident->source))) {
-            return nullptr;
-        }
-        return b.create<core::type::Vector>(el, n);
-    };
-    auto mat = [&](core::type::Type* el, uint32_t num_columns,
-                   uint32_t num_rows) -> core::type::Matrix* {
-        if (TINT_UNLIKELY(!el)) {
-            return nullptr;
-        }
-        if (TINT_UNLIKELY(!validator_.Matrix(el, ident->source))) {
-            return nullptr;
-        }
-        auto* column = vec(el, num_rows);
-        if (!column) {
-            return nullptr;
-        }
-        return b.create<core::type::Matrix>(column, num_columns);
-    };
-    auto vec_t = [&](uint32_t n) -> core::type::Vector* {
-        auto* tmpl_ident = templated_identifier(1);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-        auto* ty = Type(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!ty)) {
-            return nullptr;
-        }
-        return vec(const_cast<core::type::Type*>(ty), n);
-    };
-    auto mat_t = [&](uint32_t num_columns, uint32_t num_rows) -> core::type::Matrix* {
-        auto* tmpl_ident = templated_identifier(1);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-        auto* ty = Type(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!ty)) {
-            return nullptr;
-        }
-        return mat(const_cast<core::type::Type*>(ty), num_columns, num_rows);
-    };
-    auto array = [&]() -> core::type::Array* {
-        UniqueVector<const sem::GlobalVariable*, 4> transitively_referenced_overrides;
-        TINT_SCOPED_ASSIGNMENT(resolved_overrides_, &transitively_referenced_overrides);
-
-        auto* tmpl_ident = templated_identifier(1, 2);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-        auto* ast_el_ty = tmpl_ident->arguments[0];
-        auto* ast_count = (tmpl_ident->arguments.Length() > 1) ? tmpl_ident->arguments[1] : nullptr;
-
-        auto* el_ty = Type(ast_el_ty);
-        if (!el_ty) {
-            return nullptr;
-        }
-
-        const core::type::ArrayCount* el_count =
-            ast_count ? ArrayCount(ast_count) : builder_->create<core::type::RuntimeArrayCount>();
-        if (!el_count) {
-            return nullptr;
-        }
-
-        // Look for explicit stride via @stride(n) attribute
-        uint32_t explicit_stride = 0;
-        if (!ArrayAttributes(tmpl_ident->attributes, el_ty, explicit_stride)) {
-            return nullptr;
-        }
-
-        auto* out = Array(tmpl_ident->source,                             //
-                          ast_el_ty->source,                              //
-                          ast_count ? ast_count->source : ident->source,  //
-                          el_ty, el_count, explicit_stride);
-        if (!out) {
-            return nullptr;
-        }
-
-        if (el_ty->Is<core::type::Atomic>()) {
-            atomic_composite_info_.Add(out, &ast_el_ty->source);
-        } else {
-            if (auto found = atomic_composite_info_.Get(el_ty)) {
-                atomic_composite_info_.Add(out, *found);
-            }
-        }
-
-        // Track the pipeline-overridable constants that are transitively referenced by this
-        // array type.
-        for (auto* var : transitively_referenced_overrides) {
-            builder_->Sem().AddTransitivelyReferencedOverride(out, var);
-        }
-        return out;
-    };
-    auto atomic = [&]() -> core::type::Atomic* {
-        auto* tmpl_ident = templated_identifier(1);  // atomic<type>
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-
-        auto* ty_expr = TypeExpression(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!ty_expr)) {
-            return nullptr;
-        }
-        auto* ty = ty_expr->Type();
-
-        auto* out = builder_->create<core::type::Atomic>(ty);
-        if (!validator_.Atomic(tmpl_ident, out)) {
-            return nullptr;
-        }
-        return out;
-    };
-    auto ptr = [&]() -> core::type::Pointer* {
-        auto* tmpl_ident = templated_identifier(2, 3);  // ptr<address, type [, access]>
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-
-        auto* address_space_expr = AddressSpaceExpression(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!address_space_expr)) {
-            return nullptr;
-        }
-        auto address_space = address_space_expr->Value();
-
-        auto* store_ty_expr = TypeExpression(tmpl_ident->arguments[1]);
-        if (TINT_UNLIKELY(!store_ty_expr)) {
-            return nullptr;
-        }
-        auto* store_ty = const_cast<core::type::Type*>(store_ty_expr->Type());
-
-        auto access = DefaultAccessForAddressSpace(address_space);
-        if (tmpl_ident->arguments.Length() > 2) {
-            auto* access_expr = AccessExpression(tmpl_ident->arguments[2]);
-            if (TINT_UNLIKELY(!access_expr)) {
-                return nullptr;
-            }
-            access = access_expr->Value();
-        }
-
-        auto* out = b.create<core::type::Pointer>(address_space, store_ty, access);
-        if (!validator_.Pointer(tmpl_ident, out)) {
-            return nullptr;
-        }
-        if (!ApplyAddressSpaceUsageToType(address_space, store_ty,
-                                          store_ty_expr->Declaration()->source)) {
-            AddNote("while instantiating " + out->FriendlyName(), ident->source);
-            return nullptr;
-        }
-        return out;
-    };
-    auto sampled_texture = [&](core::type::TextureDimension dim) -> core::type::SampledTexture* {
-        auto* tmpl_ident = templated_identifier(1);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-
-        auto* ty_expr = TypeExpression(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!ty_expr)) {
-            return nullptr;
-        }
-        auto* out = b.create<core::type::SampledTexture>(dim, ty_expr->Type());
-        return validator_.SampledTexture(out, ident->source) ? out : nullptr;
-    };
-    auto multisampled_texture =
-        [&](core::type::TextureDimension dim) -> core::type::MultisampledTexture* {
-        auto* tmpl_ident = templated_identifier(1);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-
-        auto* ty_expr = TypeExpression(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!ty_expr)) {
-            return nullptr;
-        }
-        auto* out = b.create<core::type::MultisampledTexture>(dim, ty_expr->Type());
-        return validator_.MultisampledTexture(out, ident->source) ? out : nullptr;
-    };
-    auto storage_texture = [&](core::type::TextureDimension dim) -> core::type::StorageTexture* {
-        auto* tmpl_ident = templated_identifier(2);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-
-        auto* format = TexelFormatExpression(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!format)) {
-            return nullptr;
-        }
-        auto* access = AccessExpression(tmpl_ident->arguments[1]);
-        if (TINT_UNLIKELY(!access)) {
-            return nullptr;
-        }
-        auto* subtype = core::type::StorageTexture::SubtypeFor(format->Value(), builder_->Types());
-        auto* tex =
-            b.create<core::type::StorageTexture>(dim, format->Value(), access->Value(), subtype);
-        if (!validator_.StorageTexture(tex, ident->source)) {
-            return nullptr;
-        }
-        return tex;
-    };
-    auto packed_vec3_t = [&]() -> core::type::Vector* {
-        auto* tmpl_ident = templated_identifier(1);
-        if (TINT_UNLIKELY(!tmpl_ident)) {
-            return nullptr;
-        }
-        auto* el_ty = Type(tmpl_ident->arguments[0]);
-        if (TINT_UNLIKELY(!el_ty)) {
-            return nullptr;
-        }
-
-        if (TINT_UNLIKELY(!validator_.Vector(el_ty, ident->source))) {
-            return nullptr;
-        }
-        return b.create<core::type::Vector>(el_ty, 3u, true);
-    };
 
     switch (builtin_ty) {
         case core::BuiltinType::kBool:
             return check_no_tmpl_args(b.create<core::type::Bool>());
         case core::BuiltinType::kI32:
-            return check_no_tmpl_args(i32());
+            return check_no_tmpl_args(I32());
         case core::BuiltinType::kU32:
-            return check_no_tmpl_args(u32());
+            return check_no_tmpl_args(U32());
         case core::BuiltinType::kF16:
-            return check_no_tmpl_args(f16());
+            return check_no_tmpl_args(F16(ident));
         case core::BuiltinType::kF32:
             return check_no_tmpl_args(b.create<core::type::F32>());
         case core::BuiltinType::kVec2:
-            return vec_t(2);
+            return VecT(ident, builtin_ty, 2);
         case core::BuiltinType::kVec3:
-            return vec_t(3);
+            return VecT(ident, builtin_ty, 3);
         case core::BuiltinType::kVec4:
-            return vec_t(4);
+            return VecT(ident, builtin_ty, 4);
         case core::BuiltinType::kMat2X2:
-            return mat_t(2, 2);
+            return MatT(ident, builtin_ty, 2, 2);
         case core::BuiltinType::kMat2X3:
-            return mat_t(2, 3);
+            return MatT(ident, builtin_ty, 2, 3);
         case core::BuiltinType::kMat2X4:
-            return mat_t(2, 4);
+            return MatT(ident, builtin_ty, 2, 4);
         case core::BuiltinType::kMat3X2:
-            return mat_t(3, 2);
+            return MatT(ident, builtin_ty, 3, 2);
         case core::BuiltinType::kMat3X3:
-            return mat_t(3, 3);
+            return MatT(ident, builtin_ty, 3, 3);
         case core::BuiltinType::kMat3X4:
-            return mat_t(3, 4);
+            return MatT(ident, builtin_ty, 3, 4);
         case core::BuiltinType::kMat4X2:
-            return mat_t(4, 2);
+            return MatT(ident, builtin_ty, 4, 2);
         case core::BuiltinType::kMat4X3:
-            return mat_t(4, 3);
+            return MatT(ident, builtin_ty, 4, 3);
         case core::BuiltinType::kMat4X4:
-            return mat_t(4, 4);
+            return MatT(ident, builtin_ty, 4, 4);
         case core::BuiltinType::kMat2X2F:
-            return check_no_tmpl_args(mat(f32(), 2u, 2u));
+            return check_no_tmpl_args(Mat(ident, F32(), 2u, 2u));
         case core::BuiltinType::kMat2X3F:
-            return check_no_tmpl_args(mat(f32(), 2u, 3u));
+            return check_no_tmpl_args(Mat(ident, F32(), 2u, 3u));
         case core::BuiltinType::kMat2X4F:
-            return check_no_tmpl_args(mat(f32(), 2u, 4u));
+            return check_no_tmpl_args(Mat(ident, F32(), 2u, 4u));
         case core::BuiltinType::kMat3X2F:
-            return check_no_tmpl_args(mat(f32(), 3u, 2u));
+            return check_no_tmpl_args(Mat(ident, F32(), 3u, 2u));
         case core::BuiltinType::kMat3X3F:
-            return check_no_tmpl_args(mat(f32(), 3u, 3u));
+            return check_no_tmpl_args(Mat(ident, F32(), 3u, 3u));
         case core::BuiltinType::kMat3X4F:
-            return check_no_tmpl_args(mat(f32(), 3u, 4u));
+            return check_no_tmpl_args(Mat(ident, F32(), 3u, 4u));
         case core::BuiltinType::kMat4X2F:
-            return check_no_tmpl_args(mat(f32(), 4u, 2u));
+            return check_no_tmpl_args(Mat(ident, F32(), 4u, 2u));
         case core::BuiltinType::kMat4X3F:
-            return check_no_tmpl_args(mat(f32(), 4u, 3u));
+            return check_no_tmpl_args(Mat(ident, F32(), 4u, 3u));
         case core::BuiltinType::kMat4X4F:
-            return check_no_tmpl_args(mat(f32(), 4u, 4u));
+            return check_no_tmpl_args(Mat(ident, F32(), 4u, 4u));
         case core::BuiltinType::kMat2X2H:
-            return check_no_tmpl_args(mat(f16(), 2u, 2u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 2u, 2u));
         case core::BuiltinType::kMat2X3H:
-            return check_no_tmpl_args(mat(f16(), 2u, 3u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 2u, 3u));
         case core::BuiltinType::kMat2X4H:
-            return check_no_tmpl_args(mat(f16(), 2u, 4u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 2u, 4u));
         case core::BuiltinType::kMat3X2H:
-            return check_no_tmpl_args(mat(f16(), 3u, 2u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 3u, 2u));
         case core::BuiltinType::kMat3X3H:
-            return check_no_tmpl_args(mat(f16(), 3u, 3u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 3u, 3u));
         case core::BuiltinType::kMat3X4H:
-            return check_no_tmpl_args(mat(f16(), 3u, 4u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 3u, 4u));
         case core::BuiltinType::kMat4X2H:
-            return check_no_tmpl_args(mat(f16(), 4u, 2u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 4u, 2u));
         case core::BuiltinType::kMat4X3H:
-            return check_no_tmpl_args(mat(f16(), 4u, 3u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 4u, 3u));
         case core::BuiltinType::kMat4X4H:
-            return check_no_tmpl_args(mat(f16(), 4u, 4u));
+            return check_no_tmpl_args(Mat(ident, F16(ident), 4u, 4u));
         case core::BuiltinType::kVec2F:
-            return check_no_tmpl_args(vec(f32(), 2u));
+            return check_no_tmpl_args(Vec(ident, F32(), 2u));
         case core::BuiltinType::kVec3F:
-            return check_no_tmpl_args(vec(f32(), 3u));
+            return check_no_tmpl_args(Vec(ident, F32(), 3u));
         case core::BuiltinType::kVec4F:
-            return check_no_tmpl_args(vec(f32(), 4u));
+            return check_no_tmpl_args(Vec(ident, F32(), 4u));
         case core::BuiltinType::kVec2H:
-            return check_no_tmpl_args(vec(f16(), 2u));
+            return check_no_tmpl_args(Vec(ident, F16(ident), 2u));
         case core::BuiltinType::kVec3H:
-            return check_no_tmpl_args(vec(f16(), 3u));
+            return check_no_tmpl_args(Vec(ident, F16(ident), 3u));
         case core::BuiltinType::kVec4H:
-            return check_no_tmpl_args(vec(f16(), 4u));
+            return check_no_tmpl_args(Vec(ident, F16(ident), 4u));
         case core::BuiltinType::kVec2I:
-            return check_no_tmpl_args(vec(i32(), 2u));
+            return check_no_tmpl_args(Vec(ident, I32(), 2u));
         case core::BuiltinType::kVec3I:
-            return check_no_tmpl_args(vec(i32(), 3u));
+            return check_no_tmpl_args(Vec(ident, I32(), 3u));
         case core::BuiltinType::kVec4I:
-            return check_no_tmpl_args(vec(i32(), 4u));
+            return check_no_tmpl_args(Vec(ident, I32(), 4u));
         case core::BuiltinType::kVec2U:
-            return check_no_tmpl_args(vec(u32(), 2u));
+            return check_no_tmpl_args(Vec(ident, U32(), 2u));
         case core::BuiltinType::kVec3U:
-            return check_no_tmpl_args(vec(u32(), 3u));
+            return check_no_tmpl_args(Vec(ident, U32(), 3u));
         case core::BuiltinType::kVec4U:
-            return check_no_tmpl_args(vec(u32(), 4u));
+            return check_no_tmpl_args(Vec(ident, U32(), 4u));
         case core::BuiltinType::kArray:
-            return array();
+            return Array(ident);
         case core::BuiltinType::kAtomic:
-            return atomic();
+            return Atomic(ident);
         case core::BuiltinType::kPtr:
-            return ptr();
+            return Ptr(ident);
         case core::BuiltinType::kSampler:
             return check_no_tmpl_args(
-                builder_->create<core::type::Sampler>(core::type::SamplerKind::kSampler));
+                b.create<core::type::Sampler>(core::type::SamplerKind::kSampler));
         case core::BuiltinType::kSamplerComparison:
             return check_no_tmpl_args(
-                builder_->create<core::type::Sampler>(core::type::SamplerKind::kComparisonSampler));
+                b.create<core::type::Sampler>(core::type::SamplerKind::kComparisonSampler));
         case core::BuiltinType::kTexture1D:
-            return sampled_texture(core::type::TextureDimension::k1d);
+            return SampledTexture(ident, core::type::TextureDimension::k1d);
         case core::BuiltinType::kTexture2D:
-            return sampled_texture(core::type::TextureDimension::k2d);
+            return SampledTexture(ident, core::type::TextureDimension::k2d);
         case core::BuiltinType::kTexture2DArray:
-            return sampled_texture(core::type::TextureDimension::k2dArray);
+            return SampledTexture(ident, core::type::TextureDimension::k2dArray);
         case core::BuiltinType::kTexture3D:
-            return sampled_texture(core::type::TextureDimension::k3d);
+            return SampledTexture(ident, core::type::TextureDimension::k3d);
         case core::BuiltinType::kTextureCube:
-            return sampled_texture(core::type::TextureDimension::kCube);
+            return SampledTexture(ident, core::type::TextureDimension::kCube);
         case core::BuiltinType::kTextureCubeArray:
-            return sampled_texture(core::type::TextureDimension::kCubeArray);
+            return SampledTexture(ident, core::type::TextureDimension::kCubeArray);
         case core::BuiltinType::kTextureDepth2D:
             return check_no_tmpl_args(
-                builder_->create<core::type::DepthTexture>(core::type::TextureDimension::k2d));
+                b.create<core::type::DepthTexture>(core::type::TextureDimension::k2d));
         case core::BuiltinType::kTextureDepth2DArray:
             return check_no_tmpl_args(
-                builder_->create<core::type::DepthTexture>(core::type::TextureDimension::k2dArray));
+                b.create<core::type::DepthTexture>(core::type::TextureDimension::k2dArray));
         case core::BuiltinType::kTextureDepthCube:
             return check_no_tmpl_args(
-                builder_->create<core::type::DepthTexture>(core::type::TextureDimension::kCube));
+                b.create<core::type::DepthTexture>(core::type::TextureDimension::kCube));
         case core::BuiltinType::kTextureDepthCubeArray:
-            return check_no_tmpl_args(builder_->create<core::type::DepthTexture>(
-                core::type::TextureDimension::kCubeArray));
+            return check_no_tmpl_args(
+                b.create<core::type::DepthTexture>(core::type::TextureDimension::kCubeArray));
         case core::BuiltinType::kTextureDepthMultisampled2D:
-            return check_no_tmpl_args(builder_->create<core::type::DepthMultisampledTexture>(
-                core::type::TextureDimension::k2d));
+            return check_no_tmpl_args(
+                b.create<core::type::DepthMultisampledTexture>(core::type::TextureDimension::k2d));
         case core::BuiltinType::kTextureExternal:
-            return check_no_tmpl_args(builder_->create<core::type::ExternalTexture>());
+            return check_no_tmpl_args(b.create<core::type::ExternalTexture>());
         case core::BuiltinType::kTextureMultisampled2D:
-            return multisampled_texture(core::type::TextureDimension::k2d);
+            return MultisampledTexture(ident, core::type::TextureDimension::k2d);
         case core::BuiltinType::kTextureStorage1D:
-            return storage_texture(core::type::TextureDimension::k1d);
+            return StorageTexture(ident, core::type::TextureDimension::k1d);
         case core::BuiltinType::kTextureStorage2D:
-            return storage_texture(core::type::TextureDimension::k2d);
+            return StorageTexture(ident, core::type::TextureDimension::k2d);
         case core::BuiltinType::kTextureStorage2DArray:
-            return storage_texture(core::type::TextureDimension::k2dArray);
+            return StorageTexture(ident, core::type::TextureDimension::k2dArray);
         case core::BuiltinType::kTextureStorage3D:
-            return storage_texture(core::type::TextureDimension::k3d);
+            return StorageTexture(ident, core::type::TextureDimension::k3d);
         case core::BuiltinType::kPackedVec3:
-            return packed_vec3_t();
+            return PackedVec3T(ident);
         case core::BuiltinType::kAtomicCompareExchangeResultI32:
-            return core::type::CreateAtomicCompareExchangeResult(builder_->Types(),
-                                                                 builder_->Symbols(), i32());
+            return core::type::CreateAtomicCompareExchangeResult(b.Types(), b.Symbols(), I32());
         case core::BuiltinType::kAtomicCompareExchangeResultU32:
-            return core::type::CreateAtomicCompareExchangeResult(builder_->Types(),
-                                                                 builder_->Symbols(), u32());
+            return core::type::CreateAtomicCompareExchangeResult(b.Types(), b.Symbols(), U32());
         case core::BuiltinType::kFrexpResultAbstract:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(), af());
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), AF());
         case core::BuiltinType::kFrexpResultF16:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(), f16());
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), F16(ident));
         case core::BuiltinType::kFrexpResultF32:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(), f32());
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), F32());
         case core::BuiltinType::kFrexpResultVec2Abstract:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(af(), 2));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, AF(), 2));
         case core::BuiltinType::kFrexpResultVec2F16:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(f16(), 2));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, F16(ident), 2));
         case core::BuiltinType::kFrexpResultVec2F32:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(f32(), 2));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, F32(), 2));
         case core::BuiltinType::kFrexpResultVec3Abstract:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(af(), 3));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, AF(), 3));
         case core::BuiltinType::kFrexpResultVec3F16:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(f16(), 3));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, F16(ident), 3));
         case core::BuiltinType::kFrexpResultVec3F32:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(f32(), 3));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, F32(), 3));
         case core::BuiltinType::kFrexpResultVec4Abstract:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(af(), 4));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, AF(), 4));
         case core::BuiltinType::kFrexpResultVec4F16:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(f16(), 4));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, F16(ident), 4));
         case core::BuiltinType::kFrexpResultVec4F32:
-            return core::type::CreateFrexpResult(builder_->Types(), builder_->Symbols(),
-                                                 vec(f32(), 4));
+            return core::type::CreateFrexpResult(b.Types(), b.Symbols(), Vec(ident, F32(), 4));
         case core::BuiltinType::kModfResultAbstract:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(), af());
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), AF());
         case core::BuiltinType::kModfResultF16:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(), f16());
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), F16(ident));
         case core::BuiltinType::kModfResultF32:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(), f32());
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), F32());
         case core::BuiltinType::kModfResultVec2Abstract:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(af(), 2));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, AF(), 2));
         case core::BuiltinType::kModfResultVec2F16:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(f16(), 2));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, F16(ident), 2));
         case core::BuiltinType::kModfResultVec2F32:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(f32(), 2));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, F32(), 2));
         case core::BuiltinType::kModfResultVec3Abstract:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(af(), 3));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, AF(), 3));
         case core::BuiltinType::kModfResultVec3F16:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(f16(), 3));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, F16(ident), 3));
         case core::BuiltinType::kModfResultVec3F32:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(f32(), 3));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, F32(), 3));
         case core::BuiltinType::kModfResultVec4Abstract:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(af(), 4));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, AF(), 4));
         case core::BuiltinType::kModfResultVec4F16:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(f16(), 4));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, F16(ident), 4));
         case core::BuiltinType::kModfResultVec4F32:
-            return core::type::CreateModfResult(builder_->Types(), builder_->Symbols(),
-                                                vec(f32(), 4));
+            return core::type::CreateModfResult(b.Types(), b.Symbols(), Vec(ident, F32(), 4));
         case core::BuiltinType::kUndefined:
             break;
     }
@@ -3013,6 +2667,320 @@
     return nullptr;
 }
 
+core::type::AbstractFloat* Resolver::AF() {
+    return b.create<core::type::AbstractFloat>();
+}
+
+core::type::F32* Resolver::F32() {
+    return b.create<core::type::F32>();
+}
+
+core::type::I32* Resolver::I32() {
+    return b.create<core::type::I32>();
+}
+
+core::type::U32* Resolver::U32() {
+    return b.create<core::type::U32>();
+}
+
+core::type::F16* Resolver::F16(const ast::Identifier* ident) {
+    return validator_.CheckF16Enabled(ident->source) ? b.create<core::type::F16>() : nullptr;
+}
+
+core::type::Vector* Resolver::Vec(const ast::Identifier* ident, core::type::Type* el, uint32_t n) {
+    if (TINT_UNLIKELY(!el)) {
+        return nullptr;
+    }
+    if (TINT_UNLIKELY(!validator_.Vector(el, ident->source))) {
+        return nullptr;
+    }
+    return b.create<core::type::Vector>(el, n);
+}
+
+core::type::Type* Resolver::VecT(const ast::Identifier* ident,
+                                 core::BuiltinType builtin,
+                                 uint32_t n) {
+    auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>();
+    if (!tmpl_ident) {
+        // 'vecN' has no template arguments, so return an incomplete type.
+        return b.create<IncompleteType>(builtin);
+    }
+
+    if (TINT_UNLIKELY(!CheckTemplatedIdentifierArgs(tmpl_ident, 1))) {
+        return nullptr;
+    }
+
+    auto* ty = sem_.GetType(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(!ty)) {
+        return nullptr;
+    }
+
+    return Vec(ident, const_cast<core::type::Type*>(ty), n);
+}
+
+core::type::Matrix* Resolver::Mat(const ast::Identifier* ident,
+                                  core::type::Type* el,
+                                  uint32_t num_columns,
+                                  uint32_t num_rows) {
+    if (TINT_UNLIKELY(!el)) {
+        return nullptr;
+    }
+    if (TINT_UNLIKELY(!validator_.Matrix(el, ident->source))) {
+        return nullptr;
+    }
+    auto* column = Vec(ident, el, num_rows);
+    if (!column) {
+        return nullptr;
+    }
+    return b.create<core::type::Matrix>(column, num_columns);
+}
+
+core::type::Type* Resolver::MatT(const ast::Identifier* ident,
+                                 core::BuiltinType builtin,
+                                 uint32_t num_columns,
+                                 uint32_t num_rows) {
+    auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>();
+    if (!tmpl_ident) {
+        // 'vecN' has no template arguments, so return an incomplete type.
+        return b.create<IncompleteType>(builtin);
+    }
+
+    if (TINT_UNLIKELY(!CheckTemplatedIdentifierArgs(tmpl_ident, 1))) {
+        return nullptr;
+    }
+
+    auto* el_ty = sem_.GetType(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(!el_ty)) {
+        return nullptr;
+    }
+
+    return Mat(ident, const_cast<core::type::Type*>(el_ty), num_columns, num_rows);
+}
+
+core::type::Type* Resolver::Array(const ast::Identifier* ident) {
+    auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>();
+    if (!tmpl_ident) {
+        // 'array' has no template arguments, so return an incomplete type.
+        return b.create<IncompleteType>(core::BuiltinType::kArray);
+    }
+
+    if (TINT_UNLIKELY(!CheckTemplatedIdentifierArgs(tmpl_ident, 1, 2))) {
+        return nullptr;
+    }
+    auto* ast_el_ty = tmpl_ident->arguments[0];
+    auto* ast_count = (tmpl_ident->arguments.Length() > 1) ? tmpl_ident->arguments[1] : nullptr;
+
+    auto* el_ty = sem_.GetType(ast_el_ty);
+    if (!el_ty) {
+        return nullptr;
+    }
+
+    const core::type::ArrayCount* el_count =
+        ast_count ? ArrayCount(ast_count) : b.create<core::type::RuntimeArrayCount>();
+    if (!el_count) {
+        return nullptr;
+    }
+
+    // Look for explicit stride via @stride(n) attribute
+    uint32_t explicit_stride = 0;
+    if (!ArrayAttributes(tmpl_ident->attributes, el_ty, explicit_stride)) {
+        return nullptr;
+    }
+
+    auto* out = Array(tmpl_ident->source,                             //
+                      ast_el_ty->source,                              //
+                      ast_count ? ast_count->source : ident->source,  //
+                      el_ty, el_count, explicit_stride);
+    if (!out) {
+        return nullptr;
+    }
+
+    if (el_ty->Is<core::type::Atomic>()) {
+        atomic_composite_info_.Add(out, &ast_el_ty->source);
+    } else {
+        if (auto found = atomic_composite_info_.Get(el_ty)) {
+            atomic_composite_info_.Add(out, *found);
+        }
+    }
+
+    return out;
+}
+
+core::type::Atomic* Resolver::Atomic(const ast::Identifier* ident) {
+    auto* tmpl_ident = TemplatedIdentifier(ident, 1);  // atomic<type>
+    if (TINT_UNLIKELY(!tmpl_ident)) {
+        return nullptr;
+    }
+
+    auto* el_ty = sem_.GetType(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(!el_ty)) {
+        return nullptr;
+    }
+
+    auto* out = b.create<core::type::Atomic>(el_ty);
+    if (TINT_UNLIKELY(!validator_.Atomic(tmpl_ident, out))) {
+        return nullptr;
+    }
+    return out;
+}
+
+core::type::Pointer* Resolver::Ptr(const ast::Identifier* ident) {
+    auto* tmpl_ident = TemplatedIdentifier(ident, 2, 3);  // ptr<address, type [, access]>
+    if (TINT_UNLIKELY(!tmpl_ident)) {
+        return nullptr;
+    }
+
+    auto address_space = sem_.GetAddressSpace(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(address_space == core::AddressSpace::kUndefined)) {
+        return nullptr;
+    }
+
+    auto* store_ty = const_cast<core::type::Type*>(sem_.GetType(tmpl_ident->arguments[1]));
+    if (TINT_UNLIKELY(!store_ty)) {
+        return nullptr;
+    }
+
+    core::Access access = core::Access::kUndefined;
+    if (tmpl_ident->arguments.Length() > 2) {
+        access = sem_.GetAccess(tmpl_ident->arguments[2]);
+        if (TINT_UNLIKELY(access == core::Access::kUndefined)) {
+            return nullptr;
+        }
+    } else {
+        access = DefaultAccessForAddressSpace(address_space);
+    }
+
+    auto* out = b.create<core::type::Pointer>(address_space, store_ty, access);
+    if (TINT_UNLIKELY(!validator_.Pointer(tmpl_ident, out))) {
+        return nullptr;
+    }
+
+    if (!ApplyAddressSpaceUsageToType(address_space, store_ty, tmpl_ident->arguments[1]->source)) {
+        AddNote("while instantiating " + out->FriendlyName(), ident->source);
+        return nullptr;
+    }
+    return out;
+}
+
+core::type::SampledTexture* Resolver::SampledTexture(const ast::Identifier* ident,
+                                                     core::type::TextureDimension dim) {
+    auto* tmpl_ident = TemplatedIdentifier(ident, 1);
+    if (TINT_UNLIKELY(!tmpl_ident)) {
+        return nullptr;
+    }
+
+    auto* ty_expr = sem_.GetType(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(!ty_expr)) {
+        return nullptr;
+    }
+
+    auto* out = b.create<core::type::SampledTexture>(dim, ty_expr);
+    return validator_.SampledTexture(out, ident->source) ? out : nullptr;
+}
+
+core::type::MultisampledTexture* Resolver::MultisampledTexture(const ast::Identifier* ident,
+                                                               core::type::TextureDimension dim) {
+    auto* tmpl_ident = TemplatedIdentifier(ident, 1);
+    if (TINT_UNLIKELY(!tmpl_ident)) {
+        return nullptr;
+    }
+
+    auto* ty_expr = sem_.GetType(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(!ty_expr)) {
+        return nullptr;
+    }
+
+    auto* out = b.create<core::type::MultisampledTexture>(dim, ty_expr);
+    return validator_.MultisampledTexture(out, ident->source) ? out : nullptr;
+}
+
+core::type::StorageTexture* Resolver::StorageTexture(const ast::Identifier* ident,
+                                                     core::type::TextureDimension dim) {
+    auto* tmpl_ident = TemplatedIdentifier(ident, 2);
+    if (TINT_UNLIKELY(!tmpl_ident)) {
+        return nullptr;
+    }
+
+    auto format = sem_.GetTexelFormat(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(format == core::TexelFormat::kUndefined)) {
+        return nullptr;
+    }
+
+    auto access = sem_.GetAccess(tmpl_ident->arguments[1]);
+    if (TINT_UNLIKELY(access == core::Access::kUndefined)) {
+        return nullptr;
+    }
+
+    auto* subtype = core::type::StorageTexture::SubtypeFor(format, b.Types());
+    auto* tex = b.create<core::type::StorageTexture>(dim, format, access, subtype);
+    if (!validator_.StorageTexture(tex, ident->source)) {
+        return nullptr;
+    }
+
+    return tex;
+}
+
+core::type::Vector* Resolver::PackedVec3T(const ast::Identifier* ident) {
+    auto* tmpl_ident = TemplatedIdentifier(ident, 1);
+    if (TINT_UNLIKELY(!tmpl_ident)) {
+        return nullptr;
+    }
+
+    auto* el_ty = sem_.GetType(tmpl_ident->arguments[0]);
+    if (TINT_UNLIKELY(!el_ty)) {
+        return nullptr;
+    }
+
+    if (TINT_UNLIKELY(!validator_.Vector(el_ty, ident->source))) {
+        return nullptr;
+    }
+    return b.create<core::type::Vector>(el_ty, 3u, true);
+}
+
+const ast::TemplatedIdentifier* Resolver::TemplatedIdentifier(const ast::Identifier* ident,
+                                                              size_t min_args,
+                                                              size_t max_args /* = use min 0 */) {
+    auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>();
+    if (!tmpl_ident) {
+        if (TINT_UNLIKELY(min_args != 0)) {
+            AddError("expected '<' for '" + ident->symbol.Name() + "'",
+                     Source{ident->source.range.end});
+        }
+        return nullptr;
+    }
+    return CheckTemplatedIdentifierArgs(tmpl_ident, min_args, max_args) ? tmpl_ident : nullptr;
+}
+
+bool Resolver::CheckTemplatedIdentifierArgs(const ast::TemplatedIdentifier* ident,
+                                            size_t min_args,
+                                            size_t max_args /* = use min 0 */) {
+    if (max_args == 0) {
+        max_args = min_args;
+    }
+    if (min_args == max_args) {
+        if (TINT_UNLIKELY(ident->arguments.Length() != min_args)) {
+            AddError("'" + ident->symbol.Name() + "' requires " + std::to_string(min_args) +
+                         " template arguments",
+                     ident->source);
+            return false;
+        }
+    } else {
+        if (TINT_UNLIKELY(ident->arguments.Length() < min_args)) {
+            AddError("'" + ident->symbol.Name() + "' requires at least " +
+                         std::to_string(min_args) + " template arguments",
+                     ident->source);
+            return false;
+        }
+        if (TINT_UNLIKELY(ident->arguments.Length() > max_args)) {
+            AddError("'" + ident->symbol.Name() + "' requires at most " + std::to_string(max_args) +
+                         " template arguments",
+                     ident->source);
+            return false;
+        }
+    }
+    return ident;
+}
+
 size_t Resolver::NestDepth(const core::type::Type* ty) const {
     return Switch(
         ty,  //
@@ -3053,21 +3021,20 @@
     }
 }
 
-template <size_t N>
 sem::Call* Resolver::FunctionCall(const ast::CallExpression* expr,
                                   sem::Function* target,
-                                  Vector<const sem::ValueExpression*, N>& args,
+                                  VectorRef<const sem::ValueExpression*> args_in,
                                   sem::Behaviors arg_behaviors) {
+    Vector<const sem::ValueExpression*, 8> args = std::move(args_in);
     if (!MaybeMaterializeAndLoadArguments(args, target)) {
         return nullptr;
     }
 
-    // TODO(crbug.com/tint/1420): For now, assume all function calls have side
-    // effects.
+    // TODO(crbug.com/tint/1420): For now, assume all function calls have side effects.
     bool has_side_effects = true;
-    auto* call = builder_->create<sem::Call>(expr, target, core::EvaluationStage::kRuntime,
-                                             std::move(args), current_statement_,
-                                             /* constant_value */ nullptr, has_side_effects);
+    auto* call = b.create<sem::Call>(expr, target, core::EvaluationStage::kRuntime, std::move(args),
+                                     current_statement_,
+                                     /* constant_value */ nullptr, has_side_effects);
 
     target->AddCallSite(call);
 
@@ -3100,11 +3067,6 @@
         CollectTextureSamplerPairs(target, call->Arguments());
     }
 
-    // Associate the target identifier expression with the resolved function.
-    auto* fn_expr =
-        builder_->create<sem::FunctionExpression>(expr->target, current_statement_, target);
-    builder_->Sem().Add(expr->target, fn_expr);
-
     return call;
 }
 
@@ -3116,19 +3078,48 @@
     // argument passed to the current function. Leave global variables
     // as-is. Then add the mapped pair to the current function's list of
     // texture/sampler pairs.
+
+    Hashset<const sem::Variable*, 4> texture_sampler_set;
+
     for (sem::VariablePair pair : func->TextureSamplerPairs()) {
         const sem::Variable* texture = pair.first;
         const sem::Variable* sampler = pair.second;
-        if (auto* param = texture->As<sem::Parameter>()) {
+        if (auto* param = As<sem::Parameter>(texture)) {
             texture = args[param->Index()]->UnwrapLoad()->As<sem::VariableUser>()->Variable();
+            texture_sampler_set.Add(texture);
         }
-        if (sampler) {
-            if (auto* param = sampler->As<sem::Parameter>()) {
-                sampler = args[param->Index()]->UnwrapLoad()->As<sem::VariableUser>()->Variable();
-            }
+        if (auto* param = As<sem::Parameter>(sampler)) {
+            sampler = args[param->Index()]->UnwrapLoad()->As<sem::VariableUser>()->Variable();
+            texture_sampler_set.Add(sampler);
         }
         current_function_->AddTextureSamplerPair(texture, sampler);
     }
+
+    // Add any possible texture/sampler not essentially passed to builtins from the function param.
+    // This could be unused texture/sampler or texture/sampler passed to builtins that are emulated.
+
+    const auto& signature = func->Signature();
+
+    for (size_t i = 0; i < signature.parameters.Length(); i++) {
+        auto* param = signature.parameters[i];
+        if (param->Type()->Is<core::type::Texture>()) {
+            auto* user = args[i]->UnwrapLoad()->As<sem::VariableUser>();
+            auto* texture = user->Variable();
+            if (!texture_sampler_set.Contains(texture)) {
+                current_function_->AddTextureSamplerPair(texture, nullptr);
+                func->AddTextureSamplerPair(texture, nullptr);
+                texture_sampler_set.Add(texture);
+            }
+        } else if (param->Type()->Is<core::type::Sampler>()) {
+            auto* user = args[i]->UnwrapLoad()->As<sem::VariableUser>();
+            auto* sampler = user->Variable();
+            if (!texture_sampler_set.Contains(sampler)) {
+                current_function_->AddTextureSamplerPair(nullptr, sampler);
+                func->AddTextureSamplerPair(nullptr, sampler);
+                texture_sampler_set.Add(sampler);
+            }
+        }
+    }
 }
 
 sem::ValueExpression* Resolver::Literal(const ast::LiteralExpression* literal) {
@@ -3137,11 +3128,11 @@
         [&](const ast::IntLiteralExpression* i) -> core::type::Type* {
             switch (i->suffix) {
                 case ast::IntLiteralExpression::Suffix::kNone:
-                    return builder_->create<core::type::AbstractInt>();
+                    return b.create<core::type::AbstractInt>();
                 case ast::IntLiteralExpression::Suffix::kI:
-                    return builder_->create<core::type::I32>();
+                    return b.create<core::type::I32>();
                 case ast::IntLiteralExpression::Suffix::kU:
-                    return builder_->create<core::type::U32>();
+                    return b.create<core::type::U32>();
             }
             TINT_UNREACHABLE() << "Unhandled integer literal suffix: " << i->suffix;
             return nullptr;
@@ -3149,18 +3140,17 @@
         [&](const ast::FloatLiteralExpression* f) -> core::type::Type* {
             switch (f->suffix) {
                 case ast::FloatLiteralExpression::Suffix::kNone:
-                    return builder_->create<core::type::AbstractFloat>();
+                    return b.create<core::type::AbstractFloat>();
                 case ast::FloatLiteralExpression::Suffix::kF:
-                    return builder_->create<core::type::F32>();
+                    return b.create<core::type::F32>();
                 case ast::FloatLiteralExpression::Suffix::kH:
-                    return validator_.CheckF16Enabled(literal->source)
-                               ? builder_->create<core::type::F16>()
-                               : nullptr;
+                    return validator_.CheckF16Enabled(literal->source) ? b.create<core::type::F16>()
+                                                                       : nullptr;
             }
             TINT_UNREACHABLE() << "Unhandled float literal suffix: " << f->suffix;
             return nullptr;
         },
-        [&](const ast::BoolLiteralExpression*) { return builder_->create<core::type::Bool>(); },
+        [&](const ast::BoolLiteralExpression*) { return b.create<core::type::Bool>(); },
         [&](Default) {
             TINT_UNREACHABLE() << "Unhandled literal type: " << literal->TypeInfo().name;
             return nullptr;
@@ -3178,35 +3168,32 @@
     if (stage == core::EvaluationStage::kConstant) {
         val = Switch(
             literal,
-            [&](const ast::BoolLiteralExpression* lit) {
-                return builder_->constants.Get(lit->value);
-            },
+            [&](const ast::BoolLiteralExpression* lit) { return b.constants.Get(lit->value); },
             [&](const ast::IntLiteralExpression* lit) -> const core::constant::Value* {
                 switch (lit->suffix) {
                     case ast::IntLiteralExpression::Suffix::kNone:
-                        return builder_->constants.Get(AInt(lit->value));
+                        return b.constants.Get(AInt(lit->value));
                     case ast::IntLiteralExpression::Suffix::kI:
-                        return builder_->constants.Get(i32(lit->value));
+                        return b.constants.Get(i32(lit->value));
                     case ast::IntLiteralExpression::Suffix::kU:
-                        return builder_->constants.Get(u32(lit->value));
+                        return b.constants.Get(u32(lit->value));
                 }
                 return nullptr;
             },
             [&](const ast::FloatLiteralExpression* lit) -> const core::constant::Value* {
                 switch (lit->suffix) {
                     case ast::FloatLiteralExpression::Suffix::kNone:
-                        return builder_->constants.Get(AFloat(lit->value));
+                        return b.constants.Get(AFloat(lit->value));
                     case ast::FloatLiteralExpression::Suffix::kF:
-                        return builder_->constants.Get(f32(lit->value));
+                        return b.constants.Get(f32(lit->value));
                     case ast::FloatLiteralExpression::Suffix::kH:
-                        return builder_->constants.Get(f16(lit->value));
+                        return b.constants.Get(f16(lit->value));
                 }
                 return nullptr;
             });
     }
-    return builder_->create<sem::ValueExpression>(literal, ty, stage, current_statement_,
-                                                  std::move(val),
-                                                  /* has_side_effects */ false);
+    return b.create<sem::ValueExpression>(literal, ty, stage, current_statement_, std::move(val),
+                                          /* has_side_effects */ false);
 }
 
 sem::Expression* Resolver::Identifier(const ast::IdentifierExpression* expr) {
@@ -3238,8 +3225,8 @@
                     stage = core::EvaluationStage::kNotEvaluated;
                     value = nullptr;
                 }
-                auto* user = builder_->create<sem::VariableUser>(expr, stage, current_statement_,
-                                                                 value, variable);
+                auto* user =
+                    b.create<sem::VariableUser>(expr, stage, current_statement_, value, variable);
 
                 if (current_statement_) {
                     // If identifier is part of a loop continuing block, make sure it
@@ -3271,38 +3258,17 @@
                     }
                 }
 
-                auto* global = variable->As<sem::GlobalVariable>();
-                if (current_function_) {
-                    if (global) {
-                        current_function_->AddDirectlyReferencedGlobal(global);
-                        auto* refs = builder_->Sem().TransitivelyReferencedOverrides(global);
-                        if (refs) {
-                            for (auto* var : *refs) {
-                                current_function_->AddTransitivelyReferencedGlobal(var);
-                            }
-                        }
+                if (auto* global = variable->As<sem::GlobalVariable>()) {
+                    for (auto& fn : on_transitively_reference_global_) {
+                        fn(global);
                     }
-                } else if (variable->Declaration()->Is<ast::Override>()) {
-                    if (resolved_overrides_) {
-                        // Track the reference to this pipeline-overridable constant and any other
-                        // pipeline-overridable constants that it references.
-                        resolved_overrides_->Add(global);
-                        auto* refs = builder_->Sem().TransitivelyReferencedOverrides(global);
-                        if (refs) {
-                            for (auto* var : *refs) {
-                                resolved_overrides_->Add(var);
-                            }
-                        }
+                    if (!current_function_ && variable->Declaration()->Is<ast::Var>()) {
+                        // Use of a module-scope 'var' outside of a function.
+                        std::string desc = "var '" + ident->symbol.Name() + "' ";
+                        AddError(desc + "cannot be referenced at module-scope", expr->source);
+                        AddNote(desc + "declared here", variable->Declaration()->source);
+                        return nullptr;
                     }
-                } else if (variable->Declaration()->Is<ast::Var>()) {
-                    // Use of a module-scope 'var' outside of a function.
-                    // Note: The spec is currently vague around the rules here. See
-                    // https://github.com/gpuweb/gpuweb/issues/3081. Remove this comment when
-                    // resolved.
-                    std::string desc = "var '" + ident->symbol.Name() + "' ";
-                    AddError(desc + "cannot be referenced at module-scope", expr->source);
-                    AddNote(desc + "declared here", variable->Declaration()->source);
-                    return nullptr;
                 }
 
                 variable->AddUser(user);
@@ -3313,13 +3279,23 @@
                 if (!TINT_LIKELY(CheckNotTemplated("type", ident))) {
                     return nullptr;
                 }
-                return builder_->create<sem::TypeExpression>(expr, current_statement_, ty);
+
+                // Notify callers of all transitively referenced globals.
+                if (auto* arr = ty->As<sem::Array>()) {
+                    for (auto& fn : on_transitively_reference_global_) {
+                        for (auto* ref : arr->TransitivelyReferencedOverrides()) {
+                            fn(ref);
+                        }
+                    }
+                }
+
+                return b.create<sem::TypeExpression>(expr, current_statement_, ty);
             },
             [&](const sem::Function* fn) -> sem::FunctionExpression* {
                 if (!TINT_LIKELY(CheckNotTemplated("function", ident))) {
                     return nullptr;
                 }
-                return builder_->create<sem::FunctionExpression>(expr, current_statement_, fn);
+                return b.create<sem::FunctionExpression>(expr, current_statement_, fn);
             });
     }
 
@@ -3328,31 +3304,33 @@
         if (!ty) {
             return nullptr;
         }
-        return builder_->create<sem::TypeExpression>(expr, current_statement_, ty);
+        return b.create<sem::TypeExpression>(expr, current_statement_, ty);
     }
 
-    if (resolved->BuiltinFn() != wgsl::BuiltinFn::kNone) {
-        AddError("missing '(' for builtin function call", expr->source.End());
-        return nullptr;
+    if (auto fn = resolved->BuiltinFn(); fn != wgsl::BuiltinFn::kNone) {
+        return CheckNotTemplated("builtin function", ident)
+                   ? b.create<sem::BuiltinEnumExpression<wgsl::BuiltinFn>>(expr, current_statement_,
+                                                                           fn)
+                   : nullptr;
     }
 
     if (auto access = resolved->Access(); access != core::Access::kUndefined) {
         return CheckNotTemplated("access", ident)
-                   ? builder_->create<sem::BuiltinEnumExpression<core::Access>>(
-                         expr, current_statement_, access)
+                   ? b.create<sem::BuiltinEnumExpression<core::Access>>(expr, current_statement_,
+                                                                        access)
                    : nullptr;
     }
 
     if (auto addr = resolved->AddressSpace(); addr != core::AddressSpace::kUndefined) {
         return CheckNotTemplated("address space", ident)
-                   ? builder_->create<sem::BuiltinEnumExpression<core::AddressSpace>>(
+                   ? b.create<sem::BuiltinEnumExpression<core::AddressSpace>>(
                          expr, current_statement_, addr)
                    : nullptr;
     }
 
     if (auto builtin = resolved->BuiltinValue(); builtin != core::BuiltinValue::kUndefined) {
         return CheckNotTemplated("builtin value", ident)
-                   ? builder_->create<sem::BuiltinEnumExpression<core::BuiltinValue>>(
+                   ? b.create<sem::BuiltinEnumExpression<core::BuiltinValue>>(
                          expr, current_statement_, builtin)
                    : nullptr;
     }
@@ -3360,7 +3338,7 @@
     if (auto i_smpl = resolved->InterpolationSampling();
         i_smpl != core::InterpolationSampling::kUndefined) {
         return CheckNotTemplated("interpolation sampling", ident)
-                   ? builder_->create<sem::BuiltinEnumExpression<core::InterpolationSampling>>(
+                   ? b.create<sem::BuiltinEnumExpression<core::InterpolationSampling>>(
                          expr, current_statement_, i_smpl)
                    : nullptr;
     }
@@ -3368,40 +3346,20 @@
     if (auto i_type = resolved->InterpolationType();
         i_type != core::InterpolationType::kUndefined) {
         return CheckNotTemplated("interpolation type", ident)
-                   ? builder_->create<sem::BuiltinEnumExpression<core::InterpolationType>>(
+                   ? b.create<sem::BuiltinEnumExpression<core::InterpolationType>>(
                          expr, current_statement_, i_type)
                    : nullptr;
     }
 
     if (auto fmt = resolved->TexelFormat(); fmt != core::TexelFormat::kUndefined) {
         return CheckNotTemplated("texel format", ident)
-                   ? builder_->create<sem::BuiltinEnumExpression<core::TexelFormat>>(
+                   ? b.create<sem::BuiltinEnumExpression<core::TexelFormat>>(
                          expr, current_statement_, fmt)
                    : nullptr;
     }
 
-    if (auto* unresolved = resolved->Unresolved()) {
-        if (identifier_resolve_hint_.expression == expr) {
-            AddError("unresolved " + std::string(identifier_resolve_hint_.usage) + " '" +
-                         unresolved->name + "'",
-                     expr->source);
-            if (!identifier_resolve_hint_.suggestions.IsEmpty()) {
-                // Filter out suggestions that have a leading underscore.
-                Vector<const char*, 8> filtered;
-                for (auto* str : identifier_resolve_hint_.suggestions) {
-                    if (str[0] != '_') {
-                        filtered.Push(str);
-                    }
-                }
-                StringStream msg;
-                tint::SuggestAlternatives(unresolved->name,
-                                          filtered.Slice().Reinterpret<char const* const>(), msg);
-                AddNote(msg.str(), expr->source);
-            }
-        } else {
-            AddError("unresolved identifier '" + unresolved->name + "'", expr->source);
-        }
-        return nullptr;
+    if (resolved->Unresolved()) {
+        return b.create<UnresolvedIdentifier>(expr, current_statement_);
     }
 
     TINT_UNREACHABLE() << "unhandled resolved identifier: " << resolved->String();
@@ -3448,16 +3406,15 @@
 
             // If we're extracting from a reference, we return a reference.
             if (auto* ref = object_ty->As<core::type::Reference>()) {
-                ty =
-                    builder_->create<core::type::Reference>(ref->AddressSpace(), ty, ref->Access());
+                ty = b.create<core::type::Reference>(ref->AddressSpace(), ty, ref->Access());
             }
 
             const core::constant::Value* val = nullptr;
             if (auto* obj_val = object->ConstantValue()) {
                 val = obj_val->Index(static_cast<size_t>(member->Index()));
             }
-            return builder_->create<sem::StructMemberAccess>(
-                expr, ty, current_statement_, val, object, member, has_side_effects, root_ident);
+            return b.create<sem::StructMemberAccess>(expr, ty, current_statement_, val, object,
+                                                     member, has_side_effects, root_ident);
         },
 
         [&](const core::type::Vector* vec) -> sem::ValueExpression* {
@@ -3517,13 +3474,12 @@
                 ty = vec->type();
                 // If we're extracting from a reference, we return a reference.
                 if (auto* ref = object_ty->As<core::type::Reference>()) {
-                    ty = builder_->create<core::type::Reference>(ref->AddressSpace(), ty,
-                                                                 ref->Access());
+                    ty = b.create<core::type::Reference>(ref->AddressSpace(), ty, ref->Access());
                 }
             } else {
                 // The vector will have a number of components equal to the length of
                 // the swizzle.
-                ty = builder_->create<core::type::Vector>(vec->type(), static_cast<uint32_t>(size));
+                ty = b.create<core::type::Vector>(vec->type(), static_cast<uint32_t>(size));
 
                 // The load rule is invoked before the swizzle, if necessary.
                 obj_expr = Load(object);
@@ -3536,8 +3492,8 @@
                 }
                 val = res.Get();
             }
-            return builder_->create<sem::Swizzle>(expr, ty, current_statement_, val, obj_expr,
-                                                  std::move(swizzle), has_side_effects, root_ident);
+            return b.create<sem::Swizzle>(expr, ty, current_statement_, val, obj_expr,
+                                          std::move(swizzle), has_side_effects, root_ident);
         },
 
         [&](Default) {
@@ -3627,8 +3583,8 @@
     }
 
     bool has_side_effects = lhs->HasSideEffects() || rhs->HasSideEffects();
-    auto* sem = builder_->create<sem::ValueExpression>(expr, res_ty, stage, current_statement_,
-                                                       value, has_side_effects);
+    auto* sem = b.create<sem::ValueExpression>(expr, res_ty, stage, current_statement_, value,
+                                               has_side_effects);
     sem->Behaviors() = lhs->Behaviors() + rhs->Behaviors();
 
     return sem;
@@ -3664,8 +3620,8 @@
                     return nullptr;
                 }
 
-                ty = builder_->create<core::type::Pointer>(ref->AddressSpace(), ref->StoreType(),
-                                                           ref->Access());
+                ty = b.create<core::type::Pointer>(ref->AddressSpace(), ref->StoreType(),
+                                                   ref->Access());
 
                 root_ident = expr->RootIdentifier();
             } else {
@@ -3676,8 +3632,8 @@
 
         case core::UnaryOp::kIndirection:
             if (auto* ptr = expr_ty->As<core::type::Pointer>()) {
-                ty = builder_->create<core::type::Reference>(ptr->AddressSpace(), ptr->StoreType(),
-                                                             ptr->Access());
+                ty = b.create<core::type::Reference>(ptr->AddressSpace(), ptr->StoreType(),
+                                                     ptr->Access());
                 root_ident = expr->RootIdentifier();
             } else {
                 AddError("cannot dereference expression of type '" + sem_.TypeNameOf(expr_ty) + "'",
@@ -3724,8 +3680,8 @@
         }
     }
 
-    auto* sem = builder_->create<sem::ValueExpression>(unary, ty, stage, current_statement_, value,
-                                                       expr->HasSideEffects(), root_ident);
+    auto* sem = b.create<sem::ValueExpression>(unary, ty, stage, current_statement_, value,
+                                               expr->HasSideEffects(), root_ident);
     sem->Behaviors() = expr->Behaviors();
     return sem;
 }
@@ -3873,7 +3829,7 @@
 
     // If all arguments are abstract-integers, then materialize to i32.
     if (common_ty->Is<core::type::AbstractInt>()) {
-        common_ty = builder_->create<core::type::I32>();
+        common_ty = b.create<core::type::I32>();
     }
 
     for (size_t i = 0; i < args.Length(); i++) {
@@ -3912,7 +3868,7 @@
     }
     // Apply the resolved tint::sem::BuiltinEnumExpression<tint::core::BuiltinValue> to the
     // attribute.
-    builder_->Sem().Add(attr, builtin_expr);
+    b.Sem().Add(attr, builtin_expr);
     return builtin_expr->Value();
 }
 
@@ -4022,50 +3978,62 @@
         return nullptr;
     }
 
-    builder_->Sem().Add(named_type, result);
+    b.Sem().Add(named_type, result);
     return result;
 }
 
 const core::type::ArrayCount* Resolver::ArrayCount(const ast::Expression* count_expr) {
     // Evaluate the constant array count expression.
-    const auto* count_sem = Materialize(ValueExpression(count_expr));
+    const auto* count_sem = Materialize(sem_.GetVal(count_expr));
     if (!count_sem) {
         return nullptr;
     }
 
-    if (count_sem->Stage() == core::EvaluationStage::kOverride) {
-        // array count is an override expression.
-        // Is the count a named 'override'?
-        if (auto* user = count_sem->UnwrapMaterialize()->As<sem::VariableUser>()) {
-            if (auto* global = user->Variable()->As<sem::GlobalVariable>()) {
-                return builder_->create<sem::NamedOverrideArrayCount>(global);
+    switch (count_sem->Stage()) {
+        case core::EvaluationStage::kNotEvaluated:
+            // Happens in expressions like:
+            //    false && array<T, N>()[i]
+            // The end result will not be used, so just make N=1.
+            return b.create<core::type::ConstantArrayCount>(static_cast<uint32_t>(1));
+
+        case core::EvaluationStage::kOverride: {
+            // array count is an override expression.
+            // Is the count a named 'override'?
+            if (auto* user = count_sem->UnwrapMaterialize()->As<sem::VariableUser>()) {
+                if (auto* global = user->Variable()->As<sem::GlobalVariable>()) {
+                    return b.create<sem::NamedOverrideArrayCount>(global);
+                }
             }
+            return b.create<sem::UnnamedOverrideArrayCount>(count_sem);
         }
-        return builder_->create<sem::UnnamedOverrideArrayCount>(count_sem);
-    }
 
-    auto* count_val = count_sem->ConstantValue();
-    if (!count_val) {
-        AddError("array count must evaluate to a constant integer expression or override variable",
-                 count_expr->source);
-        return nullptr;
-    }
+        case core::EvaluationStage::kConstant: {
+            auto* count_val = count_sem->ConstantValue();
+            if (auto* ty = count_val->Type(); !ty->is_integer_scalar()) {
+                AddError(
+                    "array count must evaluate to a constant integer expression, but is type '" +
+                        ty->FriendlyName() + "'",
+                    count_expr->source);
+                return nullptr;
+            }
 
-    if (auto* ty = count_val->Type(); !ty->is_integer_scalar()) {
-        AddError("array count must evaluate to a constant integer expression, but is type '" +
-                     ty->FriendlyName() + "'",
-                 count_expr->source);
-        return nullptr;
-    }
+            int64_t count = count_val->ValueAs<AInt>();
+            if (count < 1) {
+                AddError("array count (" + std::to_string(count) + ") must be greater than 0",
+                         count_expr->source);
+                return nullptr;
+            }
 
-    int64_t count = count_val->ValueAs<AInt>();
-    if (count < 1) {
-        AddError("array count (" + std::to_string(count) + ") must be greater than 0",
-                 count_expr->source);
-        return nullptr;
-    }
+            return b.create<core::type::ConstantArrayCount>(static_cast<uint32_t>(count));
+        }
 
-    return builder_->create<core::type::ConstantArrayCount>(static_cast<uint32_t>(count));
+        default: {
+            AddError(
+                "array count must evaluate to a constant integer expression or override variable",
+                count_expr->source);
+            return nullptr;
+        }
+    }
 }
 
 bool Resolver::ArrayAttributes(VectorRef<const ast::Attribute*> attributes,
@@ -4104,12 +4072,12 @@
     return true;
 }
 
-core::type::Array* Resolver::Array(const Source& array_source,
-                                   const Source& el_source,
-                                   const Source& count_source,
-                                   const core::type::Type* el_ty,
-                                   const core::type::ArrayCount* el_count,
-                                   uint32_t explicit_stride) {
+sem::Array* Resolver::Array(const Source& array_source,
+                            const Source& el_source,
+                            const Source& count_source,
+                            const core::type::Type* el_ty,
+                            const core::type::ArrayCount* el_count,
+                            uint32_t explicit_stride) {
     uint32_t el_align = el_ty->Align();
     uint32_t el_size = el_ty->Size();
     uint64_t implicit_stride = el_size ? tint::RoundUp<uint64_t>(el_align, el_size) : 0;
@@ -4128,9 +4096,9 @@
     } else if (el_count->Is<core::type::RuntimeArrayCount>()) {
         size = stride;
     }
-    auto* out = builder_->create<core::type::Array>(
-        el_ty, el_count, el_align, static_cast<uint32_t>(size), static_cast<uint32_t>(stride),
-        static_cast<uint32_t>(implicit_stride));
+    auto* out =
+        b.create<sem::Array>(el_ty, el_count, el_align, static_cast<uint32_t>(size),
+                             static_cast<uint32_t>(stride), static_cast<uint32_t>(implicit_stride));
 
     // Maximum nesting depth of composite types
     //  https://gpuweb.github.io/gpuweb/wgsl/#limits
@@ -4152,10 +4120,10 @@
 
 core::type::Type* Resolver::Alias(const ast::Alias* alias) {
     auto* ty = Type(alias->type);
-    if (!ty) {
+    if (TINT_UNLIKELY(!ty)) {
         return nullptr;
     }
-    if (!validator_.Alias(alias)) {
+    if (TINT_UNLIKELY(!validator_.Alias(alias))) {
         return nullptr;
     }
     return ty;
@@ -4412,11 +4380,11 @@
             return nullptr;
         }
 
-        auto* sem_member = builder_->create<sem::StructMember>(
+        auto* sem_member = b.create<sem::StructMember>(
             member, member->name->symbol, type, static_cast<uint32_t>(sem_members.Length()),
             static_cast<uint32_t>(offset), static_cast<uint32_t>(align),
             static_cast<uint32_t>(size), attributes);
-        builder_->Sem().Add(member, sem_member);
+        b.Sem().Add(member, sem_member);
         sem_members.Push(sem_member);
 
         struct_size = offset + size;
@@ -4437,7 +4405,7 @@
         return nullptr;
     }
 
-    auto* out = builder_->create<sem::Struct>(
+    auto* out = b.create<sem::Struct>(
         str, str->name->symbol, std::move(sem_members), static_cast<uint32_t>(struct_align),
         static_cast<uint32_t>(struct_size), static_cast<uint32_t>(size_no_padding));
 
@@ -4478,8 +4446,7 @@
 }
 
 sem::Statement* Resolver::ReturnStatement(const ast::ReturnStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto& behaviors = current_statement_->Behaviors();
         behaviors = sem::Behavior::kReturn;
@@ -4500,7 +4467,7 @@
 
             value_ty = expr->Type();
         } else {
-            value_ty = builder_->create<core::type::Void>();
+            value_ty = b.create<core::type::Void>();
         }
 
         // Validate after processing the return value expression so that its type
@@ -4511,8 +4478,8 @@
 }
 
 sem::SwitchStatement* Resolver::SwitchStatement(const ast::SwitchStatement* stmt) {
-    auto* sem = builder_->create<sem::SwitchStatement>(stmt, current_compound_statement_,
-                                                       current_function_);
+    auto* sem =
+        b.create<sem::SwitchStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto& behaviors = sem->Behaviors();
 
@@ -4544,7 +4511,7 @@
         if (!common_ty || !common_ty->is_integer_scalar()) {
             // No common type found or the common type was abstract.
             // Pick i32 and let validation deal with any mismatches.
-            common_ty = builder_->create<core::type::I32>();
+            common_ty = b.create<core::type::I32>();
         }
         cond = Materialize(cond, common_ty);
         if (!cond) {
@@ -4594,8 +4561,7 @@
 }
 
 sem::Statement* Resolver::VariableDeclStatement(const ast::VariableDeclStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         Mark(stmt->variable);
 
@@ -4615,8 +4581,7 @@
 }
 
 sem::Statement* Resolver::AssignmentStatement(const ast::AssignmentStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto* lhs = ValueExpression(stmt->lhs);
         if (!lhs) {
@@ -4657,8 +4622,7 @@
 }
 
 sem::Statement* Resolver::BreakStatement(const ast::BreakStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         sem->Behaviors() = sem::Behavior::kBreak;
 
@@ -4667,8 +4631,8 @@
 }
 
 sem::Statement* Resolver::BreakIfStatement(const ast::BreakIfStatement* stmt) {
-    auto* sem = builder_->create<sem::BreakIfStatement>(stmt, current_compound_statement_,
-                                                        current_function_);
+    auto* sem =
+        b.create<sem::BreakIfStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto* cond = Load(ValueExpression(stmt->condition));
         if (!cond) {
@@ -4683,8 +4647,7 @@
 }
 
 sem::Statement* Resolver::CallStatement(const ast::CallStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         if (auto* expr = ValueExpression(stmt->expr)) {
             sem->Behaviors() = expr->Behaviors();
@@ -4696,8 +4659,7 @@
 
 sem::Statement* Resolver::CompoundAssignmentStatement(
     const ast::CompoundAssignmentStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto* lhs = ValueExpression(stmt->lhs);
         if (!lhs) {
@@ -4733,8 +4695,7 @@
 }
 
 sem::Statement* Resolver::ContinueStatement(const ast::ContinueStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         sem->Behaviors() = sem::Behavior::kContinue;
 
@@ -4751,8 +4712,7 @@
 }
 
 sem::Statement* Resolver::DiscardStatement(const ast::DiscardStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         current_function_->SetDiscardStatement(sem);
         return true;
@@ -4761,8 +4721,7 @@
 
 sem::Statement* Resolver::IncrementDecrementStatement(
     const ast::IncrementDecrementStatement* stmt) {
-    auto* sem =
-        builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
+    auto* sem = b.create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
         auto* lhs = ValueExpression(stmt->lhs);
         if (!lhs) {
@@ -4803,7 +4762,7 @@
         return true;
     }
 
-    if (auto* arr = ty->As<core::type::Array>()) {
+    if (auto* arr = ty->As<sem::Array>()) {
         if (address_space != core::AddressSpace::kStorage) {
             if (arr->Count()->Is<core::type::RuntimeArrayCount>()) {
                 AddError("runtime-sized arrays can only be used in the <storage> address space",
@@ -4836,7 +4795,7 @@
 
 template <typename SEM, typename F>
 SEM* Resolver::StatementScope(const ast::Statement* ast, SEM* sem, F&& callback) {
-    builder_->Sem().Add(ast, sem);
+    b.Sem().Add(ast, sem);
 
     auto* as_compound =
         As<sem::CompoundStatement, tint::CastFlags::kDontErrorOnImpossibleCast>(sem);
@@ -4949,13 +4908,6 @@
     return true;
 }
 
-void Resolver::ErrorMismatchedResolvedIdentifier(const Source& source,
-                                                 const ResolvedIdentifier& resolved,
-                                                 std::string_view wanted) {
-    AddError("cannot use " + resolved.String() + " as " + std::string(wanted), source);
-    sem_.NoteDeclarationSource(resolved.Node());
-}
-
 void Resolver::ErrorInvalidAttribute(const ast::Attribute* attr, std::string_view use) {
     AddError("@" + attr->Name() + " is not valid for " + std::string(use), attr->source);
 }
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index 3846d1a..2e1cbde 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -145,6 +145,83 @@
     /// @returns the resolved type from an expression, or nullptr on error
     core::type::Type* Type(const ast::Expression* ast);
 
+    /// @returns a new abstract-float
+    core::type::AbstractFloat* AF();
+
+    /// @returns a new f32
+    core::type::F32* F32();
+
+    /// @returns a new i32
+    core::type::I32* I32();
+
+    /// @returns a new u32
+    core::type::U32* U32();
+
+    /// @returns a new f16, if the f16 extension is enabled, otherwise nullptr
+    core::type::F16* F16(const ast::Identifier* ident);
+
+    /// @returns a vector with the element type @p el of width @p n resolved from the identifier @p
+    /// ident.
+    core::type::Vector* Vec(const ast::Identifier* ident, core::type::Type* el, uint32_t n);
+
+    /// @returns a vector of width @p n resolved from the templated identifier @p ident, or an
+    /// IncompleteType if the identifier is not templated.
+    core::type::Type* VecT(const ast::Identifier* ident, core::BuiltinType builtin, uint32_t n);
+
+    /// @returns a matrix with the element type @p el of dimensions @p num_columns x @p num_rows
+    /// resolved from the identifier @p ident.
+    core::type::Matrix* Mat(const ast::Identifier* ident,
+                            core::type::Type* el,
+                            uint32_t num_columns,
+                            uint32_t num_rows);
+
+    /// @returns a matrix of dimensions @p num_columns x @p num_rows resolved from the templated
+    /// identifier @p ident, or an IncompleteType if the identifier is not templated.
+    core::type::Type* MatT(const ast::Identifier* ident,
+                           core::BuiltinType builtin,
+                           uint32_t num_columns,
+                           uint32_t num_rows);
+
+    /// @returns an array resolved from the templated identifier @p ident, or an IncompleteType if
+    /// the identifier is not templated.
+    core::type::Type* Array(const ast::Identifier* ident);
+
+    /// @returns an atomic resolved from the templated identifier @p ident.
+    core::type::Atomic* Atomic(const ast::Identifier* ident);
+
+    /// @returns a pointer resolved from the templated identifier @p ident.
+    core::type::Pointer* Ptr(const ast::Identifier* ident);
+
+    /// @returns a sampled texture resolved from the templated identifier @p ident with the
+    /// dimensions @p dim.
+    core::type::SampledTexture* SampledTexture(const ast::Identifier* ident,
+                                               core::type::TextureDimension dim);
+
+    /// @returns a multisampled texture resolved from the templated identifier @p ident with the
+    /// dimensions @p dim.
+    core::type::MultisampledTexture* MultisampledTexture(const ast::Identifier* ident,
+                                                         core::type::TextureDimension dim);
+
+    /// @returns a storage texture resolved from the templated identifier @p ident with the
+    /// dimensions @p dim.
+    core::type::StorageTexture* StorageTexture(const ast::Identifier* ident,
+                                               core::type::TextureDimension dim);
+
+    /// @returns a packed vec3 resolved from the templated identifier @p ident.
+    core::type::Vector* PackedVec3T(const ast::Identifier* ident);
+
+    /// @returns @p ident cast to an ast::TemplatedIdentifier, if the identifier is templated and
+    /// the number of templated arguments are between @p min_args and @p max_args.
+    const ast::TemplatedIdentifier* TemplatedIdentifier(const ast::Identifier* ident,
+                                                        size_t min_args,
+                                                        size_t max_args = /* use min */ 0);
+
+    /// @returns true if the number of templated arguments are between @p min_args and  @p max_args
+    /// otherwise raises an error and returns false.
+    bool CheckTemplatedIdentifierArgs(const ast::TemplatedIdentifier* ident,
+                                      size_t min_args,
+                                      size_t max_args = /* use min */ 0);
+
     /// @returns the call of Expression() cast to a
     /// sem::BuiltinEnumExpression<core::AddressSpace>. If the sem::Expression is not a
     /// sem::BuiltinEnumExpression<core::AddressSpace>, then an error diagnostic is raised and
@@ -205,10 +282,9 @@
     sem::ValueExpression* Bitcast(const ast::BitcastExpression*);
     sem::Call* Call(const ast::CallExpression*);
     sem::Function* Function(const ast::Function*);
-    template <size_t N>
     sem::Call* FunctionCall(const ast::CallExpression*,
                             sem::Function* target,
-                            Vector<const sem::ValueExpression*, N>& args,
+                            VectorRef<const sem::ValueExpression*> args,
                             sem::Behaviors arg_behaviors);
     sem::Expression* Identifier(const ast::IdentifierExpression*);
     template <size_t N>
@@ -408,12 +484,12 @@
     /// @param el_ty the Array element type
     /// @param el_count the number of elements in the array.
     /// @param explicit_stride the explicit byte stride of the array. Zero means implicit stride.
-    core::type::Array* Array(const Source& array_source,
-                             const Source& el_source,
-                             const Source& count_source,
-                             const core::type::Type* el_ty,
-                             const core::type::ArrayCount* el_count,
-                             uint32_t explicit_stride);
+    sem::Array* Array(const Source& array_source,
+                      const Source& el_source,
+                      const Source& count_source,
+                      const core::type::Type* el_ty,
+                      const core::type::ArrayCount* el_count,
+                      uint32_t explicit_stride);
 
     /// Builds and returns the semantic information for the alias `alias`.
     /// This method does not mark the ast::Alias node, nor attach the generated
@@ -441,8 +517,7 @@
     /// @note this method does not resolve the attributes as these are context-dependent (global,
     /// local)
     /// @param var the variable
-    /// @param is_global true if this is module scope, otherwise function scope
-    sem::Variable* Let(const ast::Let* var, bool is_global);
+    sem::Variable* Let(const ast::Let* var);
 
     /// @returns the semantic info for the module-scope `ast::Override` `v`. If an error is raised,
     /// nullptr is returned.
@@ -531,15 +606,6 @@
     /// @returns true if @p ident is not a ast::TemplatedIdentifier.
     bool CheckNotTemplated(const char* use, const ast::Identifier* ident);
 
-    /// Raises an error diagnostic that the resolved identifier @p resolved was not of the expected
-    /// kind.
-    /// @param source the source of the error diagnostic
-    /// @param resolved the resolved identifier
-    /// @param wanted the expected kind
-    void ErrorMismatchedResolvedIdentifier(const Source& source,
-                                           const ResolvedIdentifier& resolved,
-                                           std::string_view wanted);
-
     /// Raises an error that the attribute is not valid for the given use.
     /// @param attr the invalue attribute
     /// @param use the thing that the attribute was applied to
@@ -568,8 +634,8 @@
 
     // ArrayConstructorSig represents a unique array constructor signature.
     // It is a tuple of the array type, number of arguments provided and earliest evaluation stage.
-    using ArrayConstructorSig = tint::UnorderedKeyWrapper<
-        std::tuple<const core::type::Array*, size_t, core::EvaluationStage>>;
+    using ArrayConstructorSig =
+        tint::UnorderedKeyWrapper<std::tuple<const sem::Array*, size_t, core::EvaluationStage>>;
 
     // StructConstructorSig represents a unique structure constructor signature.
     // It is a tuple of the structure type, number of arguments provided and earliest evaluation
@@ -599,18 +665,7 @@
         std::unordered_set<const sem::Variable*> parameter_reads;
     };
 
-    /// A hint for the usage of an identifier expression.
-    /// Used to provide more informative error diagnostics on resolution failure.
-    struct IdentifierResolveHint {
-        /// The expression this hint applies to
-        const ast::Expression* expression = nullptr;
-        /// The usage of the identifier.
-        const char* usage = "identifier";
-        /// Suggested strings if the identifier failed to resolve
-        tint::Slice<char const* const> suggestions = tint::Empty;
-    };
-
-    ProgramBuilder* const builder_;
+    ProgramBuilder& b;
     diag::List& diagnostics_;
     core::constant::Eval const_eval_;
     core::intrinsic::Table<wgsl::intrinsic::Dialect> intrinsic_table_;
@@ -629,12 +684,11 @@
     sem::Function* current_function_ = nullptr;
     sem::Statement* current_statement_ = nullptr;
     sem::CompoundStatement* current_compound_statement_ = nullptr;
+    Vector<std::function<void(const sem::GlobalVariable*)>, 4> on_transitively_reference_global_;
     uint32_t current_scoping_depth_ = 0;
-    UniqueVector<const sem::GlobalVariable*, 4>* resolved_overrides_ = nullptr;
     Hashset<TypeAndAddressSpace, 8> valid_type_storage_layouts_;
     Hashmap<const ast::Expression*, const ast::BinaryExpression*, 8> logical_binary_lhs_to_parent_;
     Hashset<const ast::Expression*, 8> skip_const_eval_;
-    IdentifierResolveHint identifier_resolve_hint_;
     Hashmap<const core::type::Type*, size_t, 8> nest_depth_;
     Hashmap<std::pair<core::intrinsic::Overload, wgsl::BuiltinFn>, sem::BuiltinFn*, 64> builtins_;
     Hashmap<core::intrinsic::Overload, sem::ValueConstructor*, 16> constructors_;
diff --git a/src/tint/lang/wgsl/resolver/resolver_helper_test.h b/src/tint/lang/wgsl/resolver/resolver_helper_test.h
index 7daf352..f15027c 100644
--- a/src/tint/lang/wgsl/resolver/resolver_helper_test.h
+++ b/src/tint/lang/wgsl/resolver/resolver_helper_test.h
@@ -28,6 +28,7 @@
 #include "src/tint/lang/core/type/abstract_int.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolver.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -141,14 +142,47 @@
 template <typename TO>
 using alias3 = alias<TO, 3>;
 
-/// A scalar value
-using Scalar =
-    std::variant<core::i32, core::u32, core::f32, core::f16, core::AInt, core::AFloat, bool>;
+/// Scalar represents a scalar value
+struct Scalar {
+    /// The possible types of a scalar value.
+    using Value =
+        std::variant<core::i32, core::u32, core::f32, core::f16, core::AInt, core::AFloat, bool>;
 
-/// Returns current variant value in `s` cast to type `T`
+    /// Constructor
+    /// @param val the value used to construct this Scalar
+    template <typename T>
+    Scalar(T&& val) : value(std::forward<T>(val)) {}  // NOLINT
+
+    /// @returns the Value
+    operator Value&() { return value; }
+
+    /// Equality operator
+    /// @param other the other Scalar
+    /// @return true if this Scalar is equal to @p other
+    bool operator==(const Scalar& other) const { return value == other.value; }
+
+    /// Inequality operator
+    /// @param other the other Scalar
+    /// @return true if this Scalar is not equal to @p other
+    bool operator!=(const Scalar& other) const { return value != other.value; }
+
+    /// The scalar value
+    Value value;
+};
+
+/// @param out the stream to write to
+/// @param s the Scalar
+/// @returns @p out so calls can be chained
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+STREAM& operator<<(STREAM& out, const Scalar& s) {
+    std::visit([&](auto&& v) { out << v; }, s.value);
+    return out;
+}
+
+/// @return  current variant value in @p s cast to type `T`
 template <typename T>
 T As(const Scalar& s) {
-    return std::visit([](auto&& v) { return static_cast<T>(v); }, s);
+    return std::visit([](auto&& v) { return static_cast<T>(v); }, s.value);
 }
 
 using ast_type_func_ptr = ast::Type (*)(ProgramBuilder& b);
@@ -199,7 +233,7 @@
     /// @param args args of size 1 with the boolean value to init with
     /// @return a new AST expression of the bool type
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<bool>(args[0]));
+        return b.Expr(std::get<bool>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to bool.
@@ -232,7 +266,7 @@
     /// @param args args of size 1 with the i32 value to init with
     /// @return a new AST i32 literal value expression
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<core::i32>(args[0]));
+        return b.Expr(std::get<core::i32>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to i32.
@@ -265,7 +299,7 @@
     /// @param args args of size 1 with the u32 value to init with
     /// @return a new AST u32 literal value expression
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<core::u32>(args[0]));
+        return b.Expr(std::get<core::u32>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to u32.
@@ -298,7 +332,7 @@
     /// @param args args of size 1 with the f32 value to init with
     /// @return a new AST f32 literal value expression
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<core::f32>(args[0]));
+        return b.Expr(std::get<core::f32>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to f32.
@@ -331,7 +365,7 @@
     /// @param args args of size 1 with the f16 value to init with
     /// @return a new AST f16 literal value expression
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<core::f16>(args[0]));
+        return b.Expr(std::get<core::f16>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to f16.
@@ -363,7 +397,7 @@
     /// @param args args of size 1 with the abstract-float value to init with
     /// @return a new AST abstract-float literal value expression
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<core::AFloat>(args[0]));
+        return b.Expr(std::get<core::AFloat>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to AFloat.
@@ -395,7 +429,7 @@
     /// @param args args of size 1 with the abstract-int value to init with
     /// @return a new AST abstract-int literal value expression
     static inline const ast::Expression* Expr(ProgramBuilder& b, VectorRef<Scalar> args) {
-        return b.Expr(std::get<core::AInt>(args[0]));
+        return b.Expr(std::get<core::AInt>(args[0].value));
     }
     /// @param b the ProgramBuilder
     /// @param v arg of type double that will be cast to AInt.
@@ -649,7 +683,7 @@
         } else {
             count = b.create<core::type::ConstantArrayCount>(N);
         }
-        return b.create<core::type::Array>(
+        return b.create<sem::Array>(
             /* element */ el,
             /* count */ count,
             /* align */ el->Align(),
@@ -773,7 +807,7 @@
     std::ostream& Print(std::ostream& o) const {
         o << type_name << "(";
         for (auto& a : args) {
-            std::visit([&](auto& v) { o << v; }, a);
+            o << a;
             if (&a != &args.Back()) {
                 o << ", ";
             }
@@ -806,7 +840,7 @@
 /// Creates a Value of DataType<T> from a scalar `v`
 template <typename T>
 Value Val(T v) {
-    static_assert(tint::traits::IsTypeIn<T, Scalar>, "v must be a Number of bool");
+    static_assert(tint::traits::IsTypeIn<T, Scalar::Value>, "v must be a Number of bool");
     return Value::Create<T>(Vector<Scalar, 1>{v});
 }
 
diff --git a/src/tint/lang/wgsl/resolver/resolver_test.cc b/src/tint/lang/wgsl/resolver/resolver_test.cc
index 43cf3ec..3fc4a87 100644
--- a/src/tint/lang/wgsl/resolver/resolver_test.cc
+++ b/src/tint/lang/wgsl/resolver/resolver_test.cc
@@ -39,6 +39,7 @@
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -423,7 +424,7 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref, nullptr);
-    auto* ary = ref->StoreType()->As<core::type::Array>();
+    auto* ary = ref->StoreType()->As<sem::Array>();
     EXPECT_EQ(ary->Count(), create<core::type::ConstantArrayCount>(10u));
 }
 
@@ -436,7 +437,7 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref, nullptr);
-    auto* ary = ref->StoreType()->As<core::type::Array>();
+    auto* ary = ref->StoreType()->As<sem::Array>();
     EXPECT_EQ(ary->Count(), create<core::type::ConstantArrayCount>(10u));
 }
 
@@ -451,7 +452,7 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref, nullptr);
-    auto* ary = ref->StoreType()->As<core::type::Array>();
+    auto* ary = ref->StoreType()->As<sem::Array>();
     EXPECT_EQ(ary->Count(), create<core::type::ConstantArrayCount>(10u));
 }
 
@@ -466,7 +467,7 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref, nullptr);
-    auto* ary = ref->StoreType()->As<core::type::Array>();
+    auto* ary = ref->StoreType()->As<sem::Array>();
     EXPECT_EQ(ary->Count(), create<core::type::ConstantArrayCount>(10u));
 }
 
@@ -481,7 +482,7 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref, nullptr);
-    auto* ary = ref->StoreType()->As<core::type::Array>();
+    auto* ary = ref->StoreType()->As<sem::Array>();
     auto* sem_override = Sem().Get(override);
     ASSERT_NE(sem_override, nullptr);
     EXPECT_EQ(ary->Count(), create<sem::NamedOverrideArrayCount>(sem_override));
@@ -500,12 +501,12 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref_a = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref_a, nullptr);
-    auto* ary_a = ref_a->StoreType()->As<core::type::Array>();
+    auto* ary_a = ref_a->StoreType()->As<sem::Array>();
 
     ASSERT_NE(TypeOf(b), nullptr);
     auto* ref_b = TypeOf(b)->As<core::type::Reference>();
     ASSERT_NE(ref_b, nullptr);
-    auto* ary_b = ref_b->StoreType()->As<core::type::Array>();
+    auto* ary_b = ref_b->StoreType()->As<sem::Array>();
 
     auto* sem_override = Sem().Get(override);
     ASSERT_NE(sem_override, nullptr);
@@ -526,7 +527,7 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref, nullptr);
-    auto* ary = ref->StoreType()->As<core::type::Array>();
+    auto* ary = ref->StoreType()->As<sem::Array>();
     auto* sem_override = Sem().Get(override);
     ASSERT_NE(sem_override, nullptr);
     EXPECT_EQ(ary->Count(), create<sem::UnnamedOverrideArrayCount>(Sem().Get(cnt)));
@@ -547,12 +548,12 @@
     ASSERT_NE(TypeOf(a), nullptr);
     auto* ref_a = TypeOf(a)->As<core::type::Reference>();
     ASSERT_NE(ref_a, nullptr);
-    auto* ary_a = ref_a->StoreType()->As<core::type::Array>();
+    auto* ary_a = ref_a->StoreType()->As<sem::Array>();
 
     ASSERT_NE(TypeOf(b), nullptr);
     auto* ref_b = TypeOf(b)->As<core::type::Reference>();
     ASSERT_NE(ref_b, nullptr);
-    auto* ary_b = ref_b->StoreType()->As<core::type::Array>();
+    auto* ary_b = ref_b->StoreType()->As<sem::Array>();
 
     auto* sem_override = Sem().Get(override);
     ASSERT_NE(sem_override, nullptr);
@@ -1225,7 +1226,7 @@
 
     EXPECT_FALSE(r()->Resolve()) << r()->error();
     EXPECT_EQ(r()->error(), R"(12:34 error: cannot use type 'f32' as value
-12:34 note: are you missing '()' for value constructor?)");
+12:34 note: are you missing '()'?)");
 }
 
 TEST_F(ResolverTest, Expr_MemberAccessor_Struct) {
diff --git a/src/tint/lang/wgsl/resolver/sem_helper.cc b/src/tint/lang/wgsl/resolver/sem_helper.cc
index ce4c0d6..a921ad1 100644
--- a/src/tint/lang/wgsl/resolver/sem_helper.cc
+++ b/src/tint/lang/wgsl/resolver/sem_helper.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/wgsl/resolver/sem_helper.h"
 
+#include "src/tint/lang/wgsl/resolver/incomplete_type.h"
+#include "src/tint/lang/wgsl/resolver/unresolved_identifier.h"
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/function_expression.h"
@@ -40,6 +42,27 @@
     return sem ? const_cast<core::type::Type*>(sem->Type()) : nullptr;
 }
 
+sem::TypeExpression* SemHelper::AsTypeExpression(sem::Expression* expr) const {
+    if (TINT_UNLIKELY(!expr)) {
+        return nullptr;
+    }
+
+    auto* ty_expr = expr->As<sem::TypeExpression>();
+    if (TINT_UNLIKELY(!ty_expr)) {
+        ErrorUnexpectedExprKind(expr, "type");
+        return nullptr;
+    }
+
+    auto* type = ty_expr->Type();
+    if (auto* incomplete = type->As<IncompleteType>(); TINT_UNLIKELY(incomplete)) {
+        AddError("expected '<' for '" + std::string(ToString(incomplete->builtin)) + "'",
+                 expr->Declaration()->source.End());
+        return nullptr;
+    }
+
+    return ty_expr;
+}
+
 std::string SemHelper::Describe(const sem::Expression* expr) const {
     return Switch(
         expr,  //
@@ -69,6 +92,9 @@
             auto name = fn->name->symbol.Name();
             return "function '" + name + "'";
         },
+        [&](const sem::BuiltinEnumExpression<wgsl::BuiltinFn>* fn) {
+            return "builtin function '" + tint::ToString(fn->Value()) + "'";
+        },
         [&](const sem::BuiltinEnumExpression<core::Access>* access) {
             return "access '" + tint::ToString(access->Value()) + "'";
         },
@@ -87,6 +113,10 @@
         [&](const sem::BuiltinEnumExpression<core::TexelFormat>* fmt) {
             return "texel format '" + tint::ToString(fmt->Value()) + "'";
         },
+        [&](const UnresolvedIdentifier* ui) {
+            auto name = ui->Identifier()->identifier->symbol.Name();
+            return "unresolved identifier '" + name + "'";
+        },
         [&](Default) -> std::string {
             TINT_ICE() << "unhandled sem::Expression type: "
                        << (expr ? expr->TypeInfo().name : "<null>");
@@ -94,8 +124,29 @@
         });
 }
 
-void SemHelper::ErrorUnexpectedExprKind(const sem::Expression* expr,
-                                        std::string_view wanted) const {
+void SemHelper::ErrorUnexpectedExprKind(
+    const sem::Expression* expr,
+    std::string_view wanted,
+    tint::Slice<char const* const> suggestions /* = Empty */) const {
+    if (auto* ui = expr->As<UnresolvedIdentifier>()) {
+        auto* ident = ui->Identifier();
+        auto name = ident->identifier->symbol.Name();
+        AddError("unresolved " + std::string(wanted) + " '" + name + "'", ident->source);
+        if (!suggestions.IsEmpty()) {
+            // Filter out suggestions that have a leading underscore.
+            Vector<const char*, 8> filtered;
+            for (auto* str : suggestions) {
+                if (str[0] != '_') {
+                    filtered.Push(str);
+                }
+            }
+            StringStream msg;
+            tint::SuggestAlternatives(name, filtered.Slice().Reinterpret<char const* const>(), msg);
+            AddNote(msg.str(), ident->source);
+        }
+        return;
+    }
+
     AddError("cannot use " + Describe(expr) + " as " + std::string(wanted),
              expr->Declaration()->source);
     NoteDeclarationSource(expr->Declaration());
@@ -103,9 +154,10 @@
 
 void SemHelper::ErrorExpectedValueExpr(const sem::Expression* expr) const {
     ErrorUnexpectedExprKind(expr, "value");
-    if (auto* ty_expr = expr->As<sem::TypeExpression>()) {
-        if (auto* ident = ty_expr->Declaration()->As<ast::IdentifierExpression>()) {
-            AddNote("are you missing '()' for value constructor?", ident->source.End());
+    if (auto* ident = expr->Declaration()->As<ast::IdentifierExpression>()) {
+        if (expr->IsAnyOf<sem::FunctionExpression, sem::TypeExpression,
+                          sem::BuiltinEnumExpression<wgsl::BuiltinFn>>()) {
+            AddNote("are you missing '()'?", ident->source.End());
         }
     }
 }
diff --git a/src/tint/lang/wgsl/resolver/sem_helper.h b/src/tint/lang/wgsl/resolver/sem_helper.h
index 4dfbe64..bbd7943 100644
--- a/src/tint/lang/wgsl/resolver/sem_helper.h
+++ b/src/tint/lang/wgsl/resolver/sem_helper.h
@@ -55,8 +55,8 @@
         return const_cast<T*>(As<T>(sem));
     }
 
-    /// GetVal is a helper for obtaining the semantic sem::ValueExpression for the given AST node.
-    /// Raises an error diagnostic and returns `nullptr` if the semantic node is not a
+    /// GetVal is a helper for obtaining the semantic sem::ValueExpression for the given AST
+    /// expression. Raises an error diagnostic and returns `nullptr` if the semantic node is not a
     /// sem::ValueExpression.
     /// @param ast the ast node to get the sem for
     /// @returns the sem node for @p ast
@@ -81,12 +81,17 @@
     /// @param expr the semantic node
     /// @returns nullptr if @p expr is nullptr, or @p expr cast to type::Type if the cast is
     /// successful, otherwise an error diagnostic is raised.
-    sem::TypeExpression* AsTypeExpression(sem::Expression* expr) const {
+    sem::TypeExpression* AsTypeExpression(sem::Expression* expr) const;
+
+    /// GetType is a helper for obtaining the semantic type for the given AST expression.
+    /// Raises an error diagnostic and returns `nullptr` if the semantic node is not a
+    /// sem::TypeExpression
+    /// @param ast the ast node to get the sem for
+    /// @returns the sem node for @p ast
+    const core::type::Type* GetType(const ast::Expression* ast) const {
+        auto* expr = AsTypeExpression(Get(ast));
         if (TINT_LIKELY(expr)) {
-            if (auto* ty_expr = expr->As<sem::TypeExpression>(); TINT_LIKELY(ty_expr)) {
-                return ty_expr;
-            }
-            ErrorUnexpectedExprKind(expr, "type");
+            return expr->Type();
         }
         return nullptr;
     }
@@ -115,11 +120,24 @@
             if (TINT_LIKELY(enum_expr)) {
                 return enum_expr;
             }
-            ErrorUnexpectedExprKind(expr, "address space");
+            ErrorUnexpectedExprKind(expr, "address space", core::kAddressSpaceStrings);
         }
         return nullptr;
     }
 
+    /// GetAddressSpace is a helper for obtaining the address space for the given AST expression.
+    /// Raises an error diagnostic and returns core::AddressSpace::kUndefined if the semantic node
+    /// is not a sem::BuiltinEnumExpression<core::AddressSpace>
+    /// @param ast the ast node to get the address space
+    /// @returns the sem node for @p ast
+    core::AddressSpace GetAddressSpace(const ast::Expression* ast) const {
+        auto* expr = AsAddressSpace(Get(ast));
+        if (TINT_LIKELY(expr)) {
+            return expr->Value();
+        }
+        return core::AddressSpace::kUndefined;
+    }
+
     /// @param expr the semantic node
     /// @returns nullptr if @p expr is nullptr, or @p expr cast to
     /// sem::BuiltinEnumExpression<core::BuiltinValue> if the cast is successful, otherwise an
@@ -130,7 +148,7 @@
             if (TINT_LIKELY(enum_expr)) {
                 return enum_expr;
             }
-            ErrorUnexpectedExprKind(expr, "builtin value");
+            ErrorUnexpectedExprKind(expr, "builtin value", core::kBuiltinValueStrings);
         }
         return nullptr;
     }
@@ -145,11 +163,24 @@
             if (TINT_LIKELY(enum_expr)) {
                 return enum_expr;
             }
-            ErrorUnexpectedExprKind(expr, "texel format");
+            ErrorUnexpectedExprKind(expr, "texel format", core::kTexelFormatStrings);
         }
         return nullptr;
     }
 
+    /// GetTexelFormat is a helper for obtaining the texel format for the given AST expression.
+    /// Raises an error diagnostic and returns core::TexelFormat::kUndefined if the semantic node
+    /// is not a sem::BuiltinEnumExpression<core::TexelFormat>
+    /// @param ast the ast node to get the texel format
+    /// @returns the sem node for @p ast
+    core::TexelFormat GetTexelFormat(const ast::Expression* ast) const {
+        auto* expr = AsTexelFormat(Get(ast));
+        if (TINT_LIKELY(expr)) {
+            return expr->Value();
+        }
+        return core::TexelFormat::kUndefined;
+    }
+
     /// @param expr the semantic node
     /// @returns nullptr if @p expr is nullptr, or @p expr cast to
     /// sem::BuiltinEnumExpression<core::Access> if the cast is successful, otherwise an error
@@ -160,11 +191,24 @@
             if (TINT_LIKELY(enum_expr)) {
                 return enum_expr;
             }
-            ErrorUnexpectedExprKind(expr, "access");
+            ErrorUnexpectedExprKind(expr, "access", core::kAccessStrings);
         }
         return nullptr;
     }
 
+    /// GetAccess is a helper for obtaining the access mode for the given AST expression.
+    /// Raises an error diagnostic and returns core::Access::kUndefined if the semantic node
+    /// is not a sem::BuiltinEnumExpression<core::Access>
+    /// @param ast the ast node to get the access mode
+    /// @returns the sem node for @p ast
+    core::Access GetAccess(const ast::Expression* ast) const {
+        auto* expr = AsAccess(Get(ast));
+        if (TINT_LIKELY(expr)) {
+            return expr->Value();
+        }
+        return core::Access::kUndefined;
+    }
+
     /// @param expr the semantic node
     /// @returns nullptr if @p expr is nullptr, or @p expr cast to
     /// sem::BuiltinEnumExpression<core::InterpolationSampling> if the cast is successful,
@@ -176,7 +220,8 @@
             if (TINT_LIKELY(enum_expr)) {
                 return enum_expr;
             }
-            ErrorUnexpectedExprKind(expr, "interpolation sampling");
+            ErrorUnexpectedExprKind(expr, "interpolation sampling",
+                                    core::kInterpolationSamplingStrings);
         }
         return nullptr;
     }
@@ -192,7 +237,7 @@
             if (TINT_LIKELY(enum_expr)) {
                 return enum_expr;
             }
-            ErrorUnexpectedExprKind(expr, "interpolation type");
+            ErrorUnexpectedExprKind(expr, "interpolation type", core::kInterpolationTypeStrings);
         }
         return nullptr;
     }
@@ -217,7 +262,10 @@
     /// Raises an error diagnostic that the expression @p got was not of the kind @p wanted.
     /// @param expr the expression
     /// @param wanted the expected expression kind
-    void ErrorUnexpectedExprKind(const sem::Expression* expr, std::string_view wanted) const;
+    /// @param suggestions suggested valid identifiers
+    void ErrorUnexpectedExprKind(const sem::Expression* expr,
+                                 std::string_view wanted,
+                                 tint::Slice<char const* const> suggestions = Empty) const;
 
     /// If @p node is a module-scope type, variable or function declaration, then appends a note
     /// diagnostic where this declaration was declared, otherwise the function does nothing.
diff --git a/src/tint/lang/wgsl/resolver/type_validation_test.cc b/src/tint/lang/wgsl/resolver/type_validation_test.cc
index 46284e1..b5b07d6 100644
--- a/src/tint/lang/wgsl/resolver/type_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/type_validation_test.cc
@@ -579,7 +579,20 @@
 56:78 note: while instantiating 'var' a)");
 }
 
-TEST_F(ResolverTypeValidationTest, Struct_Member_VectorNoType) {
+TEST_F(ResolverTypeValidationTest, PtrType_ArrayIncomplete) {
+    // fn f(l: ptr<function, array>) {}
+
+    Func("f",
+         Vector{
+             Param("l", ty.ptr(function, ty(Source{{12, 34}}, "array"))),
+         },
+         ty.void_(), Empty);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'array'");
+}
+
+TEST_F(ResolverTypeValidationTest, Struct_Member_VectorIncomplete) {
     // struct S {
     //   a: vec3;
     // };
@@ -592,7 +605,7 @@
     EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'vec3'");
 }
 
-TEST_F(ResolverTypeValidationTest, Struct_Member_MatrixNoType) {
+TEST_F(ResolverTypeValidationTest, Struct_Member_MatrixIncomplete) {
     // struct S {
     //   a: mat3x3;
     // };
@@ -768,6 +781,21 @@
 56:78 note: while instantiating parameter a)");
 }
 
+TEST_F(ResolverTypeValidationTest, PtrToPtr_Fail) {
+    // fn func(a : ptr<workgroup, ptr<workgroup, u32>>) {}
+    auto* param = Param("a", ty.ptr(workgroup, ty.ptr<workgroup, u32>(Source{{12, 34}})));
+
+    Func("func", Vector{param}, ty.void_(),
+         Vector{
+             Return(),
+         });
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: ptr<workgroup, u32, read_write> cannot be used as the store type of a pointer)");
+}
+
 TEST_F(ResolverTypeValidationTest, PtrToRuntimeArrayAsPointerParameter_Fail) {
     // fn func(a : ptr<workgroup, array<u32>>) {}
 
diff --git a/src/tint/lang/wgsl/resolver/uniformity.cc b/src/tint/lang/wgsl/resolver/uniformity.cc
index 506a20b..aa7190b 100644
--- a/src/tint/lang/wgsl/resolver/uniformity.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity.cc
@@ -176,8 +176,8 @@
 struct FunctionInfo {
     /// Constructor
     /// @param func the AST function
-    /// @param builder the program builder
-    FunctionInfo(const ast::Function* func, const ProgramBuilder* builder) {
+    /// @param b the program builder
+    FunctionInfo(const ast::Function* func, const ProgramBuilder& b) {
         name = func->name->symbol.Name();
         callsite_tag = {CallSiteTag::CallSiteNoRestriction};
         function_tag = NoRestriction;
@@ -197,7 +197,7 @@
         for (size_t i = 0; i < func->params.Length(); i++) {
             auto* param = func->params[i];
             auto param_name = param->name->symbol.Name();
-            auto* sem = builder->Sem().Get<sem::Parameter>(param);
+            auto* sem = b.Sem().Get<sem::Parameter>(param);
             parameters[i].sem = sem;
 
             parameters[i].value = CreateNode({"param_", param_name});
@@ -339,8 +339,8 @@
   public:
     /// Constructor.
     /// @param builder the program to analyze
-    explicit UniformityGraph(ProgramBuilder* builder)
-        : builder_(builder), sem_(builder->Sem()), diagnostics_(builder->Diagnostics()) {}
+    explicit UniformityGraph(ProgramBuilder& builder)
+        : b(builder), sem_(b.Sem()), diagnostics_(builder.Diagnostics()) {}
 
     /// Destructor.
     ~UniformityGraph() {}
@@ -374,7 +374,7 @@
     }
 
   private:
-    const ProgramBuilder* builder_;
+    const ProgramBuilder& b;
     const sem::Info& sem_;
     diag::List& diagnostics_;
 
@@ -418,7 +418,7 @@
     /// @param func the function to process
     /// @returns true if there are no uniformity issues, false otherwise
     bool ProcessFunction(const ast::Function* func) {
-        current_function_ = functions_.Add(func, FunctionInfo(func, builder_)).value;
+        current_function_ = functions_.Add(func, FunctionInfo(func, b)).value;
 
         // Process function body.
         if (func->body) {
@@ -565,21 +565,22 @@
                 return cf_r;
             },
 
-            [&](const ast::BlockStatement* b) {
+            [&](const ast::BlockStatement* block) {
                 Hashmap<const sem::Variable*, Node*, 4> scoped_assignments;
+                auto* sem = sem_.Get(block);
                 {
                     // Push a new scope for variable assignments in the block.
                     current_function_->variables.Push();
                     TINT_DEFER(current_function_->variables.Pop());
 
-                    for (auto* s : b->statements) {
+                    for (auto* s : block->statements) {
                         cf = ProcessStatement(cf, s);
                         if (!sem_.Get(s)->Behaviors().Contains(sem::Behavior::kNext)) {
                             break;
                         }
                     }
 
-                    auto* parent = sem_.Get(b)->Parent();
+                    auto* parent = sem->Parent();
                     auto* loop = parent ? parent->As<sem::LoopStatement>() : nullptr;
                     if (loop) {
                         // We've reached the end of a loop body. If there is a continuing block,
@@ -587,7 +588,7 @@
                         // loop body are visible to the continuing block.
                         if (auto* continuing =
                                 loop->Declaration()->As<ast::LoopStatement>()->continuing) {
-                            auto& loop_body_behavior = sem_.Get(b)->Behaviors();
+                            auto& loop_body_behavior = sem->Behaviors();
                             if (loop_body_behavior.Contains(sem::Behavior::kNext) ||
                                 loop_body_behavior.Contains(sem::Behavior::kContinue)) {
                                 cf = ProcessStatement(cf, continuing);
@@ -595,7 +596,7 @@
                         }
                     }
 
-                    if (sem_.Get<sem::FunctionBlockStatement>(b)) {
+                    if (sem_.Get<sem::FunctionBlockStatement>(block)) {
                         // We've reached the end of the function body.
                         // Add edges from pointer parameter outputs to their current value.
                         for (auto& param : current_function_->parameters) {
@@ -611,7 +612,7 @@
 
                 // Propagate all variables assignments to the containing scope if the behavior is
                 // 'Next'.
-                auto& behaviors = sem_.Get(b)->Behaviors();
+                auto& behaviors = sem->Behaviors();
                 if (behaviors.Contains(sem::Behavior::kNext)) {
                     for (auto var : scoped_assignments) {
                         current_function_->variables.Set(var.key, var.value);
@@ -619,16 +620,16 @@
                 }
 
                 // Remove any variables declared in this scope from the set of in-scope variables.
-                for (auto decl : sem_.Get<sem::BlockStatement>(b)->Decls()) {
+                for (auto decl : sem->Decls()) {
                     current_function_->local_var_decls.Remove(decl.value.variable);
                 }
 
                 return cf;
             },
 
-            [&](const ast::BreakStatement* b) {
+            [&](const ast::BreakStatement* brk) {
                 // Find the loop or switch statement that we are in.
-                auto* parent = sem_.Get(b)
+                auto* parent = sem_.Get(brk)
                                    ->FindFirstParent<sem::SwitchStatement, sem::LoopStatement,
                                                      sem::ForLoopStatement, sem::WhileStatement>();
 
@@ -654,20 +655,21 @@
                 return cf;
             },
 
-            [&](const ast::BreakIfStatement* b) {
+            [&](const ast::BreakIfStatement* brk) {
                 // This works very similar to the IfStatement uniformity below, execpt instead of
                 // processing the body, we directly inline the BreakStatement uniformity from
                 // above.
+                auto* sem = sem_.Get(brk);
 
-                auto [_, v_cond] = ProcessExpression(cf, b->condition);
+                auto [_, v_cond] = ProcessExpression(cf, brk->condition);
 
                 // Add a diagnostic node to capture the control flow change.
-                auto* v = CreateNode({"break_if_stmt"}, b);
+                auto* v = CreateNode({"break_if_stmt"}, brk);
                 v->affects_control_flow = true;
                 v->AddEdge(v_cond);
 
                 {
-                    auto* parent = sem_.Get(b)->FindFirstParent<sem::LoopStatement>();
+                    auto* parent = sem->FindFirstParent<sem::LoopStatement>();
                     auto& info = current_function_->LoopSwitchInfoFor(parent);
 
                     // Propagate variable values to the loop exit nodes.
@@ -689,8 +691,7 @@
                     }
                 }
 
-                auto* sem_break_if = sem_.Get(b);
-                if (sem_break_if->Behaviors() != sem::Behaviors{sem::Behavior::kNext}) {
+                if (sem->Behaviors() != sem::Behaviors{sem::Behavior::kNext}) {
                     auto* cf_end = CreateNode({"break_if_CFend"});
                     cf_end->AddEdge(v);
                     return cf_end;
@@ -1178,7 +1179,7 @@
         auto has_nonuniform_entry_point_attribute = [&](auto* obj) {
             // Only the num_workgroups and workgroup_id builtins are uniform.
             if (auto* builtin_attr = ast::GetAttribute<ast::BuiltinAttribute>(obj->attributes)) {
-                auto builtin = builder_->Sem().Get(builtin_attr)->Value();
+                auto builtin = b.Sem().Get(builtin_attr)->Value();
                 if (builtin == core::BuiltinValue::kNumWorkgroups ||
                     builtin == core::BuiltinValue::kWorkgroupId) {
                     return false;
@@ -1316,29 +1317,29 @@
         return Switch(
             expr,
 
-            [&](const ast::BinaryExpression* b) {
-                if (b->IsLogical()) {
+            [&](const ast::BinaryExpression* e) {
+                if (e->IsLogical()) {
                     // Short-circuiting binary operators are a special case.
-                    auto [cf1, v1] = ProcessExpression(cf, b->lhs);
+                    auto [cf1, v1] = ProcessExpression(cf, e->lhs);
 
                     // Add a diagnostic node to capture the control flow change.
-                    auto* v1_cf = CreateNode({"short_circuit_op"}, b);
+                    auto* v1_cf = CreateNode({"short_circuit_op"}, e);
                     v1_cf->affects_control_flow = true;
                     v1_cf->AddEdge(v1);
 
-                    auto [cf2, v2] = ProcessExpression(v1_cf, b->rhs);
+                    auto [cf2, v2] = ProcessExpression(v1_cf, e->rhs);
                     return std::pair<Node*, Node*>(cf, v2);
                 } else {
-                    auto [cf1, v1] = ProcessExpression(cf, b->lhs);
-                    auto [cf2, v2] = ProcessExpression(cf1, b->rhs);
-                    auto* result = CreateNode({"binary_expr_result"}, b);
+                    auto [cf1, v1] = ProcessExpression(cf, e->lhs);
+                    auto [cf2, v2] = ProcessExpression(cf1, e->rhs);
+                    auto* result = CreateNode({"binary_expr_result"}, e);
                     result->AddEdge(v1);
                     result->AddEdge(v2);
                     return std::pair<Node*, Node*>(cf2, result);
                 }
             },
 
-            [&](const ast::BitcastExpression* b) { return ProcessExpression(cf, b->expr); },
+            [&](const ast::BitcastExpression* e) { return ProcessExpression(cf, e->expr); },
 
             [&](const ast::CallExpression* c) { return ProcessCall(cf, c); },
 
@@ -1977,7 +1978,7 @@
 
 }  // namespace
 
-bool AnalyzeUniformity(ProgramBuilder* builder, const DependencyGraph& dependency_graph) {
+bool AnalyzeUniformity(ProgramBuilder& builder, const DependencyGraph& dependency_graph) {
     UniformityGraph graph(builder);
     return graph.Build(dependency_graph);
 }
diff --git a/src/tint/lang/wgsl/resolver/uniformity.h b/src/tint/lang/wgsl/resolver/uniformity.h
index 7fa7e04..93e8a70 100644
--- a/src/tint/lang/wgsl/resolver/uniformity.h
+++ b/src/tint/lang/wgsl/resolver/uniformity.h
@@ -32,7 +32,7 @@
 /// @param builder the program to analyze
 /// @param dependency_graph the dependency-ordered module-scope declarations
 /// @returns true if there are no uniformity issues, false otherwise
-bool AnalyzeUniformity(ProgramBuilder* builder, const resolver::DependencyGraph& dependency_graph);
+bool AnalyzeUniformity(ProgramBuilder& builder, const resolver::DependencyGraph& dependency_graph);
 
 }  // namespace tint::resolver
 
diff --git a/src/tint/lang/wgsl/resolver/uniformity_test.cc b/src/tint/lang/wgsl/resolver/uniformity_test.cc
index 36568e9..dbad0bb 100644
--- a/src/tint/lang/wgsl/resolver/uniformity_test.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity_test.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
+
 #include <memory>
 #include <string>
 #include <tuple>
diff --git a/src/tint/lang/wgsl/resolver/unresolved_identifier.cc b/src/tint/lang/wgsl/resolver/unresolved_identifier.cc
new file mode 100644
index 0000000..54305f2
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/unresolved_identifier.cc
@@ -0,0 +1,27 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/wgsl/resolver/unresolved_identifier.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::resolver::UnresolvedIdentifier);
+
+namespace tint::resolver {
+
+UnresolvedIdentifier::UnresolvedIdentifier(const ast::IdentifierExpression* i,
+                                           const sem::Statement* stmt)
+    : Base(i, stmt) {}
+
+UnresolvedIdentifier::~UnresolvedIdentifier() = default;
+
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/unresolved_identifier.h b/src/tint/lang/wgsl/resolver/unresolved_identifier.h
new file mode 100644
index 0000000..ec3be23
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/unresolved_identifier.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_WGSL_RESOLVER_UNRESOLVED_IDENTIFIER_H_
+#define SRC_TINT_LANG_WGSL_RESOLVER_UNRESOLVED_IDENTIFIER_H_
+
+#include "src/tint/lang/wgsl/ast/identifier_expression.h"
+#include "src/tint/lang/wgsl/sem/expression.h"
+
+namespace tint::resolver {
+
+/// Represents an identifier that could not be resolved.
+class UnresolvedIdentifier : public Castable<UnresolvedIdentifier, sem::Expression> {
+  public:
+    /// Constructor
+    /// @param i the identifier that could not be resolved
+    /// @param statement the statement that owns this expression
+    UnresolvedIdentifier(const ast::IdentifierExpression* i, const sem::Statement* statement);
+
+    /// Destructor
+    ~UnresolvedIdentifier() override;
+
+    /// @returns the identifier that could not be resolved
+    const ast::IdentifierExpression* Identifier() const {
+        return static_cast<const ast::IdentifierExpression*>(declaration_);
+    }
+};
+
+}  // namespace tint::resolver
+
+#endif  // SRC_TINT_LANG_WGSL_RESOLVER_UNRESOLVED_IDENTIFIER_H_
diff --git a/src/tint/lang/wgsl/resolver/validation_test.cc b/src/tint/lang/wgsl/resolver/validation_test.cc
index 0634f94..ce98dac 100644
--- a/src/tint/lang/wgsl/resolver/validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/validation_test.cc
@@ -170,7 +170,7 @@
     WrapInFunction(assign);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved identifier 'b')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved value 'b')");
 }
 
 TEST_F(ResolverValidationTest, UsingUndefinedVariableInBlockStatement_Fail) {
@@ -185,7 +185,7 @@
     WrapInFunction(body);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved identifier 'b')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved value 'b')");
 }
 
 TEST_F(ResolverValidationTest, UsingUndefinedVariableGlobalVariable_Pass) {
@@ -225,7 +225,7 @@
     WrapInFunction(outer_body);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved identifier 'a')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved value 'a')");
 }
 
 TEST_F(ResolverValidationTest, UsingUndefinedVariableOuterScope_Pass) {
@@ -265,7 +265,7 @@
     WrapInFunction(outer_body);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved identifier 'a')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unresolved value 'a')");
 }
 
 TEST_F(ResolverValidationTest, AddressSpace_FunctionVariableWorkgroupClass) {
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 67aed69..de7befe 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -21,7 +21,6 @@
 
 #include "src/tint/lang/core/fluent_types.h"
 #include "src/tint/lang/core/type/abstract_numeric.h"
-#include "src/tint/lang/core/type/array.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/core/type/depth_multisampled_texture.h"
 #include "src/tint/lang/core/type/depth_texture.h"
@@ -53,6 +52,7 @@
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/break_if_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -202,7 +202,7 @@
 // https://gpuweb.github.io/gpuweb/wgsl/#plain-types-section
 bool Validator::IsPlain(const core::type::Type* type) const {
     return type->IsAnyOf<core::type::Scalar, core::type::Atomic, core::type::Vector,
-                         core::type::Matrix, core::type::Array, core::type::Struct>();
+                         core::type::Matrix, sem::Array, core::type::Struct>();
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#fixed-footprint-types
@@ -212,7 +212,7 @@
         [&](const core::type::Vector*) { return true; },  //
         [&](const core::type::Matrix*) { return true; },  //
         [&](const core::type::Atomic*) { return true; },
-        [&](const core::type::Array* arr) {
+        [&](const sem::Array* arr) {
             return !arr->Count()->Is<core::type::RuntimeArrayCount>() &&
                    IsFixedFootprint(arr->ElemType());
         },
@@ -236,7 +236,7 @@
         type,  //
         [&](const core::type::Vector* vec) { return IsHostShareable(vec->type()); },
         [&](const core::type::Matrix* mat) { return IsHostShareable(mat->type()); },
-        [&](const core::type::Array* arr) { return IsHostShareable(arr->ElemType()); },
+        [&](const sem::Array* arr) { return IsHostShareable(arr->ElemType()); },
         [&](const core::type::Struct* str) {
             for (auto* member : str->Members()) {
                 if (!IsHostShareable(member->Type())) {
@@ -307,6 +307,12 @@
         }
     }
 
+    if (auto* store_ty = s->StoreType(); !IsStorable(store_ty)) {
+        AddError(sem_.TypeNameOf(store_ty) + " cannot be used as the store type of a pointer",
+                 a->arguments[1]->source);
+        return false;
+    }
+
     return CheckTypeAccessAddressSpace(s->StoreType(), s->Access(), s->AddressSpace(), tint::Empty,
                                        a->source);
 }
@@ -416,7 +422,7 @@
 
     auto is_uniform_struct_or_array = [address_space](const core::type::Type* ty) {
         return address_space == core::AddressSpace::kUniform &&
-               ty->IsAnyOf<core::type::Array, core::type::Struct>();
+               ty->IsAnyOf<sem::Array, core::type::Struct>();
     };
 
     auto is_uniform_struct = [address_space](const core::type::Type* ty) {
@@ -523,7 +529,7 @@
     }
 
     // For uniform buffer array members, validate that array elements are aligned to 16 bytes
-    if (auto* arr = store_ty->As<core::type::Array>()) {
+    if (auto* arr = store_ty->As<sem::Array>()) {
         // Recurse into the element type.
         // TODO(crbug.com/tint/1388): Ideally we'd pass the source for nested element type here, but
         // we can't easily get that from the semantic node. We should consider recursing through the
@@ -1635,7 +1641,8 @@
             // used instead.
             auto* builtin = call->Target()->As<sem::BuiltinFn>();
             auto name = tint::ToString(builtin->Fn());
-            AddError("builtin '" + name + "' does not return a value", call->Declaration()->source);
+            AddError("builtin function '" + name + "' does not return a value",
+                     call->Declaration()->source);
             return false;
         }
     }
@@ -1888,7 +1895,7 @@
 }
 
 bool Validator::ArrayConstructor(const ast::CallExpression* ctor,
-                                 const core::type::Array* array_type) const {
+                                 const sem::Array* array_type) const {
     auto& values = ctor->args;
     auto* elem_ty = array_type->ElemType();
     for (auto* value : values) {
@@ -2068,7 +2075,7 @@
     return true;
 }
 
-bool Validator::Array(const core::type::Array* arr, const Source& el_source) const {
+bool Validator::Array(const sem::Array* arr, const Source& el_source) const {
     auto* el_ty = arr->ElemType();
 
     if (!IsPlain(el_ty)) {
@@ -2122,7 +2129,7 @@
     auto has_index = false;
     Hashset<std::pair<uint32_t, uint32_t>, 8> locationsAndIndexes;
     for (auto* member : str->Members()) {
-        if (auto* r = member->Type()->As<core::type::Array>()) {
+        if (auto* r = member->Type()->As<sem::Array>()) {
             if (r->Count()->Is<core::type::RuntimeArrayCount>()) {
                 if (member != str->Members().Back()) {
                     AddError("runtime arrays may only appear as the last member of a struct",
@@ -2606,7 +2613,7 @@
 }
 
 bool Validator::IsArrayWithOverrideCount(const core::type::Type* ty) const {
-    if (auto* arr = ty->UnwrapRef()->As<core::type::Array>()) {
+    if (auto* arr = ty->UnwrapRef()->As<sem::Array>()) {
         if (arr->Count()->IsAnyOf<sem::NamedOverrideArrayCount, sem::UnnamedOverrideArrayCount>()) {
             return true;
         }
@@ -2714,7 +2721,7 @@
             return true;
         },
         [&](const core::type::Struct*) { return check_sub_atomics(); },  //
-        [&](const core::type::Array*) { return check_sub_atomics(); },   //
+        [&](const sem::Array*) { return check_sub_atomics(); },          //
         [&](Default) { return true; });
 }
 
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index ecce5ba..e5df0f1 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -173,7 +173,7 @@
     /// @param el_source the source of the array element, or the array if the array does not have a
     ///        locally-declared element AST node.
     /// @returns true on success, false otherwise.
-    bool Array(const core::type::Array* arr, const Source& el_source) const;
+    bool Array(const sem::Array* arr, const Source& el_source) const;
 
     /// Validates an array stride attribute
     /// @param attr the stride attribute to validate
@@ -452,7 +452,7 @@
     /// @param ctor the call expresion to validate
     /// @param arr_type the type of the array
     /// @returns true on success, false otherwise
-    bool ArrayConstructor(const ast::CallExpression* ctor, const core::type::Array* arr_type) const;
+    bool ArrayConstructor(const ast::CallExpression* ctor, const sem::Array* arr_type) const;
 
     /// Validates a texture builtin function
     /// @param call the builtin call to validate
diff --git a/src/tint/lang/wgsl/resolver/validator_is_storeable_test.cc b/src/tint/lang/wgsl/resolver/validator_is_storeable_test.cc
index 8674a75..0290231 100644
--- a/src/tint/lang/wgsl/resolver/validator_is_storeable_test.cc
+++ b/src/tint/lang/wgsl/resolver/validator_is_storeable_test.cc
@@ -17,6 +17,7 @@
 #include "gmock/gmock.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 
 namespace tint::resolver {
 namespace {
@@ -89,14 +90,14 @@
 }
 
 TEST_F(ValidatorIsStorableTest, ArraySizedOfStorable) {
-    auto* arr = create<core::type::Array>(
-        create<core::type::I32>(), create<core::type::ConstantArrayCount>(5u), 4u, 20u, 4u, 4u);
+    auto* arr = create<sem::Array>(create<core::type::I32>(),
+                                   create<core::type::ConstantArrayCount>(5u), 4u, 20u, 4u, 4u);
     EXPECT_TRUE(v()->IsStorable(arr));
 }
 
 TEST_F(ValidatorIsStorableTest, ArrayUnsizedOfStorable) {
-    auto* arr = create<core::type::Array>(create<core::type::I32>(),
-                                          create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
+    auto* arr = create<sem::Array>(create<core::type::I32>(),
+                                   create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
     EXPECT_TRUE(v()->IsStorable(arr));
 }
 
diff --git a/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc b/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
index 0dcd3a1..c962e18 100644
--- a/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
@@ -15,6 +15,7 @@
 #include "gmock/gmock.h"
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+#include "src/tint/lang/wgsl/sem/array.h"
 #include "src/tint/lang/wgsl/sem/value_constructor.h"
 #include "src/tint/lang/wgsl/sem/value_conversion.h"
 #include "src/tint/utils/text/string_stream.h"
@@ -489,7 +490,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -505,7 +506,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -524,7 +525,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -543,7 +544,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -562,7 +563,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -581,7 +582,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -600,7 +601,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -621,7 +622,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
@@ -645,7 +646,7 @@
 
     auto* call = Sem().Get<sem::Call>(tc);
     ASSERT_NE(call, nullptr);
-    EXPECT_TRUE(call->Type()->Is<core::type::Array>());
+    EXPECT_TRUE(call->Type()->Is<sem::Array>());
     auto* ctor = call->Target()->As<sem::ValueConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
diff --git a/src/tint/lang/wgsl/resolver/variable_validation_test.cc b/src/tint/lang/wgsl/resolver/variable_validation_test.cc
index cfcc146..8fcbaf0 100644
--- a/src/tint/lang/wgsl/resolver/variable_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/variable_validation_test.cc
@@ -47,7 +47,8 @@
     WrapInFunction(Var("a", NoReturnValueBuiltin));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: builtin 'storageBarrier' does not return a value");
+    EXPECT_EQ(r()->error(),
+              "12:34 error: builtin function 'storageBarrier' does not return a value");
 }
 
 TEST_F(ResolverVariableValidationTest, GlobalVarInitializerNoReturnValueBuiltin) {
@@ -56,7 +57,8 @@
     GlobalVar("a", NoReturnValueBuiltin);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: builtin 'storageBarrier' does not return a value");
+    EXPECT_EQ(r()->error(),
+              "12:34 error: builtin function 'storageBarrier' does not return a value");
 }
 
 TEST_F(ResolverVariableValidationTest, GlobalVarNoAddressSpace) {
diff --git a/src/tint/lang/wgsl/sem/BUILD.bazel b/src/tint/lang/wgsl/sem/BUILD.bazel
index a014edf..a7abdd6 100644
--- a/src/tint/lang/wgsl/sem/BUILD.bazel
+++ b/src/tint/lang/wgsl/sem/BUILD.bazel
@@ -27,6 +27,7 @@
   name = "sem",
   srcs = [
     "accessor_expression.cc",
+    "array.cc",
     "array_count.cc",
     "behavior.cc",
     "block_statement.cc",
@@ -60,6 +61,7 @@
   ],
   hdrs = [
     "accessor_expression.h",
+    "array.h",
     "array_count.h",
     "behavior.h",
     "block_statement.h",
diff --git a/src/tint/lang/wgsl/sem/BUILD.cmake b/src/tint/lang/wgsl/sem/BUILD.cmake
index 6e545fa..29749a5 100644
--- a/src/tint/lang/wgsl/sem/BUILD.cmake
+++ b/src/tint/lang/wgsl/sem/BUILD.cmake
@@ -28,6 +28,8 @@
 tint_add_target(tint_lang_wgsl_sem lib
   lang/wgsl/sem/accessor_expression.cc
   lang/wgsl/sem/accessor_expression.h
+  lang/wgsl/sem/array.cc
+  lang/wgsl/sem/array.h
   lang/wgsl/sem/array_count.cc
   lang/wgsl/sem/array_count.h
   lang/wgsl/sem/behavior.cc
diff --git a/src/tint/lang/wgsl/sem/BUILD.gn b/src/tint/lang/wgsl/sem/BUILD.gn
index 2e87dae..f0465ba 100644
--- a/src/tint/lang/wgsl/sem/BUILD.gn
+++ b/src/tint/lang/wgsl/sem/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -33,6 +33,8 @@
   sources = [
     "accessor_expression.cc",
     "accessor_expression.h",
+    "array.cc",
+    "array.h",
     "array_count.cc",
     "array_count.h",
     "behavior.cc",
@@ -121,7 +123,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "builtin_fn_test.cc",
       "diagnostic_severity_test.cc",
diff --git a/src/tint/lang/wgsl/sem/array.cc b/src/tint/lang/wgsl/sem/array.cc
new file mode 100644
index 0000000..a2aae60
--- /dev/null
+++ b/src/tint/lang/wgsl/sem/array.cc
@@ -0,0 +1,40 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/wgsl/sem/array.h"
+
+#include "src/tint/lang/wgsl/sem/variable.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::Array);
+
+namespace tint::sem {
+
+Array::Array(core::type::Type const* element,
+             const core::type::ArrayCount* count,
+             uint32_t align,
+             uint32_t size,
+             uint32_t stride,
+             uint32_t implicit_stride)
+    : Base(element, count, align, size, stride, implicit_stride) {}
+
+Array::~Array() = default;
+
+void Array::AddTransitivelyReferencedOverride(const GlobalVariable* var) {
+    transitively_referenced_overrides_.Add(var);
+    for (auto* ref : var->TransitivelyReferencedOverrides()) {
+        AddTransitivelyReferencedOverride(ref);
+    }
+}
+
+}  // namespace tint::sem
diff --git a/src/tint/lang/wgsl/sem/array.h b/src/tint/lang/wgsl/sem/array.h
new file mode 100644
index 0000000..e634a0f
--- /dev/null
+++ b/src/tint/lang/wgsl/sem/array.h
@@ -0,0 +1,67 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0(the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_WGSL_SEM_ARRAY_H_
+#define SRC_TINT_LANG_WGSL_SEM_ARRAY_H_
+
+#include "src/tint/lang/core/type/array.h"
+#include "src/tint/utils/containers/unique_vector.h"
+
+/// Forward declarations
+namespace tint::sem {
+class GlobalVariable;
+}
+
+namespace tint::sem {
+
+/// Array holds the semantic information for Arrays.
+class Array final : public Castable<Array, core::type::Array> {
+  public:
+    /// Constructor
+    /// @param element the array element type
+    /// @param count the number of elements in the array.
+    /// @param align the byte alignment of the array
+    /// @param size the byte size of the array. The size will be 0 if the array element count is
+    ///        pipeline overridable.
+    /// @param stride the number of bytes from the start of one element of the
+    ///        array to the start of the next element
+    /// @param implicit_stride the number of bytes from the start of one element
+    /// of the array to the start of the next element, if there was no `@stride`
+    /// attribute applied.
+    Array(core::type::Type const* element,
+          const core::type::ArrayCount* count,
+          uint32_t align,
+          uint32_t size,
+          uint32_t stride,
+          uint32_t implicit_stride);
+
+    /// Destructor
+    ~Array() override;
+
+    /// Records that this variable (transitively) references the given override variable.
+    /// @param var the module-scope override variable
+    void AddTransitivelyReferencedOverride(const GlobalVariable* var);
+
+    /// @returns all transitively referenced override variables
+    VectorRef<const GlobalVariable*> TransitivelyReferencedOverrides() const {
+        return transitively_referenced_overrides_;
+    }
+
+  private:
+    UniqueVector<const GlobalVariable*, 4> transitively_referenced_overrides_;
+};
+
+}  // namespace tint::sem
+
+#endif  // SRC_TINT_LANG_WGSL_SEM_ARRAY_H_
diff --git a/src/tint/lang/wgsl/sem/builtin_enum_expression.cc b/src/tint/lang/wgsl/sem/builtin_enum_expression.cc
index fe5bff6..55081b8 100644
--- a/src/tint/lang/wgsl/sem/builtin_enum_expression.cc
+++ b/src/tint/lang/wgsl/sem/builtin_enum_expression.cc
@@ -14,8 +14,25 @@
 
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 
+#include "src/tint/lang/core/access.h"
+#include "src/tint/lang/core/address_space.h"
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/core/interpolation_sampling.h"
+#include "src/tint/lang/core/interpolation_type.h"
+#include "src/tint/lang/core/texel_format.h"
+#include "src/tint/lang/wgsl/builtin_fn.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpressionBase);
 
+// Specializations
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::Access>);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::AddressSpace>);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::BuiltinValue>);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::InterpolationSampling>);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::InterpolationType>);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::core::TexelFormat>);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::BuiltinEnumExpression<tint::wgsl::BuiltinFn>);
+
 namespace tint::sem {
 
 BuiltinEnumExpressionBase::BuiltinEnumExpressionBase(const ast::Expression* declaration,
diff --git a/src/tint/lang/wgsl/sem/function.cc b/src/tint/lang/wgsl/sem/function.cc
index 1d1b9e3..bb6a8a2 100644
--- a/src/tint/lang/wgsl/sem/function.cc
+++ b/src/tint/lang/wgsl/sem/function.cc
@@ -52,6 +52,14 @@
     return ret;
 }
 
+void Function::AddTransitivelyReferencedGlobal(const sem::GlobalVariable* global) {
+    if (transitively_referenced_globals_.Add(global)) {
+        for (auto* ref : global->TransitivelyReferencedOverrides()) {
+            AddTransitivelyReferencedGlobal(ref);
+        }
+    }
+}
+
 Function::VariableBindings Function::TransitivelyReferencedUniformVariables() const {
     VariableBindings ret;
 
diff --git a/src/tint/lang/wgsl/sem/function.h b/src/tint/lang/wgsl/sem/function.h
index 585ede0..8d01a14 100644
--- a/src/tint/lang/wgsl/sem/function.h
+++ b/src/tint/lang/wgsl/sem/function.h
@@ -86,11 +86,11 @@
     }
 
     /// Records that this function directly references the given global variable.
-    /// Note: Implicitly adds this global to the transtively-called globals.
+    /// Note: Implicitly adds this global to the transitively-called globals.
     /// @param global the module-scope variable
     void AddDirectlyReferencedGlobal(const sem::GlobalVariable* global) {
         directly_referenced_globals_.Add(global);
-        transitively_referenced_globals_.Add(global);
+        AddTransitivelyReferencedGlobal(global);
     }
 
     /// @returns all transitively referenced global variables
@@ -101,9 +101,7 @@
     /// Records that this function transitively references the given global
     /// variable.
     /// @param global the module-scoped variable
-    void AddTransitivelyReferencedGlobal(const sem::GlobalVariable* global) {
-        transitively_referenced_globals_.Add(global);
-    }
+    void AddTransitivelyReferencedGlobal(const sem::GlobalVariable* global);
 
     /// @returns the list of functions that this function transitively calls.
     const UniqueVector<const Function*, 8>& TransitivelyCalledFunctions() const {
@@ -131,9 +129,10 @@
     /// that this function uses (directly or indirectly). These can only
     /// be parameters to this function or global variables. Uniqueness is
     /// ensured by texture_sampler_pairs_ being a UniqueVector.
-    /// @param texture the texture (must be non-null)
+    /// @param texture the texture (null indicates a sampler-only reference)
     /// @param sampler the sampler (null indicates a texture-only reference)
     void AddTextureSamplerPair(const sem::Variable* texture, const sem::Variable* sampler) {
+        TINT_ASSERT(texture || sampler);
         texture_sampler_pairs_.Add(VariablePair(texture, sampler));
     }
 
diff --git a/src/tint/lang/wgsl/sem/helper_test.h b/src/tint/lang/wgsl/sem/helper_test.h
index ece8694..fb45117 100644
--- a/src/tint/lang/wgsl/sem/helper_test.h
+++ b/src/tint/lang/wgsl/sem/helper_test.h
@@ -36,8 +36,11 @@
         return resolver::Resolve(*this);
     }
 };
+
+/// An alias to TestHelper templated without a test parameter
 using TestHelper = TestHelperBase<testing::Test>;
 
+/// An alias to TestHelper templated with the parameter type T
 template <typename T>
 using TestParamHelper = TestHelperBase<testing::TestWithParam<T>>;
 
diff --git a/src/tint/lang/wgsl/sem/info.h b/src/tint/lang/wgsl/sem/info.h
index 5a41a3f..a4ab07c 100644
--- a/src/tint/lang/wgsl/sem/info.h
+++ b/src/tint/lang/wgsl/sem/info.h
@@ -51,9 +51,6 @@
     using GetResultType =
         std::conditional_t<std::is_same<SEM, InferFromAST>::value, SemanticNodeTypeFor<AST>, SEM>;
 
-    /// Alias to a unique vector of transitively referenced global variables
-    using TransitivelyReferenced = UniqueVector<const GlobalVariable*, 4>;
-
     /// Constructor
     Info();
 
@@ -138,25 +135,6 @@
     /// @returns the semantic module.
     const sem::Module* Module() const { return module_; }
 
-    /// Records that this variable (transitively) references the given override variable.
-    /// @param from the item the variable is referenced from
-    /// @param var the module-scope override variable
-    void AddTransitivelyReferencedOverride(const CastableBase* from, const GlobalVariable* var) {
-        if (referenced_overrides_.count(from) == 0) {
-            referenced_overrides_.insert({from, TransitivelyReferenced{}});
-        }
-        referenced_overrides_[from].Add(var);
-    }
-
-    /// @param from the key to look up
-    /// @returns all transitively referenced override variables or nullptr if none set
-    const TransitivelyReferenced* TransitivelyReferencedOverrides(const CastableBase* from) const {
-        if (referenced_overrides_.count(from) == 0) {
-            return nullptr;
-        }
-        return &referenced_overrides_.at(from);
-    }
-
     /// Determines the severity of a filterable diagnostic rule for the AST node `ast_node`.
     /// @param ast_node the AST node
     /// @param rule the diagnostic rule
@@ -167,8 +145,6 @@
   private:
     // AST node index to semantic node
     std::vector<const CastableBase*> nodes_;
-    // Lists transitively referenced overrides for the given item
-    std::unordered_map<const CastableBase*, TransitivelyReferenced> referenced_overrides_;
     // The semantic module
     sem::Module* module_ = nullptr;
 };
diff --git a/src/tint/lang/wgsl/sem/pipeline_stage_set.h b/src/tint/lang/wgsl/sem/pipeline_stage_set.h
index c21796f..417235b 100644
--- a/src/tint/lang/wgsl/sem/pipeline_stage_set.h
+++ b/src/tint/lang/wgsl/sem/pipeline_stage_set.h
@@ -20,6 +20,7 @@
 
 namespace tint::sem {
 
+/// A set of PipelineStage
 using PipelineStageSet = tint::EnumSet<ast::PipelineStage>;
 
 }  // namespace tint::sem
diff --git a/src/tint/lang/wgsl/sem/type_expression.h b/src/tint/lang/wgsl/sem/type_expression.h
index c8f9563..4114b63 100644
--- a/src/tint/lang/wgsl/sem/type_expression.h
+++ b/src/tint/lang/wgsl/sem/type_expression.h
@@ -41,8 +41,12 @@
     /// @return the type that the expression resolved to
     const core::type::Type* Type() const { return type_; }
 
+    /// Sets the type that this expression resolved to
+    /// @param type the new type
+    void SetType(const core::type::Type* type) { type_ = type; }
+
   private:
-    core::type::Type const* const type_;
+    const core::type::Type* type_ = nullptr;
 };
 
 }  // namespace tint::sem
diff --git a/src/tint/lang/wgsl/sem/type_mappings.h b/src/tint/lang/wgsl/sem/type_mappings.h
index 2dfb6ac..7518167 100644
--- a/src/tint/lang/wgsl/sem/type_mappings.h
+++ b/src/tint/lang/wgsl/sem/type_mappings.h
@@ -27,6 +27,7 @@
 class AccessorExpression;
 class BinaryExpression;
 class BitcastExpression;
+class BlockStatement;
 class BuiltinAttribute;
 class CallExpression;
 class Expression;
@@ -42,14 +43,15 @@
 class StructMember;
 class SwitchStatement;
 class TypeDecl;
+class UnaryOpExpression;
 class Variable;
 class WhileStatement;
-class UnaryOpExpression;
 }  // namespace tint::ast
 namespace tint::core {
 enum class BuiltinValue : uint8_t;
 }
 namespace tint::sem {
+class BlockStatement;
 class Expression;
 class ForLoopStatement;
 class Function;
@@ -77,6 +79,7 @@
 /// rules will be used to infer the return type based on the argument type.
 struct TypeMappings {
     //! @cond Doxygen_Suppress
+    BlockStatement* operator()(ast::BlockStatement*);
     BuiltinEnumExpression<core::BuiltinValue>* operator()(ast::BuiltinAttribute*);
     CastableBase* operator()(ast::Node*);
     Expression* operator()(ast::Expression*);
diff --git a/src/tint/lang/wgsl/sem/variable.cc b/src/tint/lang/wgsl/sem/variable.cc
index bfff38f..54849f2 100644
--- a/src/tint/lang/wgsl/sem/variable.cc
+++ b/src/tint/lang/wgsl/sem/variable.cc
@@ -28,62 +28,34 @@
 TINT_INSTANTIATE_TYPEINFO(tint::sem::VariableUser);
 
 namespace tint::sem {
-Variable::Variable(const ast::Variable* declaration,
-                   const core::type::Type* type,
-                   core::EvaluationStage stage,
-                   core::AddressSpace address_space,
-                   core::Access access,
-                   const core::constant::Value* constant_value)
-    : declaration_(declaration),
-      type_(type),
-      stage_(stage),
-      address_space_(address_space),
-      access_(access),
-      constant_value_(constant_value) {}
+Variable::Variable(const ast::Variable* declaration) : declaration_(declaration) {}
 
 Variable::~Variable() = default;
 
-LocalVariable::LocalVariable(const ast::Variable* declaration,
-                             const core::type::Type* type,
-                             core::EvaluationStage stage,
-                             core::AddressSpace address_space,
-                             core::Access access,
-                             const sem::Statement* statement,
-                             const core::constant::Value* constant_value)
-    : Base(declaration, type, stage, address_space, access, constant_value),
-      statement_(statement) {}
+LocalVariable::LocalVariable(const ast::Variable* declaration, const sem::Statement* statement)
+    : Base(declaration), statement_(statement) {}
 
 LocalVariable::~LocalVariable() = default;
 
-GlobalVariable::GlobalVariable(const ast::Variable* declaration,
-                               const core::type::Type* type,
-                               core::EvaluationStage stage,
-                               core::AddressSpace address_space,
-                               core::Access access,
-                               const core::constant::Value* constant_value,
-                               std::optional<tint::BindingPoint> binding_point,
-                               std::optional<uint32_t> location,
-                               std::optional<uint32_t> index)
-    : Base(declaration, type, stage, address_space, access, constant_value),
-      binding_point_(binding_point),
-      location_(location),
-      index_(index) {}
+GlobalVariable::GlobalVariable(const ast::Variable* declaration) : Base(declaration) {}
 
 GlobalVariable::~GlobalVariable() = default;
 
+void GlobalVariable::AddTransitivelyReferencedOverride(const GlobalVariable* var) {
+    if (transitively_referenced_overrides_.Add(var)) {
+        for (auto* ref : var->TransitivelyReferencedOverrides()) {
+            AddTransitivelyReferencedOverride(ref);
+        }
+    }
+}
+
 Parameter::Parameter(const ast::Parameter* declaration,
-                     uint32_t index,
-                     const core::type::Type* type,
-                     core::AddressSpace address_space,
-                     core::Access access,
-                     const core::ParameterUsage usage /* = ParameterUsage::kNone */,
-                     std::optional<tint::BindingPoint> binding_point /* = {} */,
-                     std::optional<uint32_t> location /* = std::nullopt */)
-    : Base(declaration, type, core::EvaluationStage::kRuntime, address_space, access, nullptr),
-      index_(index),
-      usage_(usage),
-      binding_point_(binding_point),
-      location_(location) {}
+                     uint32_t index /* = 0 */,
+                     const core::type::Type* type /* = nullptr */,
+                     core::ParameterUsage usage /* = core::ParameterUsage::kNone */)
+    : Base(declaration), index_(index), usage_(usage) {
+    SetType(type);
+}
 
 Parameter::~Parameter() = default;
 
diff --git a/src/tint/lang/wgsl/sem/variable.h b/src/tint/lang/wgsl/sem/variable.h
index a894a83..7a7b826 100644
--- a/src/tint/lang/wgsl/sem/variable.h
+++ b/src/tint/lang/wgsl/sem/variable.h
@@ -49,17 +49,7 @@
   public:
     /// Constructor
     /// @param declaration the AST declaration node
-    /// @param type the variable type
-    /// @param stage the evaluation stage for an expression of this variable type
-    /// @param address_space the variable address space
-    /// @param access the variable access control type
-    /// @param constant_value the constant value for the variable. May be null
-    Variable(const ast::Variable* declaration,
-             const core::type::Type* type,
-             core::EvaluationStage stage,
-             core::AddressSpace address_space,
-             core::Access access,
-             const core::constant::Value* constant_value);
+    explicit Variable(const ast::Variable* declaration);
 
     /// Destructor
     ~Variable() override;
@@ -67,29 +57,44 @@
     /// @returns the AST declaration node
     const ast::Variable* Declaration() const { return declaration_; }
 
+    /// @param type the variable type
+    void SetType(const core::type::Type* type) { type_ = type; }
+
     /// @returns the canonical type for the variable
     const core::type::Type* Type() const { return type_; }
 
+    /// @param stage the evaluation stage for an expression of this variable type
+    void SetStage(core::EvaluationStage stage) { stage_ = stage; }
+
     /// @returns the evaluation stage for an expression of this variable type
     core::EvaluationStage Stage() const { return stage_; }
 
+    /// @param space the variable address space
+    void SetAddressSpace(core::AddressSpace space) { address_space_ = space; }
+
     /// @returns the address space for the variable
     core::AddressSpace AddressSpace() const { return address_space_; }
 
+    /// @param access the variable access control type
+    void SetAccess(core::Access access) { access_ = access; }
+
     /// @returns the access control for the variable
     core::Access Access() const { return access_; }
 
+    /// @param value the constant value for the variable. May be null
+    void SetConstantValue(const core::constant::Value* value) { constant_value_ = value; }
+
     /// @return the constant value of this expression
     const core::constant::Value* ConstantValue() const { return constant_value_; }
 
-    /// @returns the variable initializer expression, or nullptr if the variable
-    /// does not have one.
-    const ValueExpression* Initializer() const { return initializer_; }
-
     /// Sets the variable initializer expression.
     /// @param initializer the initializer expression to assign to this variable.
     void SetInitializer(const ValueExpression* initializer) { initializer_ = initializer; }
 
+    /// @returns the variable initializer expression, or nullptr if the variable
+    /// does not have one.
+    const ValueExpression* Initializer() const { return initializer_; }
+
     /// @returns the expressions that use the variable
     VectorRef<const VariableUser*> Users() const { return users_; }
 
@@ -97,12 +102,12 @@
     void AddUser(const VariableUser* user) { users_.Push(user); }
 
   private:
-    const ast::Variable* const declaration_;
-    const core::type::Type* const type_;
-    const core::EvaluationStage stage_;
-    const core::AddressSpace address_space_;
-    const core::Access access_;
-    const core::constant::Value* constant_value_;
+    const ast::Variable* const declaration_ = nullptr;
+    const core::type::Type* type_ = nullptr;
+    core::EvaluationStage stage_ = core::EvaluationStage::kRuntime;
+    core::AddressSpace address_space_ = core::AddressSpace::kUndefined;
+    core::Access access_ = core::Access::kUndefined;
+    const core::constant::Value* constant_value_ = nullptr;
     const ValueExpression* initializer_ = nullptr;
     tint::Vector<const VariableUser*, 8> users_;
 };
@@ -112,19 +117,8 @@
   public:
     /// Constructor
     /// @param declaration the AST declaration node
-    /// @param type the variable type
-    /// @param stage the evaluation stage for an expression of this variable type
-    /// @param address_space the variable address space
-    /// @param access the variable access control type
     /// @param statement the statement that declared this local variable
-    /// @param constant_value the constant value for the variable. May be null
-    LocalVariable(const ast::Variable* declaration,
-                  const core::type::Type* type,
-                  core::EvaluationStage stage,
-                  core::AddressSpace address_space,
-                  core::Access access,
-                  const sem::Statement* statement,
-                  const core::constant::Value* constant_value);
+    LocalVariable(const ast::Variable* declaration, const sem::Statement* statement);
 
     /// Destructor
     ~LocalVariable() override;
@@ -132,13 +126,13 @@
     /// @returns the statement that declares this local variable
     const sem::Statement* Statement() const { return statement_; }
 
-    /// @returns the Type, Function or Variable that this local variable shadows
-    const CastableBase* Shadows() const { return shadows_; }
-
     /// Sets the Type, Function or Variable that this local variable shadows
     /// @param shadows the Type, Function or Variable that this variable shadows
     void SetShadows(const CastableBase* shadows) { shadows_ = shadows; }
 
+    /// @returns the Type, Function or Variable that this local variable shadows
+    const CastableBase* Shadows() const { return shadows_; }
+
   private:
     const sem::Statement* const statement_;
     const CastableBase* shadows_ = nullptr;
@@ -149,30 +143,16 @@
   public:
     /// Constructor
     /// @param declaration the AST declaration node
-    /// @param type the variable type
-    /// @param stage the evaluation stage for an expression of this variable type
-    /// @param address_space the variable address space
-    /// @param access the variable access control type
-    /// @param constant_value the constant value for the variable. May be null
-    /// @param binding_point the optional resource binding point of the variable
-    /// @param location the location value if provided
-    /// @param index the index value if provided
-    ///
-    /// Note, a GlobalVariable generally doesn't have a `location` in WGSL, as it isn't allowed by
-    /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
-    GlobalVariable(const ast::Variable* declaration,
-                   const core::type::Type* type,
-                   core::EvaluationStage stage,
-                   core::AddressSpace address_space,
-                   core::Access access,
-                   const core::constant::Value* constant_value,
-                   std::optional<tint::BindingPoint> binding_point = std::nullopt,
-                   std::optional<uint32_t> location = std::nullopt,
-                   std::optional<uint32_t> index = std::nullopt);
+    explicit GlobalVariable(const ast::Variable* declaration);
 
     /// Destructor
     ~GlobalVariable() override;
 
+    /// @param binding_point the resource binding point for the parameter
+    void SetBindingPoint(std::optional<tint::BindingPoint> binding_point) {
+        binding_point_ = binding_point;
+    }
+
     /// @returns the resource binding point for the variable
     std::optional<tint::BindingPoint> BindingPoint() const { return binding_point_; }
 
@@ -182,40 +162,49 @@
     /// @returns the pipeline constant ID associated with the variable
     tint::OverrideId OverrideId() const { return override_id_; }
 
+    /// @param location the location value for the parameter, if set
+    /// @note a GlobalVariable generally doesn't have a `location` in WGSL, as it isn't allowed by
+    /// the spec. The location maybe attached by transforms such as CanonicalizeEntryPointIO.
+    void SetLocation(std::optional<uint32_t> location) { location_ = location; }
+
     /// @returns the location value for the parameter, if set
     std::optional<uint32_t> Location() const { return location_; }
 
+    /// @param index the index value for the parameter, if set
+    void SetIndex(std::optional<uint32_t> index) { index_ = index; }
+
     /// @returns the index value for the parameter, if set
     std::optional<uint32_t> Index() const { return index_; }
 
-  private:
-    const std::optional<tint::BindingPoint> binding_point_;
+    /// Records that this variable (transitively) references the given override variable.
+    /// @param var the module-scope override variable
+    void AddTransitivelyReferencedOverride(const GlobalVariable* var);
 
+    /// @returns all transitively referenced override variables
+    VectorRef<const GlobalVariable*> TransitivelyReferencedOverrides() const {
+        return transitively_referenced_overrides_;
+    }
+
+  private:
+    std::optional<tint::BindingPoint> binding_point_;
     tint::OverrideId override_id_;
     std::optional<uint32_t> location_;
     std::optional<uint32_t> index_;
+    UniqueVector<const GlobalVariable*, 4> transitively_referenced_overrides_;
 };
 
 /// Parameter is a function parameter
 class Parameter final : public Castable<Parameter, Variable> {
   public:
-    /// Constructor for function parameters
+    /// Constructor
     /// @param declaration the AST declaration node
-    /// @param index the index of the parmeter in the function
+    /// @param index the index of the parameter in the function
     /// @param type the variable type
-    /// @param address_space the variable address space
-    /// @param access the variable access control type
-    /// @param usage the semantic usage for the parameter
-    /// @param binding_point the optional resource binding point of the parameter
-    /// @param location the location value, if set
+    /// @param usage the parameter usage
     Parameter(const ast::Parameter* declaration,
-              uint32_t index,
-              const core::type::Type* type,
-              core::AddressSpace address_space,
-              core::Access access,
-              const core::ParameterUsage usage = core::ParameterUsage::kNone,
-              std::optional<tint::BindingPoint> binding_point = {},
-              std::optional<uint32_t> location = std::nullopt);
+              uint32_t index = 0,
+              const core::type::Type* type = nullptr,
+              core::ParameterUsage usage = core::ParameterUsage::kNone);
 
     /// Destructor
     ~Parameter() override;
@@ -225,38 +214,52 @@
         return static_cast<const ast::Parameter*>(Variable::Declaration());
     }
 
+    /// @param index the index value for the parameter, if set
+    void SetIndex(uint32_t index) { index_ = index; }
+
     /// @return the index of the parameter in the function
     uint32_t Index() const { return index_; }
 
+    /// @param usage the semantic usage for the parameter
+    void SetUsage(core::ParameterUsage usage) { usage_ = usage; }
+
     /// @returns the semantic usage for the parameter
     core::ParameterUsage Usage() const { return usage_; }
 
-    /// @returns the CallTarget owner of this parameter
-    CallTarget const* Owner() const { return owner_; }
-
     /// @param owner the CallTarget owner of this parameter
-    void SetOwner(CallTarget const* owner) { owner_ = owner; }
+    void SetOwner(const CallTarget* owner) { owner_ = owner; }
 
-    /// @returns the Type, Function or Variable that this local variable shadows
-    const CastableBase* Shadows() const { return shadows_; }
+    /// @returns the CallTarget owner of this parameter
+    const CallTarget* Owner() const { return owner_; }
 
     /// Sets the Type, Function or Variable that this local variable shadows
     /// @param shadows the Type, Function or Variable that this variable shadows
     void SetShadows(const CastableBase* shadows) { shadows_ = shadows; }
 
+    /// @returns the Type, Function or Variable that this local variable shadows
+    const CastableBase* Shadows() const { return shadows_; }
+
+    /// @param binding_point the resource binding point for the parameter
+    void SetBindingPoint(std::optional<tint::BindingPoint> binding_point) {
+        binding_point_ = binding_point;
+    }
+
     /// @returns the resource binding point for the parameter
     std::optional<tint::BindingPoint> BindingPoint() const { return binding_point_; }
 
+    /// @param location the location value for the parameter, if set
+    void SetLocation(std::optional<uint32_t> location) { location_ = location; }
+
     /// @returns the location value for the parameter, if set
     std::optional<uint32_t> Location() const { return location_; }
 
   private:
-    const uint32_t index_;
-    const core::ParameterUsage usage_;
+    uint32_t index_ = 0;
+    core::ParameterUsage usage_ = core::ParameterUsage::kNone;
     CallTarget const* owner_ = nullptr;
     const CastableBase* shadows_ = nullptr;
-    const std::optional<tint::BindingPoint> binding_point_;
-    const std::optional<uint32_t> location_;
+    std::optional<tint::BindingPoint> binding_point_;
+    std::optional<uint32_t> location_;
 };
 
 /// VariableUser holds the semantic information for an identifier expression
diff --git a/src/tint/lang/wgsl/writer/BUILD.bazel b/src/tint/lang/wgsl/writer/BUILD.bazel
index 36cadd8..262e529 100644
--- a/src/tint/lang/wgsl/writer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/BUILD.bazel
@@ -45,7 +45,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer/ast_printer",
     "//src/tint/lang/wgsl/writer/ir_to_program",
     "//src/tint/lang/wgsl/writer/raise",
     "//src/tint/lang/wgsl/writer/syntax_tree_printer",
@@ -63,17 +62,23 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
-  ],
+  ] + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer/ast_printer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "writer_bench.cc",
   ],
   deps = [
-    "//src/tint/cmd/bench",
+    "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
@@ -81,7 +86,6 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -95,8 +99,19 @@
     "//src/tint/utils/symbol",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
-  ],
+    "@benchmark",
+  ] + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/lang/wgsl/writer/BUILD.cfg b/src/tint/lang/wgsl/writer/BUILD.cfg
new file mode 100644
index 0000000..80e6a14
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_wgsl_writer"
+}
diff --git a/src/tint/lang/wgsl/writer/BUILD.cmake b/src/tint/lang/wgsl/writer/BUILD.cmake
index c9893f6..014df3e 100644
--- a/src/tint/lang/wgsl/writer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/BUILD.cmake
@@ -26,9 +26,11 @@
 include(lang/wgsl/writer/raise/BUILD.cmake)
 include(lang/wgsl/writer/syntax_tree_printer/BUILD.cmake)
 
+if(TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_wgsl_writer
 # Kind:      lib
+# Condition: TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer lib
   lang/wgsl/writer/options.cc
@@ -49,7 +51,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer_ast_printer
   tint_lang_wgsl_writer_ir_to_program
   tint_lang_wgsl_writer_raise
   tint_lang_wgsl_writer_syntax_tree_printer
@@ -69,16 +70,25 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_writer lib
+    tint_lang_wgsl_writer_ast_printer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_WGSL_WRITER)
+if(TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_wgsl_writer_bench
 # Kind:      bench
+# Condition: TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_bench bench
   lang/wgsl/writer/writer_bench.cc
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_writer_bench bench
-  tint_cmd_bench
+  tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -86,7 +96,6 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -101,3 +110,15 @@
   tint_utils_text
   tint_utils_traits
 )
+
+tint_target_add_external_dependencies(tint_lang_wgsl_writer_bench bench
+  "google-benchmark"
+)
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_writer_bench bench
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/writer/BUILD.gn b/src/tint/lang/wgsl/writer/BUILD.gn
index 21b27a4..2c531db 100644
--- a/src/tint/lang/wgsl/writer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/BUILD.gn
@@ -25,42 +25,85 @@
 
 import("${tint_src_dir}/tint.gni")
 
-libtint_source_set("writer") {
-  sources = [
-    "options.cc",
-    "options.h",
-    "output.cc",
-    "output.h",
-    "writer.cc",
-    "writer.h",
-  ]
-  deps = [
-    "${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/program",
-    "${tint_src_dir}/lang/wgsl/sem",
-    "${tint_src_dir}/lang/wgsl/writer/ast_printer",
-    "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
-    "${tint_src_dir}/lang/wgsl/writer/raise",
-    "${tint_src_dir}/lang/wgsl/writer/syntax_tree_printer",
-    "${tint_src_dir}/utils/containers",
-    "${tint_src_dir}/utils/diagnostic",
-    "${tint_src_dir}/utils/generator",
-    "${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_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_wgsl_writer) {
+  libtint_source_set("writer") {
+    sources = [
+      "options.cc",
+      "options.h",
+      "output.cc",
+      "output.h",
+      "writer.cc",
+      "writer.h",
+    ]
+    deps = [
+      "${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/program",
+      "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
+      "${tint_src_dir}/lang/wgsl/writer/raise",
+      "${tint_src_dir}/lang/wgsl/writer/syntax_tree_printer",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/generator",
+      "${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_wgsl_writer) {
+      deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer" ]
+    }
+  }
+}
+if (tint_build_benchmarks) {
+  if (tint_build_wgsl_writer) {
+    tint_unittests_source_set("bench") {
+      sources = [ "writer_bench.cc" ]
+      deps = [
+        "${tint_src_dir}:google_benchmark",
+        "${tint_src_dir}/cmd/bench:bench",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${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_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+      }
+    }
+  }
 }
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
index b80b144..f40c5b2 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
@@ -103,7 +103,6 @@
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer/ast_printer",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/generator",
@@ -119,8 +118,18 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer/ast_printer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cfg b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cfg
new file mode 100644
index 0000000..80e6a14
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_wgsl_writer"
+}
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
index 96beb17..92526ed 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
@@ -21,9 +21,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
+if(TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_wgsl_writer_ast_printer
 # Kind:      lib
+# Condition: TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_ast_printer lib
   lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -54,9 +56,12 @@
   tint_utils_traits
 )
 
+endif(TINT_BUILD_WGSL_WRITER)
+if(TINT_BUILD_WGSL_WRITER)
 ################################################################################
 # Target:    tint_lang_wgsl_writer_ast_printer_test
 # Kind:      test
+# Condition: TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_ast_printer_test test
   lang/wgsl/writer/ast_printer/alias_type_test.cc
@@ -102,7 +107,6 @@
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer_ast_printer
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_generator
@@ -122,3 +126,11 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_writer_ast_printer_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_dependencies(tint_lang_wgsl_writer_ast_printer_test test
+    tint_lang_wgsl_writer_ast_printer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
+
+endif(TINT_BUILD_WGSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
index dcdec55..7a31589 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
@@ -25,87 +25,23 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
-
-libtint_source_set("ast_printer") {
-  sources = [
-    "ast_printer.cc",
-    "ast_printer.h",
-  ]
-  deps = [
-    "${tint_src_dir}/lang/core",
-    "${tint_src_dir}/lang/core/constant",
-    "${tint_src_dir}/lang/core/type",
-    "${tint_src_dir}/lang/wgsl",
-    "${tint_src_dir}/lang/wgsl/ast",
-    "${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/generator",
-    "${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/result",
-    "${tint_src_dir}/utils/rtti",
-    "${tint_src_dir}/utils/strconv",
-    "${tint_src_dir}/utils/symbol",
-    "${tint_src_dir}/utils/text",
-    "${tint_src_dir}/utils/traits",
-  ]
-}
-if (tint_build_unittests) {
-  tint_unittests_source_set("unittests") {
-    testonly = true
+if (tint_build_wgsl_writer) {
+  libtint_source_set("ast_printer") {
     sources = [
-      "alias_type_test.cc",
-      "array_accessor_test.cc",
-      "assign_test.cc",
-      "ast_printer_test.cc",
-      "binary_test.cc",
-      "bitcast_test.cc",
-      "block_test.cc",
-      "break_test.cc",
-      "call_test.cc",
-      "case_test.cc",
-      "cast_test.cc",
-      "const_assert_test.cc",
-      "constructor_test.cc",
-      "continue_test.cc",
-      "diagnostic_test.cc",
-      "discard_test.cc",
-      "enable_test.cc",
-      "function_test.cc",
-      "global_decl_test.cc",
-      "helper_test.h",
-      "identifier_test.cc",
-      "if_test.cc",
-      "literal_test.cc",
-      "loop_test.cc",
-      "member_accessor_test.cc",
-      "return_test.cc",
-      "switch_test.cc",
-      "type_test.cc",
-      "unary_op_test.cc",
-      "variable_decl_statement_test.cc",
-      "variable_test.cc",
+      "ast_printer.cc",
+      "ast_printer.h",
     ]
     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/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
-      "${tint_src_dir}/lang/wgsl/writer/ast_printer",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/generator",
@@ -114,12 +50,81 @@
       "${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/strconv",
       "${tint_src_dir}/utils/symbol",
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
   }
 }
+if (tint_build_unittests) {
+  if (tint_build_wgsl_writer) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "alias_type_test.cc",
+        "array_accessor_test.cc",
+        "assign_test.cc",
+        "ast_printer_test.cc",
+        "binary_test.cc",
+        "bitcast_test.cc",
+        "block_test.cc",
+        "break_test.cc",
+        "call_test.cc",
+        "case_test.cc",
+        "cast_test.cc",
+        "const_assert_test.cc",
+        "constructor_test.cc",
+        "continue_test.cc",
+        "diagnostic_test.cc",
+        "discard_test.cc",
+        "enable_test.cc",
+        "function_test.cc",
+        "global_decl_test.cc",
+        "helper_test.h",
+        "identifier_test.cc",
+        "if_test.cc",
+        "literal_test.cc",
+        "loop_test.cc",
+        "member_accessor_test.cc",
+        "return_test.cc",
+        "switch_test.cc",
+        "type_test.cc",
+        "unary_op_test.cc",
+        "variable_decl_statement_test.cc",
+        "variable_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/type",
+        "${tint_src_dir}/lang/wgsl",
+        "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/program",
+        "${tint_src_dir}/lang/wgsl/resolver",
+        "${tint_src_dir}/lang/wgsl/sem",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/generator",
+        "${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_wgsl_writer) {
+        deps += [ "${tint_src_dir}/lang/wgsl/writer/ast_printer" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
index c67c82f..4e76e72 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
@@ -68,11 +68,15 @@
   name = "test",
   alwayslink = True,
   srcs = [
-    "inlining_test.cc",
-    "ir_to_program_test.cc",
     "ir_to_program_test.h",
     "rename_conflicts_test.cc",
-  ],
+  ] + select({
+    ":tint_build_wgsl_writer": [
+      "inlining_test.cc",
+      "ir_to_program_test.cc",
+    ],
+    "//conditions:default": [],
+  }),
   deps = [
     "//src/tint/api/common",
     "//src/tint/lang/core",
@@ -83,9 +87,10 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/intrinsic",
+    "//src/tint/lang/wgsl/ir",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
-    "//src/tint/lang/wgsl/writer",
     "//src/tint/lang/wgsl/writer/ir_to_program",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -101,8 +106,18 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
-  ],
+  ] + select({
+    ":tint_build_wgsl_writer": [
+      "//src/tint/lang/wgsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
   copts = COPTS,
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
index 1fa4481..b9b7546 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
@@ -66,8 +66,6 @@
 # Kind:      test
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_ir_to_program_test test
-  lang/wgsl/writer/ir_to_program/inlining_test.cc
-  lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
   lang/wgsl/writer/ir_to_program/ir_to_program_test.h
   lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
 )
@@ -82,9 +80,10 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_intrinsic
+  tint_lang_wgsl_ir
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
-  tint_lang_wgsl_writer
   tint_lang_wgsl_writer_ir_to_program
   tint_utils_containers
   tint_utils_diagnostic
@@ -104,3 +103,13 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_writer_ir_to_program_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_WRITER)
+  tint_target_add_sources(tint_lang_wgsl_writer_ir_to_program_test test
+    "lang/wgsl/writer/ir_to_program/inlining_test.cc"
+    "lang/wgsl/writer/ir_to_program/ir_to_program_test.cc"
+  )
+  tint_target_add_dependencies(tint_lang_wgsl_writer_ir_to_program_test test
+    tint_lang_wgsl_writer
+  )
+endif(TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
index c823bb4..89fdd01 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -67,10 +67,7 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
-      "inlining_test.cc",
-      "ir_to_program_test.cc",
       "ir_to_program_test.h",
       "rename_conflicts_test.cc",
     ]
@@ -85,9 +82,10 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/intrinsic",
+      "${tint_src_dir}/lang/wgsl/ir",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
-      "${tint_src_dir}/lang/wgsl/writer",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -103,5 +101,13 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_wgsl_writer) {
+      sources += [
+        "inlining_test.cc",
+        "ir_to_program_test.cc",
+      ]
+      deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
+    }
   }
 }
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc
index ae49eb3..65f748c 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// GEN_BUILD:CONDITION(tint_build_wgsl_writer)
+
 #include <string>
 
 #include "src/tint/lang/core/ir/disassembler.h"
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index faaacf4..43d5ce7 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -596,6 +596,18 @@
                     disabled_derivative_uniformity_ = true;
                 }
 
+                switch (c->Func()) {
+                    case wgsl::BuiltinFn::kTextureBarrier:
+                        Enable(wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture);
+                        break;
+                    case wgsl::BuiltinFn::kSubgroupBallot:
+                    case wgsl::BuiltinFn::kSubgroupBroadcast:
+                        Enable(wgsl::Extension::kChromiumExperimentalSubgroups);
+                        break;
+                    default:
+                        break;
+                }
+
                 auto* expr = b.Call(c->Func(), std::move(args));
                 if (!call->HasResults() || call->Result()->Type()->Is<core::type::Void>()) {
                     Append(b.CallStmt(expr));
@@ -925,6 +937,9 @@
                 return b.ty.sampled_texture(t->dim(), el);
             },
             [&](const core::type::StorageTexture* t) {
+                if (t->access() == core::Access::kRead || t->access() == core::Access::kReadWrite) {
+                    Enable(wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture);
+                }
                 return b.ty.storage_texture(t->dim(), t->texel_format(), t->access());
             },
             [&](const core::type::Sampler* s) { return b.ty.sampler(s->kind()); },
@@ -968,6 +983,9 @@
                     ast_attrs.Push(b.Index(u32(*index)));
                 }
                 if (auto builtin = ir_attrs.builtin) {
+                    if (RequiresSubgroups(*builtin)) {
+                        Enable(wgsl::Extension::kChromiumExperimentalSubgroups);
+                    }
                     ast_attrs.Push(b.Builtin(*builtin));
                 }
                 if (auto interpolation = ir_attrs.interpolation) {
@@ -1171,6 +1189,18 @@
         }
     }
 
+    /// @returns true if the builtin value requires the kChromiumExperimentalSubgroups extension to
+    /// be enabled.
+    bool RequiresSubgroups(core::BuiltinValue builtin) {
+        switch (builtin) {
+            case core::BuiltinValue::kSubgroupInvocationId:
+            case core::BuiltinValue::kSubgroupSize:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     /// @returns true if a parameter of the type @p ty requires the
     /// kChromiumExperimentalFullPtrParameters extension to be enabled.
     bool ParamRequiresFullPtrParameters(const core::type::Type* ty) {
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
index 537c11a..a189e4e 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// GEN_BUILD:CONDITION(tint_build_wgsl_writer)
+
 #include <sstream>
 #include <string>
 
 #include "src/tint/lang/core/ir/disassembler.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/wgsl/ir/builtin_call.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
@@ -29,8 +33,7 @@
 IRToProgramTest::Result IRToProgramTest::Run() {
     Result result;
 
-    tint::core::ir::Disassembler d{mod};
-    result.ir = d.Disassemble();
+    result.ir = tint::core::ir::Disassemble(mod);
 
     auto output_program = IRToProgram(mod);
     if (!output_program.IsValid()) {
@@ -3195,5 +3198,142 @@
 )");
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// chromium_experimental_read_write_storage_texture
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(IRToProgramTest, Enable_ChromiumExperimentalReadWriteStorageTexture_TextureBarrier) {
+    auto* fn = b.Function("f", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
+            b.InstructionResult(ty.void_()), wgsl::BuiltinFn::kTextureBarrier, Empty));
+        b.Return(fn);
+    });
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_read_write_storage_texture;
+
+fn f() {
+  textureBarrier();
+}
+)");
+}
+
+TEST_F(IRToProgramTest, Enable_ChromiumExperimentalReadWriteStorageTexture_ReadOnlyStorageTexture) {
+    auto* T = b.Var("T", ty.ptr<handle>(ty.Get<core::type::StorageTexture>(
+                             core::type::TextureDimension::k2d, core::TexelFormat::kR32Float,
+                             core::Access::kRead, ty.f32())));
+    T->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(T);
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_read_write_storage_texture;
+
+@group(0) @binding(0) var T : texture_storage_2d<r32float, read>;
+)");
+}
+
+TEST_F(IRToProgramTest,
+       Enable_ChromiumExperimentalReadWriteStorageTexture_ReadWriteOnlyStorageTexture) {
+    auto* T = b.Var("T", ty.ptr<handle>(ty.Get<core::type::StorageTexture>(
+                             core::type::TextureDimension::k2d, core::TexelFormat::kR32Float,
+                             core::Access::kReadWrite, ty.f32())));
+    T->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(T);
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_read_write_storage_texture;
+
+@group(0) @binding(0) var T : texture_storage_2d<r32float, read_write>;
+)");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// chromium_experimental_subgroups
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(IRToProgramTest, Enable_ChromiumExperimentalSubgroups_SubgroupBallot) {
+    auto* fn = b.Function("f", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
+            b.InstructionResult(ty.vec4<u32>()), wgsl::BuiltinFn::kSubgroupBallot, Empty));
+        b.Return(fn);
+    });
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_subgroups;
+
+fn f() {
+  _ = subgroupBallot();
+}
+)");
+}
+
+TEST_F(IRToProgramTest, Enable_ChromiumExperimentalSubgroups_SubgroupBroadcast) {
+    auto* fn = b.Function("f", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* one = b.Value(1_u);
+        b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
+            b.InstructionResult(ty.u32()), wgsl::BuiltinFn::kSubgroupBroadcast, Vector{one, one}));
+        b.Return(fn);
+    });
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_subgroups;
+
+fn f() {
+  _ = subgroupBroadcast(1u, 1u);
+}
+)");
+}
+
+TEST_F(IRToProgramTest, Enable_ChromiumExperimentalSubgroups_StructBuiltin_SubgroupInvocationId) {
+    core::type::Manager::StructMemberDesc member;
+    member.name = mod.symbols.New("a");
+    member.type = ty.u32();
+    member.attributes.builtin = core::BuiltinValue::kSubgroupInvocationId;
+
+    auto* S = ty.Struct(mod.symbols.New("S"), {member});
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({b.FunctionParam(S)});
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_subgroups;
+
+struct S {
+  @builtin(subgroup_invocation_id)
+  a : u32,
+}
+
+fn f(v : S) {
+}
+)");
+}
+
+TEST_F(IRToProgramTest, Enable_ChromiumExperimentalSubgroups_StructBuiltin_SubgroupSize) {
+    core::type::Manager::StructMemberDesc member;
+    member.name = mod.symbols.New("a");
+    member.type = ty.u32();
+    member.attributes.builtin = core::BuiltinValue::kSubgroupSize;
+
+    auto* S = ty.Struct(mod.symbols.New("S"), {member});
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({b.FunctionParam(S)});
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+
+    EXPECT_WGSL(R"(
+enable chromium_experimental_subgroups;
+
+struct S {
+  @builtin(subgroup_size)
+  a : u32,
+}
+
+fn f(v : S) {
+}
+)");
+}
+
 }  // namespace
 }  // namespace tint::wgsl::writer
diff --git a/src/tint/lang/wgsl/writer/raise/BUILD.gn b/src/tint/lang/wgsl/writer/raise/BUILD.gn
index 7dc51e0..219ad43 100644
--- a/src/tint/lang/wgsl/writer/raise/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/raise/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -61,7 +61,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "raise_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/lang/wgsl/writer/raise/raise.cc b/src/tint/lang/wgsl/writer/raise/raise.cc
index 5843d5f..53ffb6c 100644
--- a/src/tint/lang/wgsl/writer/raise/raise.cc
+++ b/src/tint/lang/wgsl/writer/raise/raise.cc
@@ -18,6 +18,8 @@
 
 #include "src/tint/lang/core/builtin_fn.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/load.h"
+#include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/wgsl/builtin_fn.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 
@@ -116,7 +118,6 @@
         CASE(kUnpack4X8Snorm)
         CASE(kUnpack4X8Unorm)
         CASE(kWorkgroupBarrier)
-        CASE(kWorkgroupUniformLoad)
         CASE(kTextureBarrier)
         CASE(kTextureDimensions)
         CASE(kTextureGather)
@@ -152,15 +153,66 @@
     }
 }
 
+void ReplaceBuiltinFnCall(core::ir::Module& mod, core::ir::CoreBuiltinCall* call) {
+    Vector<core::ir::Value*, 8> args(call->Args());
+    auto* replacement = mod.instructions.Create<wgsl::ir::BuiltinCall>(
+        call->Result(), Convert(call->Func()), std::move(args));
+    call->ReplaceWith(replacement);
+    call->ClearResults();
+    call->Destroy();
+}
+
+void ReplaceWorkgroupBarrier(core::ir::Module& mod, core::ir::CoreBuiltinCall* call) {
+    // Pattern match:
+    //    call workgroupBarrier
+    //    %value = load &ptr
+    //    call workgroupBarrier
+    // And replace with:
+    //    %value = call workgroupUniformLoad %ptr
+
+    auto* load = As<core::ir::Load>(call->next);
+    if (!load || load->From()->Type()->As<core::type::Pointer>()->AddressSpace() !=
+                     core::AddressSpace::kWorkgroup) {
+        // No match
+        ReplaceBuiltinFnCall(mod, call);
+        return;
+    }
+
+    auto* post_load = As<core::ir::CoreBuiltinCall>(load->next);
+    if (!post_load || post_load->Func() != core::BuiltinFn::kWorkgroupBarrier) {
+        // No match
+        ReplaceBuiltinFnCall(mod, call);
+        return;
+    }
+
+    // Remove both calls to workgroupBarrier
+    post_load->Destroy();
+    call->Destroy();
+
+    // Replace load with workgroupUniformLoad
+    auto* replacement = mod.instructions.Create<wgsl::ir::BuiltinCall>(
+        load->Result(), wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{load->From()});
+    load->ReplaceWith(replacement);
+    load->ClearResults();
+    load->Destroy();
+}
+
 }  // namespace
 
 Result<SuccessType> Raise(core::ir::Module& mod) {
     for (auto* inst : mod.instructions.Objects()) {
+        if (!inst->Alive()) {
+            continue;
+        }
         if (auto* call = inst->As<core::ir::CoreBuiltinCall>()) {
-            Vector<core::ir::Value*, 8> args(call->Args());
-            auto* replacement = mod.instructions.Create<wgsl::ir::BuiltinCall>(
-                call->Result(), Convert(call->Func()), std::move(args));
-            call->ReplaceWith(replacement);
+            switch (call->Func()) {
+                case core::BuiltinFn::kWorkgroupBarrier:
+                    ReplaceWorkgroupBarrier(mod, call);
+                    break;
+                default:
+                    ReplaceBuiltinFnCall(mod, call);
+                    break;
+            }
         }
     }
     return Success;
diff --git a/src/tint/lang/wgsl/writer/raise/raise_test.cc b/src/tint/lang/wgsl/writer/raise/raise_test.cc
index a4f9b05..6928dd4 100644
--- a/src/tint/lang/wgsl/writer/raise/raise_test.cc
+++ b/src/tint/lang/wgsl/writer/raise/raise_test.cc
@@ -57,5 +57,100 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(WgslWriter_RaiseTest, WorkgroupBarrier) {
+    auto* W = b.Var<workgroup, i32, read_write>("W");
+    b.ir.root_block->Append(W);
+    auto* f = b.Function("f", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
+        auto* load = b.Load(W);
+        b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
+        b.Return(f, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, i32, read_write> = var
+}
+
+%f = func():i32 -> %b2 {
+  %b2 = block {
+    %3:void = workgroupBarrier
+    %4:i32 = load %W
+    %5:void = workgroupBarrier
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, i32, read_write> = var
+}
+
+%f = func():i32 -> %b2 {
+  %b2 = block {
+    %3:i32 = wgsl.workgroupUniformLoad %W
+    ret %3
+  }
+}
+)";
+
+    Run(Raise);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(WgslWriter_RaiseTest, WorkgroupBarrier_NoMatch) {
+    auto* W = b.Var<workgroup, i32, read_write>("W");
+    b.ir.root_block->Append(W);
+    auto* f = b.Function("f", ty.i32());
+    b.Append(f->Block(), [&] {  //
+        b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
+        b.Store(W, 42_i);  // Prevents pattern match
+        auto* load = b.Load(W);
+        b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
+        b.Return(f, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, i32, read_write> = var
+}
+
+%f = func():i32 -> %b2 {
+  %b2 = block {
+    %3:void = workgroupBarrier
+    store %W, 42i
+    %4:i32 = load %W
+    %5:void = workgroupBarrier
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, i32, read_write> = var
+}
+
+%f = func():i32 -> %b2 {
+  %b2 = block {
+    %3:void = wgsl.workgroupBarrier
+    store %W, 42i
+    %4:i32 = load %W
+    %5:void = wgsl.workgroupBarrier
+    ret %4
+  }
+}
+)";
+
+    Run(Raise);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::wgsl::writer::raise
diff --git a/src/tint/lang/wgsl/writer/writer_bench.cc b/src/tint/lang/wgsl/writer/writer_bench.cc
index 05f349a..3a4157d 100644
--- a/src/tint/lang/wgsl/writer/writer_bench.cc
+++ b/src/tint/lang/wgsl/writer/writer_bench.cc
@@ -22,15 +22,14 @@
 
 void GenerateWGSL(benchmark::State& state, std::string input_name) {
     auto res = bench::LoadProgram(input_name);
-    if (auto err = std::get_if<bench::Error>(&res)) {
-        state.SkipWithError(err->msg.c_str());
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
         return;
     }
-    auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(program, {});
-        if (!res) {
-            state.SkipWithError(res.Failure().reason.str());
+        auto gen_res = Generate(res->program, {});
+        if (!gen_res) {
+            state.SkipWithError(gen_res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/tint.gni b/src/tint/tint.gni
index 6d6bd8c..9693090 100644
--- a/src/tint/tint.gni
+++ b/src/tint/tint.gni
@@ -43,11 +43,27 @@
   }
 }
 
+###############################################################################
+# Executables - only built when tint_build_cmds is enabled
+###############################################################################
+template("tint_executable") {
+  if (tint_build_cmds) {
+    executable(target_name) {
+      forward_variables_from(invoker, "*")
+    }
+  }
+}
+
+###############################################################################
+# Unit tests - only built when tint_build_unittests is enabled
+###############################################################################
 template("tint_unittests_source_set") {
   if (tint_build_unittests) {
     source_set(target_name) {
       forward_variables_from(invoker, "*", [ "configs" ])
 
+      testonly = true
+
       if (defined(invoker.configs)) {
         configs += invoker.configs
       }
@@ -59,8 +75,6 @@
         configs += [ "//build/config/compiler:no_chromium_code" ]
       }
 
-      testonly = true
-
       if (!defined(invoker.deps)) {
         deps = []
       }
@@ -69,3 +83,65 @@
     }
   }
 }
+
+###############################################################################
+# Fuzzers - only built when tint_has_fuzzers is enabled
+###############################################################################
+if (tint_has_fuzzers) {
+  import("//testing/libfuzzer/fuzzer_test.gni")
+  fuzzer_corpus_wgsl_dir = "${root_gen_dir}/fuzzers/wgsl_corpus"
+  fuzzer_corpus_wgsl_stamp = "${fuzzer_corpus_wgsl_dir}.stamp"
+
+  template("tint_fuzz_source_set") {
+    source_set(target_name) {
+      forward_variables_from(invoker, "*", [ "configs" ])
+
+      testonly = true
+
+      if (!defined(invoker.deps)) {
+        deps = []
+      }
+
+      if (defined(invoker.configs)) {
+        configs += invoker.configs
+      }
+
+      configs += [ "${tint_src_dir}:tint_common_config" ]
+
+      if (build_with_chromium) {
+        configs -= [ "//build/config/compiler:chromium_code" ]
+        configs += [ "//build/config/compiler:no_chromium_code" ]
+      }
+
+      if (!defined(invoker.public_configs)) {
+        public_configs = []
+      }
+
+      public_configs += [ "${tint_src_dir}:tint_public_config" ]
+    }
+  }
+
+  template("tint_fuzzer_test") {
+    fuzzer_test(target_name) {
+      forward_variables_from(invoker, "*")
+      exclude_main = false
+
+      if (target_name == "wgsl") {
+        dict = "dictionary.txt"
+        libfuzzer_options = [
+          "only_ascii=1",  # TODO(bclayton): Remove this to fuzz unicode?
+          "max_len=10000",
+        ]
+        seed_corpus = fuzzer_corpus_wgsl_dir
+        seed_corpus_deps = [ "${tint_src_dir}:tint_generate_wgsl_corpus" ]
+      } else {
+        assert(false, "unsupported tint fuzzer target")
+      }
+    }
+  }
+} else {
+  template("tint_fuzz_source_set") {
+  }
+  template("tint_fuzzer_test") {
+  }
+}
diff --git a/src/tint/utils/cli/BUILD.gn b/src/tint/utils/cli/BUILD.gn
index 2306667..495eb17 100644
--- a/src/tint/utils/cli/BUILD.gn
+++ b/src/tint/utils/cli/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -50,7 +50,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "cli_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/command/BUILD.gn b/src/tint/utils/command/BUILD.gn
index c9b9d86..57bd6e6 100644
--- a/src/tint/utils/command/BUILD.gn
+++ b/src/tint/utils/command/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -50,7 +50,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "command_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/containers/BUILD.gn b/src/tint/utils/containers/BUILD.gn
index b7cbe13..4c79c02 100644
--- a/src/tint/utils/containers/BUILD.gn
+++ b/src/tint/utils/containers/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -58,7 +58,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "bitset_test.cc",
       "enum_set_test.cc",
diff --git a/src/tint/utils/containers/hashmap_base.h b/src/tint/utils/containers/hashmap_base.h
index e1ea31e..403dd22 100644
--- a/src/tint/utils/containers/hashmap_base.h
+++ b/src/tint/utils/containers/hashmap_base.h
@@ -228,7 +228,7 @@
             if (current == end) {
                 return *this;
             }
-            current++;
+            ++current;
             SkipToNextValue();
             return *this;
         }
@@ -261,9 +261,11 @@
 
         using SLOT = std::conditional_t<IS_CONST, const Slot, Slot>;
 
-        IteratorT(SLOT* c, SLOT* e, [[maybe_unused]] const HashmapBase& m)
-            : current(c),
-              end(e)
+        IteratorT(VectorIterator<SLOT> c,
+                  VectorIterator<SLOT> e,
+                  [[maybe_unused]] const HashmapBase& m)
+            : current(std::move(c)),
+              end(std::move(e))
 #ifdef TINT_ASSERT_ITERATORS_NOT_INVALIDATED
               ,
               map(m),
@@ -276,12 +278,12 @@
         /// Moves the iterator forward, stopping at the next slot that is not empty.
         void SkipToNextValue() {
             while (current != end && !current->entry.has_value()) {
-                current++;
+                ++current;
             }
         }
 
-        SLOT* current;  /// The slot the iterator is pointing to
-        SLOT* end;      /// One past the last slot in the map
+        VectorIterator<SLOT> current;  /// The slot the iterator is pointing to
+        VectorIterator<SLOT> end;      /// One past the last slot in the map
 
 #ifdef TINT_ASSERT_ITERATORS_NOT_INVALIDATED
         const HashmapBase& map;     /// The hashmap that is being iterated over.
diff --git a/src/tint/utils/containers/vector.h b/src/tint/utils/containers/vector.h
index 8ba1677..9320346 100644
--- a/src/tint/utils/containers/vector.h
+++ b/src/tint/utils/containers/vector.h
@@ -18,6 +18,7 @@
 #include <stddef.h>
 #include <stdint.h>
 #include <algorithm>
+#include <atomic>
 #include <iterator>
 #include <new>
 #include <utility>
@@ -29,6 +30,20 @@
 #include "src/tint/utils/math/hash.h"
 #include "src/tint/utils/memory/bitcast.h"
 
+#ifndef TINT_VECTOR_MUTATION_CHECKS_ENABLED
+#ifdef NDEBUG
+#define TINT_VECTOR_MUTATION_CHECKS_ENABLED 0
+#else
+#define TINT_VECTOR_MUTATION_CHECKS_ENABLED 1
+#endif
+#endif
+
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+#define TINT_VECTOR_MUTATION_CHECK_ASSERT(x) TINT_ASSERT(x)
+#else
+#define TINT_VECTOR_MUTATION_CHECK_ASSERT(x)
+#endif
+
 /// Forward declarations
 namespace tint {
 template <typename>
@@ -37,6 +52,228 @@
 
 namespace tint {
 
+/// VectorIterator is a forward iterator of Vector elements.
+template <typename T, bool FORWARD = true>
+class VectorIterator {
+  public:
+    /// The iterator trait
+    using iterator_category = std::random_access_iterator_tag;
+    /// The type of an element that this iterator points to
+    using value_type = T;
+    /// The type of the difference of two iterators
+    using difference_type = std::ptrdiff_t;
+    /// A pointer of the element type
+    using pointer = T*;
+    /// A reference of the element type
+    using reference = T&;
+
+    /// Constructor
+    VectorIterator() = default;
+
+    /// Destructor
+    ~VectorIterator() {
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+        if (iterator_count_) {
+            TINT_ASSERT(*iterator_count_ > 0);
+            (*iterator_count_)--;
+        }
+#endif
+    }
+
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+    /// Constructor
+    /// @param p the pointer to the vector element
+    /// @param it_cnt a pointer to an iterator count
+    VectorIterator(T* p, std::atomic<uint32_t>* it_cnt) : ptr_(p), iterator_count_(it_cnt) {
+        (*iterator_count_)++;
+    }
+
+    /// Copy constructor
+    /// @param other the VectorIterator to copy
+    VectorIterator(const VectorIterator& other)
+        : ptr_(other.ptr_), iterator_count_(other.iterator_count_) {
+        if (iterator_count_) {
+            (*iterator_count_)++;
+        }
+    }
+
+    /// Move constructor
+    /// @param other the VectorIterator to move
+    VectorIterator(VectorIterator&& other)
+        : ptr_(other.ptr_), iterator_count_(other.iterator_count_) {
+        other.ptr_ = nullptr;
+        other.iterator_count_ = nullptr;
+    }
+#else
+    /// Constructor
+    /// @param p the pointer to the vector element
+    explicit VectorIterator(T* p) : ptr_(p) {}
+
+    /// Copy constructor
+    /// @param other the VectorIterator to copy
+    VectorIterator(const VectorIterator& other) : ptr_(other.ptr_) {}
+
+    /// Move constructor
+    /// @param other the VectorIterator to move
+    VectorIterator(VectorIterator&& other) : ptr_(other.ptr_) { other.ptr_ = nullptr; }
+#endif
+
+    /// Assignment operator
+    /// @param other the VectorIterator to copy
+    /// @return this VectorIterator
+    VectorIterator& operator=(const VectorIterator& other) {
+        ptr_ = other.ptr_;
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+        if (iterator_count_ != other.iterator_count_) {
+            if (iterator_count_) {
+                (*iterator_count_)--;
+            }
+            iterator_count_ = other.iterator_count_;
+            if (iterator_count_) {
+                (*iterator_count_)++;
+            }
+        }
+#endif
+        return *this;
+    }
+
+    /// Move-assignment operator
+    /// @param other the VectorIterator to move
+    /// @return this VectorIterator
+    VectorIterator& operator=(VectorIterator&& other) {
+        ptr_ = other.ptr_;
+        other.ptr_ = nullptr;
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+        if (iterator_count_) {
+            (*iterator_count_)--;
+        }
+        iterator_count_ = other.iterator_count_;
+        other.iterator_count_ = nullptr;
+#endif
+        return *this;
+    }
+
+    /// @return the element this iterator currently points at
+    operator T*() const { return ptr_; }
+
+    /// @return the element this iterator currently points at
+    T& operator*() const { return *ptr_; }
+
+    /// @return the element this iterator currently points at
+    T* operator->() const { return ptr_; }
+
+    /// Equality operator
+    /// @param other the other VectorIterator
+    /// @return true if this iterator is equal to @p other
+    bool operator==(const VectorIterator& other) const { return ptr_ == other.ptr_; }
+
+    /// Inequality operator
+    /// @param other the other VectorIterator
+    /// @return true if this iterator is not equal to @p other
+    bool operator!=(const VectorIterator& other) const { return ptr_ != other.ptr_; }
+
+    /// Less-than operator
+    /// @param other the other iterator
+    /// @returns true if this iterator comes before @p other
+    bool operator<(const VectorIterator& other) const { return other - *this > 0; }
+
+    /// Greater-than operator
+    /// @param other the other iterator
+    /// @returns true if this iterator comes after @p other
+    bool operator>(const VectorIterator& other) const { return *this - other > 0; }
+
+    /// Index operator
+    /// @param i the number of elements from the element this iterator points to
+    /// @return the element
+    T& operator[](std::ptrdiff_t i) const { return *(*this + i); }
+
+    /// Increments the iterator (prefix)
+    /// @returns this VectorIterator
+    VectorIterator& operator++() {
+        this->ptr_ = FORWARD ? this->ptr_ + 1 : this->ptr_ - 1;
+        return *this;
+    }
+
+    /// Decrements the iterator (prefix)
+    /// @returns this VectorIterator
+    VectorIterator& operator--() {
+        this->ptr_ = FORWARD ? this->ptr_ - 1 : this->ptr_ + 1;
+        return *this;
+    }
+
+    /// Increments the iterator (postfix)
+    /// @returns a VectorIterator that points to the element before the increment
+    VectorIterator operator++(int) {
+        VectorIterator res = *this;
+        this->ptr_ = FORWARD ? this->ptr_ + 1 : this->ptr_ - 1;
+        return res;
+    }
+
+    /// Decrements the iterator (postfix)
+    /// @returns a VectorIterator that points to the element before the decrement
+    VectorIterator operator--(int) {
+        VectorIterator res = *this;
+        this->ptr_ = FORWARD ? this->ptr_ - 1 : this->ptr_ + 1;
+        return res;
+    }
+
+    /// Moves the iterator forward by @p n elements
+    /// @param n the number of elements
+    /// @returns this VectorIterator
+    VectorIterator operator+=(std::ptrdiff_t n) {
+        this->ptr_ = FORWARD ? this->ptr_ + n : this->ptr_ - n;
+        return *this;
+    }
+
+    /// Moves the iterator backwards by @p n elements
+    /// @param n the number of elements
+    /// @returns this VectorIterator
+    VectorIterator operator-=(std::ptrdiff_t n) {
+        this->ptr_ = FORWARD ? this->ptr_ - n : this->ptr_ + n;
+        return *this;
+    }
+
+    /// @param n the number of elements
+    /// @returns a new VectorIterator progressed by @p n elements
+    VectorIterator operator+(std::ptrdiff_t n) const {
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+        return VectorIterator{FORWARD ? ptr_ + n : ptr_ - n, iterator_count_};
+#else
+        return VectorIterator{FORWARD ? ptr_ + n : ptr_ - n};
+#endif
+    }
+
+    /// @param n the number of elements
+    /// @returns a new VectorIterator regressed by @p n elements
+    VectorIterator operator-(std::ptrdiff_t n) const {
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+        return VectorIterator{FORWARD ? ptr_ - n : ptr_ + n, iterator_count_};
+#else
+        return VectorIterator{FORWARD ? ptr_ - n : ptr_ + n};
+#endif
+    }
+
+    /// @param other the other iterator
+    /// @returns the number of elements between this iterator and @p other
+    std::ptrdiff_t operator-(const VectorIterator& other) const {
+        return FORWARD ? ptr_ - other.ptr_ : other.ptr_ - ptr_;
+    }
+
+  private:
+    T* ptr_ = nullptr;
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+    std::atomic<uint32_t>* iterator_count_ = nullptr;
+#endif
+};
+
+/// @param out the stream to write to
+/// @param it the VectorIterator
+/// @returns @p out so calls can be chained
+template <typename STREAM, typename T, bool FORWARD, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& out, const VectorIterator<T, FORWARD>& it) {
+    return out << *it;
+}
+
 /// Vector is a small-object-optimized, dynamically-sized vector of contigious elements of type T.
 ///
 /// Vector will fit `N` elements internally before spilling to heap allocations. If `N` is greater
@@ -59,10 +296,14 @@
 template <typename T, size_t N>
 class Vector {
   public:
-    /// Alias to `T*`.
-    using iterator = T*;
-    /// Alias to `const T*`.
-    using const_iterator = const T*;
+    /// Alias to the non-const forward iterator
+    using iterator = VectorIterator<T, /* forward */ true>;
+    /// Alias to the const forward iterator
+    using const_iterator = VectorIterator<const T, /* forward */ true>;
+    /// Alias to the non-const reverse  iterator
+    using reverse_iterator = VectorIterator<T, /* forward */ false>;
+    /// Alias to the const reverse iterator
+    using const_reverse_iterator = VectorIterator<const T, /* forward */ false>;
     /// Alias to `T`.
     using value_type = T;
     /// Value of `N`
@@ -211,18 +452,12 @@
     /// Index operator
     /// @param i the element index. Must be less than `len`.
     /// @returns a reference to the i'th element.
-    T& operator[](size_t i) {
-        TINT_ASSERT(i < Length());
-        return impl_.slice[i];
-    }
+    T& operator[](size_t i) { return impl_.slice[i]; }
 
     /// Index operator
     /// @param i the element index. Must be less than `len`.
     /// @returns a reference to the i'th element.
-    const T& operator[](size_t i) const {
-        TINT_ASSERT(i < Length());
-        return impl_.slice[i];
-    }
+    const T& operator[](size_t i) const { return impl_.slice[i]; }
 
     /// @return the number of elements in the vector
     size_t Length() const { return impl_.slice.len; }
@@ -234,6 +469,7 @@
     /// Reserves memory to hold at least `new_cap` elements
     /// @param new_cap the new vector capacity
     void Reserve(size_t new_cap) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         if (new_cap > impl_.slice.cap) {
             auto* old_data = impl_.slice.data;
             impl_.Allocate(new_cap);
@@ -282,17 +518,17 @@
 
     /// Clears all elements from the vector, keeping the capacity the same.
     void Clear() {
-        TINT_BEGIN_DISABLE_WARNING(MAYBE_UNINITIALIZED);
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         for (size_t i = 0; i < impl_.slice.len; i++) {
             impl_.slice.data[i].~T();
         }
         impl_.slice.len = 0;
-        TINT_END_DISABLE_WARNING(MAYBE_UNINITIALIZED);
     }
 
     /// Appends a new element to the vector.
     /// @param el the element to copy to the vector.
     void Push(const T& el) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         if (impl_.slice.len >= impl_.slice.cap) {
             Grow();
         }
@@ -302,6 +538,7 @@
     /// Appends a new element to the vector.
     /// @param el the element to move to the vector.
     void Push(T&& el) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         if (impl_.slice.len >= impl_.slice.cap) {
             Grow();
         }
@@ -312,6 +549,7 @@
     /// @param args the arguments to pass to the element constructor.
     template <typename... ARGS>
     void Emplace(ARGS&&... args) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         if (impl_.slice.len >= impl_.slice.cap) {
             Grow();
         }
@@ -321,6 +559,7 @@
     /// Removes and returns the last element from the vector.
     /// @returns the popped element
     T Pop() {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         TINT_ASSERT(!IsEmpty());
         auto& el = impl_.slice.data[--impl_.slice.len];
         auto val = std::move(el);
@@ -333,6 +572,7 @@
     /// @param element the element to insert
     template <typename EL>
     void Insert(size_t before, EL&& element) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         TINT_ASSERT(before <= Length());
         size_t n = Length();
         Resize(Length() + 1);
@@ -350,6 +590,7 @@
     /// @param start the index of the first element to remove
     /// @param count the number of elements to remove
     void Erase(size_t start, size_t count = 1) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         TINT_ASSERT(start < Length());
         TINT_ASSERT((start + count) <= Length());
         // Shuffle
@@ -370,6 +611,7 @@
     /// should return `true` for elements that should be removed from the vector.
     template <typename PREDICATE>
     void EraseIf(PREDICATE&& predicate) {
+        TINT_VECTOR_MUTATION_CHECK_ASSERT(iterator_count_ == 0);
         // Shuffle
         size_t num_removed = 0;
         for (size_t i = 0; i < impl_.slice.len; i++) {
@@ -444,29 +686,65 @@
     /// @returns a reference to the last element in the vector
     const T& Back() const { return impl_.slice.Back(); }
 
-    /// @returns a pointer to the first element in the vector
-    T* begin() { return impl_.slice.begin(); }
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+    /// @returns a forward iterator to the first element of the vector
+    iterator begin() { return iterator{impl_.slice.begin(), &iterator_count_}; }
 
-    /// @returns a pointer to the first element in the vector
-    const T* begin() const { return impl_.slice.begin(); }
+    /// @returns a forward iterator to the first element of the vector
+    const const_iterator begin() const {
+        return const_iterator{impl_.slice.begin(), &iterator_count_};
+    }
 
-    /// @returns a pointer to one past the last element in the vector
-    T* end() { return impl_.slice.end(); }
+    /// @returns a forward iterator to one-pass the last element of the vector
+    iterator end() { return iterator{impl_.slice.end(), &iterator_count_}; }
 
-    /// @returns a pointer to one past the last element in the vector
-    const T* end() const { return impl_.slice.end(); }
+    /// @returns a forward iterator to one-pass the last element of the vector
+    const const_iterator end() const { return const_iterator{impl_.slice.end(), &iterator_count_}; }
 
-    /// @returns a reverse iterator starting with the last element in the vector
-    auto rbegin() { return impl_.slice.rbegin(); }
+    /// @returns a reverse iterator to the last element of the vector
+    reverse_iterator rbegin() { return reverse_iterator{impl_.slice.end(), &iterator_count_} + 1; }
 
-    /// @returns a reverse iterator starting with the last element in the vector
-    auto rbegin() const { return impl_.slice.rbegin(); }
+    /// @returns a reverse iterator to the last element of the vector
+    const const_reverse_iterator rbegin() const {
+        return const_reverse_iterator{impl_.slice.end(), &iterator_count_} + 1;
+    }
 
-    /// @returns the end for a reverse iterator
-    auto rend() { return impl_.slice.rend(); }
+    /// @returns a reverse iterator to one element before the first element of the vector
+    reverse_iterator rend() { return reverse_iterator{impl_.slice.begin(), &iterator_count_} + 1; }
 
-    /// @returns the end for a reverse iterator
-    auto rend() const { return impl_.slice.rend(); }
+    /// @returns a reverse iterator to one element before the first element of the vector
+    const const_reverse_iterator rend() const {
+        return const_reverse_iterator{impl_.slice.begin(), &iterator_count_} + 1;
+    }
+#else
+    /// @returns a forward iterator to the first element of the vector
+    iterator begin() { return iterator{impl_.slice.begin()}; }
+
+    /// @returns a forward iterator to the first element of the vector
+    const const_iterator begin() const { return const_iterator{impl_.slice.begin()}; }
+
+    /// @returns a forward iterator to one-pass the last element of the vector
+    iterator end() { return iterator{impl_.slice.end()}; }
+
+    /// @returns a forward iterator to one-pass the last element of the vector
+    const const_iterator end() const { return const_iterator{impl_.slice.end()}; }
+
+    /// @returns a reverse iterator to the last element of the vector
+    reverse_iterator rbegin() { return reverse_iterator{impl_.slice.end()} + 1; }
+
+    /// @returns a reverse iterator to the last element of the vector
+    const const_reverse_iterator rbegin() const {
+        return const_reverse_iterator{impl_.slice.end()} + 1;
+    }
+
+    /// @returns a reverse iterator to one element before the first element of the vector
+    reverse_iterator rend() { return reverse_iterator{impl_.slice.begin()} + 1; }
+
+    /// @returns a reverse iterator to one element before the first element of the vector
+    const const_reverse_iterator rend() const {
+        return const_reverse_iterator{impl_.slice.begin()} + 1;
+    }
+#endif
 
     /// @returns a hash code for this Vector
     size_t HashCode() const {
@@ -626,6 +904,9 @@
 
     /// Either a ImplWithSmallArray or ImplWithoutSmallArray based on N.
     std::conditional_t<HasSmallArray, ImplWithSmallArray, ImplWithoutSmallArray> impl_;
+
+    /// The current number of iterators referring to this vector
+    mutable std::atomic<uint32_t> iterator_count_ = 0;
 };
 
 namespace detail {
diff --git a/src/tint/utils/containers/vector_test.cc b/src/tint/utils/containers/vector_test.cc
index b276564..d40f2e5 100644
--- a/src/tint/utils/containers/vector_test.cc
+++ b/src/tint/utils/containers/vector_test.cc
@@ -18,8 +18,10 @@
 #include <tuple>
 
 #include "gmock/gmock.h"
+#include "gtest/gtest-spi.h"
 
 #include "src/tint/utils/containers/predicates.h"
+#include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/memory/bitcast.h"
 #include "src/tint/utils/text/string_stream.h"
 
@@ -1954,32 +1956,64 @@
     Vector<std::string, 3> vec{"front", "mid", "back"};
     static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.begin())>>);
     static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.end())>>);
-    EXPECT_EQ(vec.begin(), &vec[0]);
-    EXPECT_EQ(vec.end(), &vec[0] + 3);
+    EXPECT_EQ(&*vec.begin(), &vec[0]);
+    EXPECT_EQ(&*vec.end(), &vec[0] + 3);
+}
+
+TEST(TintVectorTest, RbeginRend_NoSpill) {
+    Vector<std::string, 3> vec{"front", "mid", "back"};
+    static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.rbegin())>>);
+    static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.rend())>>);
+    EXPECT_EQ(&*vec.rbegin(), &vec[0] + 2);
+    EXPECT_EQ(&*vec.rend(), &vec[0] - 1);
 }
 
 TEST(TintVectorTest, BeginEnd_WithSpill) {
     Vector<std::string, 2> vec{"front", "mid", "back"};
     static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.begin())>>);
     static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.end())>>);
-    EXPECT_EQ(vec.begin(), &vec[0]);
-    EXPECT_EQ(vec.end(), &vec[0] + 3);
+    EXPECT_EQ(&*vec.begin(), &vec[0]);
+    EXPECT_EQ(&*vec.end(), &vec[0] + 3);
+}
+
+TEST(TintVectorTest, RbeginRend_WithSpill) {
+    Vector<std::string, 2> vec{"front", "mid", "back"};
+    static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.rbegin())>>);
+    static_assert(!std::is_const_v<std::remove_reference_t<decltype(*vec.rend())>>);
+    EXPECT_EQ(&*vec.rbegin(), &vec[0] + 2);
+    EXPECT_EQ(&*vec.rend(), &vec[0] - 1);
 }
 
 TEST(TintVectorTest, ConstBeginEnd_NoSpill) {
     const Vector<std::string, 3> vec{"front", "mid", "back"};
     static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.begin())>>);
     static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.end())>>);
-    EXPECT_EQ(vec.begin(), &vec[0]);
-    EXPECT_EQ(vec.end(), &vec[0] + 3);
+    EXPECT_EQ(&*vec.begin(), &vec[0]);
+    EXPECT_EQ(&*vec.end(), &vec[0] + 3);
+}
+
+TEST(TintVectorTest, ConstRbeginRend_NoSpill) {
+    const Vector<std::string, 3> vec{"front", "mid", "back"};
+    static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.rbegin())>>);
+    static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.rend())>>);
+    EXPECT_EQ(&*vec.rbegin(), &vec[0] + 2);
+    EXPECT_EQ(&*vec.rend(), &vec[0] - 1);
 }
 
 TEST(TintVectorTest, ConstBeginEnd_WithSpill) {
     const Vector<std::string, 2> vec{"front", "mid", "back"};
     static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.begin())>>);
     static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.end())>>);
-    EXPECT_EQ(vec.begin(), &vec[0]);
-    EXPECT_EQ(vec.end(), &vec[0] + 3);
+    EXPECT_EQ(&*vec.begin(), &vec[0]);
+    EXPECT_EQ(&*vec.end(), &vec[0] + 3);
+}
+
+TEST(TintVectorTest, ConstRbeginRend_WithSpill) {
+    const Vector<std::string, 2> vec{"front", "mid", "back"};
+    static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.rbegin())>>);
+    static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec.rend())>>);
+    EXPECT_EQ(&*vec.rbegin(), &vec[0] + 2);
+    EXPECT_EQ(&*vec.rend(), &vec[0] - 1);
 }
 
 TEST(TintVectorTest, Equality) {
@@ -2059,6 +2093,70 @@
     EXPECT_EQ(ss.str(), "[1, 2, 3]");
 }
 
+TEST(TintVectorTest, AssertOOBs) {
+    TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
+    EXPECT_FATAL_FAILURE(
+        {
+            Vector vec{1};
+            [[maybe_unused]] int i = vec[1];
+        },
+        "internal compiler error");
+    TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
+}
+
+#if TINT_VECTOR_MUTATION_CHECKS_ENABLED
+TEST(TintVectorTest, AssertPushWhileIterating) {
+    TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
+    using V = Vector<int, 4>;
+    EXPECT_FATAL_FAILURE(
+        {
+            V vec;
+            vec.Push(1);
+            vec.Push(2);
+            for ([[maybe_unused]] int i : vec) {
+                vec.Push(3);
+                break;
+            }
+        },
+        "internal compiler error");
+    TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
+}
+
+TEST(TintVectorTest, AssertPopWhileIterating) {
+    TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
+    using V = Vector<int, 4>;
+    EXPECT_FATAL_FAILURE(
+        {
+            V vec;
+            vec.Push(1);
+            vec.Push(2);
+            for ([[maybe_unused]] int i : vec) {
+                vec.Pop();
+                break;
+            }
+        },
+        "internal compiler error");
+    TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
+}
+
+TEST(TintVectorTest, AssertClearWhileIterating) {
+    TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
+    using V = Vector<int, 4>;
+    EXPECT_FATAL_FAILURE(
+        {
+            V vec;
+            vec.Push(1);
+            vec.Push(2);
+            for ([[maybe_unused]] int i : vec) {
+                vec.Clear();
+                break;
+            }
+        },
+        "internal compiler error");
+    TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
+}
+#endif
+
 ////////////////////////////////////////////////////////////////////////////////
 // TintVectorRefTest
 ////////////////////////////////////////////////////////////////////////////////
@@ -2320,8 +2418,15 @@
     const VectorRef<std::string> vec_ref(vec);
     static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec_ref.begin())>>);
     static_assert(std::is_const_v<std::remove_reference_t<decltype(*vec_ref.end())>>);
-    EXPECT_EQ(vec_ref.begin(), &vec[0]);
-    EXPECT_EQ(vec_ref.end(), &vec[0] + 3);
+    EXPECT_EQ(&*vec_ref.begin(), &vec[0]);
+    EXPECT_EQ(&*vec_ref.end(), &vec[0] + 3);
+}
+
+TEST(TintVectorRefTest, RbeginRend) {
+    Vector<std::string, 3> vec{"front", "mid", "back"};
+    const VectorRef<std::string> vec_ref(vec);
+    EXPECT_EQ(&*vec_ref.rbegin(), &vec[0] + 2);
+    EXPECT_EQ(&*vec_ref.rend(), &vec[0] - 1);
 }
 
 TEST(TintVectorRefTest, ostream) {
@@ -2332,6 +2437,16 @@
     EXPECT_EQ(ss.str(), "[1, 2, 3]");
 }
 
+TEST(TintVectorRefTest, AssertOOBs) {
+    EXPECT_FATAL_FAILURE(
+        {
+            Vector vec{1};
+            const VectorRef<int> vec_ref(vec);
+            [[maybe_unused]] int i = vec_ref[1];
+        },
+        "internal compiler error");
+}
+
 }  // namespace
 }  // namespace tint
 
diff --git a/src/tint/utils/diagnostic/BUILD.gn b/src/tint/utils/diagnostic/BUILD.gn
index 90735ad..de2eeb3 100644
--- a/src/tint/utils/diagnostic/BUILD.gn
+++ b/src/tint/utils/diagnostic/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -65,7 +65,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "diagnostic_test.cc",
       "formatter_test.cc",
diff --git a/src/tint/utils/diagnostic/diagnostic.h b/src/tint/utils/diagnostic/diagnostic.h
index 1b40cae..fac58ac 100644
--- a/src/tint/utils/diagnostic/diagnostic.h
+++ b/src/tint/utils/diagnostic/diagnostic.h
@@ -89,8 +89,8 @@
 /// List is a container of Diagnostic messages.
 class List {
   public:
-    /// iterator is the type used for range based iteration.
-    using iterator = const Diagnostic*;
+    /// The iterator type for this List
+    using iterator = VectorIterator<const Diagnostic>;
 
     /// Constructs the list with no elements.
     List();
diff --git a/src/tint/utils/diagnostic/source.cc b/src/tint/utils/diagnostic/source.cc
index f223d62..0ea43bb 100644
--- a/src/tint/utils/diagnostic/source.cc
+++ b/src/tint/utils/diagnostic/source.cc
@@ -113,7 +113,7 @@
 
 }  // namespace
 
-Source::FileContent::FileContent(const std::string& body) : data(body), lines(SplitLines(data)) {}
+Source::FileContent::FileContent(std::string_view body) : data(body), lines(SplitLines(data)) {}
 
 Source::FileContent::FileContent(const FileContent& rhs)
     : data(rhs.data), lines(CopyRelativeStringViews(rhs.lines, rhs.data, data)) {}
diff --git a/src/tint/utils/diagnostic/source.h b/src/tint/utils/diagnostic/source.h
index ac3497c..6795dca 100644
--- a/src/tint/utils/diagnostic/source.h
+++ b/src/tint/utils/diagnostic/source.h
@@ -34,7 +34,7 @@
       public:
         /// Constructs the FileContent with the given file content.
         /// @param data the file contents
-        explicit FileContent(const std::string& data);
+        explicit FileContent(std::string_view data);
 
         /// Copy constructor
         /// @param rhs the FileContent to copy
@@ -55,7 +55,7 @@
         /// Constructs the File with the given file path and content.
         /// @param p the path for this file
         /// @param c the file contents
-        inline File(const std::string& p, const std::string& c) : path(p), content(c) {}
+        inline File(const std::string& p, std::string_view c) : path(p), content(c) {}
 
         /// Copy constructor
         File(const File&) = default;
diff --git a/src/tint/utils/file/BUILD.gn b/src/tint/utils/file/BUILD.gn
index cfca680..a7267e8 100644
--- a/src/tint/utils/file/BUILD.gn
+++ b/src/tint/utils/file/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -51,7 +51,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "tmpfile_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/ice/BUILD.gn b/src/tint/utils/ice/BUILD.gn
index d151d2f..e7f69a0 100644
--- a/src/tint/utils/ice/BUILD.gn
+++ b/src/tint/utils/ice/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -41,7 +41,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "ice_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/macros/BUILD.bazel b/src/tint/utils/macros/BUILD.bazel
index e3a081b..57ac024 100644
--- a/src/tint/utils/macros/BUILD.bazel
+++ b/src/tint/utils/macros/BUILD.bazel
@@ -34,6 +34,7 @@
     "defer.h",
     "foreach.h",
     "scoped_assignment.h",
+    "static_init.h",
   ],
   deps = [
   ],
@@ -46,6 +47,7 @@
   srcs = [
     "defer_test.cc",
     "scoped_assignment_test.cc",
+    "static_init_test.cc",
   ],
   deps = [
     "//src/tint/utils/macros",
diff --git a/src/tint/utils/macros/BUILD.cmake b/src/tint/utils/macros/BUILD.cmake
index 39242f1..175c753 100644
--- a/src/tint/utils/macros/BUILD.cmake
+++ b/src/tint/utils/macros/BUILD.cmake
@@ -32,6 +32,7 @@
   utils/macros/foreach.h
   utils/macros/macros.cc
   utils/macros/scoped_assignment.h
+  utils/macros/static_init.h
 )
 
 ################################################################################
@@ -41,6 +42,7 @@
 tint_add_target(tint_utils_macros_test test
   utils/macros/defer_test.cc
   utils/macros/scoped_assignment_test.cc
+  utils/macros/static_init_test.cc
 )
 
 tint_target_add_dependencies(tint_utils_macros_test test
diff --git a/src/tint/utils/macros/BUILD.gn b/src/tint/utils/macros/BUILD.gn
index 7b85feb..75705d3 100644
--- a/src/tint/utils/macros/BUILD.gn
+++ b/src/tint/utils/macros/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -37,15 +37,16 @@
     "foreach.h",
     "macros.cc",
     "scoped_assignment.h",
+    "static_init.h",
   ]
   deps = []
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "defer_test.cc",
       "scoped_assignment_test.cc",
+      "static_init_test.cc",
     ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/macros/scoped_assignment.h b/src/tint/utils/macros/scoped_assignment.h
index 9d4e98b..af3a25b 100644
--- a/src/tint/utils/macros/scoped_assignment.h
+++ b/src/tint/utils/macros/scoped_assignment.h
@@ -1,4 +1,3 @@
-
 // Copyright 2021 The Tint Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/src/tint/utils/macros/static_init.h b/src/tint/utils/macros/static_init.h
new file mode 100644
index 0000000..e1b69fb
--- /dev/null
+++ b/src/tint/utils/macros/static_init.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_MACROS_STATIC_INIT_H_
+#define SRC_TINT_UTILS_MACROS_STATIC_INIT_H_
+
+#include "src/tint/utils/macros/concat.h"
+
+/// A helper macro that executes STATEMENT the first time the macro comes into scope - typically
+/// used at global scope to call a function before main() is run.
+/// For example: `TINT_STATIC_INIT(CallAtStartup(1,2,3));`
+/// @note: This must not be used at global scope in production code, as this violates the Chromium
+/// rules around static initializers. Attempting to do this will result in a compilation error.
+#define TINT_STATIC_INIT(STATEMENT)                                                       \
+    [[maybe_unused]] static const bool TINT_CONCAT(tint_static_init_, __COUNTER__) = [] { \
+        STATEMENT;                                                                        \
+        return true;                                                                      \
+    }()
+
+#endif  // SRC_TINT_UTILS_MACROS_STATIC_INIT_H_
diff --git a/src/tint/cmd/bench/benchmark.cc b/src/tint/utils/macros/static_init_test.cc
similarity index 65%
copy from src/tint/cmd/bench/benchmark.cc
copy to src/tint/utils/macros/static_init_test.cc
index ad8658b..2f71a78 100644
--- a/src/tint/cmd/bench/benchmark.cc
+++ b/src/tint/utils/macros/static_init_test.cc
@@ -12,9 +12,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#if defined(__clang__)
-#pragma clang diagnostic ignored "-Wmissing-variable-declarations"
-#endif
+#include "src/tint/utils/macros/static_init.h"
 
-// A placeholder symbol used to emit a symbol for this lib target.
-int tint_cmd_bench_symbol = 1;
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace {
+
+int global_var = 0;
+
+void SetGlobalVar(int i) {
+    global_var = i;
+}
+
+TINT_STATIC_INIT(SetGlobalVar(42));
+
+TEST(TestStaticInit, Global) {
+    EXPECT_EQ(global_var, 42);
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/utils/math/BUILD.gn b/src/tint/utils/math/BUILD.gn
index 2498c65..594f07d 100644
--- a/src/tint/utils/math/BUILD.gn
+++ b/src/tint/utils/math/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -40,7 +40,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "crc32_test.cc",
       "hash_test.cc",
diff --git a/src/tint/utils/memory/BUILD.bazel b/src/tint/utils/memory/BUILD.bazel
index e419f7b..2d91e3c 100644
--- a/src/tint/utils/memory/BUILD.bazel
+++ b/src/tint/utils/memory/BUILD.bazel
@@ -34,6 +34,7 @@
     "bump_allocator.h",
   ],
   deps = [
+    "//src/tint/utils/macros",
     "//src/tint/utils/math",
   ],
   copts = COPTS,
@@ -48,6 +49,7 @@
     "bump_allocator_test.cc",
   ],
   deps = [
+    "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
     "@gtest",
diff --git a/src/tint/utils/memory/BUILD.cmake b/src/tint/utils/memory/BUILD.cmake
index a29c41c..1b07cd1 100644
--- a/src/tint/utils/memory/BUILD.cmake
+++ b/src/tint/utils/memory/BUILD.cmake
@@ -33,6 +33,7 @@
 )
 
 tint_target_add_dependencies(tint_utils_memory lib
+  tint_utils_macros
   tint_utils_math
 )
 
@@ -47,6 +48,7 @@
 )
 
 tint_target_add_dependencies(tint_utils_memory_test test
+  tint_utils_macros
   tint_utils_math
   tint_utils_memory
 )
diff --git a/src/tint/utils/memory/BUILD.gn b/src/tint/utils/memory/BUILD.gn
index 36728c7..79a3a1c 100644
--- a/src/tint/utils/memory/BUILD.gn
+++ b/src/tint/utils/memory/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -36,11 +36,13 @@
     "bump_allocator.h",
     "memory.cc",
   ]
-  deps = [ "${tint_src_dir}/utils/math" ]
+  deps = [
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+  ]
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "bitcast_test.cc",
       "block_allocator_test.cc",
@@ -48,6 +50,7 @@
     ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
+      "${tint_src_dir}/utils/macros",
       "${tint_src_dir}/utils/math",
       "${tint_src_dir}/utils/memory",
     ]
diff --git a/src/tint/utils/memory/block_allocator.h b/src/tint/utils/memory/block_allocator.h
index 2ee1296..978950a 100644
--- a/src/tint/utils/memory/block_allocator.h
+++ b/src/tint/utils/memory/block_allocator.h
@@ -40,6 +40,7 @@
         std::array<T*, kMax> ptrs;
         Pointers* next;
         Pointers* prev;
+        size_t count;
     };
 
     /// Block is linked list of memory blocks.
@@ -48,7 +49,7 @@
     /// Note: We're not using std::aligned_storage here as this warns / errors on MSVC.
     struct alignas(BLOCK_ALIGNMENT) Block {
         uint8_t data[BLOCK_SIZE];
-        Block* next;
+        Block* next = nullptr;
     };
 
     // Forward declaration
@@ -56,7 +57,7 @@
     class TView;
 
     /// An iterator for the objects owned by the BlockAllocator.
-    template <bool IS_CONST, bool FORWARD>
+    template <bool IS_CONST>
     class TIterator {
         using PointerTy = std::conditional_t<IS_CONST, const T*, T*>;
 
@@ -76,10 +77,12 @@
         /// Progress the iterator forward one element
         /// @returns this iterator
         TIterator& operator++() {
-            if (FORWARD) {
-                ProgressForward();
-            } else {
-                ProgressBackwards();
+            if (ptrs != nullptr) {
+                ++idx;
+                if (idx >= ptrs->count) {
+                    idx = 0;
+                    ptrs = ptrs->next;
+                }
             }
             return *this;
         }
@@ -87,44 +90,27 @@
         /// Progress the iterator backwards one element
         /// @returns this iterator
         TIterator& operator--() {
-            if (FORWARD) {
-                ProgressBackwards();
-            } else {
-                ProgressForward();
+            if (ptrs != nullptr) {
+                if (idx == 0) {
+                    ptrs = ptrs->prev;
+                    idx = ptrs->count - 1;
+                }
+                --idx;
             }
             return *this;
         }
 
         /// @returns the pointer to the object at the current iterator position
-        PointerTy operator*() const { return ptrs ? ptrs->ptrs[idx] : nullptr; }
+        PointerTy operator*() const { return ptrs->ptrs[idx]; }
 
       private:
         friend TView<IS_CONST>;  // Keep internal iterator impl private.
         explicit TIterator(const Pointers* p, size_t i) : ptrs(p), idx(i) {}
 
-        /// Progresses the iterator forwards
-        void ProgressForward() {
-            if (ptrs != nullptr) {
-                ++idx;
-                if (idx == Pointers::kMax) {
-                    idx = 0;
-                    ptrs = ptrs->next;
-                }
-            }
-        }
-        /// Progresses the iterator backwards
-        void ProgressBackwards() {
-            if (ptrs != nullptr) {
-                if (idx == 0) {
-                    idx = Pointers::kMax - 1;
-                    ptrs = ptrs->prev;
-                }
-                --idx;
-            }
-        }
-
-        const Pointers* ptrs;
-        size_t idx;
+        /// The current Pointers
+        const Pointers* ptrs = nullptr;
+        /// The current index within #ptrs
+        size_t idx = 0;
     };
 
     /// View provides begin() and end() methods for looping over the objects owned by the
@@ -133,26 +119,12 @@
     class TView {
       public:
         /// @returns an iterator to the beginning of the view
-        TIterator<IS_CONST, true> begin() const {
-            return TIterator<IS_CONST, true>{allocator_->data.pointers.root, 0};
+        TIterator<IS_CONST> begin() const {
+            return TIterator<IS_CONST>{allocator_->data.pointers.root, 0};
         }
 
         /// @returns an iterator to the end of the view
-        TIterator<IS_CONST, true> end() const {
-            return allocator_->data.pointers.current_index >= Pointers::kMax
-                       ? TIterator<IS_CONST, true>{nullptr, 0}
-                       : TIterator<IS_CONST, true>{allocator_->data.pointers.current,
-                                                   allocator_->data.pointers.current_index};
-        }
-
-        /// @returns an iterator to the beginning of the view
-        TIterator<IS_CONST, false> rbegin() const { return TIterator<IS_CONST, false>{nullptr, 0}; }
-
-        /// @returns an iterator to the end of the view
-        TIterator<IS_CONST, false> rend() const {
-            return TIterator<IS_CONST, false>{allocator_->data.pointers.current,
-                                              allocator_->data.pointers.current_index};
-        }
+        TIterator<IS_CONST> end() const { return TIterator<IS_CONST>{nullptr, 0}; }
 
       private:
         friend BlockAllocator;  // For BlockAllocator::operator View()
@@ -162,16 +134,10 @@
 
   public:
     /// A forward-iterator type over the objects of the BlockAllocator
-    using Iterator = TIterator</* const */ false, /* forward */ true>;
+    using Iterator = TIterator</* const */ false>;
 
     /// An immutable forward-iterator type over the objects of the BlockAllocator
-    using ConstIterator = TIterator</* const */ true, /* forward */ true>;
-
-    /// A reverse-iterator type over the objects of the BlockAllocator
-    using ReverseIterator = TIterator</* const */ false, /* forward */ false>;
-
-    /// An immutable reverse-iterator type over the objects of the BlockAllocator
-    using ReverseConstIterator = TIterator</* const */ true, /* forward */ false>;
+    using ConstIterator = TIterator</* const */ true>;
 
     /// View provides begin() and end() methods for looping over the objects owned by the
     /// BlockAllocator.
@@ -287,7 +253,7 @@
     void AddObjectPointer(T* ptr) {
         auto& pointers = data.pointers;
 
-        if (pointers.current_index >= Pointers::kMax) {
+        if (!pointers.current || pointers.current->count == Pointers::kMax) {
             auto* prev_pointers = pointers.current;
             pointers.current = Allocate<Pointers>();
             if (!pointers.current) {
@@ -295,7 +261,7 @@
             }
             pointers.current->next = nullptr;
             pointers.current->prev = prev_pointers;
-            pointers.current_index = 0;
+            pointers.current->count = 0;
 
             if (prev_pointers) {
                 prev_pointers->next = pointers.current;
@@ -304,7 +270,7 @@
             }
         }
 
-        pointers.current->ptrs[pointers.current_index++] = ptr;
+        pointers.current->ptrs[pointers.current->count++] = ptr;
     }
 
     struct {
@@ -326,10 +292,6 @@
             /// The current (end) Pointers structure of the pointers linked list.
             /// AddObjectPointer() adds to this structure.
             Pointers* current = nullptr;
-            /// The array index in #current for the next append.
-            /// Initialized with Pointers::kMax so that the first append triggers a allocation of
-            /// the Pointers structure.
-            size_t current_index = Pointers::kMax;
         } pointers;
 
         size_t count = 0;
diff --git a/src/tint/utils/memory/block_allocator_test.cc b/src/tint/utils/memory/block_allocator_test.cc
index d6da564..821e150 100644
--- a/src/tint/utils/memory/block_allocator_test.cc
+++ b/src/tint/utils/memory/block_allocator_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/utils/memory/block_allocator.h"
 
+#include <vector>
+
 #include "gtest/gtest.h"
 
 namespace tint {
@@ -160,5 +162,30 @@
     }
 }
 
+TEST_F(BlockAllocatorTest, AddWhileIterating) {
+    using Allocator = BlockAllocator<size_t>;
+
+    Allocator allocator;
+    for (int i = 0; i < 20; i++) {
+        allocator.Create(allocator.Count());
+
+        std::vector<size_t*> seen;
+        for (auto* j : allocator.Objects()) {
+            if (*j % 3 == 0) {
+                allocator.Create(allocator.Count());
+            }
+            seen.push_back(j);
+        }
+
+        // Check that iteration-while-adding saw the same list of objects as an
+        // iteration-without-adding.
+        size_t n = 0;
+        for (auto* obj : allocator.Objects()) {
+            ASSERT_TRUE(n < seen.size());
+            EXPECT_EQ(seen[n++], obj);
+        }
+    }
+}
+
 }  // namespace
 }  // namespace tint
diff --git a/src/tint/utils/memory/bump_allocator.h b/src/tint/utils/memory/bump_allocator.h
index 5d4f594..c1df71a 100644
--- a/src/tint/utils/memory/bump_allocator.h
+++ b/src/tint/utils/memory/bump_allocator.h
@@ -15,10 +15,12 @@
 #ifndef SRC_TINT_UTILS_MEMORY_BUMP_ALLOCATOR_H_
 #define SRC_TINT_UTILS_MEMORY_BUMP_ALLOCATOR_H_
 
+#include <algorithm>
 #include <array>
 #include <cstring>
 #include <utility>
 
+#include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/math/math.h"
 #include "src/tint/utils/memory/bitcast.h"
 
@@ -27,16 +29,17 @@
 /// A allocator for chunks of memory. The memory is owned by the BumpAllocator. When the
 /// BumpAllocator is freed all of the allocated memory is freed.
 class BumpAllocator {
-    static constexpr size_t kBlockSize = 64 * 1024;
-
-    /// Block is linked list of memory blocks.
+    /// BlockHeader is linked list of memory blocks.
     /// Blocks are allocated out of heap memory.
-    struct Block {
-        uint8_t data[kBlockSize];
-        Block* next;
+    struct BlockHeader {
+        BlockHeader* next;
     };
 
   public:
+    /// The default size for a block's data. Allocations can be greater than this, but smaller
+    /// allocations will use this size.
+    static constexpr size_t kDefaultBlockDataSize = 64 * 1024;
+
     /// Constructor
     BumpAllocator() = default;
 
@@ -61,38 +64,42 @@
     /// Allocates @p size_in_bytes from the current block, or from a newly allocated block if the
     /// current block is full.
     /// @param size_in_bytes the number of bytes to allocate
-    /// @returns the pointer to the allocated memory or |nullptr| if the memory can not be allocated
-    char* Allocate(size_t size_in_bytes) {
-        auto& block = data.block;
-        if (block.current_offset + size_in_bytes > kBlockSize) {
+    /// @returns the pointer to the allocated memory or `nullptr` if the memory can not be allocated
+    uint8_t* Allocate(size_t size_in_bytes) {
+        if (TINT_UNLIKELY(data.current_offset + size_in_bytes < size_in_bytes)) {
+            return nullptr;  // integer overflow
+        }
+        if (data.current_offset + size_in_bytes > data.current_data_size) {
             // Allocate a new block from the heap
-            auto* prev_block = block.current;
-            block.current = new Block;
-            if (!block.current) {
+            auto* prev_block = data.current;
+            data.current_data_size = std::max(size_in_bytes, kDefaultBlockDataSize);
+            data.current =
+                Bitcast<BlockHeader*>(new uint8_t[sizeof(BlockHeader) + data.current_data_size]);
+            if (!data.current) {
                 return nullptr;  // out of memory
             }
-            block.current->next = nullptr;
-            block.current_offset = 0;
+            data.current->next = nullptr;
+            data.current_offset = 0;
             if (prev_block) {
-                prev_block->next = block.current;
+                prev_block->next = data.current;
             } else {
-                block.root = block.current;
+                data.root = data.current;
             }
         }
 
-        auto* base = &block.current->data[0];
-        auto* ptr = reinterpret_cast<char*>(base + block.current_offset);
-        block.current_offset += size_in_bytes;
+        auto* base = Bitcast<uint8_t*>(data.current) + sizeof(BlockHeader);
+        auto* ptr = base + data.current_offset;
+        data.current_offset += size_in_bytes;
         data.count++;
         return ptr;
     }
 
     /// Frees all allocations from the allocator.
     void Reset() {
-        auto* block = data.block.root;
+        auto* block = data.root;
         while (block != nullptr) {
             auto* next = block->next;
-            delete block;
+            delete[] Bitcast<uint8_t*>(block);
             block = next;
         }
         data = {};
@@ -106,18 +113,16 @@
     BumpAllocator& operator=(const BumpAllocator&) = delete;
 
     struct {
-        struct {
-            /// The root block of the block linked list
-            Block* root = nullptr;
-            /// The current (end) block of the blocked linked list.
-            /// New allocations come from this block
-            Block* current = nullptr;
-            /// The byte offset in #current for the next allocation.
-            /// Initialized with kBlockSize so that the first allocation triggers a block
-            /// allocation.
-            size_t current_offset = kBlockSize;
-        } block;
-
+        /// The root block of the block linked list
+        BlockHeader* root = nullptr;
+        /// The current (end) block of the blocked linked list.
+        /// New allocations come from this block
+        BlockHeader* current = nullptr;
+        /// The byte offset in #current for the next allocation.
+        size_t current_offset = 0;
+        /// The size of the #current, excluding the header size
+        size_t current_data_size = 0;
+        /// Total number of allocations
         size_t count = 0;
     } data;
 };
diff --git a/src/tint/utils/memory/bump_allocator_test.cc b/src/tint/utils/memory/bump_allocator_test.cc
index 4241699..31f3809 100644
--- a/src/tint/utils/memory/bump_allocator_test.cc
+++ b/src/tint/utils/memory/bump_allocator_test.cc
@@ -21,6 +21,29 @@
 
 using BumpAllocatorTest = testing::Test;
 
+TEST_F(BumpAllocatorTest, AllocationSizes) {
+    BumpAllocator allocator;
+    for (size_t n : {1u, 0x10u, 0x100u, 0x1000u, 0x10000u, 0x100000u,  //
+                     2u, 0x34u, 0x567u, 0x8912u, 0x34567u, 0x891234u}) {
+        auto ptr = allocator.Allocate(n);
+        memset(ptr, 0x42, n);
+    }
+}
+
+TEST_F(BumpAllocatorTest, AllocationSizesAroundBlockSize) {
+    for (size_t n : {
+             BumpAllocator::kDefaultBlockDataSize - sizeof(void*),
+             BumpAllocator::kDefaultBlockDataSize - 4,
+             BumpAllocator::kDefaultBlockDataSize,
+             BumpAllocator::kDefaultBlockDataSize + 4,
+             BumpAllocator::kDefaultBlockDataSize + sizeof(void*),
+         }) {
+        BumpAllocator allocator;
+        auto ptr = allocator.Allocate(n);
+        memset(ptr, 0x42, n);
+    }
+}
+
 TEST_F(BumpAllocatorTest, Count) {
     for (size_t n : {0u, 1u, 10u, 16u, 20u, 32u, 50u, 64u, 100u, 256u, 300u, 512u, 500u, 512u}) {
         BumpAllocator allocator;
diff --git a/src/tint/utils/reflection/BUILD.gn b/src/tint/utils/reflection/BUILD.gn
index 86c94c2..4be79b1 100644
--- a/src/tint/utils/reflection/BUILD.gn
+++ b/src/tint/utils/reflection/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -38,7 +38,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "reflection_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/result/BUILD.gn b/src/tint/utils/result/BUILD.gn
index f607347..27838cc 100644
--- a/src/tint/utils/result/BUILD.gn
+++ b/src/tint/utils/result/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -48,7 +48,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "result_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/rtti/BUILD.bazel b/src/tint/utils/rtti/BUILD.bazel
index e724929..893a78a 100644
--- a/src/tint/utils/rtti/BUILD.bazel
+++ b/src/tint/utils/rtti/BUILD.bazel
@@ -61,6 +61,7 @@
 )
 cc_library(
   name = "bench",
+  alwayslink = True,
   srcs = [
     "switch_bench.cc",
   ],
@@ -70,6 +71,7 @@
     "//src/tint/utils/memory",
     "//src/tint/utils/rtti",
     "//src/tint/utils/traits",
+    "@benchmark",
   ],
   copts = COPTS,
   visibility = ["//visibility:public"],
diff --git a/src/tint/utils/rtti/BUILD.cmake b/src/tint/utils/rtti/BUILD.cmake
index 9b256ff..c06aae6 100644
--- a/src/tint/utils/rtti/BUILD.cmake
+++ b/src/tint/utils/rtti/BUILD.cmake
@@ -74,3 +74,7 @@
   tint_utils_rtti
   tint_utils_traits
 )
+
+tint_target_add_external_dependencies(tint_utils_rtti_bench bench
+  "google-benchmark"
+)
diff --git a/src/tint/utils/rtti/BUILD.gn b/src/tint/utils/rtti/BUILD.gn
index cd23285..c54b499 100644
--- a/src/tint/utils/rtti/BUILD.gn
+++ b/src/tint/utils/rtti/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -44,7 +44,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "castable_test.cc",
       "switch_test.cc",
@@ -59,3 +58,16 @@
     ]
   }
 }
+if (tint_build_benchmarks) {
+  tint_unittests_source_set("bench") {
+    sources = [ "switch_bench.cc" ]
+    deps = [
+      "${tint_src_dir}:google_benchmark",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/traits",
+    ]
+  }
+}
diff --git a/src/tint/utils/strconv/BUILD.gn b/src/tint/utils/strconv/BUILD.gn
index e386dcb..aefde2c 100644
--- a/src/tint/utils/strconv/BUILD.gn
+++ b/src/tint/utils/strconv/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -52,7 +52,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "float_to_string_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/utils/symbol/BUILD.gn b/src/tint/utils/symbol/BUILD.gn
index 4eec052..1be7d46 100644
--- a/src/tint/utils/symbol/BUILD.gn
+++ b/src/tint/utils/symbol/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -50,7 +50,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "symbol_table_test.cc",
       "symbol_test.cc",
diff --git a/src/tint/utils/symbol/symbol_table.cc b/src/tint/utils/symbol/symbol_table.cc
index f541b6a..2830a41 100644
--- a/src/tint/utils/symbol/symbol_table.cc
+++ b/src/tint/utils/symbol/symbol_table.cc
@@ -37,7 +37,7 @@
 }
 
 Symbol SymbolTable::RegisterInternal(std::string_view name) {
-    char* name_mem = name_allocator_.Allocate(name.length() + 1);
+    char* name_mem = Bitcast<char*>(name_allocator_.Allocate(name.length() + 1));
     if (name_mem == nullptr) {
         return Symbol();
     }
diff --git a/src/tint/utils/text/BUILD.gn b/src/tint/utils/text/BUILD.gn
index 6c0b1a1..b2df428 100644
--- a/src/tint/utils/text/BUILD.gn
+++ b/src/tint/utils/text/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -50,7 +50,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [
       "string_stream_test.cc",
       "string_test.cc",
diff --git a/src/tint/utils/traits/BUILD.gn b/src/tint/utils/traits/BUILD.gn
index 215b58e..69f793b 100644
--- a/src/tint/utils/traits/BUILD.gn
+++ b/src/tint/utils/traits/BUILD.gn
@@ -25,7 +25,7 @@
 
 import("${tint_src_dir}/tint.gni")
 
-if (tint_build_unittests) {
+if (tint_build_unittests || tint_build_benchmarks) {
   import("//testing/test.gni")
 }
 
@@ -38,7 +38,6 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    testonly = true
     sources = [ "traits_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
index e8b1165..5df49a4 100644
--- a/third_party/CMakeLists.txt
+++ b/third_party/CMakeLists.txt
@@ -14,7 +14,7 @@
 
 if (${TINT_BUILD_BENCHMARKS})
   set(BENCHMARK_ENABLE_TESTING FALSE CACHE BOOL FALSE FORCE)
-  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/benchmark EXCLUDE_FROM_ALL)
+  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/google_benchmark/src EXCLUDE_FROM_ALL)
 endif()
 
 if (${TINT_BUILD_TESTS} AND NOT TARGET gmock)
diff --git a/third_party/google_benchmark/BUILD.gn b/third_party/google_benchmark/BUILD.gn
new file mode 100644
index 0000000..779cf2a
--- /dev/null
+++ b/third_party/google_benchmark/BUILD.gn
@@ -0,0 +1,91 @@
+# Copyright 2019 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build_overrides/build.gni")
+
+config("benchmark_config") {
+  include_dirs = [ "src/include" ]
+
+  if (!is_component_build) {
+    defines = [ "BENCHMARK_STATIC_DEFINE" ]
+  }
+}
+
+component("google_benchmark") {
+  testonly = true
+
+  public = [
+    "src/include/benchmark/benchmark.h",
+    "src/include/benchmark/export.h",
+  ]
+
+  sources = [
+    "src/src/arraysize.h",
+    "src/src/benchmark.cc",
+    "src/src/benchmark_api_internal.cc",
+    "src/src/benchmark_api_internal.h",
+    "src/src/benchmark_name.cc",
+    "src/src/benchmark_register.cc",
+    "src/src/benchmark_register.h",
+    "src/src/benchmark_runner.cc",
+    "src/src/benchmark_runner.h",
+    "src/src/check.cc",
+    "src/src/check.h",
+    "src/src/colorprint.cc",
+    "src/src/colorprint.h",
+    "src/src/commandlineflags.cc",
+    "src/src/commandlineflags.h",
+    "src/src/complexity.cc",
+    "src/src/complexity.h",
+    "src/src/console_reporter.cc",
+    "src/src/counter.cc",
+    "src/src/counter.h",
+    "src/src/csv_reporter.cc",
+    "src/src/cycleclock.h",
+    "src/src/internal_macros.h",
+    "src/src/json_reporter.cc",
+    "src/src/log.h",
+    "src/src/mutex.h",
+    "src/src/perf_counters.cc",
+    "src/src/perf_counters.h",
+    "src/src/re.h",
+    "src/src/reporter.cc",
+    "src/src/statistics.cc",
+    "src/src/statistics.h",
+    "src/src/string_util.cc",
+    "src/src/string_util.h",
+    "src/src/sysinfo.cc",
+    "src/src/thread_manager.h",
+    "src/src/thread_timer.h",
+    "src/src/timers.cc",
+    "src/src/timers.h",
+  ]
+
+  all_dependent_configs = [ ":benchmark_config" ]
+
+  if (build_with_chromium) {
+    configs -= [ "//build/config/compiler:chromium_code" ]
+    configs += [ "//build/config/compiler:no_chromium_code" ]
+  }
+
+  if (is_win) {
+    configs -= [ "//build/config/win:nominmax" ]
+  }
+
+  defines = [
+    "benchmark_EXPORTS=1",
+
+    # Tell gtest to always use standard regular expressions.
+    "HAVE_GNU_POSIX_REGEX=0",
+    "HAVE_POSIX_REGEX=0",
+    "HAVE_STD_REGEX=1",
+  ]
+}
+
+component("benchmark_main") {
+  testonly = true
+  sources = [ "src/src/benchmark_main.cc" ]
+  defines = [ "benchmark_EXPORTS=1" ]
+  deps = [ ":google_benchmark" ]
+}
diff --git a/third_party/google_benchmark/README.chromium b/third_party/google_benchmark/README.chromium
new file mode 100644
index 0000000..637538b
--- /dev/null
+++ b/third_party/google_benchmark/README.chromium
@@ -0,0 +1,13 @@
+Name: Google Benchmark
+Short Name: benchmark
+URL: https://github.com/google/benchmark
+Version: efc89f0b524780b1994d5dddd83a92718e5be492
+License: Apache 2.0
+Security Critical: no
+Shipped: no
+
+Description:
+A microbenchmark support library.
+
+Local Additions:
+* gn file for building in chromium