Import Tint changes from Dawn

Changes:
  - 1f236ba13f56a2cf588662fe0dc8c28e95e7eb06 [tint] Header-build fixes by Ben Clayton <bclayton@google.com>
  - 0e1a335bca4d455664b616a5a3bd63c99b5da78f tint: Add inspection of pixel_local var member types. by Corentin Wallez <cwallez@chromium.org>
  - d6f6f005b3d0c0b5bbb175a500df8e638fd7ea81 tint/hlsl: fix RemoveContinueInSwitch transform to handle... by Antonio Maiorano <amaiorano@google.com>
  - 932aa8d9bdbc8b407f87c7603fe40ab8ab5e6038 [fuzzers] Do not fuzz IR->MSL yet by James Price <jrprice@google.com>
  - fd8a4fd3925942ad96e1fcfe933af9452d9ef399 [fuzzers] Skip clamp_frag_depth + push constants by James Price <jrprice@google.com>
  - 94a6a818d57cd9e0d1be58f4ea896f5bedc4f58d [ir] Validate builtin return types by James Price <jrprice@google.com>
  - 26c29b7adf7f5e1af71d868d88396f8dc59d6a37 [tint][msl] Support pixel_local attachments by Ben Clayton <bclayton@google.com>
  - 538b3b52ad803690c63e0036aec59514922748db [tint][wgsl/writer] Print even on diagnostic errors by Ben Clayton <bclayton@google.com>
  - a4ad8e7c863944e0360a53cc9b04c05781b630c9 Compat: Make vertex_index/instance_index take an attribute by Gregg Tavares <gman@chromium.org>
  - 0a9d5363cc80e5012b5305f8a086c5404cad069f [ir] Make Robustness ICE for ignored bindings by James Price <jrprice@google.com>
  - 6367c8c43399171544f28722d0d7cdf94a314e6d [ir] Disable runtime-sized array clamping by James Price <jrprice@google.com>
  - 4e6f10fe26cb47222d53b3dbc089720314ef7a7b [ir] Add robustness for texture builtins by James Price <jrprice@google.com>
  - 18e089f18ce028fd1ca85a07874269ba74128737 [ir] Add Robustness transform by James Price <jrprice@google.com>
  - 797063c91dc2542776d150ea5a3c856c4702317d tint: Move inspection of workgroup_storage_size to the en... by Corentin Wallez <cwallez@chromium.org>
  - ecd562000d8105c4366bdab02ce8c90ef29071ae tint: remove the unused Inspector::GetStorageSize method. by Corentin Wallez <cwallez@chromium.org>
  - 656255591ca485191ab95a46215411797ad69475 [tint] Add BUILD.bazel files for Tint by Ben Clayton <bclayton@google.com>
  - 4e800c45d5b42e2051be542601c6eed5595065c0 [tint] More tightly constrain pixel_local use by Ben Clayton <bclayton@google.com>
  - 56e31f92052cf038cac7164fc2d957bdb8342b04 [tint] Remove redundant 'case' statement by Ben Clayton <bclayton@google.com>
  - 4604ea84b4b7ce727f2bcc32cf7d4584c84508b5 [tint][msl] Add validation support for 2.3 by Ben Clayton <bclayton@google.com>
  - 7541f3b9eaf1b35aba01f476b0e6925b508e3554 [tint] Rename tint_enable_local_storage_extension -> tint... by Ben Clayton <bclayton@google.com>
  - f7c0ede118e2951cecad53ab9aa74f499591cc0e [ir] Move select over to spirv builtins. by dan sinclair <dsinclair@chromium.org>
  - 669edf7eba06cd8c91d47e45d9e2f4afeab814b5 [ir] Move dot over to spirv builtins. by dan sinclair <dsinclair@chromium.org>
  - e9fe772be150b105ef9e2d91c56d85070bb1d496 [ir] Move atomics over to spirv builtins. by dan sinclair <dsinclair@chromium.org>
  - 721f6389050a1be352beb7372d6d1f0281ec6416 [ir] Move array_length intrinsic to a builtin by dan sinclair <dsinclair@chromium.org>
  - 18909c6e1f419225d4b8585a07b2f2e8f54f6412 Add tint.natvis to CMake and fix outdated visualizers by Antonio Maiorano <amaiorano@google.com>
  - e1c1f0ded0da91ad0b66a8618765db1c9bdb8b2f [ir][spirv-writer] Handle storage texture loads by James Price <jrprice@google.com>
  - ddffd5e3968d5f5e6dd90e7660b186d4f0df5886 [ir] Move matrix and vector intrinsics to builtins by dan sinclair <dsinclair@chromium.org>
  - 759dfc685678005b43bbcd3ff7d5a8ebbc295545 [tint] Polyfill bgra8unorm textureLoad results by James Price <jrprice@google.com>
  - 51db5cf955fec2c70c0084fa1938ce8ef9fcd0d5 VarForDynamicIndex: Skip unevaluated expressions by James Price <jrprice@google.com>
  - b88e1c7c339853fea31bd5209a5dd71066c6a903 [ir] Validate spirv builtin calls by dan sinclair <dsinclair@chromium.org>
  - 65a18313e1091109b7e6e290825cbafdec22775c [tint] Support sparse .def files by Ben Clayton <bclayton@google.com>
  - c7d70317eb4ca783ee56f288eb83b67806f930f3 [tint] Validate 'pixel_local' pipeline stages by Ben Clayton <bclayton@google.com>
  - 14500bc78ca76971f77eb52a4e33dd9c20e6ed84 Tint/Dawn: SPIRV AST writer to support experimental subgr... by Zhaoming Jiang <zhaoming.jiang@intel.com>
  - 3b4b3fdec2b74b02751dedc232fc3e9c4cf28dae [ir] Remove the build ir flags by dan sinclair <dsinclair@chromium.org>
  - 8d733d3f2eb5f0b4406de896b529f7803b4d96f6 Move intrinsic table methods to free functions. by dan sinclair <dsinclair@chromium.org>
  - d305da9acec395be2ae02bb9c66fe1b5196245a6 [tint] Move GLSL AST transforms to lang/glsl/writer/ast_r... by Ben Clayton <bclayton@google.com>
  - f52f569d040e8c6ebb5998b04820b75150fdb412 [tint] Move MSL AST transforms to lang/msl/writer/ast_raise by Ben Clayton <bclayton@google.com>
  - c706b4bb571d4a6e3c6f9c5febe07a4ddd0cf7b6 [ir] Move spirv.def file. by dan sinclair <dsinclair@chromium.org>
  - 774b6a4c44a10f14da7405a0c21548428a9ef3f8 [ir] Add a SPIRV BuiltinCall instruction by dan sinclair <dsinclair@chromium.org>
  - d8a06aed62152e36d4a0c9025100b77407f789f8 [ir] Use FriendlyName for instructions by dan sinclair <dsinclair@chromium.org>
  - 3047f882319f43577a01c6a9882b64e9fcd1ad30 [tint] Move SPIR-V AST transforms to lang/spirv/writer/as... by Ben Clayton <bclayton@google.com>
  - 815d5db31ff56228ff8f67e8504ccc8d9586712c [tint] Move SPIR-V AST transforms to lang/spirv/writer/as... by Ben Clayton <bclayton@google.com>
  - 7a5f54eafc202b9c51cefae379f9e63fcbd14dcd [tint] Move HLSL AST transforms to lang/hlsl/writer/ast_r... by Ben Clayton <bclayton@google.com>
  - 032c1403099cb1a02ef63e8068d28d282d76258a [ir] Move SPIR-V IntrinsicCall to out of ir by dan sinclair <dsinclair@chromium.org>
  - 1997409ad728cc0dcaba4245ff1961575a477848 [tint] Validate 'pixel_local' data types by Ben Clayton <bclayton@google.com>
  - 6cd15d28f2cda74b909c7f270069609ffab0b8e8 [tint] Add 'pixel_local' address space by Ben Clayton <bclayton@google.com>
  - 59f488872a6e8e4e8311394c5ae778d9c627d375 [tint][cmake] Fix TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER by Ben Clayton <bclayton@google.com>
  - 6a56e27e446a266b1a57fa8840ad4e95c3470b2b [tint][ir] Validate builtin calls with the intrinsic table by Ben Clayton <bclayton@google.com>
  - d1368d7cfc777145c8ab83d81f2b59e29793f106 tint: workaround DXC bug with splatted vector constant st... by Antonio Maiorano <amaiorano@google.com>
  - a985f73277b71dd33393652d011e9389640bfe02 [tint] Reflect missing writer options by James Price <jrprice@google.com>
  - f3bcd41e7e96b02875dc9893c6cf57821d472658 [tint][hlsl] Fix validation on unix by Ben Clayton <bclayton@google.com>
  - 89b98283a31c166cd31458350b6e0d1131bb13a5 [tint][build] Link CoreGraphics for metal validation by Ben Clayton <bclayton@google.com>
  - 6c3d888f6340d13df5b4e4fdb492b4d2062bf564 [tint][cmake] Error if building fuzzers without Clang by Ben Clayton <bclayton@google.com>
  - 4ebed9da86ac0c703beee8fd083b6146355c3285 [tint][ir] Move Validator class definition into cc file by Ben Clayton <bclayton@google.com>
  - 62ce2929c61810d2bd6b67cb516e114618f8c991 [tint][glsl] Use CheckSupportedExtensions() in writer by Ben Clayton <bclayton@google.com>
  - 74688c954b958a1b606ce1328c8946058adfb66b [tint] Add 'chromium_experimental_pixel_local' extension by Ben Clayton <bclayton@google.com>
  - b6f45484545f685dca85cad9b18dd8f0953840eb [tint][utils] Remove pointless nullptr check in vector Fr... by Ben Clayton <bclayton@google.com>
  - b6ef5273e530e6186ce6545551ae79d65d4fcbc8 tint/fuzzers: Move specializations out of class scope by Ben Clayton <bclayton@google.com>
  - ba982f25792d4b11f669b1e6b015ec1ff9809f4e clang-format codebase by Ben Clayton <bclayton@google.com>
  - 385155300519e4580d2c1312446701e0d66b4aae tint/spirv/writer: Disable GCC false-positive warning by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 1f236ba13f56a2cf588662fe0dc8c28e95e7eb06
Change-Id: I63c6bfb26a71b91ceef14c1886ce28c9ac9c303e
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/149980
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/include/tint/tint.h b/include/tint/tint.h
index 899de9a..832bf7e 100644
--- a/include/tint/tint.h
+++ b/include/tint/tint.h
@@ -25,6 +25,7 @@
 #include "src/tint/api/options/array_length_from_uniform.h"
 #include "src/tint/api/options/binding_remapper.h"
 #include "src/tint/api/options/external_texture.h"
+#include "src/tint/api/options/pixel_local.h"
 #include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/api/tint.h"
 #include "src/tint/lang/core/type/manager.h"
diff --git a/src/tint/BUILD.bazel b/src/tint/BUILD.bazel
new file mode 100644
index 0000000..ca63402
--- /dev/null
+++ b/src/tint/BUILD.bazel
@@ -0,0 +1,30 @@
+# 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.
+
+# GEN_BUILD:DO_NOT_GENERATE - This is a hand-crafted file.
+
+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)
+
+# 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 51c6b92..d6570a1 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -89,12 +89,6 @@
     defines += [ "TINT_BUILD_SYNTAX_TREE_WRITER=0" ]
   }
 
-  if (tint_build_ir) {
-    defines += [ "TINT_BUILD_IR=1" ]
-  } else {
-    defines += [ "TINT_BUILD_IR=0" ]
-  }
-
   include_dirs = [
     "${tint_root_dir}/",
     "${tint_root_dir}/include/",
@@ -115,6 +109,7 @@
 
 source_set("metal") {
   frameworks = [
+    "CoreGraphics.framework",
     "Foundation.framework",
     "Metal.framework",
   ]
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 8e14741..2da7f95 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -42,7 +42,6 @@
   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}>)
-  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IR=$<BOOL:${TINT_BUILD_IR}>)
 
   common_compile_options(${TARGET})
 endfunction()
@@ -169,6 +168,11 @@
   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_test_cmd_compile_options TARGET)
@@ -217,11 +221,59 @@
 # 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)
+  # Glob all the files at TINT_EXTERNAL_BENCHMARK_CORPUS_DIR, and create a header
+  # that lists these with the macros:
+  # TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS()
+  # TINT_BENCHMARK_EXTERNAL_SPV_PROGRAMS()
+  set(TINT_BENCHMARK_GEN_DIR "${DAWN_BUILD_GEN_DIR}/src/tint/benchmark/")
+  set(TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER "${TINT_BENCHMARK_GEN_DIR}/external_wgsl_programs.h")
+  message("Globbing ${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}...")
+
+  file(GLOB_RECURSE
+    TINT_EXTERNAL_WGSL_BENCHMARK_FILES
+    RELATIVE "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}"
+    "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/**.wgsl"
+  )
+  list(TRANSFORM TINT_EXTERNAL_WGSL_BENCHMARK_FILES REPLACE
+    "(.+)"
+    "    BENCHMARK_CAPTURE\(FUNC, \"\\1\", \"${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/\\1\")"
+  )
+  list(JOIN TINT_EXTERNAL_WGSL_BENCHMARK_FILES "; \\\n" TINT_EXTERNAL_WGSL_BENCHMARK_FILES)
+
+  file(GLOB_RECURSE
+    TINT_EXTERNAL_SPV_BENCHMARK_FILES
+    RELATIVE "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}"
+    "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/**.spv")
+
+  list(TRANSFORM TINT_EXTERNAL_SPV_BENCHMARK_FILES REPLACE
+    "(.+)"
+    "    BENCHMARK_CAPTURE\(FUNC, \"\\1\", \"${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/\\1\")"
+  )
+  list(JOIN TINT_EXTERNAL_SPV_BENCHMARK_FILES "; \\\n" TINT_EXTERNAL_SPV_BENCHMARK_FILES)
+
+  file(CONFIGURE
+    OUTPUT "${TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER}"
+    CONTENT "
+#define TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS(FUNC) \\
+${TINT_EXTERNAL_WGSL_BENCHMARK_FILES};
+
+#define TINT_BENCHMARK_EXTERNAL_SPV_PROGRAMS(FUNC) \\
+${TINT_EXTERNAL_SPV_BENCHMARK_FILES};"
+  )
+endif(TINT_BUILD_BENCHMARKS AND TINT_EXTERNAL_BENCHMARK_CORPUS_DIR)
+
+################################################################################
 # Functions used by BUILD.cmake files
 # The CMake build handles the target kinds in different ways:
 # 'cmd'       - Translates to a CMake executable target.
@@ -434,9 +486,14 @@
         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 ${FoundationFramework} ${MetalFramework})
+        target_link_libraries(${TARGET} PRIVATE
+          ${CoreGraphicsFramework}
+          ${FoundationFramework}
+          ${MetalFramework}
+        )
       elseif(${DEPENDENCY} STREQUAL "spirv-headers")
         tint_spvheaders_compile_options(${TARGET})
       elseif(${DEPENDENCY} STREQUAL "spirv-tools")
@@ -501,6 +558,13 @@
 include("BUILD.cmake")
 
 ################################################################################
+# Bespoke target settings
+################################################################################
+if (MSVC)
+  target_sources(tint_api PRIVATE tint.natvis)
+endif()
+
+################################################################################
 # Additional fuzzer tests
 ################################################################################
 if(TINT_BUILD_TESTS)
@@ -520,56 +584,6 @@
 endif(TINT_BUILD_TESTS)
 
 ################################################################################
-# Benchmarks
-################################################################################
-if(TINT_BUILD_BENCHMARKS AND TINT_EXTERNAL_BENCHMARK_CORPUS_DIR)
-  # Glob all the files at TINT_EXTERNAL_BENCHMARK_CORPUS_DIR, and create a header
-  # that lists these with the macros:
-  # TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS()
-  # TINT_BENCHMARK_EXTERNAL_SPV_PROGRAMS()
-  set(TINT_BENCHMARK_GEN_DIR "${DAWN_BUILD_GEN_DIR}/src/tint/benchmark/")
-  set(TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER "${TINT_BENCHMARK_GEN_DIR}/external_wgsl_programs.h")
-  message("Globbing ${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}...")
-
-  file(GLOB_RECURSE
-    TINT_EXTERNAL_WGSL_BENCHMARK_FILES
-    RELATIVE "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}"
-    "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/**.wgsl"
-  )
-  list(TRANSFORM TINT_EXTERNAL_WGSL_BENCHMARK_FILES REPLACE
-    "(.+)"
-    "    BENCHMARK_CAPTURE\(FUNC, \"\\1\", \"${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/\\1\")"
-  )
-  list(JOIN TINT_EXTERNAL_WGSL_BENCHMARK_FILES "; \\\n" TINT_EXTERNAL_WGSL_BENCHMARK_FILES)
-
-  file(GLOB_RECURSE
-    TINT_EXTERNAL_SPV_BENCHMARK_FILES
-    RELATIVE "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}"
-    "${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/**.spv")
-
-  list(TRANSFORM TINT_EXTERNAL_SPV_BENCHMARK_FILES REPLACE
-    "(.+)"
-    "    BENCHMARK_CAPTURE\(FUNC, \"\\1\", \"${TINT_EXTERNAL_BENCHMARK_CORPUS_DIR}/\\1\")"
-  )
-  list(JOIN TINT_EXTERNAL_SPV_BENCHMARK_FILES "; \\\n" TINT_EXTERNAL_SPV_BENCHMARK_FILES)
-
-  file(CONFIGURE
-    OUTPUT "${TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER}"
-    CONTENT "
-#define TINT_BENCHMARK_EXTERNAL_WGSL_PROGRAMS(FUNC) \\
-${TINT_EXTERNAL_WGSL_BENCHMARK_FILES};
-
-#define TINT_BENCHMARK_EXTERNAL_SPV_PROGRAMS(FUNC) \\
-${TINT_EXTERNAL_SPV_BENCHMARK_FILES};"
-  )
-
-  # Define TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER to the generated header path
-  target_compile_definitions(tint_cmd_bench_bench_cmd PRIVATE
-    "TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER=\"${TINT_BENCHMARK_EXTERNAL_SHADERS_HEADER}\""
-  )
-endif(TINT_BUILD_BENCHMARKS AND TINT_EXTERNAL_BENCHMARK_CORPUS_DIR)
-
-################################################################################
 # Target aliases
 ################################################################################
 add_library(libtint ALIAS tint_api)
diff --git a/src/tint/api/BUILD.bazel b/src/tint/api/BUILD.bazel
new file mode 100644
index 0000000..950f202
--- /dev/null
+++ b/src/tint/api/BUILD.bazel
@@ -0,0 +1,117 @@
+# 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 = "api",
+  srcs = [
+    "tint.cc",
+  ],
+  hdrs = [
+    "tint.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/spirv/reader/common",
+    "//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",
+    "//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_glsl_writer": [
+      "//src/tint/lang/glsl/writer",
+      "//src/tint/lang/glsl/writer/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer",
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer",
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
diff --git a/src/tint/api/common/BUILD.bazel b/src/tint/api/common/BUILD.bazel
new file mode 100644
index 0000000..e22264a
--- /dev/null
+++ b/src/tint/api/common/BUILD.bazel
@@ -0,0 +1,45 @@
+# 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 = "common",
+  srcs = [
+    "common.cc",
+  ],
+  hdrs = [
+    "binding_point.h",
+    "override_id.h",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/api/options/BUILD.bazel b/src/tint/api/options/BUILD.bazel
new file mode 100644
index 0000000..0e57862
--- /dev/null
+++ b/src/tint/api/options/BUILD.bazel
@@ -0,0 +1,50 @@
+# 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 = "options",
+  srcs = [
+    "options.cc",
+  ],
+  hdrs = [
+    "array_length_from_uniform.h",
+    "binding_remapper.h",
+    "external_texture.h",
+    "pixel_local.h",
+    "texture_builtins_from_uniform.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/api/options/BUILD.cmake b/src/tint/api/options/BUILD.cmake
index bb857ab..6f28382 100644
--- a/src/tint/api/options/BUILD.cmake
+++ b/src/tint/api/options/BUILD.cmake
@@ -30,6 +30,7 @@
   api/options/binding_remapper.h
   api/options/external_texture.h
   api/options/options.cc
+  api/options/pixel_local.h
   api/options/texture_builtins_from_uniform.h
 )
 
diff --git a/src/tint/api/options/BUILD.gn b/src/tint/api/options/BUILD.gn
index fa8c423..6b5ccb4 100644
--- a/src/tint/api/options/BUILD.gn
+++ b/src/tint/api/options/BUILD.gn
@@ -31,6 +31,7 @@
     "binding_remapper.h",
     "external_texture.h",
     "options.cc",
+    "pixel_local.h",
     "texture_builtins_from_uniform.h",
   ]
   deps = [
diff --git a/src/tint/api/options/pixel_local.h b/src/tint/api/options/pixel_local.h
new file mode 100644
index 0000000..7ab898d
--- /dev/null
+++ b/src/tint/api/options/pixel_local.h
@@ -0,0 +1,35 @@
+// 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_API_OPTIONS_PIXEL_LOCAL_H_
+#define SRC_TINT_API_OPTIONS_PIXEL_LOCAL_H_
+
+#include <unordered_map>
+
+#include "src/tint/utils/reflection/reflection.h"
+
+namespace tint {
+
+/// Options used to specify pixel local mappings
+struct PixelLocalOptions {
+    /// Index of pixel_local structure member index to attachment index
+    std::unordered_map<uint32_t, uint32_t> attachments;
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(attachments);
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_API_OPTIONS_PIXEL_LOCAL_H_
diff --git a/src/tint/cmd/BUILD.bazel b/src/tint/cmd/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/cmd/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/bench/BUILD.bazel b/src/tint/cmd/bench/BUILD.bazel
new file mode 100644
index 0000000..450439a
--- /dev/null
+++ b/src/tint/cmd/bench/BUILD.bazel
@@ -0,0 +1,144 @@
+# 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 = "bench",
+  srcs = [
+    "benchmark.cc",
+  ],
+  hdrs = [
+    "bench.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/core:bench",
+    "//src/tint/lang/spirv/reader/common",
+    "//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/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/rtti:bench",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_glsl_writer": [
+      "//src/tint/lang/glsl/writer:bench",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer:bench",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer:bench",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer:bench",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
diff --git a/src/tint/cmd/common/BUILD.bazel b/src/tint/cmd/common/BUILD.bazel
new file mode 100644
index 0000000..4215394
--- /dev/null
+++ b/src/tint/cmd/common/BUILD.bazel
@@ -0,0 +1,129 @@
+# 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 = "common",
+  srcs = [
+    "generate_external_texture_bindings.cc",
+    "helper.cc",
+  ],
+  hdrs = [
+    "generate_external_texture_bindings.h",
+    "helper.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/reader/common",
+    "//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",
+    "//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_reader": [
+      "//src/tint/lang/spirv/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "generate_external_texture_bindings_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/cmd/info/BUILD.bazel b/src/tint/cmd/info/BUILD.bazel
new file mode 100644
index 0000000..9e5f3b2
--- /dev/null
+++ b/src/tint/cmd/info/BUILD.bazel
@@ -0,0 +1,83 @@
+# 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_binary(
+  name = "cmd",
+  srcs = [
+    "main.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/cmd/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/inspector",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/command",
+    "//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_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/cmd/loopy/BUILD.bazel b/src/tint/cmd/loopy/BUILD.bazel
new file mode 100644
index 0000000..2d2910c
--- /dev/null
+++ b/src/tint/cmd/loopy/BUILD.bazel
@@ -0,0 +1,120 @@
+# 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_binary(
+  name = "cmd",
+  srcs = [
+    "main.cc",
+  ],
+  deps = [
+    "//src/tint/api",
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+    "//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_glsl_writer": [
+      "//src/tint/lang/glsl/writer",
+      "//src/tint/lang/glsl/writer/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer",
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer",
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
diff --git a/src/tint/cmd/loopy/BUILD.cmake b/src/tint/cmd/loopy/BUILD.cmake
index 20e11d3..0e12784 100644
--- a/src/tint/cmd/loopy/BUILD.cmake
+++ b/src/tint/cmd/loopy/BUILD.cmake
@@ -36,6 +36,7 @@
   tint_cmd_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
   tint_lang_spirv_reader_common
@@ -44,6 +45,7 @@
   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
@@ -74,13 +76,6 @@
   )
 endif(TINT_BUILD_HLSL_WRITER)
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_cmd_loopy_cmd cmd
-    tint_lang_core_ir
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_cmd_loopy_cmd cmd
     tint_lang_msl_writer
diff --git a/src/tint/cmd/loopy/BUILD.gn b/src/tint/cmd/loopy/BUILD.gn
index 844d0b8..fa59018 100644
--- a/src/tint/cmd/loopy/BUILD.gn
+++ b/src/tint/cmd/loopy/BUILD.gn
@@ -35,6 +35,7 @@
     "${tint_src_dir}/cmd/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/hlsl/writer/common",
     "${tint_src_dir}/lang/spirv/reader/common",
@@ -43,6 +44,7 @@
     "${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",
@@ -71,13 +73,6 @@
     deps += [ "${tint_src_dir}/lang/hlsl/writer" ]
   }
 
-  if (tint_build_ir) {
-    deps += [
-      "${tint_src_dir}/lang/core/ir",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-    ]
-  }
-
   if (tint_build_msl_writer) {
     deps += [
       "${tint_src_dir}/lang/msl/writer",
diff --git a/src/tint/cmd/loopy/main.cc b/src/tint/cmd/loopy/main.cc
index b6b5eca..8949dc0 100644
--- a/src/tint/cmd/loopy/main.cc
+++ b/src/tint/cmd/loopy/main.cc
@@ -17,11 +17,8 @@
 #include "src/tint/api/tint.h"
 #include "src/tint/cmd/common/generate_external_texture_bindings.h"
 #include "src/tint/cmd/common/helper.h"
-
-#if TINT_BUILD_IR
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
-#endif  // TINT_BUILD_IR
 
 #if TINT_BUILD_GLSL_WRITER
 #include "src/tint/lang/glsl/writer/writer.h"
@@ -388,7 +385,6 @@
         program = std::move(info.program);
         source_file = std::move(info.source_file);
     }
-#if TINT_BUILD_WGSL_READER && TINT_BUILD_IR
     {
         uint32_t loop_count = 1;
         if (options.loop == Looper::kIRGenerate) {
@@ -401,7 +397,6 @@
             }
         }
     }
-#endif  // TINT_BUILD_IR
 
     bool success = false;
     {
diff --git a/src/tint/cmd/remote_compile/BUILD.bazel b/src/tint/cmd/remote_compile/BUILD.bazel
new file mode 100644
index 0000000..c0275e7
--- /dev/null
+++ b/src/tint/cmd/remote_compile/BUILD.bazel
@@ -0,0 +1,52 @@
+# 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_binary(
+  name = "cmd",
+  srcs = [
+    "main.cc",
+  ],
+  deps = [
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/socket",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    
+  ] + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/validate",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/cmd/remote_compile/main.cc b/src/tint/cmd/remote_compile/main.cc
index 12f5fdb..95e2c48 100644
--- a/src/tint/cmd/remote_compile/main.cc
+++ b/src/tint/cmd/remote_compile/main.cc
@@ -427,6 +427,9 @@
                     if (req.version_major == 2 && req.version_minor == 1) {
                         version = tint::msl::validate::MslVersion::kMsl_2_1;
                     }
+                    if (req.version_major == 2 && req.version_minor == 3) {
+                        version = tint::msl::validate::MslVersion::kMsl_2_3;
+                    }
                     auto result = tint::msl::validate::UsingMetalAPI(req.source, version);
                     CompileResponse resp;
                     if (result.failed) {
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
new file mode 100644
index 0000000..51a2798
--- /dev/null
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -0,0 +1,135 @@
+# 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_binary(
+  name = "test_cmd",
+  srcs = [
+    "main_test.cc",
+  ],
+  deps = [
+    "//src/tint/api",
+    "//src/tint/cmd/common:test",
+    "//src/tint/lang/core/constant:test",
+    "//src/tint/lang/core/intrinsic:test",
+    "//src/tint/lang/core/ir/transform:test",
+    "//src/tint/lang/core/ir:test",
+    "//src/tint/lang/core/type:test",
+    "//src/tint/lang/core: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/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:test",
+    "//src/tint/utils/cli:test",
+    "//src/tint/utils/command:test",
+    "//src/tint/utils/containers:test",
+    "//src/tint/utils/diagnostic:test",
+    "//src/tint/utils/file:test",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/ice:test",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/macros:test",
+    "//src/tint/utils/math:test",
+    "//src/tint/utils/memory:test",
+    "//src/tint/utils/reflection:test",
+    "//src/tint/utils/result:test",
+    "//src/tint/utils/rtti:test",
+    "//src/tint/utils/strconv:test",
+    "//src/tint/utils/symbol:test",
+    "//src/tint/utils/text:test",
+    "//src/tint/utils/traits:test",
+    "@gtest",
+  ] + select({
+    ":tint_build_glsl_writer": [
+      "//src/tint/lang/glsl/writer/ast_printer:test",
+      "//src/tint/lang/glsl/writer/ast_raise:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer/ast_printer:test",
+      "//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": [
+      "//src/tint/lang/spirv/reader/ast_lower:test",
+      "//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": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index 4bde5e7..2c2689d 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -34,6 +34,8 @@
   tint_cmd_common_test
   tint_lang_core_constant_test
   tint_lang_core_intrinsic_test
+  tint_lang_core_ir_transform_test
+  tint_lang_core_ir_test
   tint_lang_core_type_test
   tint_lang_core_test
   tint_lang_wgsl_ast_transform_test
@@ -42,9 +44,11 @@
   tint_lang_wgsl_inspector_test
   tint_lang_wgsl_program_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_test
   tint_utils_cli_test
   tint_utils_command_test
@@ -73,39 +77,29 @@
 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_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_IR)
-  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
-    tint_lang_core_ir_transform_test
-    tint_lang_core_ir_test
-    tint_lang_wgsl_reader_program_to_ir_test
-    tint_lang_wgsl_writer_ir_to_program_test
-  )
-endif(TINT_BUILD_IR)
-
 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_MSL_WRITER AND TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
-    tint_lang_msl_writer_printer_test
-  )
-endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
-
 if(TINT_BUILD_SPV_READER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_spirv_reader_ast_lower_test
     tint_lang_spirv_reader_ast_parser_test
   )
 endif(TINT_BUILD_SPV_READER)
@@ -113,15 +107,11 @@
 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_IR)
-  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
-    tint_lang_spirv_writer_raise_test
-  )
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-
 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 96240b3..6649884 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -39,6 +39,8 @@
       "${tint_src_dir}/lang/core:unittests",
       "${tint_src_dir}/lang/core/constant:unittests",
       "${tint_src_dir}/lang/core/intrinsic:unittests",
+      "${tint_src_dir}/lang/core/ir:unittests",
+      "${tint_src_dir}/lang/core/ir/transform:unittests",
       "${tint_src_dir}/lang/core/type:unittests",
       "${tint_src_dir}/lang/wgsl:unittests",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
@@ -47,9 +49,11 @@
       "${tint_src_dir}/lang/wgsl/inspector:unittests",
       "${tint_src_dir}/lang/wgsl/program: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}/utils/cli:unittests",
       "${tint_src_dir}/utils/command:unittests",
       "${tint_src_dir}/utils/containers:unittests",
@@ -71,48 +75,44 @@
     ]
 
     if (tint_build_glsl_writer) {
-      deps += [ "${tint_src_dir}/lang/glsl/writer/ast_printer:unittests" ]
+      deps += [
+        "${tint_src_dir}/lang/glsl/writer/ast_printer:unittests",
+        "${tint_src_dir}/lang/glsl/writer/ast_raise:unittests",
+      ]
     }
 
     if (tint_build_hlsl_writer) {
-      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_printer:unittests" ]
-    }
-
-    if (tint_build_ir) {
       deps += [
-        "${tint_src_dir}/lang/core/ir:unittests",
-        "${tint_src_dir}/lang/core/ir/transform:unittests",
-        "${tint_src_dir}/lang/wgsl/reader/program_to_ir:unittests",
-        "${tint_src_dir}/lang/wgsl/writer/ir_to_program:unittests",
+        "${tint_src_dir}/lang/hlsl/writer/ast_printer:unittests",
+        "${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_msl_writer && tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/msl/writer/printer:unittests" ]
-    }
-
     if (tint_build_spv_reader) {
-      deps += [ "${tint_src_dir}/lang/spirv/reader/ast_parser:unittests" ]
+      deps += [
+        "${tint_src_dir}/lang/spirv/reader/ast_lower:unittests",
+        "${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",
       ]
     }
-
-    if (tint_build_spv_writer && tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/spirv/writer/raise:unittests" ]
-    }
     testonly = true
     configs += [ "${tint_src_dir}:tint_unittests_config" ]
 
diff --git a/src/tint/cmd/tint/BUILD.bazel b/src/tint/cmd/tint/BUILD.bazel
new file mode 100644
index 0000000..410ea9d
--- /dev/null
+++ b/src/tint/cmd/tint/BUILD.bazel
@@ -0,0 +1,141 @@
+# 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_binary(
+  name = "cmd",
+  srcs = [
+    "main.cc",
+  ],
+  deps = [
+    "//src/tint/api",
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//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",
+    "//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/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_glsl_writer": [
+      "//src/tint/lang/glsl/writer",
+      "//src/tint/lang/glsl/writer/common",
+      
+      
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/validate",
+      "//src/tint/lang/hlsl/writer",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/validate",
+      "//src/tint/lang/msl/writer",
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer",
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/cmd/tint/BUILD.cmake b/src/tint/cmd/tint/BUILD.cmake
index d50b976..32fc985 100644
--- a/src/tint/cmd/tint/BUILD.cmake
+++ b/src/tint/cmd/tint/BUILD.cmake
@@ -36,6 +36,7 @@
   tint_cmd_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_hlsl_writer_common
   tint_lang_spirv_reader_common
@@ -45,6 +46,7 @@
   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
@@ -83,13 +85,6 @@
   )
 endif(TINT_BUILD_HLSL_WRITER)
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_cmd_tint_cmd cmd
-    tint_lang_core_ir
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_cmd_tint_cmd cmd
     tint_lang_msl_validate
diff --git a/src/tint/cmd/tint/BUILD.gn b/src/tint/cmd/tint/BUILD.gn
index 75726ae..b53853e 100644
--- a/src/tint/cmd/tint/BUILD.gn
+++ b/src/tint/cmd/tint/BUILD.gn
@@ -35,6 +35,7 @@
     "${tint_src_dir}/cmd/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/hlsl/writer/common",
     "${tint_src_dir}/lang/spirv/reader/common",
@@ -44,6 +45,7 @@
     "${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",
@@ -80,13 +82,6 @@
     ]
   }
 
-  if (tint_build_ir) {
-    deps += [
-      "${tint_src_dir}/lang/core/ir",
-      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-    ]
-  }
-
   if (tint_build_msl_writer) {
     deps += [
       "${tint_src_dir}/lang/msl/validate",
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 8c7d05d..da47439 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -33,11 +33,12 @@
 #include "spirv-tools/libspirv.hpp"
 #endif  // TINT_BUILD_SPV_READER || TINT_BUILD_SPV_WRITER
 
+#include "src/tint/api/options/pixel_local.h"
 #include "src/tint/api/tint.h"
 #include "src/tint/cmd/common/generate_external_texture_bindings.h"
 #include "src/tint/cmd/common/helper.h"
-#include "src/tint/lang/hlsl/validate/val.h"
-#include "src/tint/lang/msl/validate/val.h"
+#include "src/tint/lang/core/ir/disassembler.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/wgsl/ast/module.h"
 #include "src/tint/lang/wgsl/ast/transform/first_index_offset.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
@@ -45,6 +46,7 @@
 #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"
@@ -71,10 +73,12 @@
 #endif  // TINT_BUILD_WGSL_WRITER
 
 #if TINT_BUILD_MSL_WRITER
+#include "src/tint/lang/msl/validate/val.h"
 #include "src/tint/lang/msl/writer/writer.h"
 #endif  // TINT_BUILD_MSL_WRITER
 
 #if TINT_BUILD_HLSL_WRITER
+#include "src/tint/lang/hlsl/validate/val.h"
 #include "src/tint/lang/hlsl/writer/writer.h"
 #endif  // TINT_BUILD_HLSL_WRITER
 
@@ -82,12 +86,6 @@
 #include "src/tint/lang/glsl/writer/writer.h"
 #endif  // TINT_BUILD_GLSL_WRITER
 
-#if TINT_BUILD_IR
-#include "src/tint/lang/core/ir/disassembler.h"                     // nogncheck
-#include "src/tint/lang/core/ir/module.h"                           // nogncheck
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"  // nogncheck
-#endif                                                              // TINT_BUILD_IR
-
 #if TINT_BUILD_SPV_WRITER
 #define SPV_WRITER_ONLY(x) x
 #else
@@ -121,7 +119,7 @@
 namespace {
 
 /// Prints the given hash value in a format string that the end-to-end test runner can parse.
-void PrintHash(uint32_t hash) {
+[[maybe_unused]] void PrintHash(uint32_t hash) {
     std::cout << "<<HASH: 0x" << std::hex << hash << ">>" << std::endl;
 }
 
@@ -169,11 +167,10 @@
     std::string xcrun_path;
     tint::Hashmap<std::string, double, 8> overrides;
     std::optional<tint::BindingPoint> hlsl_root_constant_binding_point;
+    tint::PixelLocalOptions pixel_local_options;
 
-#if TINT_BUILD_IR
     bool dump_ir = false;
     bool use_ir = false;
-#endif  // TINT_BUILD_IR
 
 #if TINT_BUILD_SYNTAX_TREE_WRITER
     bool dump_ast = false;
@@ -279,7 +276,6 @@
         }
     });
 
-#if TINT_BUILD_IR
     auto& dump_ir = options.Add<BoolOption>("dump-ir", "Writes the IR to stdout", Alias{"emit-ir"},
                                             Default{false});
     TINT_DEFER(opts->dump_ir = *dump_ir.value);
@@ -287,7 +283,6 @@
     auto& use_ir = options.Add<BoolOption>(
         "use-ir", "Use the IR for writers and transforms when possible", Default{false});
     TINT_DEFER(opts->use_ir = *use_ir.value);
-#endif  // TINT_BUILD_IR
 
     auto& verbose =
         options.Add<BoolOption>("verbose", "Verbose output", ShortName{"v"}, Default{false});
@@ -365,6 +360,15 @@
 default to binding 0 of the largest used group plus 1,
 or group 0 if no resource bound)");
 
+    auto& pixel_local_attachments =
+        options.Add<StringOption>("pixel_local_attachments",
+                                  R"(Pixel local storage attachment bindings, comma-separated
+Each binding is of the form MEMBER_INDEX=ATTACHMENT_INDEX,
+where MEMBER_INDEX is the pixel-local structure member
+index and ATTACHMENT_INDEX is the index of the emitted
+attachment.
+)");
+
     auto& skip_hash = options.Add<StringOption>(
         "skip-hash", R"(Skips validation if the hash of the output is equal to any
 of the hash codes in the comma separated list of hashes)");
@@ -445,6 +449,32 @@
         opts->hlsl_root_constant_binding_point = tint::BindingPoint{group.Get(), binding.Get()};
     }
 
+    if (pixel_local_attachments.value.has_value()) {
+        auto bindings = tint::Split(*pixel_local_attachments.value, ",");
+        for (auto& binding : bindings) {
+            auto values = tint::Split(binding, "=");
+            if (values.Length() != 2) {
+                std::cerr << "Invalid binding " << pixel_local_attachments.name << ": " << binding
+                          << std::endl;
+                return false;
+            }
+            auto member_index = tint::ParseUint32(values[0]);
+            if (!member_index) {
+                std::cerr << "Invalid member index for " << pixel_local_attachments.name << ": "
+                          << values[0] << std::endl;
+                return false;
+            }
+            auto attachment_index = tint::ParseUint32(values[1]);
+            if (!attachment_index) {
+                std::cerr << "Invalid attachment index for " << pixel_local_attachments.name << ": "
+                          << values[1] << std::endl;
+                return false;
+            }
+            opts->pixel_local_options.attachments.emplace(member_index.Get(),
+                                                          attachment_index.Get());
+        }
+    }
+
     auto files = result.Get();
     if (files.Length() > 1) {
         std::cerr << "More than one input file specified: "
@@ -551,9 +581,8 @@
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.external_texture_options.bindings_map =
         tint::cmd::GenerateExternalTextureBindings(program);
-#if TINT_BUILD_IR
     gen_options.use_tint_ir = options.use_ir;
-#endif
+
     auto result = tint::spirv::writer::Generate(program, gen_options);
     if (!result) {
         tint::cmd::PrintWGSL(std::cerr, *program);
@@ -658,11 +687,10 @@
 
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::msl::writer::Options gen_options;
-#if TINT_BUILD_IR
     gen_options.use_tint_ir = options.use_ir;
-#endif
     gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
+    gen_options.pixel_local_options = options.pixel_local_options;
     gen_options.external_texture_options.bindings_map =
         tint::cmd::GenerateExternalTextureBindings(input_program);
     gen_options.array_length_from_uniform.ubo_binding = tint::BindingPoint{0, 30};
@@ -691,7 +719,10 @@
     auto msl_version = tint::msl::validate::MslVersion::kMsl_1_2;
     for (auto* enable : program->AST().Enables()) {
         if (enable->HasExtension(tint::core::Extension::kChromiumExperimentalSubgroups)) {
-            msl_version = tint::msl::validate::MslVersion::kMsl_2_1;
+            msl_version = std::max(msl_version, tint::msl::validate::MslVersion::kMsl_2_1);
+        }
+        if (enable->HasExtension(tint::core::Extension::kChromiumExperimentalPixelLocal)) {
+            msl_version = std::max(msl_version, tint::msl::validate::MslVersion::kMsl_2_3);
         }
     }
 
@@ -1084,7 +1115,7 @@
     }
 #endif  // TINT_BUILD_SYNTAX_TREE_WRITER
 
-#if TINT_BUILD_WGSL_READER && TINT_BUILD_IR
+#if TINT_BUILD_WGSL_READER
     if (options.dump_ir) {
         auto result = tint::wgsl::reader::ProgramToIR(program.get());
         if (!result) {
@@ -1097,7 +1128,7 @@
             }
         }
     }
-#endif  // TINT_BUILD_WGSL_READER && TINT_BUILD_IR
+#endif  // TINT_BUILD_WGSL_READER
 
     tint::inspector::Inspector inspector(program.get());
     if (options.dump_inspector_bindings) {
diff --git a/src/tint/flags.bzl b/src/tint/flags.bzl
new file mode 100644
index 0000000..08b5d60
--- /dev/null
+++ b/src/tint/flags.bzl
@@ -0,0 +1,96 @@
+load("@bazel_skylib//rules:common_settings.bzl", "string_flag", "bool_flag")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+
+def declare_bool_flag(name, default):
+    """Create a boolean flag and two config_settings with the names: <name>_true, <name>_false.
+
+    declare_bool_flag is a Bazel Macro that defines a boolean flag with the given name two
+    config_settings, one for True, one for False. Reminder that Bazel has special syntax for
+    unsetting boolean flags, but this does not work well with aliases.
+    https://docs.bazel.build/versions/main/skylark/config.html#using-build-settings-on-the-command-line
+    Thus it is best to define both an "enabled" alias and a "disabled" alias.
+
+    Args:
+        name: string, the name of the flag to create and use for the config_settings
+        default: boolean, if the flag should default to on or off.
+    """
+
+    bool_flag(name = name, build_setting_default = default)
+
+    native.config_setting(
+        name = name + "_true",
+        flag_values = {
+            ":" + name: "True",
+        },
+        visibility = ["//visibility:public"],
+    )
+
+    native.config_setting(
+        name = name + "_false",
+        flag_values = {
+            ":" + name: "False",
+        },
+        visibility = ["//visibility:public"],
+    )
+
+def declare_os_flag():
+    """Creates the 'os' string flag that specifies the OS to target, and a pair of
+    'is_<os>_true' and 'is_<os>_false' targets.
+
+    The OS flag can be specified on the command line with '--//src/tint:os=<os>'
+    """
+
+    OSes = [
+        "win",
+        "linux",
+        "mac",
+        "other"
+    ]
+
+    string_flag(
+        name = "os",
+        build_setting_default = "other",
+        values = OSes,
+    )
+
+    for os in OSes:
+        native.config_setting(
+            name = "is_{}_true".format(os),
+            flag_values = { ":os": os },
+            visibility = ["//visibility:public"],
+        )
+        selects.config_setting_group(
+            name = "is_{}_false".format(os),
+            match_any = [ "is_{}_true".format(other) for other in OSes if other != os],
+            visibility = ["//visibility:public"],
+        )
+
+COPTS = [
+    "-fno-rtti",
+    "-fno-exceptions",
+    "--std=c++17",
+] + select({
+    "//src/tint:tint_build_glsl_writer_true": [ "-DTINT_BUILD_GLSL_WRITER" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_hlsl_writer_true": [ "-DTINT_BUILD_HLSL_WRITER" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_ir_true":          [ "-DTINT_BUILD_IR" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_msl_writer_true":  [ "-DTINT_BUILD_MSL_WRITER" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_spv_reader_true":  [ "-DTINT_BUILD_SPV_READER" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_spv_writer_true":  [ "-DTINT_BUILD_SPV_WRITER" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_wgsl_reader_true": [ "-DTINT_BUILD_WGSL_READER" ],
+    "//conditions:default": [],
+}) + select({
+    "//src/tint:tint_build_wgsl_writer_true": [ "-DTINT_BUILD_WGSL_WRITER" ],
+    "//conditions:default": [],
+})
diff --git a/src/tint/fuzzers/CMakeLists.txt b/src/tint/fuzzers/CMakeLists.txt
index 32eb73e..6fb5e6e 100644
--- a/src/tint/fuzzers/CMakeLists.txt
+++ b/src/tint/fuzzers/CMakeLists.txt
@@ -59,7 +59,7 @@
   add_tint_fuzzer(tint_wgsl_reader_spv_writer_fuzzer)
 endif()
 
-if (TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_IR)
+if (TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
   add_tint_fuzzer(tint_ir_roundtrip_fuzzer)
   target_link_libraries(tint_ir_roundtrip_fuzzer PRIVATE tint_lang_wgsl_writer_ir_to_program)
 endif()
diff --git a/src/tint/fuzzers/data_builder.h b/src/tint/fuzzers/data_builder.h
index 1600a98..89af25f 100644
--- a/src/tint/fuzzers/data_builder.h
+++ b/src/tint/fuzzers/data_builder.h
@@ -137,63 +137,63 @@
             return out;
         }
     };
+};
 
-    /// Specialization for bool
-    template <>
-    struct BuildImpl<bool> {
-        /// Generate a pseudo-random bool
-        /// @param b - data builder to use
-        /// @returns a boolean with even odds of being true or false
-        static bool impl(DataBuilder* b) { return b->generator_.GetBool(); }
-    };
+/// Specialization for bool
+template <>
+struct DataBuilder::BuildImpl<bool> {
+    /// Generate a pseudo-random bool
+    /// @param b - data builder to use
+    /// @returns a boolean with even odds of being true or false
+    static bool impl(DataBuilder* b) { return b->generator_.GetBool(); }
+};
 
-    /// Specialization for std::string
-    template <>
-    struct BuildImpl<std::string> {
-        /// Generate a pseudo-random string
-        /// @param b - data builder to use
-        /// @returns a string filled with pseudo-random data
-        static std::string impl(DataBuilder* b) {
-            auto count = b->build<uint8_t>();
-            if (count == 0) {
-                return "";
-            }
-            std::vector<uint8_t> source(count);
-            b->build(source.data(), count);
-            return {source.begin(), source.end()};
+/// Specialization for std::string
+template <>
+struct DataBuilder::BuildImpl<std::string> {
+    /// Generate a pseudo-random string
+    /// @param b - data builder to use
+    /// @returns a string filled with pseudo-random data
+    static std::string impl(DataBuilder* b) {
+        auto count = b->build<uint8_t>();
+        if (count == 0) {
+            return "";
         }
-    };
+        std::vector<uint8_t> source(count);
+        b->build(source.data(), count);
+        return {source.begin(), source.end()};
+    }
+};
 
-    /// Specialization for std::optional
-    template <typename T>
-    struct BuildImpl<std::optional<T>> {
-        /// Generate a pseudo-random optional<T>
-        /// @param b - data builder to use
-        /// @returns a either a nullopt, or a randomly filled T
-        static std::optional<T> impl(DataBuilder* b) {
-            if (b->build<bool>()) {
-                return b->build<T>();
-            }
-            return std::nullopt;
+/// Specialization for std::optional
+template <typename T>
+struct DataBuilder::BuildImpl<std::optional<T>> {
+    /// Generate a pseudo-random optional<T>
+    /// @param b - data builder to use
+    /// @returns a either a nullopt, or a randomly filled T
+    static std::optional<T> impl(DataBuilder* b) {
+        if (b->build<bool>()) {
+            return b->build<T>();
         }
-    };
+        return std::nullopt;
+    }
+};
 
-    /// Specialization for std::unordered_map<K, V>
-    template <typename K, typename V>
-    struct BuildImpl<std::unordered_map<K, V>> {
-        /// Generate a pseudo-random std::unordered_map<K, V>
-        /// @param b - data builder to use
-        /// @returns std::unordered_map<K, V> filled with
-        /// pseudo-random data
-        static std::unordered_map<K, V> impl(DataBuilder* b) {
-            std::unordered_map<K, V> out;
-            uint8_t count = b->build<uint8_t>();
-            for (uint8_t i = 0; i < count; ++i) {
-                out.emplace(b->build<K>(), b->build<V>());
-            }
-            return out;
+/// Specialization for std::unordered_map<K, V>
+template <typename K, typename V>
+struct DataBuilder::BuildImpl<std::unordered_map<K, V>> {
+    /// Generate a pseudo-random std::unordered_map<K, V>
+    /// @param b - data builder to use
+    /// @returns std::unordered_map<K, V> filled with
+    /// pseudo-random data
+    static std::unordered_map<K, V> impl(DataBuilder* b) {
+        std::unordered_map<K, V> out;
+        uint8_t count = b->build<uint8_t>();
+        for (uint8_t i = 0; i < count; ++i) {
+            out.emplace(b->build<K>(), b->build<V>());
         }
-    };
+        return out;
+    }
 };
 
 }  // namespace tint::fuzzers
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index aa5c65d..421cbbc 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -188,6 +188,16 @@
         return 0;
     }
 
+    // Helper that returns `true` if the program uses the given extension.
+    auto uses_extension = [&program](tint::core::Extension extension) {
+        for (auto* enable : program.AST().Enables()) {
+            if (enable->HasExtension(extension)) {
+                return true;
+            }
+        }
+        return false;
+    };
+
 #if TINT_BUILD_SPV_READER
     if (input_ == InputFormat::kSpv && !SPIRVToolsValidationCheck(program, spirv_input)) {
         FATAL_ERROR(program.Diagnostics(),
@@ -289,6 +299,13 @@
         }
         case OutputFormat::kSpv: {
 #if TINT_BUILD_SPV_WRITER
+            // Skip fuzzing the SPIR-V writer when the `clamp_frag_depth` option is used with a
+            // module that already contains push constants.
+            if (uses_extension(tint::core::Extension::kChromiumExperimentalPushConstant) &&
+                options_spirv_.clamp_frag_depth) {
+                return 0;
+            }
+
             auto result = spirv::writer::Generate(&program, options_spirv_);
             if (result) {
                 generated_spirv_ = std::move(result->spirv);
@@ -310,6 +327,12 @@
         }
         case OutputFormat::kMSL: {
 #if TINT_BUILD_MSL_WRITER
+            // TODO(crbug.com/tint/1967): Skip fuzzing of the IR version of the MSL writer, which is
+            // still under construction.
+            if (options_msl_.use_tint_ir) {
+                return 0;
+            }
+
             // Remap resource numbers to a flat namespace.
             // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
             auto input_program = &program;
@@ -346,9 +369,6 @@
     CHECK_INSPECTOR(program, inspector);
 
     for (auto& ep : entry_points) {
-        inspector.GetStorageSize(ep.name);
-        CHECK_INSPECTOR(program, inspector);
-
         inspector.GetResourceBindings(ep.name);
         CHECK_INSPECTOR(program, inspector);
 
@@ -387,9 +407,6 @@
 
         inspector.GetSamplerTextureUses(ep.name);
         CHECK_INSPECTOR(program, inspector);
-
-        inspector.GetWorkgroupStorageSize(ep.name);
-        CHECK_INSPECTOR(program, inspector);
     }
 }
 
diff --git a/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc b/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
index fab8e32..4df9f49 100644
--- a/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
+++ b/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
@@ -50,6 +50,7 @@
             switch (ext->name) {
                 case tint::core::Extension::kChromiumExperimentalDp4A:
                 case tint::core::Extension::kChromiumExperimentalFullPtrParameters:
+                case tint::core::Extension::kChromiumExperimentalPixelLocal:
                 case tint::core::Extension::kChromiumExperimentalPushConstant:
                 case tint::core::Extension::kChromiumInternalDualSourceBlending:
                 case tint::core::Extension::kChromiumInternalRelaxedUniformLayout:
diff --git a/src/tint/fuzzers/transform_builder.h b/src/tint/fuzzers/transform_builder.h
index 0f921d4..e2c4289 100644
--- a/src/tint/fuzzers/transform_builder.h
+++ b/src/tint/fuzzers/transform_builder.h
@@ -83,137 +83,137 @@
     /// @tparam T - A fuzzer transform
     template <typename T>
     struct AddTransformImpl;
+};
 
-    /// Implementation of AddTransform for ShuffleTransform
-    template <>
-    struct AddTransformImpl<ShuffleTransform> {
-        /// Add instance of ShuffleTransform to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) {
-            tb->manager()->Add<ShuffleTransform>(tb->builder_.build<size_t>());
-        }
-    };
+/// Implementation of AddTransform for ShuffleTransform
+template <>
+struct TransformBuilder::AddTransformImpl<ShuffleTransform> {
+    /// Add instance of ShuffleTransform to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+        tb->manager()->Add<ShuffleTransform>(tb->builder_.build<size_t>());
+    }
+};
 
-    /// Implementation of AddTransform for ast::transform::Robustness
-    template <>
-    struct AddTransformImpl<ast::transform::Robustness> {
-        /// Add instance of ast::transform::Robustness to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) { tb->manager()->Add<ast::transform::Robustness>(); }
-    };
+/// Implementation of AddTransform for ast::transform::Robustness
+template <>
+struct TransformBuilder::AddTransformImpl<ast::transform::Robustness> {
+    /// Add instance of ast::transform::Robustness to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) { tb->manager()->Add<ast::transform::Robustness>(); }
+};
 
-    /// Implementation of AddTransform for ast::transform::FirstIndexOffset
-    template <>
-    struct AddTransformImpl<ast::transform::FirstIndexOffset> {
-        /// Add instance of ast::transform::FirstIndexOffset to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) {
-            struct Config {
-                uint32_t group;
-                uint32_t binding;
-            };
+/// Implementation of AddTransform for ast::transform::FirstIndexOffset
+template <>
+struct TransformBuilder::AddTransformImpl<ast::transform::FirstIndexOffset> {
+    /// Add instance of ast::transform::FirstIndexOffset to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+        struct Config {
+            uint32_t group;
+            uint32_t binding;
+        };
 
-            Config config = tb->builder()->build<Config>();
+        Config config = tb->builder()->build<Config>();
 
-            tb->data_map()->Add<tint::ast::transform::FirstIndexOffset::BindingPoint>(
-                config.binding, config.group);
-            tb->manager()->Add<ast::transform::FirstIndexOffset>();
-        }
-    };
+        tb->data_map()->Add<tint::ast::transform::FirstIndexOffset::BindingPoint>(config.binding,
+                                                                                  config.group);
+        tb->manager()->Add<ast::transform::FirstIndexOffset>();
+    }
+};
 
-    /// Implementation of AddTransform for ast::transform::BindingRemapper
-    template <>
-    struct AddTransformImpl<ast::transform::BindingRemapper> {
-        /// Add instance of ast::transform::BindingRemapper to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) {
-            struct Config {
-                uint8_t old_group;
-                uint8_t old_binding;
-                uint8_t new_group;
-                uint8_t new_binding;
-                core::Access new_access;
-            };
+/// Implementation of AddTransform for ast::transform::BindingRemapper
+template <>
+struct TransformBuilder::AddTransformImpl<ast::transform::BindingRemapper> {
+    /// Add instance of ast::transform::BindingRemapper to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+        struct Config {
+            uint8_t old_group;
+            uint8_t old_binding;
+            uint8_t new_group;
+            uint8_t new_binding;
+            core::Access new_access;
+        };
 
-            std::vector<Config> configs = tb->builder()->vector<Config>();
-            ast::transform::BindingRemapper::BindingPoints binding_points;
-            ast::transform::BindingRemapper::AccessControls accesses;
-            for (const auto& config : configs) {
-                binding_points[{config.old_binding, config.old_group}] = {config.new_binding,
-                                                                          config.new_group};
-                accesses[{config.old_binding, config.old_group}] = config.new_access;
-            }
-
-            tb->data_map()->Add<ast::transform::BindingRemapper::Remappings>(
-                binding_points, accesses, tb->builder()->build<bool>());
-            tb->manager()->Add<ast::transform::BindingRemapper>();
-        }
-    };
-
-    /// Implementation of AddTransform for ast::transform::Renamer
-    template <>
-    struct AddTransformImpl<ast::transform::Renamer> {
-        /// Add instance of ast::transform::Renamer to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) { tb->manager()->Add<ast::transform::Renamer>(); }
-    };
-
-    /// Implementation of AddTransform for ast::transform::SingleEntryPoint
-    template <>
-    struct AddTransformImpl<ast::transform::SingleEntryPoint> {
-        /// Add instance of ast::transform::SingleEntryPoint to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) {
-            auto input = tb->builder()->build<std::string>();
-            ast::transform::SingleEntryPoint::Config cfg(input);
-
-            tb->data_map()->Add<ast::transform::SingleEntryPoint::Config>(cfg);
-            tb->manager()->Add<ast::transform::SingleEntryPoint>();
-        }
-    };  // struct AddTransformImpl<ast::transform::SingleEntryPoint>
-
-    /// Implementation of AddTransform for ast::transform::VertexPulling
-    template <>
-    struct AddTransformImpl<ast::transform::VertexPulling> {
-        /// Add instance of ast::transform::VertexPulling to TransformBuilder
-        /// @param tb - TransformBuilder to add transform to
-        static void impl(TransformBuilder* tb) {
-            ast::transform::VertexPulling::Config cfg;
-            cfg.vertex_state = tb->builder()->vector<ast::transform::VertexBufferLayoutDescriptor>(
-                GenerateVertexBufferLayoutDescriptor);
-            cfg.pulling_group = tb->builder()->build<uint32_t>();
-
-            tb->data_map()->Add<ast::transform::VertexPulling::Config>(cfg);
-            tb->manager()->Add<ast::transform::VertexPulling>();
+        std::vector<Config> configs = tb->builder()->vector<Config>();
+        ast::transform::BindingRemapper::BindingPoints binding_points;
+        ast::transform::BindingRemapper::AccessControls accesses;
+        for (const auto& config : configs) {
+            binding_points[{config.old_binding, config.old_group}] = {config.new_binding,
+                                                                      config.new_group};
+            accesses[{config.old_binding, config.old_group}] = config.new_access;
         }
 
-      private:
-        /// Generate an instance of ast::transform::VertexAttributeDescriptor
-        /// @param b - DataBuilder to use
-        static ast::transform::VertexAttributeDescriptor GenerateVertexAttributeDescriptor(
-            DataBuilder* b) {
-            ast::transform::VertexAttributeDescriptor desc{};
-            desc.format = b->enum_class<ast::transform::VertexFormat>(
-                static_cast<uint8_t>(ast::transform::VertexFormat::kLastEntry) + 1);
-            desc.offset = b->build<uint32_t>();
-            desc.shader_location = b->build<uint32_t>();
-            return desc;
-        }
+        tb->data_map()->Add<ast::transform::BindingRemapper::Remappings>(
+            binding_points, accesses, tb->builder()->build<bool>());
+        tb->manager()->Add<ast::transform::BindingRemapper>();
+    }
+};
 
-        /// Generate an instance of VertexBufferLayoutDescriptor
-        /// @param b - DataBuilder to use
-        static ast::transform::VertexBufferLayoutDescriptor GenerateVertexBufferLayoutDescriptor(
-            DataBuilder* b) {
-            ast::transform::VertexBufferLayoutDescriptor desc;
-            desc.array_stride = b->build<uint32_t>();
-            desc.step_mode = b->enum_class<ast::transform::VertexStepMode>(
-                static_cast<uint8_t>(ast::transform::VertexStepMode::kLastEntry) + 1);
-            desc.attributes = b->vector<ast::transform::VertexAttributeDescriptor>(
-                GenerateVertexAttributeDescriptor);
-            return desc;
-        }
-    };
-};  // class TransformBuilder
+/// Implementation of AddTransform for ast::transform::Renamer
+template <>
+struct TransformBuilder::AddTransformImpl<ast::transform::Renamer> {
+    /// Add instance of ast::transform::Renamer to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) { tb->manager()->Add<ast::transform::Renamer>(); }
+};
+
+/// Implementation of AddTransform for ast::transform::SingleEntryPoint
+template <>
+struct TransformBuilder::AddTransformImpl<ast::transform::SingleEntryPoint> {
+    /// Add instance of ast::transform::SingleEntryPoint to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+        auto input = tb->builder()->build<std::string>();
+        ast::transform::SingleEntryPoint::Config cfg(input);
+
+        tb->data_map()->Add<ast::transform::SingleEntryPoint::Config>(cfg);
+        tb->manager()->Add<ast::transform::SingleEntryPoint>();
+    }
+};
+
+/// Implementation of AddTransform for ast::transform::VertexPulling
+template <>
+struct TransformBuilder::AddTransformImpl<ast::transform::VertexPulling> {
+    /// Add instance of ast::transform::VertexPulling to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+        ast::transform::VertexPulling::Config cfg;
+        cfg.vertex_state = tb->builder()->vector<ast::transform::VertexBufferLayoutDescriptor>(
+            GenerateVertexBufferLayoutDescriptor);
+        cfg.pulling_group = tb->builder()->build<uint32_t>();
+
+        tb->data_map()->Add<ast::transform::VertexPulling::Config>(cfg);
+        tb->manager()->Add<ast::transform::VertexPulling>();
+    }
+
+  private:
+    /// Generate an instance of ast::transform::VertexAttributeDescriptor
+    /// @param b - DataBuilder to use
+    static ast::transform::VertexAttributeDescriptor GenerateVertexAttributeDescriptor(
+        DataBuilder* b) {
+        ast::transform::VertexAttributeDescriptor desc{};
+        desc.format = b->enum_class<ast::transform::VertexFormat>(
+            static_cast<uint8_t>(ast::transform::VertexFormat::kLastEntry) + 1);
+        desc.offset = b->build<uint32_t>();
+        desc.shader_location = b->build<uint32_t>();
+        return desc;
+    }
+
+    /// Generate an instance of VertexBufferLayoutDescriptor
+    /// @param b - DataBuilder to use
+    static ast::transform::VertexBufferLayoutDescriptor GenerateVertexBufferLayoutDescriptor(
+        DataBuilder* b) {
+        ast::transform::VertexBufferLayoutDescriptor desc;
+        desc.array_stride = b->build<uint32_t>();
+        desc.step_mode = b->enum_class<ast::transform::VertexStepMode>(
+            static_cast<uint8_t>(ast::transform::VertexStepMode::kLastEntry) + 1);
+        desc.attributes =
+            b->vector<ast::transform::VertexAttributeDescriptor>(GenerateVertexAttributeDescriptor);
+        return desc;
+    }
+};
 
 }  // namespace tint::fuzzers
 
diff --git a/src/tint/lang/BUILD.bazel b/src/tint/lang/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/lang/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/lang/core/BUILD.bazel b/src/tint/lang/core/BUILD.bazel
new file mode 100644
index 0000000..ce01f3f
--- /dev/null
+++ b/src/tint/lang/core/BUILD.bazel
@@ -0,0 +1,155 @@
+# 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 = "core",
+  srcs = [
+    "access.cc",
+    "address_space.cc",
+    "attribute.cc",
+    "binary_op.cc",
+    "builtin.cc",
+    "builtin_value.cc",
+    "diagnostic_rule.cc",
+    "diagnostic_severity.cc",
+    "extension.cc",
+    "function.cc",
+    "interpolation_sampling.cc",
+    "interpolation_type.cc",
+    "number.cc",
+    "parameter_usage.cc",
+    "texel_format.cc",
+    "unary_op.cc",
+  ],
+  hdrs = [
+    "access.h",
+    "address_space.h",
+    "attribute.h",
+    "binary_op.h",
+    "builtin.h",
+    "builtin_value.h",
+    "diagnostic_rule.h",
+    "diagnostic_severity.h",
+    "evaluation_stage.h",
+    "extension.h",
+    "fluent_types.h",
+    "function.h",
+    "interpolation.h",
+    "interpolation_sampling.h",
+    "interpolation_type.h",
+    "number.h",
+    "parameter_usage.h",
+    "texel_format.h",
+    "unary_op.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "access_test.cc",
+    "address_space_test.cc",
+    "attribute_test.cc",
+    "builtin_test.cc",
+    "builtin_value_test.cc",
+    "diagnostic_rule_test.cc",
+    "diagnostic_severity_test.cc",
+    "extension_test.cc",
+    "interpolation_sampling_test.cc",
+    "interpolation_type_test.cc",
+    "number_test.cc",
+    "texel_format_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "access_bench.cc",
+    "address_space_bench.cc",
+    "attribute_bench.cc",
+    "builtin_bench.cc",
+    "builtin_value_bench.cc",
+    "diagnostic_rule_bench.cc",
+    "diagnostic_severity_bench.cc",
+    "extension_bench.cc",
+    "interpolation_sampling_bench.cc",
+    "interpolation_type_bench.cc",
+    "texel_format_bench.cc",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//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/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/core/address_space.cc b/src/tint/lang/core/address_space.cc
index efc87a6..c09e8a7 100644
--- a/src/tint/lang/core/address_space.cc
+++ b/src/tint/lang/core/address_space.cc
@@ -38,6 +38,9 @@
     if (str == "function") {
         return AddressSpace::kFunction;
     }
+    if (str == "pixel_local") {
+        return AddressSpace::kPixelLocal;
+    }
     if (str == "private") {
         return AddressSpace::kPrivate;
     }
@@ -68,6 +71,8 @@
             return "function";
         case AddressSpace::kHandle:
             return "handle";
+        case AddressSpace::kPixelLocal:
+            return "pixel_local";
         case AddressSpace::kPrivate:
             return "private";
         case AddressSpace::kPushConstant:
diff --git a/src/tint/lang/core/address_space.h b/src/tint/lang/core/address_space.h
index d88857f..9c98159 100644
--- a/src/tint/lang/core/address_space.h
+++ b/src/tint/lang/core/address_space.h
@@ -37,6 +37,7 @@
     kOut,
     kFunction,
     kHandle,  // Tint-internal enum entry - not parsed
+    kPixelLocal,
     kPrivate,
     kPushConstant,
     kStorage,
@@ -62,7 +63,8 @@
 AddressSpace ParseAddressSpace(std::string_view str);
 
 constexpr const char* kAddressSpaceStrings[] = {
-    "__in", "__out", "function", "private", "push_constant", "storage", "uniform", "workgroup",
+    "__in",          "__out",   "function", "pixel_local", "private",
+    "push_constant", "storage", "uniform",  "workgroup",
 };
 
 /// @returns true if the AddressSpace is host-shareable
diff --git a/src/tint/lang/core/address_space_bench.cc b/src/tint/lang/core/address_space_bench.cc
index b2933d6..6ccc78d 100644
--- a/src/tint/lang/core/address_space_bench.cc
+++ b/src/tint/lang/core/address_space_bench.cc
@@ -53,41 +53,48 @@
         "funEtion",
         "PPncTTion",
         "xxuncddon",
-        "p44ivate",
-        "prSSvaVVe",
-        "RriR22e",
+        "pixe44_local",
+        "SSVVxel_local",
+        "pixRR_local",
+        "pixel_local",
+        "pixel_lF9a",
+        "pixel_loca",
+        "pOOxVRl_locH",
+        "prvaye",
+        "llnrrrv77te",
+        "priv4t00",
         "private",
-        "pFva9e",
-        "priate",
-        "VOORRHte",
-        "push_constyn",
-        "punnh_crr77stallt",
-        "pu4h_cons00ant",
+        "rvooe",
+        "zzvate",
+        "piiippa1",
+        "puXXh_constant",
+        "pusII9_nn55nstant",
+        "YusHH_coaastSSrnt",
         "push_constant",
-        "puoo_costan",
-        "ushzzcnstant",
-        "push_coii11apt",
-        "storaXXe",
-        "9II5tnnrage",
-        "stoaSSrHHYe",
+        "pushonkkHan",
+        "jush_consgRt",
+        "puh_cobsant",
+        "storaje",
+        "torage",
+        "qrage",
         "storage",
-        "stkke",
-        "jtogRa",
-        "sbrag",
+        "stoNNge",
+        "torgvv",
+        "QQorage",
+        "unffor",
         "unifojm",
-        "niform",
-        "qform",
+        "uNNwfor8",
         "uniform",
-        "uniNNrm",
-        "nifrvv",
-        "QQiform",
-        "workrorf",
-        "workjroup",
-        "wNNorkrou2",
+        "uniorm",
+        "urriform",
+        "Gniform",
+        "workgrFFup",
+        "Eokgru",
+        "worrgroup",
         "workgroup",
-        "workgrop",
-        "rrorkgroup",
-        "workgroGp",
+        "wokgrou",
+        "woJDkgoup",
+        "okroup",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/core/address_space_test.cc b/src/tint/lang/core/address_space_test.cc
index fa78e4c..c17b37b 100644
--- a/src/tint/lang/core/address_space_test.cc
+++ b/src/tint/lang/core/address_space_test.cc
@@ -47,6 +47,7 @@
     {"__in", AddressSpace::kIn},
     {"__out", AddressSpace::kOut},
     {"function", AddressSpace::kFunction},
+    {"pixel_local", AddressSpace::kPixelLocal},
     {"private", AddressSpace::kPrivate},
     {"push_constant", AddressSpace::kPushConstant},
     {"storage", AddressSpace::kStorage},
@@ -55,18 +56,20 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"ccin", AddressSpace::kUndefined},          {"3", AddressSpace::kUndefined},
-    {"_Vin", AddressSpace::kUndefined},          {"__ou1", AddressSpace::kUndefined},
-    {"qq_Jt", AddressSpace::kUndefined},         {"__oll7t", AddressSpace::kUndefined},
-    {"qquntppHon", AddressSpace::kUndefined},    {"cnciv", AddressSpace::kUndefined},
-    {"funGion", AddressSpace::kUndefined},       {"priviive", AddressSpace::kUndefined},
-    {"8WWivate", AddressSpace::kUndefined},      {"pxxvate", AddressSpace::kUndefined},
-    {"pXh_cggnstant", AddressSpace::kUndefined}, {"pX_Vonstanu", AddressSpace::kUndefined},
-    {"push_consta3t", AddressSpace::kUndefined}, {"Etorage", AddressSpace::kUndefined},
-    {"sPTTrage", AddressSpace::kUndefined},      {"storadxx", AddressSpace::kUndefined},
-    {"u44iform", AddressSpace::kUndefined},      {"unSSfoVVm", AddressSpace::kUndefined},
-    {"RniR22m", AddressSpace::kUndefined},       {"w9rFroup", AddressSpace::kUndefined},
-    {"workgoup", AddressSpace::kUndefined},      {"woVROOrHup", AddressSpace::kUndefined},
+    {"ccin", AddressSpace::kUndefined},           {"3", AddressSpace::kUndefined},
+    {"_Vin", AddressSpace::kUndefined},           {"__ou1", AddressSpace::kUndefined},
+    {"qq_Jt", AddressSpace::kUndefined},          {"__oll7t", AddressSpace::kUndefined},
+    {"qquntppHon", AddressSpace::kUndefined},     {"cnciv", AddressSpace::kUndefined},
+    {"funGion", AddressSpace::kUndefined},        {"pivel_liical", AddressSpace::kUndefined},
+    {"pixel_lWW8al", AddressSpace::kUndefined},   {"piel_xxoMal", AddressSpace::kUndefined},
+    {"pXvatgg", AddressSpace::kUndefined},        {"rvaXe", AddressSpace::kUndefined},
+    {"priv3te", AddressSpace::kUndefined},        {"push_constanE", AddressSpace::kUndefined},
+    {"push_TTPnstant", AddressSpace::kUndefined}, {"puxxdh_constan", AddressSpace::kUndefined},
+    {"s44orage", AddressSpace::kUndefined},       {"stSSraVVe", AddressSpace::kUndefined},
+    {"RtoR22e", AddressSpace::kUndefined},        {"uFfo9m", AddressSpace::kUndefined},
+    {"uniorm", AddressSpace::kUndefined},         {"VOORRHrm", AddressSpace::kUndefined},
+    {"woykgoup", AddressSpace::kUndefined},       {"l77nnrrkgroGp", AddressSpace::kUndefined},
+    {"wo4kgr00up", AddressSpace::kUndefined},
 };
 
 using AddressSpaceParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/core/constant/BUILD.bazel b/src/tint/lang/core/constant/BUILD.bazel
new file mode 100644
index 0000000..95c577b
--- /dev/null
+++ b/src/tint/lang/core/constant/BUILD.bazel
@@ -0,0 +1,118 @@
+# 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 = "constant",
+  srcs = [
+    "composite.cc",
+    "eval.cc",
+    "manager.cc",
+    "node.cc",
+    "scalar.cc",
+    "splat.cc",
+    "value.cc",
+  ],
+  hdrs = [
+    "clone_context.h",
+    "composite.h",
+    "eval.h",
+    "manager.h",
+    "node.h",
+    "scalar.h",
+    "splat.h",
+    "value.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "composite_test.cc",
+    "eval_binary_op_test.cc",
+    "eval_bitcast_test.cc",
+    "eval_builtin_test.cc",
+    "eval_construction_test.cc",
+    "eval_conversion_test.cc",
+    "eval_indexing_test.cc",
+    "eval_member_access_test.cc",
+    "eval_runtime_semantics_test.cc",
+    "eval_test.h",
+    "eval_unary_op_test.cc",
+    "helper_test.h",
+    "manager_test.cc",
+    "scalar_test.cc",
+    "splat_test.cc",
+    "value_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/core/type:test",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/core/core.def b/src/tint/lang/core/core.def
index 45f2ed5..de0c81e 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -84,6 +84,8 @@
   chromium_internal_relaxed_uniform_layout
   // A Chromium-specific extension that enables dual source blending.
   chromium_internal_dual_source_blending
+  // A Chromium-specific extension that enables pixel local storage.
+  chromium_experimental_pixel_local
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#storage-class
@@ -94,6 +96,7 @@
   uniform
   storage
   push_constant
+  pixel_local
   __in
   __out
   @internal handle
diff --git a/src/tint/lang/core/extension.cc b/src/tint/lang/core/extension.cc
index 6d4ff48..102d612 100644
--- a/src/tint/lang/core/extension.cc
+++ b/src/tint/lang/core/extension.cc
@@ -38,6 +38,9 @@
     if (str == "chromium_experimental_full_ptr_parameters") {
         return Extension::kChromiumExperimentalFullPtrParameters;
     }
+    if (str == "chromium_experimental_pixel_local") {
+        return Extension::kChromiumExperimentalPixelLocal;
+    }
     if (str == "chromium_experimental_push_constant") {
         return Extension::kChromiumExperimentalPushConstant;
     }
@@ -69,6 +72,8 @@
             return "chromium_experimental_dp4a";
         case Extension::kChromiumExperimentalFullPtrParameters:
             return "chromium_experimental_full_ptr_parameters";
+        case Extension::kChromiumExperimentalPixelLocal:
+            return "chromium_experimental_pixel_local";
         case Extension::kChromiumExperimentalPushConstant:
             return "chromium_experimental_push_constant";
         case Extension::kChromiumExperimentalReadWriteStorageTexture:
diff --git a/src/tint/lang/core/extension.h b/src/tint/lang/core/extension.h
index da1c9f4..7672ee0 100644
--- a/src/tint/lang/core/extension.h
+++ b/src/tint/lang/core/extension.h
@@ -36,6 +36,7 @@
     kChromiumDisableUniformityAnalysis,
     kChromiumExperimentalDp4A,
     kChromiumExperimentalFullPtrParameters,
+    kChromiumExperimentalPixelLocal,
     kChromiumExperimentalPushConstant,
     kChromiumExperimentalReadWriteStorageTexture,
     kChromiumExperimentalSubgroups,
@@ -62,15 +63,11 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr const char* kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",
-    "chromium_experimental_dp4a",
-    "chromium_experimental_full_ptr_parameters",
-    "chromium_experimental_push_constant",
-    "chromium_experimental_read_write_storage_texture",
-    "chromium_experimental_subgroups",
-    "chromium_internal_dual_source_blending",
-    "chromium_internal_relaxed_uniform_layout",
-    "f16",
+    "chromium_disable_uniformity_analysis",      "chromium_experimental_dp4a",
+    "chromium_experimental_full_ptr_parameters", "chromium_experimental_pixel_local",
+    "chromium_experimental_push_constant",       "chromium_experimental_read_write_storage_texture",
+    "chromium_experimental_subgroups",           "chromium_internal_dual_source_blending",
+    "chromium_internal_relaxed_uniform_layout",  "f16",
 };
 
 // A unique vector of extensions
diff --git a/src/tint/lang/core/extension_bench.cc b/src/tint/lang/core/extension_bench.cc
index d5e13d0..b0602c7 100644
--- a/src/tint/lang/core/extension_bench.cc
+++ b/src/tint/lang/core/extension_bench.cc
@@ -53,48 +53,55 @@
         "chromium_experimentalEfull_ptr_parameters",
         "chromium_experimentalfull_ptr_PPaTTameters",
         "chromium_ddxperimental_fullptrxxparameters",
-        "c44romium_experimental_push_constant",
-        "chromium_experimental_pSSsVV_constant",
-        "chrom22Rm_experimental_pushRonstant",
+        "chromium_experi44ental_pixel_local",
+        "chromium_experimental_VVSixel_local",
+        "chroRium_experimental_pix22Rlocal",
+        "chromium_experimental_pixel_local",
+        "chromiuF_experiment9lpixel_local",
+        "chromium_experimental_pixel_loca",
+        "Vhromium_expeOOimentalHpixRRl_lcal",
+        "chromiym_experimental_push_contant",
+        "nnhro77ium_experimenGal_push_conrrllant",
+        "chromium_experimental_push_c4nstan00",
         "chromium_experimental_push_constant",
-        "chromium_exp9rimFntal_ush_constant",
-        "chrmium_experimental_push_constant",
-        "cOOromium_experiVeHtal_puh_conRRtant",
-        "chromium_experimental_rad_write_storageytexture",
-        "chromiu77_experimentlll_read_write_sGornngerrtexture",
-        "00hromium_experimental_read_write_sto4age_texture",
+        "chooomum_experimental_ush_constat",
+        "chromium_xperimntal_zzush_constant",
+        "chromi11m_experimepptal_psh_ciistant",
+        "chromium_experimental_read_writeXXstorage_texture",
+        "chromium_exII55rimental_read_write99storagnn_texture",
+        "chromiumSSexperaamental_read_wYitrr_storage_HHexture",
         "chromium_experimental_read_write_storage_texture",
-        "chromium_eperimental_read_write_ootrge_texture",
-        "chromium_experimentalzzread_ite_storage_texture",
-        "chiiomium_expperimnal_read_w11ite_storage_texture",
-        "chromium_experimental_subgroXXps",
-        "chromium55eIIperimental_subgnno99ps",
-        "chraamiuSS_experimentaHHr_subgrouYs",
+        "Hhromium_experimeta_rkkad_write_strage_texture",
+        "chromium_experijental_red_wrRgte_storag_texture",
+        "chromium_exbeimentalread_write_storage_texture",
+        "chromium_experimental_sjbgroups",
+        "chromium_experimental_sbgroups",
+        "cromum_experimentalqsubgroups",
         "chromium_experimental_subgroups",
-        "chkkomium_eperimntal_subgroup",
-        "jhromium_experRmental_subgogps",
-        "chromiubexperiental_subgroups",
-        "chromium_internal_dujl_source_blending",
-        "chromium_intenal_dual_source_blending",
-        "chqomium_internal_dual_source_beding",
+        "chromium_expNNrimental_subgoups",
+        "chromium_experimetal_svvbgrous",
+        "chromium_experiQental_subgroups",
+        "chrorum_internal_dal_source_bleffding",
+        "chromium_internal_dual_source_jlending",
+        "chromiNNm_internal_dua8_sourwwe_blening",
         "chromium_internal_dual_source_blending",
-        "chroium_NNnternal_dual_source_blending",
-        "chovvium_internal_dual_source_lending",
-        "chromium_internQQl_dual_sorce_blending",
-        "chromirm_intenal_rfflaxed_unifrm_layout",
-        "chromium_internal_jelaxed_uniform_layout",
-        "chromium_interna_relNNxed_uwwiform_lay82t",
+        "chromium_internal_dual_soure_blending",
+        "chromium_irrternal_dual_source_blending",
+        "chromium_internal_duaG_source_blending",
+        "chromium_internalFFrelaxed_uniform_layout",
+        "chromEum_internal_relaxed_unifrmlyout",
+        "chromium_internalrrrelaxd_uniform_layout",
         "chromium_internal_relaxed_uniform_layout",
-        "chromium_internal_relaxed_uniform_layut",
-        "chromium_internal_relaxed_rrniform_layout",
-        "chromium_internal_relaxedGuniform_layout",
-        "FF16",
-        "",
-        "rr1",
+        "chromiuminternal_relaxed_uniform_layut",
+        "cXroDium_internal_rJJlaed_uniform_layout",
+        "chromium_int8nal_relaed_uniform_layut",
+        "k",
+        "16",
+        "J1",
         "f16",
-        "1",
-        "DJ1",
-        "",
+        "c16",
+        "fO6",
+        "_KKttvv",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/core/extension_test.cc b/src/tint/lang/core/extension_test.cc
index 4e21e78..32149f6 100644
--- a/src/tint/lang/core/extension_test.cc
+++ b/src/tint/lang/core/extension_test.cc
@@ -48,6 +48,7 @@
     {"chromium_experimental_dp4a", Extension::kChromiumExperimentalDp4A},
     {"chromium_experimental_full_ptr_parameters",
      Extension::kChromiumExperimentalFullPtrParameters},
+    {"chromium_experimental_pixel_local", Extension::kChromiumExperimentalPixelLocal},
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
     {"chromium_experimental_read_write_storage_texture",
      Extension::kChromiumExperimentalReadWriteStorageTexture},
@@ -67,24 +68,27 @@
     {"chroium_experimental_full_ptr_paqqppmetHHrs", Extension::kUndefined},
     {"chrium_evperiental_full_ptr_paraceters", Extension::kUndefined},
     {"chromium_expGimental_fullbptr_parameters", Extension::kUndefined},
-    {"chvomium_experimental_push_constiint", Extension::kUndefined},
-    {"chromiu8WWexperimental_push_constant", Extension::kUndefined},
-    {"chromium_experiMental_push_costanxx", Extension::kUndefined},
-    {"chromum_experimental_read_write_stggXage_texture", Extension::kUndefined},
-    {"chromium_exVerimentl_rXad_writu_storge_texture", Extension::kUndefined},
-    {"chromium_experimental3read_write_storage_texture", Extension::kUndefined},
-    {"cEromium_experimental_subgroups", Extension::kUndefined},
-    {"TThromium_experiPPental_sugroups", Extension::kUndefined},
-    {"chddomium_experimental_subgroxxs", Extension::kUndefined},
-    {"chromium_internal_44ual_source_blending", Extension::kUndefined},
-    {"chromium_inteSSnal_dual_source_blendinVV", Extension::kUndefined},
-    {"chromiuR_interna22dual_source_blenRing", Extension::kUndefined},
-    {"chromium_int9rnal_relaxed_Fnifor_layout", Extension::kUndefined},
-    {"chrmium_internal_relaxed_uniform_layout", Extension::kUndefined},
-    {"VRhHomium_internal_relaxd_uniform_OOayout", Extension::kUndefined},
-    {"y1", Extension::kUndefined},
-    {"l77rrn6", Extension::kUndefined},
-    {"4016", Extension::kUndefined},
+    {"vhromium_experimental_pixel_liical", Extension::kUndefined},
+    {"chromium_experiment8l_pixel_lWWcal", Extension::kUndefined},
+    {"chromium_expeimentMl_xxixel_local", Extension::kUndefined},
+    {"chrXmium_experimeggtal_ush_constant", Extension::kUndefined},
+    {"chromiu_experVmentalpusX_constant", Extension::kUndefined},
+    {"chro3ium_experimental_push_constant", Extension::kUndefined},
+    {"chromium_experimentEl_read_write_storage_texture", Extension::kUndefined},
+    {"chromium_experimePPtTT_read_write_storage_texture", Extension::kUndefined},
+    {"chromium_expeimental_read_write_stoddagexxtexture", Extension::kUndefined},
+    {"chromium_experimental_44ubgroups", Extension::kUndefined},
+    {"cSSromVVum_experimental_subgroups", Extension::kUndefined},
+    {"chrmium_e22perimental_suRgrRups", Extension::kUndefined},
+    {"chroFium_internal_dual_source_bl9ndig", Extension::kUndefined},
+    {"chrmium_internal_dual_source_blending", Extension::kUndefined},
+    {"cVromium_interHal_dualOOsouRRce_blening", Extension::kUndefined},
+    {"chromium_internl_relaxyd_uniform_layout", Extension::kUndefined},
+    {"chromnnum_internrr77_Gelaxell_uniform_layout", Extension::kUndefined},
+    {"chromium_intern4l_relaxe00_uniform_layout", Extension::kUndefined},
+    {"5", Extension::kUndefined},
+    {"u16", Extension::kUndefined},
+    {"f", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/core/intrinsic/BUILD.bazel b/src/tint/lang/core/intrinsic/BUILD.bazel
new file mode 100644
index 0000000..de13dd6
--- /dev/null
+++ b/src/tint/lang/core/intrinsic/BUILD.bazel
@@ -0,0 +1,94 @@
+# 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 = "intrinsic",
+  srcs = [
+    "ctor_conv.cc",
+    "table.cc",
+  ],
+  hdrs = [
+    "ctor_conv.h",
+    "table.h",
+    "table_data.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "table_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/core/type:test",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/resolver:test",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/core/intrinsic/data/BUILD.bazel b/src/tint/lang/core/intrinsic/data/BUILD.bazel
new file mode 100644
index 0000000..314ffb3
--- /dev/null
+++ b/src/tint/lang/core/intrinsic/data/BUILD.bazel
@@ -0,0 +1,55 @@
+# 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 = "data",
+  srcs = [
+    "data.cc",
+  ],
+  hdrs = [
+    "data.h",
+    "type_matchers.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//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"],
+)
+
diff --git a/src/tint/lang/core/intrinsic/data/data.cc b/src/tint/lang/core/intrinsic/data/data.cc
index fa6d930..d423120 100644
--- a/src/tint/lang/core/intrinsic/data/data.cc
+++ b/src/tint/lang/core/intrinsic/data/data.cc
@@ -69,7 +69,7 @@
     if (!MatchBool(state, ty)) {
       return nullptr;
     }
-    return BuildBool(state);
+    return BuildBool(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "bool";
@@ -83,7 +83,7 @@
     if (!MatchIa(state, ty)) {
       return nullptr;
     }
-    return BuildIa(state);
+    return BuildIa(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     StringStream ss;
@@ -99,7 +99,7 @@
     if (!MatchFa(state, ty)) {
       return nullptr;
     }
-    return BuildFa(state);
+    return BuildFa(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     StringStream ss;
@@ -115,7 +115,7 @@
     if (!MatchI32(state, ty)) {
       return nullptr;
     }
-    return BuildI32(state);
+    return BuildI32(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "i32";
@@ -129,7 +129,7 @@
     if (!MatchU32(state, ty)) {
       return nullptr;
     }
-    return BuildU32(state);
+    return BuildU32(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "u32";
@@ -143,7 +143,7 @@
     if (!MatchF32(state, ty)) {
       return nullptr;
     }
-    return BuildF32(state);
+    return BuildF32(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "f32";
@@ -157,7 +157,7 @@
     if (!MatchF16(state, ty)) {
       return nullptr;
     }
-    return BuildF16(state);
+    return BuildF16(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "f16";
@@ -176,7 +176,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildVec2(state, T);
+    return BuildVec2(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -196,7 +196,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildVec3(state, T);
+    return BuildVec3(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -216,7 +216,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildVec4(state, T);
+    return BuildVec4(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -236,7 +236,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat2X2(state, T);
+    return BuildMat2X2(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -256,7 +256,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat2X3(state, T);
+    return BuildMat2X3(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -276,7 +276,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat2X4(state, T);
+    return BuildMat2X4(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -296,7 +296,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat3X2(state, T);
+    return BuildMat3X2(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -316,7 +316,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat3X3(state, T);
+    return BuildMat3X3(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -336,7 +336,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat3X4(state, T);
+    return BuildMat3X4(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -356,7 +356,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat4X2(state, T);
+    return BuildMat4X2(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -376,7 +376,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat4X3(state, T);
+    return BuildMat4X3(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -396,7 +396,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat4X4(state, T);
+    return BuildMat4X4(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -421,7 +421,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildVec(state, N, T);
+    return BuildVec(state, ty, N, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string N = state->NumName();
@@ -454,7 +454,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildMat(state, N, M, T);
+    return BuildMat(state, ty, N, M, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string N = state->NumName();
@@ -488,7 +488,7 @@
     if (!A.IsValid()) {
       return nullptr;
     }
-    return BuildPtr(state, S, T, A);
+    return BuildPtr(state, ty, S, T, A);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string S = state->NumName();
@@ -510,7 +510,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildAtomic(state, T);
+    return BuildAtomic(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -530,7 +530,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildArray(state, T);
+    return BuildArray(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -545,7 +545,7 @@
     if (!MatchSampler(state, ty)) {
       return nullptr;
     }
-    return BuildSampler(state);
+    return BuildSampler(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "sampler";
@@ -559,7 +559,7 @@
     if (!MatchSamplerComparison(state, ty)) {
       return nullptr;
     }
-    return BuildSamplerComparison(state);
+    return BuildSamplerComparison(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "sampler_comparison";
@@ -578,7 +578,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTexture1D(state, T);
+    return BuildTexture1D(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -598,7 +598,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTexture2D(state, T);
+    return BuildTexture2D(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -618,7 +618,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTexture2DArray(state, T);
+    return BuildTexture2DArray(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -638,7 +638,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTexture3D(state, T);
+    return BuildTexture3D(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -658,7 +658,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTextureCube(state, T);
+    return BuildTextureCube(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -678,7 +678,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTextureCubeArray(state, T);
+    return BuildTextureCubeArray(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -698,7 +698,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildTextureMultisampled2D(state, T);
+    return BuildTextureMultisampled2D(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -713,7 +713,7 @@
     if (!MatchTextureDepth2D(state, ty)) {
       return nullptr;
     }
-    return BuildTextureDepth2D(state);
+    return BuildTextureDepth2D(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "texture_depth_2d";
@@ -727,7 +727,7 @@
     if (!MatchTextureDepth2DArray(state, ty)) {
       return nullptr;
     }
-    return BuildTextureDepth2DArray(state);
+    return BuildTextureDepth2DArray(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "texture_depth_2d_array";
@@ -741,7 +741,7 @@
     if (!MatchTextureDepthCube(state, ty)) {
       return nullptr;
     }
-    return BuildTextureDepthCube(state);
+    return BuildTextureDepthCube(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "texture_depth_cube";
@@ -755,7 +755,7 @@
     if (!MatchTextureDepthCubeArray(state, ty)) {
       return nullptr;
     }
-    return BuildTextureDepthCubeArray(state);
+    return BuildTextureDepthCubeArray(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "texture_depth_cube_array";
@@ -769,7 +769,7 @@
     if (!MatchTextureDepthMultisampled2D(state, ty)) {
       return nullptr;
     }
-    return BuildTextureDepthMultisampled2D(state);
+    return BuildTextureDepthMultisampled2D(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "texture_depth_multisampled_2d";
@@ -793,7 +793,7 @@
     if (!A.IsValid()) {
       return nullptr;
     }
-    return BuildTextureStorage1D(state, F, A);
+    return BuildTextureStorage1D(state, ty, F, A);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string F = state->NumName();
@@ -819,7 +819,7 @@
     if (!A.IsValid()) {
       return nullptr;
     }
-    return BuildTextureStorage2D(state, F, A);
+    return BuildTextureStorage2D(state, ty, F, A);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string F = state->NumName();
@@ -845,7 +845,7 @@
     if (!A.IsValid()) {
       return nullptr;
     }
-    return BuildTextureStorage2DArray(state, F, A);
+    return BuildTextureStorage2DArray(state, ty, F, A);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string F = state->NumName();
@@ -871,7 +871,7 @@
     if (!A.IsValid()) {
       return nullptr;
     }
-    return BuildTextureStorage3D(state, F, A);
+    return BuildTextureStorage3D(state, ty, F, A);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string F = state->NumName();
@@ -887,7 +887,7 @@
     if (!MatchTextureExternal(state, ty)) {
       return nullptr;
     }
-    return BuildTextureExternal(state);
+    return BuildTextureExternal(state, ty);
   },
 /* string */ [](MatchState*) -> std::string {
     return "texture_external";
@@ -906,7 +906,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildPackedVec3(state, T);
+    return BuildPackedVec3(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -926,7 +926,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildModfResult(state, T);
+    return BuildModfResult(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -953,7 +953,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildModfResultVec(state, N, T);
+    return BuildModfResultVec(state, ty, N, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string N = state->NumName();
@@ -976,7 +976,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildFrexpResult(state, T);
+    return BuildFrexpResult(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -1003,7 +1003,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildFrexpResultVec(state, N, T);
+    return BuildFrexpResultVec(state, ty, N, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string N = state->NumName();
@@ -1026,7 +1026,7 @@
     if (T == nullptr) {
       return nullptr;
     }
-    return BuildAtomicCompareExchangeResult(state, T);
+    return BuildAtomicCompareExchangeResult(state, ty, T);
   },
 /* string */ [](MatchState* state) -> std::string {
   const std::string T = state->TypeName();
@@ -1039,25 +1039,25 @@
 constexpr TypeMatcher kScalarMatcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     if (MatchBool(state, ty)) {
-      return BuildBool(state);
+      return BuildBool(state, ty);
     }
     return nullptr;
   },
@@ -1074,19 +1074,19 @@
 constexpr TypeMatcher kConcreteScalarMatcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     if (MatchBool(state, ty)) {
-      return BuildBool(state);
+      return BuildBool(state, ty);
     }
     return nullptr;
   },
@@ -1103,22 +1103,22 @@
 constexpr TypeMatcher kScalarNoF32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     if (MatchBool(state, ty)) {
-      return BuildBool(state);
+      return BuildBool(state, ty);
     }
     return nullptr;
   },
@@ -1135,22 +1135,22 @@
 constexpr TypeMatcher kScalarNoF16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchBool(state, ty)) {
-      return BuildBool(state);
+      return BuildBool(state, ty);
     }
     return nullptr;
   },
@@ -1167,22 +1167,22 @@
 constexpr TypeMatcher kScalarNoI32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     if (MatchBool(state, ty)) {
-      return BuildBool(state);
+      return BuildBool(state, ty);
     }
     return nullptr;
   },
@@ -1199,22 +1199,22 @@
 constexpr TypeMatcher kScalarNoU32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     if (MatchBool(state, ty)) {
-      return BuildBool(state);
+      return BuildBool(state, ty);
     }
     return nullptr;
   },
@@ -1231,22 +1231,22 @@
 constexpr TypeMatcher kScalarNoBoolMatcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1263,22 +1263,22 @@
 constexpr TypeMatcher kFiaFiu32F16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1295,19 +1295,19 @@
 constexpr TypeMatcher kFiaFi32F16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1324,19 +1324,19 @@
 constexpr TypeMatcher kFiaFiu32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     return nullptr;
   },
@@ -1353,10 +1353,10 @@
 constexpr TypeMatcher kFaF32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     return nullptr;
   },
@@ -1373,13 +1373,13 @@
 constexpr TypeMatcher kFaF32F16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchFa(state, ty)) {
-      return BuildFa(state);
+      return BuildFa(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1396,13 +1396,13 @@
 constexpr TypeMatcher kIaIu32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     return nullptr;
   },
@@ -1419,10 +1419,10 @@
 constexpr TypeMatcher kIaI32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchIa(state, ty)) {
-      return BuildIa(state);
+      return BuildIa(state, ty);
     }
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     return nullptr;
   },
@@ -1439,16 +1439,16 @@
 constexpr TypeMatcher kFiu32F16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1465,13 +1465,13 @@
 constexpr TypeMatcher kFiu32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     return nullptr;
   },
@@ -1488,13 +1488,13 @@
 constexpr TypeMatcher kFi32F16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1511,10 +1511,10 @@
 constexpr TypeMatcher kFi32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     return nullptr;
   },
@@ -1531,10 +1531,10 @@
 constexpr TypeMatcher kF32F16Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchF32(state, ty)) {
-      return BuildF32(state);
+      return BuildF32(state, ty);
     }
     if (MatchF16(state, ty)) {
-      return BuildF16(state);
+      return BuildF16(state, ty);
     }
     return nullptr;
   },
@@ -1551,10 +1551,10 @@
 constexpr TypeMatcher kIu32Matcher {
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
     if (MatchI32(state, ty)) {
-      return BuildI32(state);
+      return BuildI32(state, ty);
     }
     if (MatchU32(state, ty)) {
-      return BuildU32(state);
+      return BuildU32(state, ty);
     }
     return nullptr;
   },
@@ -1570,14 +1570,14 @@
 /// EnumMatcher for 'match f32_texel_format'
 constexpr NumberMatcher kF32TexelFormatMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<TexelFormat>(number.Value())) {
-      case TexelFormat::kBgra8Unorm:
-      case TexelFormat::kRgba8Unorm:
-      case TexelFormat::kRgba8Snorm:
-      case TexelFormat::kRgba16Float:
-      case TexelFormat::kR32Float:
-      case TexelFormat::kRg32Float:
-      case TexelFormat::kRgba32Float:
+    switch (static_cast<core::TexelFormat>(number.Value())) {
+      case core::TexelFormat::kBgra8Unorm:
+      case core::TexelFormat::kRgba8Unorm:
+      case core::TexelFormat::kRgba8Snorm:
+      case core::TexelFormat::kRgba16Float:
+      case core::TexelFormat::kR32Float:
+      case core::TexelFormat::kRg32Float:
+      case core::TexelFormat::kRgba32Float:
         return number;
       default:
         return Number::invalid;
@@ -1591,12 +1591,12 @@
 /// EnumMatcher for 'match i32_texel_format'
 constexpr NumberMatcher kI32TexelFormatMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<TexelFormat>(number.Value())) {
-      case TexelFormat::kRgba8Sint:
-      case TexelFormat::kRgba16Sint:
-      case TexelFormat::kR32Sint:
-      case TexelFormat::kRg32Sint:
-      case TexelFormat::kRgba32Sint:
+    switch (static_cast<core::TexelFormat>(number.Value())) {
+      case core::TexelFormat::kRgba8Sint:
+      case core::TexelFormat::kRgba16Sint:
+      case core::TexelFormat::kR32Sint:
+      case core::TexelFormat::kRg32Sint:
+      case core::TexelFormat::kRgba32Sint:
         return number;
       default:
         return Number::invalid;
@@ -1610,12 +1610,12 @@
 /// EnumMatcher for 'match u32_texel_format'
 constexpr NumberMatcher kU32TexelFormatMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<TexelFormat>(number.Value())) {
-      case TexelFormat::kRgba8Uint:
-      case TexelFormat::kRgba16Uint:
-      case TexelFormat::kR32Uint:
-      case TexelFormat::kRg32Uint:
-      case TexelFormat::kRgba32Uint:
+    switch (static_cast<core::TexelFormat>(number.Value())) {
+      case core::TexelFormat::kRgba8Uint:
+      case core::TexelFormat::kRgba16Uint:
+      case core::TexelFormat::kR32Uint:
+      case core::TexelFormat::kRg32Uint:
+      case core::TexelFormat::kRgba32Uint:
         return number;
       default:
         return Number::invalid;
@@ -1629,8 +1629,8 @@
 /// EnumMatcher for 'match write'
 constexpr NumberMatcher kWriteMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    if (number.IsAny() || number.Value() == static_cast<uint32_t>(Access::kWrite)) {
-      return Number(static_cast<uint32_t>(Access::kWrite));
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::Access::kWrite)) {
+      return Number(static_cast<uint32_t>(core::Access::kWrite));
     }
     return Number::invalid;
   },
@@ -1642,8 +1642,8 @@
 /// EnumMatcher for 'match read_write'
 constexpr NumberMatcher kReadWriteMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    if (number.IsAny() || number.Value() == static_cast<uint32_t>(Access::kReadWrite)) {
-      return Number(static_cast<uint32_t>(Access::kReadWrite));
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::Access::kReadWrite)) {
+      return Number(static_cast<uint32_t>(core::Access::kReadWrite));
     }
     return Number::invalid;
   },
@@ -1655,9 +1655,9 @@
 /// EnumMatcher for 'match readable'
 constexpr NumberMatcher kReadableMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<Access>(number.Value())) {
-      case Access::kRead:
-      case Access::kReadWrite:
+    switch (static_cast<core::Access>(number.Value())) {
+      case core::Access::kRead:
+      case core::Access::kReadWrite:
         return number;
       default:
         return Number::invalid;
@@ -1671,9 +1671,9 @@
 /// EnumMatcher for 'match writable'
 constexpr NumberMatcher kWritableMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<Access>(number.Value())) {
-      case Access::kWrite:
-      case Access::kReadWrite:
+    switch (static_cast<core::Access>(number.Value())) {
+      case core::Access::kWrite:
+      case core::Access::kReadWrite:
         return number;
       default:
         return Number::invalid;
@@ -1687,10 +1687,10 @@
 /// EnumMatcher for 'match function_private_workgroup'
 constexpr NumberMatcher kFunctionPrivateWorkgroupMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<AddressSpace>(number.Value())) {
-      case AddressSpace::kFunction:
-      case AddressSpace::kPrivate:
-      case AddressSpace::kWorkgroup:
+    switch (static_cast<core::AddressSpace>(number.Value())) {
+      case core::AddressSpace::kFunction:
+      case core::AddressSpace::kPrivate:
+      case core::AddressSpace::kWorkgroup:
         return number;
       default:
         return Number::invalid;
@@ -1704,9 +1704,9 @@
 /// EnumMatcher for 'match workgroup_or_storage'
 constexpr NumberMatcher kWorkgroupOrStorageMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<AddressSpace>(number.Value())) {
-      case AddressSpace::kWorkgroup:
-      case AddressSpace::kStorage:
+    switch (static_cast<core::AddressSpace>(number.Value())) {
+      case core::AddressSpace::kWorkgroup:
+      case core::AddressSpace::kStorage:
         return number;
       default:
         return Number::invalid;
@@ -1720,8 +1720,8 @@
 /// EnumMatcher for 'match storage'
 constexpr NumberMatcher kStorageMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    if (number.IsAny() || number.Value() == static_cast<uint32_t>(AddressSpace::kStorage)) {
-      return Number(static_cast<uint32_t>(AddressSpace::kStorage));
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::AddressSpace::kStorage)) {
+      return Number(static_cast<uint32_t>(core::AddressSpace::kStorage));
     }
     return Number::invalid;
   },
@@ -1733,8 +1733,8 @@
 /// EnumMatcher for 'match workgroup'
 constexpr NumberMatcher kWorkgroupMatcher {
 /* match */ [](MatchState&, Number number) -> Number {
-    if (number.IsAny() || number.Value() == static_cast<uint32_t>(AddressSpace::kWorkgroup)) {
-      return Number(static_cast<uint32_t>(AddressSpace::kWorkgroup));
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::AddressSpace::kWorkgroup)) {
+      return Number(static_cast<uint32_t>(core::AddressSpace::kWorkgroup));
     }
     return Number::invalid;
   },
@@ -2053,2455 +2053,2455 @@
 constexpr ParameterInfo kParameters[] = {
   {
     /* [0] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(3),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(4),
   },
   {
     /* [1] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [2] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [3] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [4] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [5] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [6] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [7] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [8] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [9] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [10] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [11] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [12] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [13] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [14] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [15] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [16] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [17] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [18] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [19] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [20] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [21] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [22] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [23] */
-    /* usage */ ParameterUsage::kDdx,
+    /* usage */ core::ParameterUsage::kDdx,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [24] */
-    /* usage */ ParameterUsage::kDdy,
+    /* usage */ core::ParameterUsage::kDdy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [25] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [26] */
-    /* usage */ ParameterUsage::kComponent,
+    /* usage */ core::ParameterUsage::kComponent,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [27] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [28] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [29] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [30] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [31] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [32] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [33] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [34] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [35] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [36] */
-    /* usage */ ParameterUsage::kDepthRef,
+    /* usage */ core::ParameterUsage::kDepthRef,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [37] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [38] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [39] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [40] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [41] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [42] */
-    /* usage */ ParameterUsage::kBias,
+    /* usage */ core::ParameterUsage::kBias,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [43] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [44] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [45] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [46] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [47] */
-    /* usage */ ParameterUsage::kDdx,
+    /* usage */ core::ParameterUsage::kDdx,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [48] */
-    /* usage */ ParameterUsage::kDdy,
+    /* usage */ core::ParameterUsage::kDdy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [49] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [50] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [51] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [52] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [53] */
-    /* usage */ ParameterUsage::kDdx,
+    /* usage */ core::ParameterUsage::kDdx,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [54] */
-    /* usage */ ParameterUsage::kDdy,
+    /* usage */ core::ParameterUsage::kDdy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [55] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [56] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [57] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [58] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [59] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [60] */
-    /* usage */ ParameterUsage::kDdx,
+    /* usage */ core::ParameterUsage::kDdx,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [61] */
-    /* usage */ ParameterUsage::kDdy,
+    /* usage */ core::ParameterUsage::kDdy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [62] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [63] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [64] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [65] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [66] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [67] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [68] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [69] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [70] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [71] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [72] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [73] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [74] */
-    /* usage */ ParameterUsage::kComponent,
+    /* usage */ core::ParameterUsage::kComponent,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [75] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [76] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [77] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [78] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [79] */
-    /* usage */ ParameterUsage::kComponent,
+    /* usage */ core::ParameterUsage::kComponent,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [80] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(46),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [81] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [82] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [83] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [84] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [85] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [86] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [87] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [88] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [89] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [90] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [91] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [92] */
-    /* usage */ ParameterUsage::kDepthRef,
+    /* usage */ core::ParameterUsage::kDepthRef,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [93] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [94] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [95] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [96] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [97] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [98] */
-    /* usage */ ParameterUsage::kDepthRef,
+    /* usage */ core::ParameterUsage::kDepthRef,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [99] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(60),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [100] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [101] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [102] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [103] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [104] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [105] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [106] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [107] */
-    /* usage */ ParameterUsage::kBias,
+    /* usage */ core::ParameterUsage::kBias,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [108] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [109] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [110] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [111] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [112] */
-    /* usage */ ParameterUsage::kBias,
+    /* usage */ core::ParameterUsage::kBias,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [113] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [114] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [115] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [116] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [117] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [118] */
-    /* usage */ ParameterUsage::kBias,
+    /* usage */ core::ParameterUsage::kBias,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [119] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [120] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [121] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [122] */
-    /* usage */ ParameterUsage::kDdx,
+    /* usage */ core::ParameterUsage::kDdx,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [123] */
-    /* usage */ ParameterUsage::kDdy,
+    /* usage */ core::ParameterUsage::kDdy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [124] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [125] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [126] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [127] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [128] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [129] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [130] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [131] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [132] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [133] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [134] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(68),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [135] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [136] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [137] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [138] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [139] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [140] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [141] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [142] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [143] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [144] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [145] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [146] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [147] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [148] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [149] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [150] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [151] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [152] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(35),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [153] */
-    /* usage */ ParameterUsage::kComponent,
+    /* usage */ core::ParameterUsage::kComponent,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [154] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(44),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [155] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [156] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [157] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [158] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [159] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [160] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [161] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [162] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(172),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [163] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [164] */
-    /* usage */ ParameterUsage::kDepthRef,
+    /* usage */ core::ParameterUsage::kDepthRef,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [165] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(58),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [166] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [167] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [168] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [169] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(62),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [170] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [171] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [172] */
-    /* usage */ ParameterUsage::kOffset,
+    /* usage */ core::ParameterUsage::kOffset,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(64),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [173] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [174] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [175] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [176] */
-    /* usage */ ParameterUsage::kBias,
+    /* usage */ core::ParameterUsage::kBias,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [177] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(66),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [178] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [179] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [180] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [181] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [182] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [183] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(54),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [184] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [185] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [186] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [187] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [188] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(28),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [189] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [190] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [191] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [192] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [193] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [194] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [195] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [196] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [197] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [198] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [199] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [200] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(173),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [201] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [202] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [203] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [204] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [205] */
-    /* usage */ ParameterUsage::kX,
+    /* usage */ core::ParameterUsage::kX,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [206] */
-    /* usage */ ParameterUsage::kY,
+    /* usage */ core::ParameterUsage::kY,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [207] */
-    /* usage */ ParameterUsage::kZ,
+    /* usage */ core::ParameterUsage::kZ,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [208] */
-    /* usage */ ParameterUsage::kW,
+    /* usage */ core::ParameterUsage::kW,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [209] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [210] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [211] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [212] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [213] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [214] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [215] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [216] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [217] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [218] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [219] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [220] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(50),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [221] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [222] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [223] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [224] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [225] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [226] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [227] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [228] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [229] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [230] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [231] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [232] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [233] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [234] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [235] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(56),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [236] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [237] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [238] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [239] */
-    /* usage */ ParameterUsage::kSampler,
+    /* usage */ core::ParameterUsage::kSampler,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(170),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [240] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [241] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [242] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [243] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(28),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [244] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [245] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [246] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(28),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [247] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
   },
   {
     /* [248] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [249] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(28),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [250] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [251] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [252] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [253] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [254] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [255] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [256] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(10),
   },
   {
     /* [257] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [258] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [259] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [260] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [261] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [262] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [263] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [264] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [265] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(12),
   },
   {
     /* [266] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [267] */
-    /* usage */ ParameterUsage::kValue,
+    /* usage */ core::ParameterUsage::kValue,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(74),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [268] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [269] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [270] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [271] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [272] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [273] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [274] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(42),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [275] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(78),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [276] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [277] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(48),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [278] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [279] */
-    /* usage */ ParameterUsage::kSampleIndex,
+    /* usage */ core::ParameterUsage::kSampleIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(171),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [280] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [281] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [282] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [283] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(164),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [284] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [285] */
-    /* usage */ ParameterUsage::kSampleIndex,
+    /* usage */ core::ParameterUsage::kSampleIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [286] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [287] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [288] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [289] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [290] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [291] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [292] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
   },
   {
     /* [293] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [294] */
-    /* usage */ ParameterUsage::kArrayIndex,
+    /* usage */ core::ParameterUsage::kArrayIndex,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [295] */
-    /* usage */ ParameterUsage::kXy,
+    /* usage */ core::ParameterUsage::kXy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [296] */
-    /* usage */ ParameterUsage::kZ,
+    /* usage */ core::ParameterUsage::kZ,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [297] */
-    /* usage */ ParameterUsage::kW,
+    /* usage */ core::ParameterUsage::kW,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [298] */
-    /* usage */ ParameterUsage::kX,
+    /* usage */ core::ParameterUsage::kX,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [299] */
-    /* usage */ ParameterUsage::kYz,
+    /* usage */ core::ParameterUsage::kYz,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [300] */
-    /* usage */ ParameterUsage::kW,
+    /* usage */ core::ParameterUsage::kW,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [301] */
-    /* usage */ ParameterUsage::kX,
+    /* usage */ core::ParameterUsage::kX,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [302] */
-    /* usage */ ParameterUsage::kY,
+    /* usage */ core::ParameterUsage::kY,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [303] */
-    /* usage */ ParameterUsage::kZw,
+    /* usage */ core::ParameterUsage::kZw,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [304] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [305] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [306] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [307] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(20),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [308] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(32),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [309] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [310] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(36),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [311] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [312] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(38),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [313] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [314] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(42),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [315] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [316] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(44),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [317] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [318] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(46),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [319] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(21),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [320] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(160),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [321] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [322] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(161),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [323] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [324] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(162),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [325] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [326] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(163),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [327] */
-    /* usage */ ParameterUsage::kLevel,
+    /* usage */ core::ParameterUsage::kLevel,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [328] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(169),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [329] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [330] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [331] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [332] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [333] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [334] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
   },
   {
     /* [335] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [336] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [337] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [338] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [339] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [340] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
   },
   {
     /* [341] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [342] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(14),
   },
   {
     /* [343] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [344] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(16),
   },
   {
     /* [345] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [346] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(18),
   },
   {
     /* [347] */
-    /* usage */ ParameterUsage::kCoords,
+    /* usage */ core::ParameterUsage::kCoords,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [348] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [349] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [350] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(82),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [351] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [352] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [353] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [354] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [355] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [356] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(3),
   },
   {
     /* [357] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [358] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(22),
   },
   {
     /* [359] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(3),
   },
   {
     /* [360] */
-    /* usage */ ParameterUsage::kXy,
+    /* usage */ core::ParameterUsage::kXy,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [361] */
-    /* usage */ ParameterUsage::kZw,
+    /* usage */ core::ParameterUsage::kZw,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(70),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [362] */
-    /* usage */ ParameterUsage::kXyz,
+    /* usage */ core::ParameterUsage::kXyz,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [363] */
-    /* usage */ ParameterUsage::kW,
+    /* usage */ core::ParameterUsage::kW,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [364] */
-    /* usage */ ParameterUsage::kX,
+    /* usage */ core::ParameterUsage::kX,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [365] */
-    /* usage */ ParameterUsage::kZyw,
+    /* usage */ core::ParameterUsage::kZyw,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [366] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(0),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(0),
   },
   {
     /* [367] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [368] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [369] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(14),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
   },
   {
     /* [370] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(26),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [371] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(28),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [372] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(30),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(6),
   },
   {
     /* [373] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(165),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [374] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(166),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [375] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(167),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [376] */
-    /* usage */ ParameterUsage::kTexture,
+    /* usage */ core::ParameterUsage::kTexture,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(168),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(2),
   },
   {
     /* [377] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(53),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [378] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(87),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [379] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(76),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [380] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(78),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [381] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(98),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [382] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(104),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [383] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [384] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(106),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [385] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(110),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [386] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [387] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(112),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [388] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(116),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [389] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [390] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(118),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [391] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(122),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [392] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [393] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(124),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [394] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(128),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [395] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [396] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(130),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [397] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(134),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [398] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [399] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(136),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [400] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(140),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [401] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [402] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(142),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [403] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(146),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [404] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [405] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(148),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [406] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(152),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [407] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(156),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
   {
     /* [408] */
-    /* usage */ ParameterUsage::kNone,
+    /* usage */ core::ParameterUsage::kNone,
     /* type_matcher_indices */ TypeMatcherIndicesIndex(154),
     /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
   },
@@ -4752,112 +4752,112 @@
 static_assert(TemplateNumberIndex::CanIndex(kTemplateNumbers),
               "TemplateNumberIndex is not large enough to index kTemplateNumbers");
 
-constexpr constant::Eval::Function kConstEvalFunctions[] = {
-  /* [0] */ &constant::Eval::abs,
-  /* [1] */ &constant::Eval::acos,
-  /* [2] */ &constant::Eval::acosh,
-  /* [3] */ &constant::Eval::all,
-  /* [4] */ &constant::Eval::any,
-  /* [5] */ &constant::Eval::asin,
-  /* [6] */ &constant::Eval::asinh,
-  /* [7] */ &constant::Eval::atan,
-  /* [8] */ &constant::Eval::atan2,
-  /* [9] */ &constant::Eval::atanh,
-  /* [10] */ &constant::Eval::ceil,
-  /* [11] */ &constant::Eval::clamp,
-  /* [12] */ &constant::Eval::cos,
-  /* [13] */ &constant::Eval::cosh,
-  /* [14] */ &constant::Eval::countLeadingZeros,
-  /* [15] */ &constant::Eval::countOneBits,
-  /* [16] */ &constant::Eval::countTrailingZeros,
-  /* [17] */ &constant::Eval::cross,
-  /* [18] */ &constant::Eval::degrees,
-  /* [19] */ &constant::Eval::determinant,
-  /* [20] */ &constant::Eval::distance,
-  /* [21] */ &constant::Eval::dot,
-  /* [22] */ &constant::Eval::exp,
-  /* [23] */ &constant::Eval::exp2,
-  /* [24] */ &constant::Eval::extractBits,
-  /* [25] */ &constant::Eval::faceForward,
-  /* [26] */ &constant::Eval::firstLeadingBit,
-  /* [27] */ &constant::Eval::firstTrailingBit,
-  /* [28] */ &constant::Eval::floor,
-  /* [29] */ &constant::Eval::fma,
-  /* [30] */ &constant::Eval::fract,
-  /* [31] */ &constant::Eval::frexp,
-  /* [32] */ &constant::Eval::insertBits,
-  /* [33] */ &constant::Eval::inverseSqrt,
-  /* [34] */ &constant::Eval::ldexp,
-  /* [35] */ &constant::Eval::length,
-  /* [36] */ &constant::Eval::log,
-  /* [37] */ &constant::Eval::log2,
-  /* [38] */ &constant::Eval::max,
-  /* [39] */ &constant::Eval::min,
-  /* [40] */ &constant::Eval::mix,
-  /* [41] */ &constant::Eval::modf,
-  /* [42] */ &constant::Eval::normalize,
-  /* [43] */ &constant::Eval::pack2x16float,
-  /* [44] */ &constant::Eval::pack2x16snorm,
-  /* [45] */ &constant::Eval::pack2x16unorm,
-  /* [46] */ &constant::Eval::pack4x8snorm,
-  /* [47] */ &constant::Eval::pack4x8unorm,
-  /* [48] */ &constant::Eval::pow,
-  /* [49] */ &constant::Eval::quantizeToF16,
-  /* [50] */ &constant::Eval::radians,
-  /* [51] */ &constant::Eval::reflect,
-  /* [52] */ &constant::Eval::refract,
-  /* [53] */ &constant::Eval::reverseBits,
-  /* [54] */ &constant::Eval::round,
-  /* [55] */ &constant::Eval::saturate,
-  /* [56] */ &constant::Eval::select_bool,
-  /* [57] */ &constant::Eval::select_boolvec,
-  /* [58] */ &constant::Eval::sign,
-  /* [59] */ &constant::Eval::sin,
-  /* [60] */ &constant::Eval::sinh,
-  /* [61] */ &constant::Eval::smoothstep,
-  /* [62] */ &constant::Eval::sqrt,
-  /* [63] */ &constant::Eval::step,
-  /* [64] */ &constant::Eval::tan,
-  /* [65] */ &constant::Eval::tanh,
-  /* [66] */ &constant::Eval::transpose,
-  /* [67] */ &constant::Eval::trunc,
-  /* [68] */ &constant::Eval::unpack2x16float,
-  /* [69] */ &constant::Eval::unpack2x16snorm,
-  /* [70] */ &constant::Eval::unpack2x16unorm,
-  /* [71] */ &constant::Eval::unpack4x8snorm,
-  /* [72] */ &constant::Eval::unpack4x8unorm,
-  /* [73] */ &constant::Eval::Identity,
-  /* [74] */ &constant::Eval::Not,
-  /* [75] */ &constant::Eval::Complement,
-  /* [76] */ &constant::Eval::UnaryMinus,
-  /* [77] */ &constant::Eval::Plus,
-  /* [78] */ &constant::Eval::Minus,
-  /* [79] */ &constant::Eval::Multiply,
-  /* [80] */ &constant::Eval::MultiplyMatVec,
-  /* [81] */ &constant::Eval::MultiplyVecMat,
-  /* [82] */ &constant::Eval::MultiplyMatMat,
-  /* [83] */ &constant::Eval::Divide,
-  /* [84] */ &constant::Eval::Modulo,
-  /* [85] */ &constant::Eval::Xor,
-  /* [86] */ &constant::Eval::And,
-  /* [87] */ &constant::Eval::Or,
-  /* [88] */ &constant::Eval::LogicalAnd,
-  /* [89] */ &constant::Eval::LogicalOr,
-  /* [90] */ &constant::Eval::Equal,
-  /* [91] */ &constant::Eval::NotEqual,
-  /* [92] */ &constant::Eval::LessThan,
-  /* [93] */ &constant::Eval::GreaterThan,
-  /* [94] */ &constant::Eval::LessThanEqual,
-  /* [95] */ &constant::Eval::GreaterThanEqual,
-  /* [96] */ &constant::Eval::ShiftLeft,
-  /* [97] */ &constant::Eval::ShiftRight,
-  /* [98] */ &constant::Eval::Zero,
-  /* [99] */ &constant::Eval::Conv,
-  /* [100] */ &constant::Eval::VecSplat,
-  /* [101] */ &constant::Eval::VecInitS,
-  /* [102] */ &constant::Eval::VecInitM,
-  /* [103] */ &constant::Eval::MatInitS,
-  /* [104] */ &constant::Eval::MatInitV,
+constexpr core::constant::Eval::Function kConstEvalFunctions[] = {
+  /* [0] */ &core::constant::Eval::abs,
+  /* [1] */ &core::constant::Eval::acos,
+  /* [2] */ &core::constant::Eval::acosh,
+  /* [3] */ &core::constant::Eval::all,
+  /* [4] */ &core::constant::Eval::any,
+  /* [5] */ &core::constant::Eval::asin,
+  /* [6] */ &core::constant::Eval::asinh,
+  /* [7] */ &core::constant::Eval::atan,
+  /* [8] */ &core::constant::Eval::atan2,
+  /* [9] */ &core::constant::Eval::atanh,
+  /* [10] */ &core::constant::Eval::ceil,
+  /* [11] */ &core::constant::Eval::clamp,
+  /* [12] */ &core::constant::Eval::cos,
+  /* [13] */ &core::constant::Eval::cosh,
+  /* [14] */ &core::constant::Eval::countLeadingZeros,
+  /* [15] */ &core::constant::Eval::countOneBits,
+  /* [16] */ &core::constant::Eval::countTrailingZeros,
+  /* [17] */ &core::constant::Eval::cross,
+  /* [18] */ &core::constant::Eval::degrees,
+  /* [19] */ &core::constant::Eval::determinant,
+  /* [20] */ &core::constant::Eval::distance,
+  /* [21] */ &core::constant::Eval::dot,
+  /* [22] */ &core::constant::Eval::exp,
+  /* [23] */ &core::constant::Eval::exp2,
+  /* [24] */ &core::constant::Eval::extractBits,
+  /* [25] */ &core::constant::Eval::faceForward,
+  /* [26] */ &core::constant::Eval::firstLeadingBit,
+  /* [27] */ &core::constant::Eval::firstTrailingBit,
+  /* [28] */ &core::constant::Eval::floor,
+  /* [29] */ &core::constant::Eval::fma,
+  /* [30] */ &core::constant::Eval::fract,
+  /* [31] */ &core::constant::Eval::frexp,
+  /* [32] */ &core::constant::Eval::insertBits,
+  /* [33] */ &core::constant::Eval::inverseSqrt,
+  /* [34] */ &core::constant::Eval::ldexp,
+  /* [35] */ &core::constant::Eval::length,
+  /* [36] */ &core::constant::Eval::log,
+  /* [37] */ &core::constant::Eval::log2,
+  /* [38] */ &core::constant::Eval::max,
+  /* [39] */ &core::constant::Eval::min,
+  /* [40] */ &core::constant::Eval::mix,
+  /* [41] */ &core::constant::Eval::modf,
+  /* [42] */ &core::constant::Eval::normalize,
+  /* [43] */ &core::constant::Eval::pack2x16float,
+  /* [44] */ &core::constant::Eval::pack2x16snorm,
+  /* [45] */ &core::constant::Eval::pack2x16unorm,
+  /* [46] */ &core::constant::Eval::pack4x8snorm,
+  /* [47] */ &core::constant::Eval::pack4x8unorm,
+  /* [48] */ &core::constant::Eval::pow,
+  /* [49] */ &core::constant::Eval::quantizeToF16,
+  /* [50] */ &core::constant::Eval::radians,
+  /* [51] */ &core::constant::Eval::reflect,
+  /* [52] */ &core::constant::Eval::refract,
+  /* [53] */ &core::constant::Eval::reverseBits,
+  /* [54] */ &core::constant::Eval::round,
+  /* [55] */ &core::constant::Eval::saturate,
+  /* [56] */ &core::constant::Eval::select_bool,
+  /* [57] */ &core::constant::Eval::select_boolvec,
+  /* [58] */ &core::constant::Eval::sign,
+  /* [59] */ &core::constant::Eval::sin,
+  /* [60] */ &core::constant::Eval::sinh,
+  /* [61] */ &core::constant::Eval::smoothstep,
+  /* [62] */ &core::constant::Eval::sqrt,
+  /* [63] */ &core::constant::Eval::step,
+  /* [64] */ &core::constant::Eval::tan,
+  /* [65] */ &core::constant::Eval::tanh,
+  /* [66] */ &core::constant::Eval::transpose,
+  /* [67] */ &core::constant::Eval::trunc,
+  /* [68] */ &core::constant::Eval::unpack2x16float,
+  /* [69] */ &core::constant::Eval::unpack2x16snorm,
+  /* [70] */ &core::constant::Eval::unpack2x16unorm,
+  /* [71] */ &core::constant::Eval::unpack4x8snorm,
+  /* [72] */ &core::constant::Eval::unpack4x8unorm,
+  /* [73] */ &core::constant::Eval::Identity,
+  /* [74] */ &core::constant::Eval::Not,
+  /* [75] */ &core::constant::Eval::Complement,
+  /* [76] */ &core::constant::Eval::UnaryMinus,
+  /* [77] */ &core::constant::Eval::Plus,
+  /* [78] */ &core::constant::Eval::Minus,
+  /* [79] */ &core::constant::Eval::Multiply,
+  /* [80] */ &core::constant::Eval::MultiplyMatVec,
+  /* [81] */ &core::constant::Eval::MultiplyVecMat,
+  /* [82] */ &core::constant::Eval::MultiplyMatMat,
+  /* [83] */ &core::constant::Eval::Divide,
+  /* [84] */ &core::constant::Eval::Modulo,
+  /* [85] */ &core::constant::Eval::Xor,
+  /* [86] */ &core::constant::Eval::And,
+  /* [87] */ &core::constant::Eval::Or,
+  /* [88] */ &core::constant::Eval::LogicalAnd,
+  /* [89] */ &core::constant::Eval::LogicalOr,
+  /* [90] */ &core::constant::Eval::Equal,
+  /* [91] */ &core::constant::Eval::NotEqual,
+  /* [92] */ &core::constant::Eval::LessThan,
+  /* [93] */ &core::constant::Eval::GreaterThan,
+  /* [94] */ &core::constant::Eval::LessThanEqual,
+  /* [95] */ &core::constant::Eval::GreaterThanEqual,
+  /* [96] */ &core::constant::Eval::ShiftLeft,
+  /* [97] */ &core::constant::Eval::ShiftRight,
+  /* [98] */ &core::constant::Eval::Zero,
+  /* [99] */ &core::constant::Eval::Conv,
+  /* [100] */ &core::constant::Eval::VecSplat,
+  /* [101] */ &core::constant::Eval::VecInitS,
+  /* [102] */ &core::constant::Eval::VecInitM,
+  /* [103] */ &core::constant::Eval::MatInitS,
+  /* [104] */ &core::constant::Eval::MatInitV,
 };
 
 static_assert(ConstEvalFunctionIndex::CanIndex(kConstEvalFunctions),
@@ -12214,27 +12214,27 @@
     /* const_eval_functions */ kConstEvalFunctions,
     /* ctor_conv */ kConstructorsAndConverters,
     /* builtins */ kBuiltins,
-    /* binary_plus */ kBinaryOperators[kBinaryOperatorPlus],
-    /* binary_minus */ kBinaryOperators[kBinaryOperatorMinus],
-    /* binary_star */ kBinaryOperators[kBinaryOperatorStar],
-    /* binary_divide */ kBinaryOperators[kBinaryOperatorDivide],
-    /* binary_modulo */ kBinaryOperators[kBinaryOperatorModulo],
-    /* binary_xor */ kBinaryOperators[kBinaryOperatorXor],
-    /* binary_and */ kBinaryOperators[kBinaryOperatorAnd],
-    /* binary_or */ kBinaryOperators[kBinaryOperatorOr],
-    /* binary_logical_and */ kBinaryOperators[kBinaryOperatorLogicalAnd],
-    /* binary_logical_or */ kBinaryOperators[kBinaryOperatorLogicalOr],
-    /* binary_equal */ kBinaryOperators[kBinaryOperatorEqual],
-    /* binary_not_equal */ kBinaryOperators[kBinaryOperatorNotEqual],
-    /* binary_less_than */ kBinaryOperators[kBinaryOperatorLessThan],
-    /* binary_greater_than */ kBinaryOperators[kBinaryOperatorGreaterThan],
-    /* binary_less_than_equal */ kBinaryOperators[kBinaryOperatorLessThanEqual],
-    /* binary_greater_than_equal */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
-    /* binary_shift_left */ kBinaryOperators[kBinaryOperatorShiftLeft],
-    /* binary_shift_right */ kBinaryOperators[kBinaryOperatorShiftRight],
-    /* unary_not */ kUnaryOperators[kUnaryOperatorNot],
-    /* unary_complement */ kUnaryOperators[kUnaryOperatorComplement],
-    /* unary_minus */ kUnaryOperators[kUnaryOperatorMinus],
+    /* binary '+' */ kBinaryOperators[kBinaryOperatorPlus],
+    /* binary '-' */ kBinaryOperators[kBinaryOperatorMinus],
+    /* binary '*' */ kBinaryOperators[kBinaryOperatorStar],
+    /* binary '/' */ kBinaryOperators[kBinaryOperatorDivide],
+    /* binary '%' */ kBinaryOperators[kBinaryOperatorModulo],
+    /* binary '^' */ kBinaryOperators[kBinaryOperatorXor],
+    /* binary '&' */ kBinaryOperators[kBinaryOperatorAnd],
+    /* binary '|' */ kBinaryOperators[kBinaryOperatorOr],
+    /* binary '&&' */ kBinaryOperators[kBinaryOperatorLogicalAnd],
+    /* binary '||' */ kBinaryOperators[kBinaryOperatorLogicalOr],
+    /* binary '==' */ kBinaryOperators[kBinaryOperatorEqual],
+    /* binary '!=' */ kBinaryOperators[kBinaryOperatorNotEqual],
+    /* binary '<' */ kBinaryOperators[kBinaryOperatorLessThan],
+    /* binary '>' */ kBinaryOperators[kBinaryOperatorGreaterThan],
+    /* binary '<=' */ kBinaryOperators[kBinaryOperatorLessThanEqual],
+    /* binary '>=' */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
+    /* binary '<<' */ kBinaryOperators[kBinaryOperatorShiftLeft],
+    /* binary '>>' */ kBinaryOperators[kBinaryOperatorShiftRight],
+    /* unary '!' */ kUnaryOperators[kUnaryOperatorNot],
+    /* unary '~' */ kUnaryOperators[kUnaryOperatorComplement],
+    /* unary '-' */ kUnaryOperators[kUnaryOperatorMinus],
 };
 
 }  // namespace tint::core::intrinsic::data
diff --git a/src/tint/lang/core/intrinsic/data/type_matchers.h b/src/tint/lang/core/intrinsic/data/type_matchers.h
index c2beb3c..1dbd965 100644
--- a/src/tint/lang/core/intrinsic/data/type_matchers.h
+++ b/src/tint/lang/core/intrinsic/data/type_matchers.h
@@ -46,29 +46,29 @@
     return ty->IsAnyOf<intrinsic::Any, type::Bool>();
 }
 
-inline const type::AbstractFloat* BuildFa(intrinsic::MatchState& state) {
+inline const type::AbstractFloat* BuildFa(intrinsic::MatchState& state, const type::Type*) {
     return state.types.AFloat();
 }
 
 inline bool MatchFa(intrinsic::MatchState& state, const type::Type* ty) {
     return (state.earliest_eval_stage <= EvaluationStage::kConstant) &&
-           ty->IsAnyOf<intrinsic::Any, core::type::AbstractNumeric>();
+           ty->IsAnyOf<intrinsic::Any, type::AbstractNumeric>();
 }
 
-inline const type::AbstractInt* BuildIa(intrinsic::MatchState& state) {
+inline const type::AbstractInt* BuildIa(intrinsic::MatchState& state, const type::Type*) {
     return state.types.AInt();
 }
 
 inline bool MatchIa(intrinsic::MatchState& state, const type::Type* ty) {
     return (state.earliest_eval_stage <= EvaluationStage::kConstant) &&
-           ty->IsAnyOf<intrinsic::Any, core::type::AbstractInt>();
+           ty->IsAnyOf<intrinsic::Any, type::AbstractInt>();
 }
 
-inline const type::Bool* BuildBool(intrinsic::MatchState& state) {
+inline const type::Bool* BuildBool(intrinsic::MatchState& state, const type::Type*) {
     return state.types.bool_();
 }
 
-inline const type::F16* BuildF16(intrinsic::MatchState& state) {
+inline const type::F16* BuildF16(intrinsic::MatchState& state, const type::Type*) {
     return state.types.f16();
 }
 
@@ -76,7 +76,7 @@
     return ty->IsAnyOf<intrinsic::Any, type::F16, type::AbstractNumeric>();
 }
 
-inline const type::F32* BuildF32(intrinsic::MatchState& state) {
+inline const type::F32* BuildF32(intrinsic::MatchState& state, const type::Type*) {
     return state.types.f32();
 }
 
@@ -84,7 +84,7 @@
     return ty->IsAnyOf<intrinsic::Any, type::F32, type::AbstractNumeric>();
 }
 
-inline const type::I32* BuildI32(intrinsic::MatchState& state) {
+inline const type::I32* BuildI32(intrinsic::MatchState& state, const type::Type*) {
     return state.types.i32();
 }
 
@@ -92,7 +92,7 @@
     return ty->IsAnyOf<intrinsic::Any, type::I32, type::AbstractInt>();
 }
 
-inline const type::U32* BuildU32(intrinsic::MatchState& state) {
+inline const type::U32* BuildU32(intrinsic::MatchState& state, const type::Type*) {
     return state.types.u32();
 }
 
@@ -110,7 +110,7 @@
         return true;
     }
 
-    if (auto* v = ty->As<core::type::Vector>()) {
+    if (auto* v = ty->As<type::Vector>()) {
         N = v->Width();
         T = v->type();
         return true;
@@ -125,7 +125,7 @@
         return true;
     }
 
-    if (auto* v = ty->As<core::type::Vector>()) {
+    if (auto* v = ty->As<type::Vector>()) {
         if (v->Width() == N) {
             T = v->type();
             return true;
@@ -135,13 +135,16 @@
 }
 
 inline const type::Vector* BuildVec(intrinsic::MatchState& state,
+                                    const type::Type*,
                                     intrinsic::Number N,
                                     const type::Type* el) {
     return state.types.vec(el, N.Value());
 }
 
 template <uint32_t N>
-inline const type::Vector* BuildVec(intrinsic::MatchState& state, const type::Type* el) {
+inline const type::Vector* BuildVec(intrinsic::MatchState& state,
+                                    const type::Type*,
+                                    const type::Type* el) {
     return state.types.vec(el, N);
 }
 
@@ -159,7 +162,7 @@
         return true;
     }
 
-    if (auto* v = ty->As<core::type::Vector>()) {
+    if (auto* v = ty->As<type::Vector>()) {
         if (v->Packed()) {
             T = v->type();
             return true;
@@ -168,7 +171,9 @@
     return false;
 }
 
-inline const type::Vector* BuildPackedVec3(intrinsic::MatchState& state, const type::Type* el) {
+inline const type::Vector* BuildPackedVec3(intrinsic::MatchState& state,
+                                           const type::Type*,
+                                           const type::Type* el) {
     return state.types.Get<type::Vector>(el, 3u, /* packed */ true);
 }
 
@@ -183,7 +188,7 @@
         T = ty;
         return true;
     }
-    if (auto* m = ty->As<core::type::Matrix>()) {
+    if (auto* m = ty->As<type::Matrix>()) {
         M = m->columns();
         N = m->ColumnType()->Width();
         T = m->type();
@@ -198,7 +203,7 @@
         T = ty;
         return true;
     }
-    if (auto* m = ty->As<core::type::Matrix>()) {
+    if (auto* m = ty->As<type::Matrix>()) {
         if (m->columns() == C && m->rows() == R) {
             T = m->type();
             return true;
@@ -208,6 +213,7 @@
 }
 
 inline const type::Matrix* BuildMat(intrinsic::MatchState& state,
+                                    const type::Type*,
                                     intrinsic::Number C,
                                     intrinsic::Number R,
                                     const type::Type* T) {
@@ -216,7 +222,9 @@
 }
 
 template <uint32_t C, uint32_t R>
-inline const type::Matrix* BuildMat(intrinsic::MatchState& state, const type::Type* T) {
+inline const type::Matrix* BuildMat(intrinsic::MatchState& state,
+                                    const type::Type*,
+                                    const type::Type* T) {
     auto* column_type = state.types.vec(T, R);
     return state.types.mat(column_type, C);
 }
@@ -247,8 +255,8 @@
         return true;
     }
 
-    if (auto* a = ty->As<core::type::Array>()) {
-        if (a->Count()->Is<core::type::RuntimeArrayCount>()) {
+    if (auto* a = ty->As<type::Array>()) {
+        if (a->Count()->Is<type::RuntimeArrayCount>()) {
             T = a->ElemType();
             return true;
         }
@@ -256,7 +264,9 @@
     return false;
 }
 
-inline const type::Array* BuildArray(intrinsic::MatchState& state, const type::Type* el) {
+inline const type::Array* BuildArray(intrinsic::MatchState& state,
+                                     const type::Type*,
+                                     const type::Type* el) {
     return state.types.Get<type::Array>(el,
                                         /* count */ state.types.Get<type::RuntimeArrayCount>(),
                                         /* align */ 0u,
@@ -277,7 +287,7 @@
         return true;
     }
 
-    if (auto* p = ty->As<core::type::Pointer>()) {
+    if (auto* p = ty->As<type::Pointer>()) {
         S = intrinsic::Number(static_cast<uint32_t>(p->AddressSpace()));
         T = p->StoreType();
         A = intrinsic::Number(static_cast<uint32_t>(p->Access()));
@@ -287,6 +297,7 @@
 }
 
 inline const type::Pointer* BuildPtr(intrinsic::MatchState& state,
+                                     const type::Type*,
                                      intrinsic::Number S,
                                      const type::Type* T,
                                      intrinsic::Number& A) {
@@ -300,14 +311,16 @@
         return true;
     }
 
-    if (auto* a = ty->As<core::type::Atomic>()) {
+    if (auto* a = ty->As<type::Atomic>()) {
         T = a->Type();
         return true;
     }
     return false;
 }
 
-inline const type::Atomic* BuildAtomic(intrinsic::MatchState& state, const type::Type* T) {
+inline const type::Atomic* BuildAtomic(intrinsic::MatchState& state,
+                                       const type::Type*,
+                                       const type::Type* T) {
     return state.types.atomic(T);
 }
 
@@ -315,12 +328,10 @@
     if (ty->Is<intrinsic::Any>()) {
         return true;
     }
-    return ty->Is([](const core::type::Sampler* s) {
-        return s->kind() == core::type::SamplerKind::kSampler;
-    });
+    return ty->Is([](const type::Sampler* s) { return s->kind() == type::SamplerKind::kSampler; });
 }
 
-inline const type::Sampler* BuildSampler(intrinsic::MatchState& state) {
+inline const type::Sampler* BuildSampler(intrinsic::MatchState& state, const type::Type*) {
     return state.types.sampler();
 }
 
@@ -328,12 +339,12 @@
     if (ty->Is<intrinsic::Any>()) {
         return true;
     }
-    return ty->Is([](const core::type::Sampler* s) {
-        return s->kind() == core::type::SamplerKind::kComparisonSampler;
-    });
+    return ty->Is(
+        [](const type::Sampler* s) { return s->kind() == type::SamplerKind::kComparisonSampler; });
 }
 
-inline const type::Sampler* BuildSamplerComparison(intrinsic::MatchState& state) {
+inline const type::Sampler* BuildSamplerComparison(intrinsic::MatchState& state,
+                                                   const type::Type*) {
     return state.types.comparison_sampler();
 }
 
@@ -345,7 +356,7 @@
         T = ty;
         return true;
     }
-    if (auto* v = ty->As<core::type::SampledTexture>()) {
+    if (auto* v = ty->As<type::SampledTexture>()) {
         if (v->dim() == dim) {
             T = v->type();
             return true;
@@ -356,14 +367,14 @@
 
 #define JOIN(a, b) a##b
 
-#define DECLARE_SAMPLED_TEXTURE(suffix, dim)                                                     \
-    inline bool JOIN(MatchTexture, suffix)(intrinsic::MatchState & state, const type::Type* ty,  \
-                                           const type::Type*& T) {                               \
-        return MatchTexture(state, ty, dim, T);                                                  \
-    }                                                                                            \
-    inline const type::SampledTexture* JOIN(BuildTexture, suffix)(intrinsic::MatchState & state, \
-                                                                  const type::Type* T) {         \
-        return state.types.Get<type::SampledTexture>(dim, T);                                    \
+#define DECLARE_SAMPLED_TEXTURE(suffix, dim)                                                    \
+    inline bool JOIN(MatchTexture, suffix)(intrinsic::MatchState & state, const type::Type* ty, \
+                                           const type::Type*& T) {                              \
+        return MatchTexture(state, ty, dim, T);                                                 \
+    }                                                                                           \
+    inline const type::SampledTexture* JOIN(BuildTexture, suffix)(                              \
+        intrinsic::MatchState & state, const type::Type*, const type::Type* T) {                \
+        return state.types.Get<type::SampledTexture>(dim, T);                                   \
     }
 
 DECLARE_SAMPLED_TEXTURE(1D, type::TextureDimension::k1d)
@@ -382,7 +393,7 @@
         T = ty;
         return true;
     }
-    if (auto* v = ty->As<core::type::MultisampledTexture>()) {
+    if (auto* v = ty->As<type::MultisampledTexture>()) {
         if (v->dim() == dim) {
             T = v->type();
             return true;
@@ -397,7 +408,7 @@
         return MatchTextureMultisampled(state, ty, dim, T);                          \
     }                                                                                \
     inline const type::MultisampledTexture* JOIN(BuildTextureMultisampled, suffix)(  \
-        intrinsic::MatchState & state, const type::Type* T) {                        \
+        intrinsic::MatchState & state, const type::Type*, const type::Type* T) {     \
         return state.types.Get<type::MultisampledTexture>(dim, T);                   \
     }
 
@@ -410,17 +421,17 @@
     if (ty->Is<intrinsic::Any>()) {
         return true;
     }
-    return ty->Is([&](const core::type::DepthTexture* t) { return t->dim() == dim; });
+    return ty->Is([&](const type::DepthTexture* t) { return t->dim() == dim; });
 }
 
-#define DECLARE_DEPTH_TEXTURE(suffix, dim)                                         \
-    inline bool JOIN(MatchTextureDepth, suffix)(intrinsic::MatchState & state,     \
-                                                const type::Type* ty) {            \
-        return MatchTextureDepth(state, ty, dim);                                  \
-    }                                                                              \
-    inline const type::DepthTexture* JOIN(BuildTextureDepth,                       \
-                                          suffix)(intrinsic::MatchState & state) { \
-        return state.types.Get<type::DepthTexture>(dim);                           \
+#define DECLARE_DEPTH_TEXTURE(suffix, dim)                                     \
+    inline bool JOIN(MatchTextureDepth, suffix)(intrinsic::MatchState & state, \
+                                                const type::Type* ty) {        \
+        return MatchTextureDepth(state, ty, dim);                              \
+    }                                                                          \
+    inline const type::DepthTexture* JOIN(BuildTextureDepth, suffix)(          \
+        intrinsic::MatchState & state, const type::Type*) {                    \
+        return state.types.Get<type::DepthTexture>(dim);                       \
     }
 
 DECLARE_DEPTH_TEXTURE(2D, type::TextureDimension::k2d)
@@ -433,14 +444,14 @@
     if (ty->Is<intrinsic::Any>()) {
         return true;
     }
-    return ty->Is([&](const core::type::DepthMultisampledTexture* t) {
-        return t->dim() == core::type::TextureDimension::k2d;
+    return ty->Is([&](const type::DepthMultisampledTexture* t) {
+        return t->dim() == type::TextureDimension::k2d;
     });
 }
 
-inline type::DepthMultisampledTexture* BuildTextureDepthMultisampled2D(
-    intrinsic::MatchState& state) {
-    return state.types.Get<core::type::DepthMultisampledTexture>(core::type::TextureDimension::k2d);
+inline type::DepthMultisampledTexture* BuildTextureDepthMultisampled2D(intrinsic::MatchState& state,
+                                                                       const type::Type*) {
+    return state.types.Get<type::DepthMultisampledTexture>(type::TextureDimension::k2d);
 }
 
 inline bool MatchTextureStorage(intrinsic::MatchState&,
@@ -453,7 +464,7 @@
         A = intrinsic::Number::any;
         return true;
     }
-    if (auto* v = ty->As<core::type::StorageTexture>()) {
+    if (auto* v = ty->As<type::StorageTexture>()) {
         if (v->dim() == dim) {
             F = intrinsic::Number(static_cast<uint32_t>(v->texel_format()));
             A = intrinsic::Number(static_cast<uint32_t>(v->access()));
@@ -470,7 +481,8 @@
         return MatchTextureStorage(state, ty, dim, F, A);                                     \
     }                                                                                         \
     inline const type::StorageTexture* JOIN(BuildTextureStorage, suffix)(                     \
-        intrinsic::MatchState & state, intrinsic::Number F, intrinsic::Number A) {            \
+        intrinsic::MatchState & state, const type::Type*, intrinsic::Number F,                \
+        intrinsic::Number A) {                                                                \
         auto format = static_cast<TexelFormat>(F.Value());                                    \
         auto access = static_cast<Access>(A.Value());                                         \
         auto* T = type::StorageTexture::SubtypeFor(format, state.types);                      \
@@ -487,7 +499,8 @@
     return ty->IsAnyOf<intrinsic::Any, type::ExternalTexture>();
 }
 
-inline const type::ExternalTexture* BuildTextureExternal(intrinsic::MatchState& state) {
+inline const type::ExternalTexture* BuildTextureExternal(intrinsic::MatchState& state,
+                                                         const type::Type*) {
     return state.types.Get<type::ExternalTexture>();
 }
 
@@ -541,29 +554,36 @@
     return false;
 }
 
-inline const type::Struct* BuildModfResult(intrinsic::MatchState& state, const type::Type* el) {
+inline const type::Struct* BuildModfResult(intrinsic::MatchState& state,
+                                           const type::Type*,
+                                           const type::Type* el) {
     return type::CreateModfResult(state.types, state.symbols, el);
 }
 
 inline const type::Struct* BuildModfResultVec(intrinsic::MatchState& state,
+                                              const type::Type*,
                                               intrinsic::Number& n,
                                               const type::Type* el) {
     auto* vec = state.types.vec(el, n.Value());
-    return core::type::CreateModfResult(state.types, state.symbols, vec);
+    return type::CreateModfResult(state.types, state.symbols, vec);
 }
 
-inline const type::Struct* BuildFrexpResult(intrinsic::MatchState& state, const type::Type* el) {
+inline const type::Struct* BuildFrexpResult(intrinsic::MatchState& state,
+                                            const type::Type*,
+                                            const type::Type* el) {
     return type::CreateFrexpResult(state.types, state.symbols, el);
 }
 
 inline const type::Struct* BuildFrexpResultVec(intrinsic::MatchState& state,
+                                               const type::Type*,
                                                intrinsic::Number& n,
                                                const type::Type* el) {
     auto* vec = state.types.vec(el, n.Value());
-    return core::type::CreateFrexpResult(state.types, state.symbols, vec);
+    return type::CreateFrexpResult(state.types, state.symbols, vec);
 }
 
 inline const type::Struct* BuildAtomicCompareExchangeResult(intrinsic::MatchState& state,
+                                                            const type::Type*,
                                                             const type::Type* ty) {
     return type::CreateAtomicCompareExchangeResult(state.types, state.symbols, ty);
 }
diff --git a/src/tint/lang/core/intrinsic/table.cc b/src/tint/lang/core/intrinsic/table.cc
index 9c29b12..221f43b 100644
--- a/src/tint/lang/core/intrinsic/table.cc
+++ b/src/tint/lang/core/intrinsic/table.cc
@@ -53,144 +53,120 @@
 namespace {
 
 /// The Vector `N` template argument value for arrays of parameters.
-constexpr const size_t kNumFixedParams = decltype(Table::Overload{}.parameters)::static_length;
+constexpr const size_t kNumFixedParams = decltype(Overload{}.parameters)::static_length;
 
 /// The Vector `N` template argument value for arrays of overload candidates.
 constexpr const size_t kNumFixedCandidates = 8;
 
-/// Impl is the private implementation of the Table interface.
-class Impl : public Table {
-  public:
-    Impl(const TableData& td, core::type::Manager& tys, SymbolTable& syms, diag::List& d);
-
-    Result<Overload> Lookup(core::Function builtin_type,
-                            VectorRef<const core::type::Type*> args,
-                            EvaluationStage earliest_eval_stage,
-                            const Source& source) override;
-
-    Result<Overload> Lookup(core::UnaryOp op,
-                            const core::type::Type* arg,
-                            EvaluationStage earliest_eval_stage,
-                            const Source& source) override;
-
-    Result<Overload> Lookup(core::BinaryOp op,
-                            const core::type::Type* lhs,
-                            const core::type::Type* rhs,
-                            EvaluationStage earliest_eval_stage,
-                            const Source& source,
-                            bool is_compound) override;
-
-    Result<Overload> Lookup(CtorConv type,
-                            const core::type::Type* template_arg,
-                            VectorRef<const core::type::Type*> args,
-                            EvaluationStage earliest_eval_stage,
-                            const Source& source) override;
-
-  private:
-    /// Candidate holds information about an overload evaluated for resolution.
-    struct Candidate {
-        /// The candidate overload
-        const OverloadInfo* overload;
-        /// The template types and numbers
-        TemplateState templates;
-        /// The parameter types for the candidate overload
-        Vector<Table::Overload::Parameter, kNumFixedParams> parameters;
-        /// The match-score of the candidate overload.
-        /// A score of zero indicates an exact match.
-        /// Non-zero scores are used for diagnostics when no overload matches.
-        /// Lower scores are displayed first (top-most).
-        size_t score;
-    };
-
-    /// A list of candidates
-    using Candidates = Vector<Candidate, kNumFixedCandidates>;
-
-    /// Callback function when no overloads match.
-    using OnNoMatch = std::function<void(VectorRef<Candidate>)>;
-
-    /// Sorts the candidates based on their score, with the lowest (best-ranking) scores first.
-    static inline void SortCandidates(Candidates& candidates) {
-        std::stable_sort(candidates.begin(), candidates.end(),
-                         [&](const Candidate& a, const Candidate& b) { return a.score < b.score; });
-    }
-
-    /// Attempts to find a single intrinsic overload that matches the provided argument types.
-    /// @param intrinsic the intrinsic being called
-    /// @param intrinsic_name the name of the intrinsic
-    /// @param args the argument types
-    /// @param templates initial template state. This may contain explicitly specified template
-    ///                  arguments. For example `vec3<f32>()` would have the first template-type
-    ///                  defined as `f32`.
-    /// @param on_no_match an error callback when no intrinsic overloads matched the provided
-    ///                    arguments.
-    /// @returns the matched intrinsic
-    Result<Table::Overload> MatchIntrinsic(const IntrinsicInfo& intrinsic,
-                                           const char* intrinsic_name,
-                                           VectorRef<const core::type::Type*> args,
-                                           EvaluationStage earliest_eval_stage,
-                                           TemplateState templates,
-                                           const OnNoMatch& on_no_match) const;
-
-    /// Evaluates the single overload for the provided argument types.
-    /// @param overload the overload being considered
-    /// @param args the argument types
-    /// @param templates initial template state. This may contain explicitly specified template
-    ///                  arguments. For example `vec3<f32>()` would have the first template-type
-    ///                  template as `f32`.
-    /// @returns the evaluated Candidate information.
-    Candidate ScoreOverload(const OverloadInfo& overload,
-                            VectorRef<const core::type::Type*> args,
-                            EvaluationStage earliest_eval_stage,
-                            const TemplateState& templates) const;
-
-    /// Performs overload resolution given the list of candidates, by ranking the conversions of
-    /// arguments to the each of the candidate's parameter types.
-    /// @param candidates the list of candidate overloads
-    /// @param intrinsic_name the name of the intrinsic
-    /// @param args the argument types
-    /// @param templates initial template state. This may contain explicitly specified template
-    ///                  arguments. For example `vec3<f32>()` would have the first template-type
-    ///                  template as `f32`.
-    /// @see https://www.w3.org/TR/WGSL/#overload-resolution-section
-    /// @returns the resolved Candidate.
-    Candidate ResolveCandidate(Candidates&& candidates,
-                               const char* intrinsic_name,
-                               VectorRef<const core::type::Type*> args,
-                               TemplateState templates) const;
-
-    /// Match constructs a new MatchState
-    /// @param templates the template state used for matcher evaluation
-    /// @param overload the overload being evaluated
-    /// @param type_matcher_indices pointer to a list of type matcher indices
-    /// @param number_matcher_indices pointer to a list of number matcher indices
-    MatchState Match(TemplateState& templates,
-                     const OverloadInfo& overload,
-                     const TypeMatcherIndex* type_matcher_indices,
-                     const NumberMatcherIndex* number_matcher_indices,
-                     EvaluationStage earliest_eval_stage) const;
-
-    // Prints the overload for emitting diagnostics
-    void PrintOverload(StringStream& ss,
-                       const OverloadInfo& overload,
-                       const char* intrinsic_name) const;
-
-    // Prints the list of candidates for emitting diagnostics
-    void PrintCandidates(StringStream& ss,
-                         VectorRef<Candidate> candidates,
-                         const char* intrinsic_name) const;
-
-    /// Raises an error when no overload is a clear winner of overload resolution
-    void ErrAmbiguousOverload(const char* intrinsic_name,
-                              VectorRef<const core::type::Type*> args,
-                              TemplateState templates,
-                              VectorRef<Candidate> candidates) const;
-
-    const TableData& data;
-    core::type::Manager& types;
-    SymbolTable& symbols;
-    diag::List& diags;
+/// Candidate holds information about an overload evaluated for resolution.
+struct Candidate {
+    /// The candidate overload
+    const OverloadInfo* overload;
+    /// The template types and numbers
+    TemplateState templates;
+    /// The parameter types for the candidate overload
+    Vector<Overload::Parameter, kNumFixedParams> parameters;
+    /// The match-score of the candidate overload.
+    /// A score of zero indicates an exact match.
+    /// Non-zero scores are used for diagnostics when no overload matches.
+    /// Lower scores are displayed first (top-most).
+    size_t score;
 };
 
+/// A list of candidates
+using Candidates = Vector<Candidate, kNumFixedCandidates>;
+
+/// Callback function when no overloads match.
+using OnNoMatch = std::function<void(VectorRef<Candidate>)>;
+
+/// Sorts the candidates based on their score, with the lowest (best-ranking) scores first.
+static inline void SortCandidates(Candidates& candidates) {
+    std::stable_sort(candidates.begin(), candidates.end(),
+                     [&](const Candidate& a, const Candidate& b) { return a.score < b.score; });
+}
+
+/// Attempts to find a single intrinsic overload that matches the provided argument types.
+/// @param context the intrinsic context
+/// @param intrinsic the intrinsic being called
+/// @param intrinsic_name the name of the intrinsic
+/// @param args the argument types
+/// @param templates initial template state. This may contain explicitly specified template
+///                  arguments. For example `vec3<f32>()` would have the first template-type
+///                  defined as `f32`.
+/// @param on_no_match an error callback when no intrinsic overloads matched the provided
+///                    arguments.
+/// @returns the matched intrinsic
+Result<Overload> MatchIntrinsic(Context& context,
+                                const IntrinsicInfo& intrinsic,
+                                const char* intrinsic_name,
+                                VectorRef<const core::type::Type*> args,
+                                EvaluationStage earliest_eval_stage,
+                                TemplateState templates,
+                                const OnNoMatch& on_no_match);
+
+/// Evaluates the single overload for the provided argument types.
+/// @param context the intrinsic context
+/// @param overload the overload being considered
+/// @param args the argument types
+/// @param templates initial template state. This may contain explicitly specified template
+///                  arguments. For example `vec3<f32>()` would have the first template-type
+///                  template as `f32`.
+/// @returns the evaluated Candidate information.
+Candidate ScoreOverload(Context& context,
+                        const OverloadInfo& overload,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const TemplateState& templates);
+
+/// Performs overload resolution given the list of candidates, by ranking the conversions of
+/// arguments to the each of the candidate's parameter types.
+/// @param context the intrinsic context
+/// @param candidates the list of candidate overloads
+/// @param intrinsic_name the name of the intrinsic
+/// @param args the argument types
+/// @param templates initial template state. This may contain explicitly specified template
+///                  arguments. For example `vec3<f32>()` would have the first template-type
+///                  template as `f32`.
+/// @see https://www.w3.org/TR/WGSL/#overload-resolution-section
+/// @returns the resolved Candidate.
+Candidate ResolveCandidate(Context& context,
+                           Candidates&& candidates,
+                           const char* intrinsic_name,
+                           VectorRef<const core::type::Type*> args,
+                           TemplateState templates);
+
+/// Match constructs a new MatchState
+/// @param context the intrinsic context
+/// @param templates the template state used for matcher evaluation
+/// @param overload the overload being evaluated
+/// @param type_matcher_indices pointer to a list of type matcher indices
+/// @param number_matcher_indices pointer to a list of number matcher indices
+MatchState Match(Context& context,
+                 TemplateState& templates,
+                 const OverloadInfo& overload,
+                 const TypeMatcherIndex* type_matcher_indices,
+                 const NumberMatcherIndex* number_matcher_indices,
+                 EvaluationStage earliest_eval_stage);
+
+// Prints the overload for emitting diagnostics
+void PrintOverload(StringStream& ss,
+                   Context& context,
+                   const OverloadInfo& overload,
+                   const char* intrinsic_name);
+
+// Prints the list of candidates for emitting diagnostics
+void PrintCandidates(StringStream& ss,
+                     Context& context,
+                     VectorRef<Candidate> candidates,
+                     const char* intrinsic_name);
+
+/// Raises an error when no overload is a clear winner of overload resolution
+void ErrAmbiguousOverload(Context& context,
+                          const char* intrinsic_name,
+                          VectorRef<const core::type::Type*> args,
+                          TemplateState templates,
+                          VectorRef<Candidate> candidates);
+
 /// @return a string representing a call to a builtin with the given argument
 /// types.
 std::string CallSignature(const char* intrinsic_name,
@@ -217,240 +193,21 @@
     return ss.str();
 }
 
-Impl::Impl(const TableData& td, core::type::Manager& tys, SymbolTable& syms, diag::List& d)
-    : data(td), types(tys), symbols(syms), diags(d) {}
-
-Result<Table::Overload> Impl::Lookup(core::Function builtin_type,
-                                     VectorRef<const core::type::Type*> args,
-                                     EvaluationStage earliest_eval_stage,
-                                     const Source& source) {
-    const char* intrinsic_name = core::str(builtin_type);
-
-    // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching call to " << CallSignature(intrinsic_name, args) << std::endl;
-        if (!candidates.IsEmpty()) {
-            ss << std::endl
-               << candidates.Length() << " candidate function"
-               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
-            PrintCandidates(ss, candidates, intrinsic_name);
-        }
-        diags.add_error(diag::System::Intrinsics, ss.str(), source);
-    };
-
-    // Resolve the intrinsic overload
-    return MatchIntrinsic(data.builtins[static_cast<size_t>(builtin_type)], intrinsic_name, args,
-                          earliest_eval_stage, TemplateState{}, on_no_match);
-}
-
-Result<Table::Overload> Impl::Lookup(core::UnaryOp op,
-                                     const core::type::Type* arg,
-                                     EvaluationStage earliest_eval_stage,
-                                     const Source& source) {
-    const IntrinsicInfo* intrinsic_info = nullptr;
-    const char* intrinsic_name = nullptr;
-    switch (op) {
-        case core::UnaryOp::kComplement:
-            intrinsic_info = &data.unary_complement;
-            intrinsic_name = "operator ~ ";
-            break;
-        case core::UnaryOp::kNegation:
-            intrinsic_info = &data.unary_minus;
-            intrinsic_name = "operator - ";
-            break;
-        case core::UnaryOp::kNot:
-            intrinsic_info = &data.unary_not;
-            intrinsic_name = "operator ! ";
-            break;
-        default:
-            TINT_UNREACHABLE() << "invalid unary op: " << op;
-            return Failure;
-    }
-
-    Vector args{arg};
-
-    // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&, name = intrinsic_name](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching overload for " << CallSignature(name, args) << std::endl;
-        if (!candidates.IsEmpty()) {
-            ss << std::endl
-               << candidates.Length() << " candidate operator"
-               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
-            PrintCandidates(ss, candidates, name);
-        }
-        diags.add_error(diag::System::Intrinsics, ss.str(), source);
-    };
-
-    // Resolve the intrinsic overload
-    return MatchIntrinsic(*intrinsic_info, intrinsic_name, args, earliest_eval_stage,
-                          TemplateState{}, on_no_match);
-}
-
-Result<Table::Overload> Impl::Lookup(core::BinaryOp op,
-                                     const core::type::Type* lhs,
-                                     const core::type::Type* rhs,
-                                     EvaluationStage earliest_eval_stage,
-                                     const Source& source,
-                                     bool is_compound) {
-    const IntrinsicInfo* intrinsic_info = nullptr;
-    const char* intrinsic_name = nullptr;
-    switch (op) {
-        case core::BinaryOp::kAnd:
-            intrinsic_info = &data.binary_and;
-            intrinsic_name = is_compound ? "operator &= " : "operator & ";
-            break;
-        case core::BinaryOp::kOr:
-            intrinsic_info = &data.binary_or;
-            intrinsic_name = is_compound ? "operator |= " : "operator | ";
-            break;
-        case core::BinaryOp::kXor:
-            intrinsic_info = &data.binary_xor;
-            intrinsic_name = is_compound ? "operator ^= " : "operator ^ ";
-            break;
-        case core::BinaryOp::kLogicalAnd:
-            intrinsic_info = &data.binary_logical_and;
-            intrinsic_name = "operator && ";
-            break;
-        case core::BinaryOp::kLogicalOr:
-            intrinsic_info = &data.binary_logical_or;
-            intrinsic_name = "operator || ";
-            break;
-        case core::BinaryOp::kEqual:
-            intrinsic_info = &data.binary_equal;
-            intrinsic_name = "operator == ";
-            break;
-        case core::BinaryOp::kNotEqual:
-            intrinsic_info = &data.binary_not_equal;
-            intrinsic_name = "operator != ";
-            break;
-        case core::BinaryOp::kLessThan:
-            intrinsic_info = &data.binary_less_than;
-            intrinsic_name = "operator < ";
-            break;
-        case core::BinaryOp::kGreaterThan:
-            intrinsic_info = &data.binary_greater_than;
-            intrinsic_name = "operator > ";
-            break;
-        case core::BinaryOp::kLessThanEqual:
-            intrinsic_info = &data.binary_less_than_equal;
-            intrinsic_name = "operator <= ";
-            break;
-        case core::BinaryOp::kGreaterThanEqual:
-            intrinsic_info = &data.binary_greater_than_equal;
-            intrinsic_name = "operator >= ";
-            break;
-        case core::BinaryOp::kShiftLeft:
-            intrinsic_info = &data.binary_shift_left;
-            intrinsic_name = is_compound ? "operator <<= " : "operator << ";
-            break;
-        case core::BinaryOp::kShiftRight:
-            intrinsic_info = &data.binary_shift_right;
-            intrinsic_name = is_compound ? "operator >>= " : "operator >> ";
-            break;
-        case core::BinaryOp::kAdd:
-            intrinsic_info = &data.binary_plus;
-            intrinsic_name = is_compound ? "operator += " : "operator + ";
-            break;
-        case core::BinaryOp::kSubtract:
-            intrinsic_info = &data.binary_minus;
-            intrinsic_name = is_compound ? "operator -= " : "operator - ";
-            break;
-        case core::BinaryOp::kMultiply:
-            intrinsic_info = &data.binary_star;
-            intrinsic_name = is_compound ? "operator *= " : "operator * ";
-            break;
-        case core::BinaryOp::kDivide:
-            intrinsic_info = &data.binary_divide;
-            intrinsic_name = is_compound ? "operator /= " : "operator / ";
-            break;
-        case core::BinaryOp::kModulo:
-            intrinsic_info = &data.binary_modulo;
-            intrinsic_name = is_compound ? "operator %= " : "operator % ";
-            break;
-    }
-
-    Vector args{lhs, rhs};
-
-    // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&, name = intrinsic_name](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching overload for " << CallSignature(name, args) << std::endl;
-        if (!candidates.IsEmpty()) {
-            ss << std::endl
-               << candidates.Length() << " candidate operator"
-               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
-            PrintCandidates(ss, candidates, name);
-        }
-        diags.add_error(diag::System::Intrinsics, ss.str(), source);
-    };
-
-    // Resolve the intrinsic overload
-    return MatchIntrinsic(*intrinsic_info, intrinsic_name, args, earliest_eval_stage,
-                          TemplateState{}, on_no_match);
-}
-
-Result<Table::Overload> Impl::Lookup(CtorConv type,
-                                     const core::type::Type* template_arg,
-                                     VectorRef<const core::type::Type*> args,
-                                     EvaluationStage earliest_eval_stage,
-                                     const Source& source) {
-    auto name = str(type);
-
-    // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching constructor for " << CallSignature(name, args, template_arg)
-           << std::endl;
-        Candidates ctor, conv;
-        for (auto candidate : candidates) {
-            if (candidate.overload->flags.Contains(OverloadFlag::kIsConstructor)) {
-                ctor.Push(candidate);
-            } else {
-                conv.Push(candidate);
-            }
-        }
-        if (!ctor.IsEmpty()) {
-            ss << std::endl
-               << ctor.Length() << " candidate constructor" << (ctor.Length() > 1 ? "s:" : ":")
-               << std::endl;
-            PrintCandidates(ss, ctor, name);
-        }
-        if (!conv.IsEmpty()) {
-            ss << std::endl
-               << conv.Length() << " candidate conversion" << (conv.Length() > 1 ? "s:" : ":")
-               << std::endl;
-            PrintCandidates(ss, conv, name);
-        }
-        diags.add_error(diag::System::Intrinsics, ss.str(), source);
-    };
-
-    // If a template type was provided, then close the 0'th type with this.
-    TemplateState templates;
-    if (template_arg) {
-        templates.Type(0, template_arg);
-    }
-
-    // Resolve the intrinsic overload
-    return MatchIntrinsic(data.ctor_conv[static_cast<size_t>(type)], name, args,
-                          earliest_eval_stage, templates, on_no_match);
-}
-
-Result<Table::Overload> Impl::MatchIntrinsic(const IntrinsicInfo& intrinsic,
-                                             const char* intrinsic_name,
-                                             VectorRef<const core::type::Type*> args,
-                                             EvaluationStage earliest_eval_stage,
-                                             TemplateState templates,
-                                             const OnNoMatch& on_no_match) const {
+Result<Overload> MatchIntrinsic(Context& context,
+                                const IntrinsicInfo& intrinsic,
+                                const char* intrinsic_name,
+                                VectorRef<const core::type::Type*> args,
+                                EvaluationStage earliest_eval_stage,
+                                TemplateState templates,
+                                const OnNoMatch& on_no_match) {
     size_t num_matched = 0;
     size_t match_idx = 0;
     Vector<Candidate, kNumFixedCandidates> candidates;
     candidates.Reserve(intrinsic.num_overloads);
     for (size_t overload_idx = 0; overload_idx < static_cast<size_t>(intrinsic.num_overloads);
          overload_idx++) {
-        auto& overload = data[intrinsic.overloads + overload_idx];
-        auto candidate = ScoreOverload(overload, args, earliest_eval_stage, templates);
+        auto& overload = context.data[intrinsic.overloads + overload_idx];
+        auto candidate = ScoreOverload(context, overload, args, earliest_eval_stage, templates);
         if (candidate.score == 0) {
             match_idx = overload_idx;
             num_matched++;
@@ -471,7 +228,8 @@
     if (num_matched == 1) {
         match = std::move(candidates[match_idx]);
     } else {
-        match = ResolveCandidate(std::move(candidates), intrinsic_name, args, std::move(templates));
+        match = ResolveCandidate(context, std::move(candidates), intrinsic_name, args,
+                                 std::move(templates));
         if (!match.overload) {
             // Ambiguous overload. ResolveCandidate() will have already raised an error diagnostic.
             return Failure;
@@ -480,10 +238,10 @@
 
     // Build the return type
     const core::type::Type* return_type = nullptr;
-    if (auto* type_indices = data[match.overload->return_type_matcher_indices]) {
-        auto* number_indices = data[match.overload->return_number_matcher_indices];
+    if (auto* type_indices = context.data[match.overload->return_type_matcher_indices]) {
+        auto* number_indices = context.data[match.overload->return_number_matcher_indices];
         Any any;
-        return_type = Match(match.templates, *match.overload, type_indices, number_indices,
+        return_type = Match(context, match.templates, *match.overload, type_indices, number_indices,
                             earliest_eval_stage)
                           .Type(&any);
         if (TINT_UNLIKELY(!return_type)) {
@@ -491,17 +249,18 @@
             return Failure;
         }
     } else {
-        return_type = types.void_();
+        return_type = context.types.void_();
     }
 
-    return Table::Overload{match.overload, return_type, std::move(match.parameters),
-                           data[match.overload->const_eval_fn]};
+    return Overload{match.overload, return_type, std::move(match.parameters),
+                    context.data[match.overload->const_eval_fn]};
 }
 
-Impl::Candidate Impl::ScoreOverload(const OverloadInfo& overload,
-                                    VectorRef<const core::type::Type*> args,
-                                    EvaluationStage earliest_eval_stage,
-                                    const TemplateState& in_templates) const {
+Candidate ScoreOverload(Context& context,
+                        const OverloadInfo& overload,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const TemplateState& in_templates) {
     // Penalty weights for overload mismatching.
     // This scoring is used to order the suggested overloads in diagnostic on overload mismatch, and
     // has no impact for a correct program.
@@ -545,10 +304,10 @@
     // Note that inferred template types are not tested against their matchers at this point.
     auto num_params = std::min(num_parameters, num_arguments);
     for (size_t p = 0; p < num_params; p++) {
-        auto& parameter = data[overload.parameters + p];
-        auto* type_indices = data[parameter.type_matcher_indices];
-        auto* number_indices = data[parameter.number_matcher_indices];
-        if (!Match(templates, overload, type_indices, number_indices, earliest_eval_stage)
+        auto& parameter = context.data[overload.parameters + p];
+        auto* type_indices = context.data[parameter.type_matcher_indices];
+        auto* number_indices = context.data[parameter.number_matcher_indices];
+        if (!Match(context, templates, overload, type_indices, number_indices, earliest_eval_stage)
                  .Type(args[p]->UnwrapRef())) {
             score += kMismatchedParamTypePenalty;
         }
@@ -562,12 +321,12 @@
         // important here, which can be controlled with the [[precedence(N)]] decorations on the
         // types in intrinsics.def.
         for (size_t ot = 0; ot < overload.num_template_types; ot++) {
-            auto* matcher_idx = &data[overload.template_types + ot].matcher_index;
+            auto* matcher_idx = &context.data[overload.template_types + ot].matcher_index;
             if (matcher_idx->IsValid()) {
                 if (auto* type = templates.Type(ot)) {
-                    if (auto* ty =
-                            Match(templates, overload, matcher_idx, nullptr, earliest_eval_stage)
-                                .Type(type)) {
+                    if (auto* ty = Match(context, templates, overload, matcher_idx, nullptr,
+                                         earliest_eval_stage)
+                                       .Type(type)) {
                         // Template type matched one of the types in the template type's matcher.
                         // Replace the template type with this type.
                         templates.SetType(ot, ty);
@@ -585,11 +344,11 @@
         // inferred number matches the constraints on the overload. Increments `score` if the
         // template numbers do not match their constraint matchers.
         for (size_t on = 0; on < overload.num_template_numbers; on++) {
-            auto* matcher_idx = &data[overload.template_numbers + on].matcher_index;
+            auto* matcher_idx = &context.data[overload.template_numbers + on].matcher_index;
             if (matcher_idx->IsValid()) {
                 auto number = templates.Num(on);
                 if (!number.IsValid() ||
-                    !Match(templates, overload, nullptr, matcher_idx, earliest_eval_stage)
+                    !Match(context, templates, overload, nullptr, matcher_idx, earliest_eval_stage)
                          .Num(number)
                          .IsValid()) {
                     score += kMismatchedTemplateNumberPenalty;
@@ -599,14 +358,15 @@
     }
 
     // Now that all the template types have been finalized, we can construct the parameters.
-    Vector<Table::Overload::Parameter, kNumFixedParams> parameters;
+    Vector<Overload::Parameter, kNumFixedParams> parameters;
     if (score == 0) {
         parameters.Reserve(num_params);
         for (size_t p = 0; p < num_params; p++) {
-            auto& parameter = data[overload.parameters + p];
-            auto* type_indices = data[parameter.type_matcher_indices];
-            auto* number_indices = data[parameter.number_matcher_indices];
-            auto* ty = Match(templates, overload, type_indices, number_indices, earliest_eval_stage)
+            auto& parameter = context.data[overload.parameters + p];
+            auto* type_indices = context.data[parameter.type_matcher_indices];
+            auto* number_indices = context.data[parameter.number_matcher_indices];
+            auto* ty = Match(context, templates, overload, type_indices, number_indices,
+                             earliest_eval_stage)
                            .Type(args[p]->UnwrapRef());
             parameters.Emplace(ty, parameter.usage);
         }
@@ -615,10 +375,11 @@
     return Candidate{&overload, templates, parameters, score};
 }
 
-Impl::Candidate Impl::ResolveCandidate(Impl::Candidates&& candidates,
-                                       const char* intrinsic_name,
-                                       VectorRef<const core::type::Type*> args,
-                                       TemplateState templates) const {
+Candidate ResolveCandidate(Context& context,
+                           Candidates&& candidates,
+                           const char* intrinsic_name,
+                           VectorRef<const core::type::Type*> args,
+                           TemplateState templates) {
     Vector<uint32_t, kNumFixedParams> best_ranks;
     best_ranks.Resize(args.Length(), 0xffffffff);
     size_t num_matched = 0;
@@ -673,31 +434,28 @@
         // Re-sort the candidates with the most promising first
         SortCandidates(candidates);
         // Raise an error
-        ErrAmbiguousOverload(intrinsic_name, args, templates, candidates);
+        ErrAmbiguousOverload(context, intrinsic_name, args, templates, candidates);
         return {};
     }
 
     return std::move(*best);
 }
 
-MatchState Impl::Match(TemplateState& templates,
-                       const OverloadInfo& overload,
-                       const TypeMatcherIndex* type_matcher_indices,
-                       const NumberMatcherIndex* number_matcher_indices,
-                       EvaluationStage earliest_eval_stage) const {
-    return MatchState{types,
-                      symbols,
-                      templates,
-                      data,
-                      overload,
-                      type_matcher_indices,
-                      number_matcher_indices,
-                      earliest_eval_stage};
+MatchState Match(Context& context,
+                 TemplateState& templates,
+                 const OverloadInfo& overload,
+                 const TypeMatcherIndex* type_matcher_indices,
+                 const NumberMatcherIndex* number_matcher_indices,
+                 EvaluationStage earliest_eval_stage) {
+    return MatchState{context.types,          context.symbols,    templates,
+                      context.data,           overload,           type_matcher_indices,
+                      number_matcher_indices, earliest_eval_stage};
 }
 
-void Impl::PrintOverload(StringStream& ss,
-                         const OverloadInfo& overload,
-                         const char* intrinsic_name) const {
+void PrintOverload(StringStream& ss,
+                   Context& context,
+                   const OverloadInfo& overload,
+                   const char* intrinsic_name) {
     TemplateState templates;
 
     // TODO(crbug.com/tint/1730): Use input evaluation stage to output only relevant overloads.
@@ -720,29 +478,29 @@
     }
     if (print_template_type) {
         ss << "<";
-        ss << data[overload.template_types].name;
+        ss << context.data[overload.template_types].name;
         ss << ">";
     }
     ss << "(";
     for (size_t p = 0; p < overload.num_parameters; p++) {
-        auto& parameter = data[overload.parameters + p];
+        auto& parameter = context.data[overload.parameters + p];
         if (p > 0) {
             ss << ", ";
         }
         if (parameter.usage != ParameterUsage::kNone) {
             ss << ToString(parameter.usage) << ": ";
         }
-        auto* type_indices = data[parameter.type_matcher_indices];
-        auto* number_indices = data[parameter.number_matcher_indices];
-        ss << Match(templates, overload, type_indices, number_indices, earliest_eval_stage)
+        auto* type_indices = context.data[parameter.type_matcher_indices];
+        auto* number_indices = context.data[parameter.number_matcher_indices];
+        ss << Match(context, templates, overload, type_indices, number_indices, earliest_eval_stage)
                   .TypeName();
     }
     ss << ")";
     if (overload.return_type_matcher_indices.IsValid()) {
         ss << " -> ";
-        auto* type_indices = data[overload.return_type_matcher_indices];
-        auto* number_indices = data[overload.return_number_matcher_indices];
-        ss << Match(templates, overload, type_indices, number_indices, earliest_eval_stage)
+        auto* type_indices = context.data[overload.return_type_matcher_indices];
+        auto* number_indices = context.data[overload.return_number_matcher_indices];
+        ss << Match(context, templates, overload, type_indices, number_indices, earliest_eval_stage)
                   .TypeName();
     }
 
@@ -752,41 +510,45 @@
         first = false;
     };
     for (size_t i = 0; i < overload.num_template_types; i++) {
-        auto& template_type = data[overload.template_types + i];
+        auto& template_type = context.data[overload.template_types + i];
         if (template_type.matcher_index.IsValid()) {
             separator();
             ss << template_type.name;
             auto* index = &template_type.matcher_index;
             ss << " is "
-               << Match(templates, overload, index, nullptr, earliest_eval_stage).TypeName();
+               << Match(context, templates, overload, index, nullptr, earliest_eval_stage)
+                      .TypeName();
         }
     }
     for (size_t i = 0; i < overload.num_template_numbers; i++) {
-        auto& template_number = data[overload.template_numbers + i];
+        auto& template_number = context.data[overload.template_numbers + i];
         if (template_number.matcher_index.IsValid()) {
             separator();
             ss << template_number.name;
             auto* index = &template_number.matcher_index;
             ss << " is "
-               << Match(templates, overload, nullptr, index, earliest_eval_stage).NumName();
+               << Match(context, templates, overload, nullptr, index, earliest_eval_stage)
+                      .NumName();
         }
     }
 }
 
-void Impl::PrintCandidates(StringStream& ss,
-                           VectorRef<Candidate> candidates,
-                           const char* intrinsic_name) const {
+void PrintCandidates(StringStream& ss,
+                     Context& context,
+                     VectorRef<Candidate> candidates,
+                     const char* intrinsic_name) {
     for (auto& candidate : candidates) {
         ss << "  ";
-        PrintOverload(ss, *candidate.overload, intrinsic_name);
+        PrintOverload(ss, context, *candidate.overload, intrinsic_name);
         ss << std::endl;
     }
 }
 
-void Impl::ErrAmbiguousOverload(const char* intrinsic_name,
-                                VectorRef<const core::type::Type*> args,
-                                TemplateState templates,
-                                VectorRef<Candidate> candidates) const {
+void ErrAmbiguousOverload(Context& context,
+                          const char* intrinsic_name,
+                          VectorRef<const core::type::Type*> args,
+                          TemplateState templates,
+                          VectorRef<Candidate> candidates) {
     StringStream ss;
     ss << "ambiguous overload while attempting to match " << intrinsic_name;
     for (size_t i = 0; i < std::numeric_limits<size_t>::max(); i++) {
@@ -812,7 +574,7 @@
     for (auto& candidate : candidates) {
         if (candidate.score == 0) {
             ss << "  ";
-            PrintOverload(ss, *candidate.overload, intrinsic_name);
+            PrintOverload(ss, context, *candidate.overload, intrinsic_name);
             ss << std::endl;
         }
     }
@@ -821,14 +583,234 @@
 
 }  // namespace
 
-std::unique_ptr<Table> Table::Create(const TableData& data,
-                                     core::type::Manager& types,
-                                     SymbolTable& symbols,
-                                     diag::List& diags) {
-    return std::make_unique<Impl>(data, types, symbols, diags);
+Result<Overload> Lookup(Context& context,
+                        core::Function builtin_type,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source) {
+    return Lookup(context, core::str(builtin_type), static_cast<size_t>(builtin_type), args,
+                  earliest_eval_stage, source);
 }
 
-Table::~Table() = default;
+Result<Overload> Lookup(Context& context,
+                        const char* intrinsic_name,
+                        size_t function_id,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source) {
+    // Generates an error when no overloads match the provided arguments
+    auto on_no_match = [&](VectorRef<Candidate> candidates) {
+        StringStream ss;
+        ss << "no matching call to " << CallSignature(intrinsic_name, args) << std::endl;
+        if (!candidates.IsEmpty()) {
+            ss << std::endl
+               << candidates.Length() << " candidate function"
+               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
+            PrintCandidates(ss, context, candidates, intrinsic_name);
+        }
+        context.diags.add_error(diag::System::Intrinsics, ss.str(), source);
+    };
+
+    // Resolve the intrinsic overload
+    return MatchIntrinsic(context, context.data.builtins[function_id], intrinsic_name, args,
+                          earliest_eval_stage, TemplateState{}, on_no_match);
+}
+
+Result<Overload> Lookup(Context& context,
+                        core::UnaryOp op,
+                        const core::type::Type* arg,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source) {
+    const IntrinsicInfo* intrinsic_info = nullptr;
+    const char* intrinsic_name = nullptr;
+    switch (op) {
+        case core::UnaryOp::kComplement:
+            intrinsic_info = &context.data.unary_complement;
+            intrinsic_name = "operator ~ ";
+            break;
+        case core::UnaryOp::kNegation:
+            intrinsic_info = &context.data.unary_minus;
+            intrinsic_name = "operator - ";
+            break;
+        case core::UnaryOp::kNot:
+            intrinsic_info = &context.data.unary_not;
+            intrinsic_name = "operator ! ";
+            break;
+        default:
+            TINT_UNREACHABLE() << "invalid unary op: " << op;
+            return Failure;
+    }
+
+    Vector args{arg};
+
+    // Generates an error when no overloads match the provided arguments
+    auto on_no_match = [&, name = intrinsic_name](VectorRef<Candidate> candidates) {
+        StringStream ss;
+        ss << "no matching overload for " << CallSignature(name, args) << std::endl;
+        if (!candidates.IsEmpty()) {
+            ss << std::endl
+               << candidates.Length() << " candidate operator"
+               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
+            PrintCandidates(ss, context, candidates, name);
+        }
+        context.diags.add_error(diag::System::Intrinsics, ss.str(), source);
+    };
+
+    // Resolve the intrinsic overload
+    return MatchIntrinsic(context, *intrinsic_info, intrinsic_name, args, earliest_eval_stage,
+                          TemplateState{}, on_no_match);
+}
+
+Result<Overload> Lookup(Context& context,
+                        core::BinaryOp op,
+                        const core::type::Type* lhs,
+                        const core::type::Type* rhs,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source,
+                        bool is_compound) {
+    const IntrinsicInfo* intrinsic_info = nullptr;
+    const char* intrinsic_name = nullptr;
+    switch (op) {
+        case core::BinaryOp::kAnd:
+            intrinsic_info = &context.data.binary_and;
+            intrinsic_name = is_compound ? "operator &= " : "operator & ";
+            break;
+        case core::BinaryOp::kOr:
+            intrinsic_info = &context.data.binary_or;
+            intrinsic_name = is_compound ? "operator |= " : "operator | ";
+            break;
+        case core::BinaryOp::kXor:
+            intrinsic_info = &context.data.binary_xor;
+            intrinsic_name = is_compound ? "operator ^= " : "operator ^ ";
+            break;
+        case core::BinaryOp::kLogicalAnd:
+            intrinsic_info = &context.data.binary_logical_and;
+            intrinsic_name = "operator && ";
+            break;
+        case core::BinaryOp::kLogicalOr:
+            intrinsic_info = &context.data.binary_logical_or;
+            intrinsic_name = "operator || ";
+            break;
+        case core::BinaryOp::kEqual:
+            intrinsic_info = &context.data.binary_equal;
+            intrinsic_name = "operator == ";
+            break;
+        case core::BinaryOp::kNotEqual:
+            intrinsic_info = &context.data.binary_not_equal;
+            intrinsic_name = "operator != ";
+            break;
+        case core::BinaryOp::kLessThan:
+            intrinsic_info = &context.data.binary_less_than;
+            intrinsic_name = "operator < ";
+            break;
+        case core::BinaryOp::kGreaterThan:
+            intrinsic_info = &context.data.binary_greater_than;
+            intrinsic_name = "operator > ";
+            break;
+        case core::BinaryOp::kLessThanEqual:
+            intrinsic_info = &context.data.binary_less_than_equal;
+            intrinsic_name = "operator <= ";
+            break;
+        case core::BinaryOp::kGreaterThanEqual:
+            intrinsic_info = &context.data.binary_greater_than_equal;
+            intrinsic_name = "operator >= ";
+            break;
+        case core::BinaryOp::kShiftLeft:
+            intrinsic_info = &context.data.binary_shift_left;
+            intrinsic_name = is_compound ? "operator <<= " : "operator << ";
+            break;
+        case core::BinaryOp::kShiftRight:
+            intrinsic_info = &context.data.binary_shift_right;
+            intrinsic_name = is_compound ? "operator >>= " : "operator >> ";
+            break;
+        case core::BinaryOp::kAdd:
+            intrinsic_info = &context.data.binary_plus;
+            intrinsic_name = is_compound ? "operator += " : "operator + ";
+            break;
+        case core::BinaryOp::kSubtract:
+            intrinsic_info = &context.data.binary_minus;
+            intrinsic_name = is_compound ? "operator -= " : "operator - ";
+            break;
+        case core::BinaryOp::kMultiply:
+            intrinsic_info = &context.data.binary_star;
+            intrinsic_name = is_compound ? "operator *= " : "operator * ";
+            break;
+        case core::BinaryOp::kDivide:
+            intrinsic_info = &context.data.binary_divide;
+            intrinsic_name = is_compound ? "operator /= " : "operator / ";
+            break;
+        case core::BinaryOp::kModulo:
+            intrinsic_info = &context.data.binary_modulo;
+            intrinsic_name = is_compound ? "operator %= " : "operator % ";
+            break;
+    }
+
+    Vector args{lhs, rhs};
+
+    // Generates an error when no overloads match the provided arguments
+    auto on_no_match = [&, name = intrinsic_name](VectorRef<Candidate> candidates) {
+        StringStream ss;
+        ss << "no matching overload for " << CallSignature(name, args) << std::endl;
+        if (!candidates.IsEmpty()) {
+            ss << std::endl
+               << candidates.Length() << " candidate operator"
+               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
+            PrintCandidates(ss, context, candidates, name);
+        }
+        context.diags.add_error(diag::System::Intrinsics, ss.str(), source);
+    };
+
+    // Resolve the intrinsic overload
+    return MatchIntrinsic(context, *intrinsic_info, intrinsic_name, args, earliest_eval_stage,
+                          TemplateState{}, on_no_match);
+}
+
+Result<Overload> Lookup(Context& context,
+                        CtorConv type,
+                        const core::type::Type* template_arg,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source) {
+    auto name = str(type);
+
+    // Generates an error when no overloads match the provided arguments
+    auto on_no_match = [&](VectorRef<Candidate> candidates) {
+        StringStream ss;
+        ss << "no matching constructor for " << CallSignature(name, args, template_arg)
+           << std::endl;
+        Candidates ctor, conv;
+        for (auto candidate : candidates) {
+            if (candidate.overload->flags.Contains(OverloadFlag::kIsConstructor)) {
+                ctor.Push(candidate);
+            } else {
+                conv.Push(candidate);
+            }
+        }
+        if (!ctor.IsEmpty()) {
+            ss << std::endl
+               << ctor.Length() << " candidate constructor" << (ctor.Length() > 1 ? "s:" : ":")
+               << std::endl;
+            PrintCandidates(ss, context, ctor, name);
+        }
+        if (!conv.IsEmpty()) {
+            ss << std::endl
+               << conv.Length() << " candidate conversion" << (conv.Length() > 1 ? "s:" : ":")
+               << std::endl;
+            PrintCandidates(ss, context, conv, name);
+        }
+        context.diags.add_error(diag::System::Intrinsics, ss.str(), source);
+    };
+
+    // If a template type was provided, then close the 0'th type with this.
+    TemplateState templates;
+    if (template_arg) {
+        templates.Type(0, template_arg);
+    }
+
+    // Resolve the intrinsic overload
+    return MatchIntrinsic(context, context.data.ctor_conv[static_cast<size_t>(type)], name, args,
+                          earliest_eval_stage, templates, on_no_match);
+}
 
 }  // namespace tint::core::intrinsic
 
diff --git a/src/tint/lang/core/intrinsic/table.h b/src/tint/lang/core/intrinsic/table.h
index faae544..edae840 100644
--- a/src/tint/lang/core/intrinsic/table.h
+++ b/src/tint/lang/core/intrinsic/table.h
@@ -36,154 +36,178 @@
 
 namespace tint::core::intrinsic {
 
-/// Table is a lookup table of all the WGSL builtin functions and intrinsic operators
-class Table {
-  public:
-    /// @param data the intrinsic table data
-    /// @param types the type manager
-    /// @param symbols the symbol table
-    /// @param diags the diagnostic list to append errors to
-    /// @return a pointer to a newly created Table
-    static std::unique_ptr<Table> Create(const TableData& data,
-                                         core::type::Manager& types,
-                                         SymbolTable& symbols,
-                                         diag::List& diags);
-
-    /// Destructor
-    virtual ~Table();
-
-    /// Overload describes a fully matched builtin function overload
-    struct Overload {
-        /// Parameter describes a single parameter
-        struct Parameter {
-            /// Parameter type
-            const core::type::Type* const type;
-            /// Parameter usage
-            core::ParameterUsage const usage = core::ParameterUsage::kNone;
-
-            /// Equality operator
-            /// @param other the parameter to compare against
-            /// @returns true if this parameter and @p other are the same
-            bool operator==(const Parameter& other) const {
-                return type == other.type && usage == other.usage;
-            }
-
-            /// Inequality operator
-            /// @param other the parameter to compare against
-            /// @returns false if this parameter and @p other are the same
-            bool operator!=(const Parameter& other) const { return !(*this == other); }
-        };
-
-        /// The overload information
-        const OverloadInfo* info = nullptr;
-
-        /// The resolved overload return type
-        core::type::Type const* return_type = nullptr;
-
-        /// The resolved overload parameters
-        Vector<Parameter, 8> parameters;
-
-        /// The constant evaluation function
-        constant::Eval::Function const_eval_fn = nullptr;
+/// Overload describes a fully matched builtin function overload
+struct Overload {
+    /// Parameter describes a single parameter
+    struct Parameter {
+        /// Parameter type
+        const core::type::Type* const type;
+        /// Parameter usage
+        core::ParameterUsage const usage = core::ParameterUsage::kNone;
 
         /// Equality operator
-        /// @param other the overload to compare against
-        /// @returns true if this overload and @p other are the same
-        bool operator==(const Overload& other) const {
-            return info == other.info && return_type == other.return_type &&
-                   parameters == other.parameters;
+        /// @param other the parameter to compare against
+        /// @returns true if this parameter and @p other are the same
+        bool operator==(const Parameter& other) const {
+            return type == other.type && usage == other.usage;
         }
 
         /// Inequality operator
-        /// @param other the overload to compare against
-        /// @returns false if this overload and @p other are the same
-        bool operator!=(const Overload& other) const { return !(*this == other); }
+        /// @param other the parameter to compare against
+        /// @returns false if this parameter and @p other are the same
+        bool operator!=(const Parameter& other) const { return !(*this == other); }
     };
 
-    /// Lookup looks for the builtin overload with the given signature, raising an error diagnostic
-    /// if the builtin was not found.
-    /// @param type the builtin type
-    /// @param args the argument types passed to the builtin function
-    /// @param earliest_eval_stage the the earliest evaluation stage that a call to
-    ///        the builtin can be made. This can alter the overloads considered.
-    ///        For example, if the earliest evaluation stage is `EvaluationStage::kRuntime`, then
-    ///        only overloads with concrete argument types will be considered, as all
-    ///        abstract-numerics will have been materialized after shader creation time
-    ///        (EvaluationStage::kConstant).
-    /// @param source the source of the builtin call
-    /// @return the resolved builtin function overload
-    virtual Result<Overload> Lookup(core::Function type,
-                                    VectorRef<const core::type::Type*> args,
-                                    EvaluationStage earliest_eval_stage,
-                                    const Source& source) = 0;
+    /// The overload information
+    const OverloadInfo* info = nullptr;
 
-    /// Lookup looks for the unary op overload with the given signature, raising an error
-    /// diagnostic if the operator was not found.
-    /// @param op the unary operator
-    /// @param arg the type of the expression passed to the operator
-    /// @param earliest_eval_stage the the earliest evaluation stage that a call to
-    ///        the unary operator can be made. This can alter the overloads considered.
-    ///        For example, if the earliest evaluation stage is
-    ///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
-    ///        will be considered, as all abstract-numerics will have been materialized
-    ///        after shader creation time (EvaluationStage::kConstant).
-    /// @param source the source of the operator call
-    /// @return the resolved unary operator overload
-    virtual Result<Overload> Lookup(core::UnaryOp op,
-                                    const core::type::Type* arg,
-                                    EvaluationStage earliest_eval_stage,
-                                    const Source& source) = 0;
+    /// The resolved overload return type
+    core::type::Type const* return_type = nullptr;
 
-    /// Lookup looks for the binary op overload with the given signature, raising an error
-    /// diagnostic if the operator was not found.
-    /// @param op the binary operator
-    /// @param lhs the LHS value type passed to the operator
-    /// @param rhs the RHS value type passed to the operator
-    /// @param source the source of the operator call
-    /// @param earliest_eval_stage the the earliest evaluation stage that a call to
-    ///        the binary operator can be made. This can alter the overloads considered.
-    ///        For example, if the earliest evaluation stage is
-    ///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
-    ///        will be considered, as all abstract-numerics will have been materialized
-    ///        after shader creation time (EvaluationStage::kConstant).
-    /// @param is_compound true if the binary operator is being used as a compound assignment
-    /// @return the resolved binary operator overload
-    virtual Result<Overload> Lookup(core::BinaryOp op,
-                                    const core::type::Type* lhs,
-                                    const core::type::Type* rhs,
-                                    EvaluationStage earliest_eval_stage,
-                                    const Source& source,
-                                    bool is_compound) = 0;
+    /// The resolved overload parameters
+    Vector<Parameter, 8> parameters;
 
-    /// Lookup looks for the value constructor or conversion overload for the given CtorConv.
-    /// @param type the type being constructed or converted
-    /// @param template_arg the optional template argument
-    /// @param args the argument types passed to the constructor / conversion call
-    /// @param earliest_eval_stage the the earliest evaluation stage that a call to
-    ///        the constructor or conversion can be made. This can alter the overloads considered.
-    ///        For example, if the earliest evaluation stage is
-    ///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
-    ///        will be considered, as all abstract-numerics will have been materialized
-    ///        after shader creation time (EvaluationStage::kConstant).
-    /// @param source the source of the call
-    /// @return the resolved type constructor or conversion function overload
-    virtual Result<Overload> Lookup(CtorConv type,
-                                    const core::type::Type* template_arg,
-                                    VectorRef<const core::type::Type*> args,
-                                    EvaluationStage earliest_eval_stage,
-                                    const Source& source) = 0;
+    /// The constant evaluation function
+    constant::Eval::Function const_eval_fn = nullptr;
+
+    /// Equality operator
+    /// @param other the overload to compare against
+    /// @returns true if this overload and @p other are the same
+    bool operator==(const Overload& other) const {
+        return info == other.info && return_type == other.return_type &&
+               parameters == other.parameters;
+    }
+
+    /// Inequality operator
+    /// @param other the overload to compare against
+    /// @returns false if this overload and @p other are the same
+    bool operator!=(const Overload& other) const { return !(*this == other); }
 };
 
+/// The context data used to lookup intrinsic information
+struct Context {
+    /// The table table
+    const TableData& data;
+    /// The type manager
+    core::type::Manager& types;
+    /// The symbol table
+    SymbolTable& symbols;
+    /// The diagnostics
+    diag::List& diags;
+};
+
+/// Lookup looks for the builtin overload with the given signature, raising an error diagnostic
+/// if the builtin was not found.
+/// @param context the intrinsic context
+/// @param builtin_type the builtin identifier
+/// @param args the argument types passed to the builtin function
+/// @param earliest_eval_stage the the earliest evaluation stage that a call to
+///        the builtin can be made. This can alter the overloads considered.
+///        For example, if the earliest evaluation stage is `EvaluationStage::kRuntime`, then
+///        only overloads with concrete argument types will be considered, as all
+///        abstract-numerics will have been materialized after shader creation time
+///        (EvaluationStage::kConstant).
+/// @param source the source of the builtin call
+/// @return the resolved builtin function overload
+Result<Overload> Lookup(Context& context,
+                        core::Function builtin_type,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source);
+
+/// Lookup looks for the builtin overload with the given signature, raising an error diagnostic
+/// if the builtin was not found.
+/// @param context the intrinsic context
+/// @param intrinsic_name the name of the intrinsi
+/// @param function_id the function identifier
+/// @param args the argument types passed to the builtin function
+/// @param earliest_eval_stage the the earliest evaluation stage that a call to
+///        the builtin can be made. This can alter the overloads considered.
+///        For example, if the earliest evaluation stage is `EvaluationStage::kRuntime`, then
+///        only overloads with concrete argument types will be considered, as all
+///        abstract-numerics will have been materialized after shader creation time
+///        (EvaluationStage::kConstant).
+/// @param source the source of the builtin call
+/// @return the resolved builtin function overload
+Result<Overload> Lookup(Context& context,
+                        const char* intrinsic_name,
+                        size_t function_id,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source);
+
+/// Lookup looks for the unary op overload with the given signature, raising an error
+/// diagnostic if the operator was not found.
+/// @param context the intrinsic context
+/// @param op the unary operator
+/// @param arg the type of the expression passed to the operator
+/// @param earliest_eval_stage the the earliest evaluation stage that a call to
+///        the unary operator can be made. This can alter the overloads considered.
+///        For example, if the earliest evaluation stage is
+///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
+///        will be considered, as all abstract-numerics will have been materialized
+///        after shader creation time (EvaluationStage::kConstant).
+/// @param source the source of the operator call
+/// @return the resolved unary operator overload
+Result<Overload> Lookup(Context& context,
+                        core::UnaryOp op,
+                        const core::type::Type* arg,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source);
+
+/// Lookup looks for the binary op overload with the given signature, raising an error
+/// diagnostic if the operator was not found.
+/// @param context the intrinsic context
+/// @param op the binary operator
+/// @param lhs the LHS value type passed to the operator
+/// @param rhs the RHS value type passed to the operator
+/// @param source the source of the operator call
+/// @param earliest_eval_stage the the earliest evaluation stage that a call to
+///        the binary operator can be made. This can alter the overloads considered.
+///        For example, if the earliest evaluation stage is
+///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
+///        will be considered, as all abstract-numerics will have been materialized
+///        after shader creation time (EvaluationStage::kConstant).
+/// @param is_compound true if the binary operator is being used as a compound assignment
+/// @return the resolved binary operator overload
+Result<Overload> Lookup(Context& context,
+                        core::BinaryOp op,
+                        const core::type::Type* lhs,
+                        const core::type::Type* rhs,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source,
+                        bool is_compound);
+
+/// Lookup looks for the value constructor or conversion overload for the given CtorConv.
+/// @param context the intrinsic context
+/// @param type the type being constructed or converted
+/// @param template_arg the optional template argument
+/// @param args the argument types passed to the constructor / conversion call
+/// @param earliest_eval_stage the the earliest evaluation stage that a call to
+///        the constructor or conversion can be made. This can alter the overloads considered.
+///        For example, if the earliest evaluation stage is
+///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
+///        will be considered, as all abstract-numerics will have been materialized
+///        after shader creation time (EvaluationStage::kConstant).
+/// @param source the source of the call
+/// @return the resolved type constructor or conversion function overload
+Result<Overload> Lookup(Context& context,
+                        CtorConv type,
+                        const core::type::Type* template_arg,
+                        VectorRef<const core::type::Type*> args,
+                        EvaluationStage earliest_eval_stage,
+                        const Source& source);
+
 }  // namespace tint::core::intrinsic
 
 namespace tint {
 
-/// Hasher specialization for core::intrinsic::Table::Overload
+/// Hasher specialization for core::intrinsic::Overload
 template <>
-struct Hasher<core::intrinsic::Table::Overload> {
-    /// @param i the core::intrinsic::Table::Overload to create a hash for
+struct Hasher<core::intrinsic::Overload> {
+    /// @param i the core::intrinsic::Overload to create a hash for
     /// @return the hash value
-    inline std::size_t operator()(const core::intrinsic::Table::Overload& i) const {
+    inline std::size_t operator()(const core::intrinsic::Overload& i) const {
         size_t hash = Hash(i.parameters.Length());
         for (auto& p : i.parameters) {
             hash = HashCombine(hash, p.type, p.usage);
diff --git a/src/tint/lang/core/intrinsic/table_data.h b/src/tint/lang/core/intrinsic/table_data.h
index baea171..5d540aa 100644
--- a/src/tint/lang/core/intrinsic/table_data.h
+++ b/src/tint/lang/core/intrinsic/table_data.h
@@ -208,6 +208,9 @@
     const OverloadIndex overloads;
 };
 
+/// A IntrinsicInfo with no overloads
+static constexpr IntrinsicInfo kNoOverloads{0, OverloadIndex(OverloadIndex::kInvalid)};
+
 /// Number is an 32 bit unsigned integer, which can be in one of three states:
 /// * Invalid - Number has not been assigned a value
 /// * Valid   - a fixed integer value
diff --git a/src/tint/lang/core/intrinsic/table_test.cc b/src/tint/lang/core/intrinsic/table_test.cc
index 4eef64f..c0a3387 100644
--- a/src/tint/lang/core/intrinsic/table_test.cc
+++ b/src/tint/lang/core/intrinsic/table_test.cc
@@ -50,14 +50,13 @@
 
 class IntrinsicTableTest : public testing::Test, public ProgramBuilder {
   public:
-    std::unique_ptr<Table> table =
-        Table::Create(core::intrinsic::data::kData, Types(), Symbols(), Diagnostics());
+    Context context{core::intrinsic::data::kData, Types(), Symbols(), Diagnostics()};
 };
 
 TEST_F(IntrinsicTableTest, MatchF32) {
     auto* f32 = create<core::type::F32>();
     auto result =
-        table->Lookup(core::Function::kCos, Vector{f32}, EvaluationStage::kConstant, Source{});
+        Lookup(context, core::Function::kCos, Vector{f32}, EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -68,7 +67,7 @@
 TEST_F(IntrinsicTableTest, MismatchF32) {
     auto* i32 = create<core::type::I32>();
     auto result =
-        table->Lookup(core::Function::kCos, Vector{i32}, EvaluationStage::kConstant, Source{});
+        Lookup(context, core::Function::kCos, Vector{i32}, EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -77,8 +76,8 @@
     auto* f32 = create<core::type::F32>();
     auto* u32 = create<core::type::U32>();
     auto* vec2_f32 = create<core::type::Vector>(f32, 2u);
-    auto result = table->Lookup(core::Function::kUnpack2X16Float, Vector{u32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kUnpack2X16Float, Vector{u32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec2_f32);
@@ -88,8 +87,8 @@
 
 TEST_F(IntrinsicTableTest, MismatchU32) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kUnpack2X16Float, Vector{f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kUnpack2X16Float, Vector{f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -99,8 +98,8 @@
     auto* i32 = create<core::type::I32>();
     auto* vec4_f32 = create<core::type::Vector>(f32, 4u);
     auto* tex = create<core::type::SampledTexture>(core::type::TextureDimension::k1d, f32);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, i32, i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, i32, i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec4_f32);
@@ -116,16 +115,16 @@
 TEST_F(IntrinsicTableTest, MismatchI32) {
     auto* f32 = create<core::type::F32>();
     auto* tex = create<core::type::SampledTexture>(core::type::TextureDimension::k1d, f32);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
 TEST_F(IntrinsicTableTest, MatchIU32AsI32) {
     auto* i32 = create<core::type::I32>();
-    auto result = table->Lookup(core::Function::kCountOneBits, Vector{i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kCountOneBits, Vector{i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, i32);
@@ -135,8 +134,8 @@
 
 TEST_F(IntrinsicTableTest, MatchIU32AsU32) {
     auto* u32 = create<core::type::U32>();
-    auto result = table->Lookup(core::Function::kCountOneBits, Vector{u32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kCountOneBits, Vector{u32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, u32);
@@ -146,16 +145,16 @@
 
 TEST_F(IntrinsicTableTest, MismatchIU32) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kCountOneBits, Vector{f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kCountOneBits, Vector{f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsI32) {
     auto* i32 = create<core::type::I32>();
-    auto result = table->Lookup(core::Function::kClamp, Vector{i32, i32, i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{i32, i32, i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, i32);
@@ -167,8 +166,8 @@
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsU32) {
     auto* u32 = create<core::type::U32>();
-    auto result = table->Lookup(core::Function::kClamp, Vector{u32, u32, u32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{u32, u32, u32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, u32);
@@ -180,8 +179,8 @@
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsF32) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kClamp, Vector{f32, f32, f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{f32, f32, f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -193,8 +192,8 @@
 
 TEST_F(IntrinsicTableTest, MismatchFIU32) {
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::Function::kClamp, Vector{bool_, bool_, bool_},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{bool_, bool_, bool_},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -202,8 +201,8 @@
 TEST_F(IntrinsicTableTest, MatchBool) {
     auto* f32 = create<core::type::F32>();
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::Function::kSelect, Vector{f32, f32, bool_},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kSelect, Vector{f32, f32, bool_},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -215,8 +214,8 @@
 
 TEST_F(IntrinsicTableTest, MismatchBool) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kSelect, Vector{f32, f32, f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kSelect, Vector{f32, f32, f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -226,8 +225,8 @@
     auto* atomicI32 = create<core::type::Atomic>(i32);
     auto* ptr = create<core::type::Pointer>(core::AddressSpace::kWorkgroup, atomicI32,
                                             core::Access::kReadWrite);
-    auto result = table->Lookup(core::Function::kAtomicLoad, Vector{ptr},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kAtomicLoad, Vector{ptr},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, i32);
@@ -238,8 +237,8 @@
 TEST_F(IntrinsicTableTest, MismatchPointer) {
     auto* i32 = create<core::type::I32>();
     auto* atomicI32 = create<core::type::Atomic>(i32);
-    auto result = table->Lookup(core::Function::kAtomicLoad, Vector{atomicI32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kAtomicLoad, Vector{atomicI32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -249,8 +248,8 @@
                                           create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
     auto* arr_ptr =
         create<core::type::Pointer>(core::AddressSpace::kStorage, arr, core::Access::kReadWrite);
-    auto result = table->Lookup(core::Function::kArrayLength, Vector{arr_ptr},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kArrayLength, Vector{arr_ptr},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_TRUE(result->return_type->Is<core::type::U32>());
@@ -262,8 +261,8 @@
 
 TEST_F(IntrinsicTableTest, MismatchArray) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kArrayLength, Vector{f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kArrayLength, Vector{f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -274,8 +273,8 @@
     auto* vec4_f32 = create<core::type::Vector>(f32, 4u);
     auto* tex = create<core::type::SampledTexture>(core::type::TextureDimension::k2d, f32);
     auto* sampler = create<core::type::Sampler>(core::type::SamplerKind::kSampler);
-    auto result = table->Lookup(core::Function::kTextureSample, Vector{tex, sampler, vec2_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureSample, Vector{tex, sampler, vec2_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec4_f32);
@@ -292,8 +291,8 @@
     auto* f32 = create<core::type::F32>();
     auto* vec2_f32 = create<core::type::Vector>(f32, 2u);
     auto* tex = create<core::type::SampledTexture>(core::type::TextureDimension::k2d, f32);
-    auto result = table->Lookup(core::Function::kTextureSample, Vector{tex, f32, vec2_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureSample, Vector{tex, f32, vec2_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -304,8 +303,8 @@
     auto* vec2_i32 = create<core::type::Vector>(i32, 2u);
     auto* vec4_f32 = create<core::type::Vector>(f32, 4u);
     auto* tex = create<core::type::SampledTexture>(core::type::TextureDimension::k2d, f32);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec4_f32);
@@ -324,8 +323,8 @@
     auto* vec2_i32 = create<core::type::Vector>(i32, 2u);
     auto* vec4_f32 = create<core::type::Vector>(f32, 4u);
     auto* tex = create<core::type::MultisampledTexture>(core::type::TextureDimension::k2d, f32);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec4_f32);
@@ -343,8 +342,8 @@
     auto* i32 = create<core::type::I32>();
     auto* vec2_i32 = create<core::type::Vector>(i32, 2u);
     auto* tex = create<core::type::DepthTexture>(core::type::TextureDimension::k2d);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -362,8 +361,8 @@
     auto* i32 = create<core::type::I32>();
     auto* vec2_i32 = create<core::type::Vector>(i32, 2u);
     auto* tex = create<core::type::DepthMultisampledTexture>(core::type::TextureDimension::k2d);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -382,8 +381,8 @@
     auto* vec2_i32 = create<core::type::Vector>(i32, 2u);
     auto* vec4_f32 = create<core::type::Vector>(f32, 4u);
     auto* tex = create<core::type::ExternalTexture>();
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{tex, vec2_i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec4_f32);
@@ -404,8 +403,8 @@
                                                    core::TexelFormat::kR32Float,
                                                    core::Access::kWrite, subtype);
 
-    auto result = table->Lookup(core::Function::kTextureStore, Vector{tex, vec2_i32, vec4_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureStore, Vector{tex, vec2_i32, vec4_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_TRUE(result->return_type->Is<type::Void>());
@@ -422,20 +421,20 @@
     auto* f32 = create<core::type::F32>();
     auto* i32 = create<core::type::I32>();
     auto* vec2_i32 = create<core::type::Vector>(i32, 2u);
-    auto result = table->Lookup(core::Function::kTextureLoad, Vector{f32, vec2_i32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureLoad, Vector{f32, vec2_i32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
 TEST_F(IntrinsicTableTest, ImplicitLoadOnReference) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kCos,
-                                Vector{
-                                    create<core::type::Reference>(core::AddressSpace::kFunction,
-                                                                  f32, core::Access::kReadWrite),
-                                },
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kCos,
+                         Vector{
+                             create<core::type::Reference>(core::AddressSpace::kFunction, f32,
+                                                           core::Access::kReadWrite),
+                         },
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -445,8 +444,8 @@
 
 TEST_F(IntrinsicTableTest, MatchTemplateType) {
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(core::Function::kClamp, Vector{f32, f32, f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{f32, f32, f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -458,8 +457,8 @@
 TEST_F(IntrinsicTableTest, MismatchTemplateType) {
     auto* f32 = create<core::type::F32>();
     auto* u32 = create<core::type::U32>();
-    auto result = table->Lookup(core::Function::kClamp, Vector{f32, u32, f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{f32, u32, f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -467,8 +466,8 @@
 TEST_F(IntrinsicTableTest, MatchOpenSizeVector) {
     auto* f32 = create<core::type::F32>();
     auto* vec2_f32 = create<core::type::Vector>(f32, 2u);
-    auto result = table->Lookup(core::Function::kClamp, Vector{vec2_f32, vec2_f32, vec2_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{vec2_f32, vec2_f32, vec2_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, vec2_f32);
@@ -482,8 +481,8 @@
     auto* f32 = create<core::type::F32>();
     auto* u32 = create<core::type::U32>();
     auto* vec2_f32 = create<core::type::Vector>(f32, 2u);
-    auto result = table->Lookup(core::Function::kClamp, Vector{vec2_f32, u32, vec2_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kClamp, Vector{vec2_f32, u32, vec2_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -492,8 +491,8 @@
     auto* f32 = create<core::type::F32>();
     auto* vec3_f32 = create<core::type::Vector>(f32, 3u);
     auto* mat3_f32 = create<core::type::Matrix>(vec3_f32, 3u);
-    auto result = table->Lookup(core::Function::kDeterminant, Vector{mat3_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kDeterminant, Vector{mat3_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result->return_type, f32);
@@ -505,8 +504,8 @@
     auto* f32 = create<core::type::F32>();
     auto* vec2_f32 = create<core::type::Vector>(f32, 2u);
     auto* mat3x2_f32 = create<core::type::Matrix>(vec2_f32, 3u);
-    auto result = table->Lookup(core::Function::kDeterminant, Vector{mat3x2_f32},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kDeterminant, Vector{mat3x2_f32},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -514,8 +513,8 @@
 TEST_F(IntrinsicTableTest, MatchDifferentArgsElementType_Builtin_ConstantEval) {
     auto* af = create<core::type::AbstractFloat>();
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::Function::kSelect, Vector{af, af, bool_},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kSelect, Vector{af, af, bool_},
+                         EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_NE(result->const_eval_fn, nullptr);
@@ -530,8 +529,8 @@
     auto* af = create<core::type::AbstractFloat>();
     auto* bool_ref = create<core::type::Reference>(
         core::AddressSpace::kFunction, create<core::type::Bool>(), core::Access::kReadWrite);
-    auto result = table->Lookup(core::Function::kSelect, Vector{af, af, bool_ref},
-                                EvaluationStage::kRuntime, Source{});
+    auto result = Lookup(context, core::Function::kSelect, Vector{af, af, bool_ref},
+                         EvaluationStage::kRuntime, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_NE(result->const_eval_fn, nullptr);
@@ -545,8 +544,8 @@
 TEST_F(IntrinsicTableTest, MatchDifferentArgsElementType_Binary_ConstantEval) {
     auto* ai = create<core::type::AbstractInt>();
     auto* u32 = create<core::type::U32>();
-    auto result = table->Lookup(core::BinaryOp::kShiftLeft, ai, u32, EvaluationStage::kConstant,
-                                Source{}, false);
+    auto result = Lookup(context, core::BinaryOp::kShiftLeft, ai, u32, EvaluationStage::kConstant,
+                         Source{}, false);
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_NE(result->const_eval_fn, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
@@ -558,8 +557,8 @@
 TEST_F(IntrinsicTableTest, MatchDifferentArgsElementType_Binary_RuntimeEval) {
     auto* ai = create<core::type::AbstractInt>();
     auto* u32 = create<core::type::U32>();
-    auto result = table->Lookup(core::BinaryOp::kShiftLeft, ai, u32, EvaluationStage::kRuntime,
-                                Source{}, false);
+    auto result = Lookup(context, core::BinaryOp::kShiftLeft, ai, u32, EvaluationStage::kRuntime,
+                         Source{}, false);
     ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_NE(result->const_eval_fn, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
@@ -572,8 +571,8 @@
     // None of the arguments match, so expect the overloads with 2 parameters to
     // come first
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::Function::kTextureDimensions, Vector{bool_, bool_},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureDimensions, Vector{bool_, bool_},
+                         EvaluationStage::kConstant, Source{});
     EXPECT_FALSE(result);
     ASSERT_EQ(Diagnostics().str(),
               R"(error: no matching call to textureDimensions(bool, bool)
@@ -612,8 +611,8 @@
 TEST_F(IntrinsicTableTest, OverloadOrderByMatchingParameter) {
     auto* tex = create<core::type::DepthTexture>(core::type::TextureDimension::k2d);
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::Function::kTextureDimensions, Vector{tex, bool_},
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kTextureDimensions, Vector{tex, bool_},
+                         EvaluationStage::kConstant, Source{});
     EXPECT_FALSE(result);
     ASSERT_EQ(Diagnostics().str(),
               R"(error: no matching call to textureDimensions(texture_depth_2d, bool)
@@ -652,16 +651,16 @@
 TEST_F(IntrinsicTableTest, MatchUnaryOp) {
     auto* i32 = create<core::type::I32>();
     auto* vec3_i32 = create<core::type::Vector>(i32, 3u);
-    auto result = table->Lookup(core::UnaryOp::kNegation, vec3_i32, EvaluationStage::kConstant,
-                                Source{{12, 34}});
+    auto result = Lookup(context, core::UnaryOp::kNegation, vec3_i32, EvaluationStage::kConstant,
+                         Source{{12, 34}});
     EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_EQ(Diagnostics().str(), "");
 }
 
 TEST_F(IntrinsicTableTest, MismatchUnaryOp) {
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::UnaryOp::kNegation, bool_, EvaluationStage::kConstant,
-                                Source{{12, 34}});
+    auto result = Lookup(context, core::UnaryOp::kNegation, bool_, EvaluationStage::kConstant,
+                         Source{{12, 34}});
     ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator - (bool)
 
@@ -674,7 +673,7 @@
 TEST_F(IntrinsicTableTest, MatchUnaryOp_Constant) {
     auto* ai = create<core::type::AbstractInt>();
     auto result =
-        table->Lookup(core::UnaryOp::kNegation, ai, EvaluationStage::kConstant, Source{{12, 34}});
+        Lookup(context, core::UnaryOp::kNegation, ai, EvaluationStage::kConstant, Source{{12, 34}});
     EXPECT_EQ(result->return_type, ai);
     EXPECT_EQ(Diagnostics().str(), "");
 }
@@ -682,7 +681,7 @@
 TEST_F(IntrinsicTableTest, MatchUnaryOp_Runtime) {
     auto* ai = create<core::type::AbstractInt>();
     auto result =
-        table->Lookup(core::UnaryOp::kNegation, ai, EvaluationStage::kRuntime, Source{{12, 34}});
+        Lookup(context, core::UnaryOp::kNegation, ai, EvaluationStage::kRuntime, Source{{12, 34}});
     EXPECT_NE(result->return_type, ai);
     EXPECT_TRUE(result->return_type->Is<core::type::I32>());
     EXPECT_EQ(Diagnostics().str(), "");
@@ -691,9 +690,9 @@
 TEST_F(IntrinsicTableTest, MatchBinaryOp) {
     auto* i32 = create<core::type::I32>();
     auto* vec3_i32 = create<core::type::Vector>(i32, 3u);
-    auto result = table->Lookup(core::BinaryOp::kMultiply, i32, vec3_i32,
-                                EvaluationStage::kConstant, Source{{12, 34}},
-                                /* is_compound */ false);
+    auto result = Lookup(context, core::BinaryOp::kMultiply, i32, vec3_i32,
+                         EvaluationStage::kConstant, Source{{12, 34}},
+                         /* is_compound */ false);
     EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_EQ(result->parameters[0].type, i32);
     EXPECT_EQ(result->parameters[1].type, vec3_i32);
@@ -703,9 +702,9 @@
 TEST_F(IntrinsicTableTest, MismatchBinaryOp) {
     auto* f32 = create<core::type::F32>();
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
-                                Source{{12, 34}},
-                                /* is_compound */ false);
+    auto result = Lookup(context, core::BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
+                         Source{{12, 34}},
+                         /* is_compound */ false);
     ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator * (f32, bool)
 
@@ -725,9 +724,9 @@
 TEST_F(IntrinsicTableTest, MatchCompoundOp) {
     auto* i32 = create<core::type::I32>();
     auto* vec3_i32 = create<core::type::Vector>(i32, 3u);
-    auto result = table->Lookup(core::BinaryOp::kMultiply, i32, vec3_i32,
-                                EvaluationStage::kConstant, Source{{12, 34}},
-                                /* is_compound */ true);
+    auto result = Lookup(context, core::BinaryOp::kMultiply, i32, vec3_i32,
+                         EvaluationStage::kConstant, Source{{12, 34}},
+                         /* is_compound */ true);
     EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_EQ(result->parameters[0].type, i32);
     EXPECT_EQ(result->parameters[1].type, vec3_i32);
@@ -737,9 +736,9 @@
 TEST_F(IntrinsicTableTest, MismatchCompoundOp) {
     auto* f32 = create<core::type::F32>();
     auto* bool_ = create<core::type::Bool>();
-    auto result = table->Lookup(core::BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
-                                Source{{12, 34}},
-                                /* is_compound */ true);
+    auto result = Lookup(context, core::BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
+                         Source{{12, 34}},
+                         /* is_compound */ true);
     ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator *= (f32, bool)
 
@@ -759,8 +758,8 @@
 TEST_F(IntrinsicTableTest, MatchTypeInitializerImplicit) {
     auto* i32 = create<core::type::I32>();
     auto* vec3_i32 = create<core::type::Vector>(i32, 3u);
-    auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{i32, i32, i32},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, nullptr, Vector{i32, i32, i32},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_TRUE(result->info->flags.Contains(OverloadFlag::kIsConstructor));
@@ -774,8 +773,8 @@
 TEST_F(IntrinsicTableTest, MatchTypeInitializerExplicit) {
     auto* i32 = create<core::type::I32>();
     auto* vec3_i32 = create<core::type::Vector>(i32, 3u);
-    auto result = table->Lookup(CtorConv::kVec3, i32, Vector{i32, i32, i32},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, i32, Vector{i32, i32, i32},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_TRUE(result->info->flags.Contains(OverloadFlag::kIsConstructor));
@@ -789,8 +788,8 @@
 TEST_F(IntrinsicTableTest, MismatchTypeInitializerImplicit) {
     auto* i32 = create<core::type::I32>();
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{i32, f32, i32},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, nullptr, Vector{i32, f32, i32},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3(i32, f32, i32)
@@ -816,8 +815,8 @@
 TEST_F(IntrinsicTableTest, MismatchTypeInitializerExplicit) {
     auto* i32 = create<core::type::I32>();
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(CtorConv::kVec3, i32, Vector{i32, f32, i32},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, i32, Vector{i32, f32, i32},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3<i32>(i32, f32, i32)
@@ -843,8 +842,8 @@
 TEST_F(IntrinsicTableTest, MatchTypeInitializerImplicitVecFromVecAbstract) {
     auto* ai = create<core::type::AbstractInt>();
     auto* vec3_ai = create<core::type::Vector>(ai, 3u);
-    auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{vec3_ai},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, nullptr, Vector{vec3_ai},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_EQ(result->return_type, vec3_ai);
     EXPECT_TRUE(result->info->flags.Contains(OverloadFlag::kIsConstructor));
@@ -858,8 +857,8 @@
     auto* vec2_ai = create<core::type::Vector>(create<core::type::AbstractInt>(), 2u);
     auto* vec2_af = create<core::type::Vector>(af, 2u);
     auto* mat2x2_af = create<core::type::Matrix>(vec2_af, 2u);
-    auto result = table->Lookup(CtorConv::kMat2x2, nullptr, Vector{vec2_ai, vec2_ai},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kMat2x2, nullptr, Vector{vec2_ai, vec2_ai},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_TYPE(result->return_type, mat2x2_af);
     EXPECT_TRUE(result->info->flags.Contains(OverloadFlag::kIsConstructor));
@@ -872,8 +871,8 @@
 TEST_F(IntrinsicTableTest, MatchTypeInitializer_ConstantEval) {
     auto* ai = create<core::type::AbstractInt>();
     auto* vec3_ai = create<core::type::Vector>(ai, 3u);
-    auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{ai, ai, ai},
-                                EvaluationStage::kConstant, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, nullptr, Vector{ai, ai, ai},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_NE(result->const_eval_fn, nullptr);
     EXPECT_EQ(result->return_type, vec3_ai);
@@ -887,8 +886,8 @@
 
 TEST_F(IntrinsicTableTest, MatchTypeInitializer_RuntimeEval) {
     auto* ai = create<core::type::AbstractInt>();
-    auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{ai, ai, ai},
-                                EvaluationStage::kRuntime, Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, nullptr, Vector{ai, ai, ai},
+                         EvaluationStage::kRuntime, Source{{12, 34}});
     auto* i32 = create<type::I32>();
     auto* vec3_i32 = create<type::Vector>(i32, 3u);
     ASSERT_TRUE(result) << Diagnostics().str();
@@ -907,8 +906,8 @@
     auto* vec3_i32 = create<core::type::Vector>(i32, 3u);
     auto* f32 = create<core::type::F32>();
     auto* vec3_f32 = create<core::type::Vector>(f32, 3u);
-    auto result = table->Lookup(CtorConv::kVec3, i32, Vector{vec3_f32}, EvaluationStage::kConstant,
-                                Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, i32, Vector{vec3_f32},
+                         EvaluationStage::kConstant, Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_FALSE(result->info->flags.Contains(OverloadFlag::kIsConstructor));
@@ -920,8 +919,8 @@
     auto* arr = create<core::type::Array>(create<core::type::U32>(),
                                           create<core::type::RuntimeArrayCount>(), 4u, 4u, 4u, 4u);
     auto* f32 = create<core::type::F32>();
-    auto result = table->Lookup(CtorConv::kVec3, f32, Vector{arr}, EvaluationStage::kConstant,
-                                Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, f32, Vector{arr}, EvaluationStage::kConstant,
+                         Source{{12, 34}});
     ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3<f32>(array<u32>)
@@ -950,8 +949,8 @@
     auto* vec3_ai = create<core::type::Vector>(ai, 3u);
     auto* f32 = create<core::type::F32>();
     auto* vec3_f32 = create<core::type::Vector>(f32, 3u);
-    auto result = table->Lookup(CtorConv::kVec3, af, Vector{vec3_ai}, EvaluationStage::kConstant,
-                                Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, af, Vector{vec3_ai}, EvaluationStage::kConstant,
+                         Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_NE(result->const_eval_fn, nullptr);
     // NOTE: Conversions are explicit, so there's no way to have it return abstracts
@@ -967,8 +966,8 @@
     auto* vec3_ai = create<core::type::Vector>(ai, 3u);
     auto* vec3_f32 = create<core::type::Vector>(create<core::type::F32>(), 3u);
     auto* vec3_i32 = create<core::type::Vector>(create<core::type::I32>(), 3u);
-    auto result = table->Lookup(CtorConv::kVec3, af, Vector{vec3_ai}, EvaluationStage::kRuntime,
-                                Source{{12, 34}});
+    auto result = Lookup(context, CtorConv::kVec3, af, Vector{vec3_ai}, EvaluationStage::kRuntime,
+                         Source{{12, 34}});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_NE(result->const_eval_fn, nullptr);
     EXPECT_EQ(result->return_type, vec3_f32);
@@ -981,8 +980,8 @@
     auto* f32 = create<core::type::F32>();
     Vector<const core::type::Type*, 0> arg_tys;
     arg_tys.Resize(257, f32);
-    auto result = table->Lookup(core::Function::kAbs, std::move(arg_tys),
-                                EvaluationStage::kConstant, Source{});
+    auto result = Lookup(context, core::Function::kAbs, std::move(arg_tys),
+                         EvaluationStage::kConstant, Source{});
     ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -995,7 +994,7 @@
     auto* ai = create<core::type::AbstractInt>();
     auto* i32 = create<core::type::I32>();
     auto result =
-        table->Lookup(CtorConv::kI32, nullptr, Vector{ai}, EvaluationStage::kConstant, Source{});
+        Lookup(context, CtorConv::kI32, nullptr, Vector{ai}, EvaluationStage::kConstant, Source{});
     ASSERT_TRUE(result) << Diagnostics().str();
     EXPECT_EQ(result->return_type, i32);
     EXPECT_EQ(result->parameters.Length(), 1u);
@@ -1031,16 +1030,15 @@
 };
 
 struct IntrinsicTableAbstractBinaryTest : public resolver::ResolverTestWithParam<Case> {
-    std::unique_ptr<Table> table =
-        Table::Create(core::intrinsic::data::kData, Types(), Symbols(), Diagnostics());
+    Context context{core::intrinsic::data::kData, Types(), Symbols(), Diagnostics()};
 };
 
 TEST_P(IntrinsicTableAbstractBinaryTest, MatchAdd) {
     auto* arg_lhs = GetParam().arg_lhs(*this);
     auto* arg_rhs = GetParam().arg_rhs(*this);
-    auto result = table->Lookup(core::BinaryOp::kAdd, arg_lhs, arg_rhs, EvaluationStage::kConstant,
-                                Source{{12, 34}},
-                                /* is_compound */ false);
+    auto result = Lookup(context, core::BinaryOp::kAdd, arg_lhs, arg_rhs,
+                         EvaluationStage::kConstant, Source{{12, 34}},
+                         /* is_compound */ false);
 
     bool matched = result;
     bool expected_match = GetParam().expected_match;
@@ -1216,16 +1214,15 @@
 };
 
 struct IntrinsicTableAbstractTernaryTest : public resolver::ResolverTestWithParam<Case> {
-    std::unique_ptr<Table> table =
-        Table::Create(core::intrinsic::data::kData, Types(), Symbols(), Diagnostics());
+    Context context{core::intrinsic::data::kData, Types(), Symbols(), Diagnostics()};
 };
 
 TEST_P(IntrinsicTableAbstractTernaryTest, MatchClamp) {
     auto* arg_a = GetParam().arg_a(*this);
     auto* arg_b = GetParam().arg_b(*this);
     auto* arg_c = GetParam().arg_c(*this);
-    auto builtin = table->Lookup(core::Function::kClamp, Vector{arg_a, arg_b, arg_c},
-                                 EvaluationStage::kConstant, Source{{12, 34}});
+    auto builtin = Lookup(context, core::Function::kClamp, Vector{arg_a, arg_b, arg_c},
+                          EvaluationStage::kConstant, Source{{12, 34}});
 
     bool expected_match = GetParam().expected_match;
     EXPECT_EQ(builtin == true, expected_match) << Diagnostics().str();
diff --git a/src/tint/lang/core/ir/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel
new file mode 100644
index 0000000..7d7188cd
--- /dev/null
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -0,0 +1,223 @@
+# 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 = "ir",
+  srcs = [
+    "access.cc",
+    "binary.cc",
+    "bitcast.cc",
+    "block.cc",
+    "block_param.cc",
+    "break_if.cc",
+    "builder.cc",
+    "builtin_call.cc",
+    "call.cc",
+    "constant.cc",
+    "construct.cc",
+    "continue.cc",
+    "control_instruction.cc",
+    "convert.cc",
+    "core_builtin_call.cc",
+    "disassembler.cc",
+    "discard.cc",
+    "exit.cc",
+    "exit_if.cc",
+    "exit_loop.cc",
+    "exit_switch.cc",
+    "function.cc",
+    "function_param.cc",
+    "if.cc",
+    "instruction.cc",
+    "instruction_result.cc",
+    "intrinsic_call.cc",
+    "let.cc",
+    "load.cc",
+    "load_vector_element.cc",
+    "loop.cc",
+    "module.cc",
+    "multi_in_block.cc",
+    "next_iteration.cc",
+    "operand_instruction.cc",
+    "return.cc",
+    "store.cc",
+    "store_vector_element.cc",
+    "switch.cc",
+    "swizzle.cc",
+    "terminate_invocation.cc",
+    "terminator.cc",
+    "unary.cc",
+    "unreachable.cc",
+    "user_call.cc",
+    "validator.cc",
+    "value.cc",
+    "var.cc",
+  ],
+  hdrs = [
+    "access.h",
+    "binary.h",
+    "bitcast.h",
+    "block.h",
+    "block_param.h",
+    "break_if.h",
+    "builder.h",
+    "builtin_call.h",
+    "call.h",
+    "constant.h",
+    "construct.h",
+    "continue.h",
+    "control_instruction.h",
+    "convert.h",
+    "core_builtin_call.h",
+    "disassembler.h",
+    "discard.h",
+    "exit.h",
+    "exit_if.h",
+    "exit_loop.h",
+    "exit_switch.h",
+    "function.h",
+    "function_param.h",
+    "if.h",
+    "instruction.h",
+    "instruction_result.h",
+    "intrinsic_call.h",
+    "let.h",
+    "load.h",
+    "load_vector_element.h",
+    "location.h",
+    "loop.h",
+    "module.h",
+    "multi_in_block.h",
+    "next_iteration.h",
+    "operand_instruction.h",
+    "return.h",
+    "store.h",
+    "store_vector_element.h",
+    "switch.h",
+    "swizzle.h",
+    "terminate_invocation.h",
+    "terminator.h",
+    "unary.h",
+    "unreachable.h",
+    "user_call.h",
+    "validator.h",
+    "value.h",
+    "var.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "access_test.cc",
+    "binary_test.cc",
+    "bitcast_test.cc",
+    "block_param_test.cc",
+    "block_test.cc",
+    "break_if_test.cc",
+    "constant_test.cc",
+    "construct_test.cc",
+    "continue_test.cc",
+    "convert_test.cc",
+    "core_builtin_call_test.cc",
+    "discard_test.cc",
+    "exit_if_test.cc",
+    "exit_loop_test.cc",
+    "exit_switch_test.cc",
+    "function_param_test.cc",
+    "function_test.cc",
+    "if_test.cc",
+    "instruction_result_test.cc",
+    "instruction_test.cc",
+    "ir_helper_test.h",
+    "let_test.cc",
+    "load_test.cc",
+    "load_vector_element_test.cc",
+    "loop_test.cc",
+    "module_test.cc",
+    "multi_in_block_test.cc",
+    "next_iteration_test.cc",
+    "operand_instruction_test.cc",
+    "return_test.cc",
+    "store_test.cc",
+    "store_vector_element_test.cc",
+    "switch_test.cc",
+    "swizzle_test.cc",
+    "unary_test.cc",
+    "user_call_test.cc",
+    "validator_test.cc",
+    "value_test.cc",
+    "var_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/core/ir/BUILD.cfg b/src/tint/lang/core/ir/BUILD.cfg
deleted file mode 100644
index 0ca3ed3..0000000
--- a/src/tint/lang/core/ir/BUILD.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "condition": "tint_build_ir"
-}
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index f9c1c07..3e21f9e 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -23,11 +23,9 @@
 
 include(lang/core/ir/transform/BUILD.cmake)
 
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_core_ir
 # Kind:      lib
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_core_ir lib
   lang/core/ir/access.cc
@@ -133,6 +131,8 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
   tint_lang_core_type
   tint_utils_containers
   tint_utils_diagnostic
@@ -149,12 +149,9 @@
   tint_utils_traits
 )
 
-endif(TINT_BUILD_IR)
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_core_ir_test
 # Kind:      test
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_core_ir_test test
   lang/core/ir/access_test.cc
@@ -177,7 +174,6 @@
   lang/core/ir/if_test.cc
   lang/core/ir/instruction_result_test.cc
   lang/core/ir/instruction_test.cc
-  lang/core/ir/intrinsic_call_test.cc
   lang/core/ir/ir_helper_test.h
   lang/core/ir/let_test.cc
   lang/core/ir/load_test.cc
@@ -203,6 +199,9 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
   tint_utils_containers
   tint_utils_diagnostic
@@ -222,11 +221,3 @@
 tint_target_add_external_dependencies(tint_lang_core_ir_test test
   "gtest"
 )
-
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_core_ir_test test
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
\ No newline at end of file
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index b665385..4a844c9 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -28,111 +28,181 @@
 if (tint_build_unittests) {
   import("//testing/test.gni")
 }
-if (tint_build_ir) {
-  libtint_source_set("ir") {
+
+libtint_source_set("ir") {
+  sources = [
+    "access.cc",
+    "access.h",
+    "binary.cc",
+    "binary.h",
+    "bitcast.cc",
+    "bitcast.h",
+    "block.cc",
+    "block.h",
+    "block_param.cc",
+    "block_param.h",
+    "break_if.cc",
+    "break_if.h",
+    "builder.cc",
+    "builder.h",
+    "builtin_call.cc",
+    "builtin_call.h",
+    "call.cc",
+    "call.h",
+    "constant.cc",
+    "constant.h",
+    "construct.cc",
+    "construct.h",
+    "continue.cc",
+    "continue.h",
+    "control_instruction.cc",
+    "control_instruction.h",
+    "convert.cc",
+    "convert.h",
+    "core_builtin_call.cc",
+    "core_builtin_call.h",
+    "disassembler.cc",
+    "disassembler.h",
+    "discard.cc",
+    "discard.h",
+    "exit.cc",
+    "exit.h",
+    "exit_if.cc",
+    "exit_if.h",
+    "exit_loop.cc",
+    "exit_loop.h",
+    "exit_switch.cc",
+    "exit_switch.h",
+    "function.cc",
+    "function.h",
+    "function_param.cc",
+    "function_param.h",
+    "if.cc",
+    "if.h",
+    "instruction.cc",
+    "instruction.h",
+    "instruction_result.cc",
+    "instruction_result.h",
+    "intrinsic_call.cc",
+    "intrinsic_call.h",
+    "let.cc",
+    "let.h",
+    "load.cc",
+    "load.h",
+    "load_vector_element.cc",
+    "load_vector_element.h",
+    "location.h",
+    "loop.cc",
+    "loop.h",
+    "module.cc",
+    "module.h",
+    "multi_in_block.cc",
+    "multi_in_block.h",
+    "next_iteration.cc",
+    "next_iteration.h",
+    "operand_instruction.cc",
+    "operand_instruction.h",
+    "return.cc",
+    "return.h",
+    "store.cc",
+    "store.h",
+    "store_vector_element.cc",
+    "store_vector_element.h",
+    "switch.cc",
+    "switch.h",
+    "swizzle.cc",
+    "swizzle.h",
+    "terminate_invocation.cc",
+    "terminate_invocation.h",
+    "terminator.cc",
+    "terminator.h",
+    "unary.cc",
+    "unary.h",
+    "unreachable.cc",
+    "unreachable.h",
+    "user_call.cc",
+    "user_call.h",
+    "validator.cc",
+    "validator.h",
+    "value.cc",
+    "value.h",
+    "var.cc",
+    "var.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/intrinsic/data",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
+if (tint_build_unittests) {
+  tint_unittests_source_set("unittests") {
+    testonly = true
     sources = [
-      "access.cc",
-      "access.h",
-      "binary.cc",
-      "binary.h",
-      "bitcast.cc",
-      "bitcast.h",
-      "block.cc",
-      "block.h",
-      "block_param.cc",
-      "block_param.h",
-      "break_if.cc",
-      "break_if.h",
-      "builder.cc",
-      "builder.h",
-      "builtin_call.cc",
-      "builtin_call.h",
-      "call.cc",
-      "call.h",
-      "constant.cc",
-      "constant.h",
-      "construct.cc",
-      "construct.h",
-      "continue.cc",
-      "continue.h",
-      "control_instruction.cc",
-      "control_instruction.h",
-      "convert.cc",
-      "convert.h",
-      "core_builtin_call.cc",
-      "core_builtin_call.h",
-      "disassembler.cc",
-      "disassembler.h",
-      "discard.cc",
-      "discard.h",
-      "exit.cc",
-      "exit.h",
-      "exit_if.cc",
-      "exit_if.h",
-      "exit_loop.cc",
-      "exit_loop.h",
-      "exit_switch.cc",
-      "exit_switch.h",
-      "function.cc",
-      "function.h",
-      "function_param.cc",
-      "function_param.h",
-      "if.cc",
-      "if.h",
-      "instruction.cc",
-      "instruction.h",
-      "instruction_result.cc",
-      "instruction_result.h",
-      "intrinsic_call.cc",
-      "intrinsic_call.h",
-      "let.cc",
-      "let.h",
-      "load.cc",
-      "load.h",
-      "load_vector_element.cc",
-      "load_vector_element.h",
-      "location.h",
-      "loop.cc",
-      "loop.h",
-      "module.cc",
-      "module.h",
-      "multi_in_block.cc",
-      "multi_in_block.h",
-      "next_iteration.cc",
-      "next_iteration.h",
-      "operand_instruction.cc",
-      "operand_instruction.h",
-      "return.cc",
-      "return.h",
-      "store.cc",
-      "store.h",
-      "store_vector_element.cc",
-      "store_vector_element.h",
-      "switch.cc",
-      "switch.h",
-      "swizzle.cc",
-      "swizzle.h",
-      "terminate_invocation.cc",
-      "terminate_invocation.h",
-      "terminator.cc",
-      "terminator.h",
-      "unary.cc",
-      "unary.h",
-      "unreachable.cc",
-      "unreachable.h",
-      "user_call.cc",
-      "user_call.h",
-      "validator.cc",
-      "validator.h",
-      "value.cc",
-      "value.h",
-      "var.cc",
-      "var.h",
+      "access_test.cc",
+      "binary_test.cc",
+      "bitcast_test.cc",
+      "block_param_test.cc",
+      "block_test.cc",
+      "break_if_test.cc",
+      "constant_test.cc",
+      "construct_test.cc",
+      "continue_test.cc",
+      "convert_test.cc",
+      "core_builtin_call_test.cc",
+      "discard_test.cc",
+      "exit_if_test.cc",
+      "exit_loop_test.cc",
+      "exit_switch_test.cc",
+      "function_param_test.cc",
+      "function_test.cc",
+      "if_test.cc",
+      "instruction_result_test.cc",
+      "instruction_test.cc",
+      "ir_helper_test.h",
+      "let_test.cc",
+      "load_test.cc",
+      "load_vector_element_test.cc",
+      "loop_test.cc",
+      "module_test.cc",
+      "multi_in_block_test.cc",
+      "next_iteration_test.cc",
+      "operand_instruction_test.cc",
+      "return_test.cc",
+      "store_test.cc",
+      "store_vector_element_test.cc",
+      "switch_test.cc",
+      "swizzle_test.cc",
+      "unary_test.cc",
+      "user_call_test.cc",
+      "validator_test.cc",
+      "value_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/intrinsic",
+      "${tint_src_dir}/lang/core/intrinsic/data",
+      "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -150,76 +220,3 @@
     ]
   }
 }
-if (tint_build_unittests) {
-  if (tint_build_ir) {
-    tint_unittests_source_set("unittests") {
-      testonly = true
-      sources = [
-        "access_test.cc",
-        "binary_test.cc",
-        "bitcast_test.cc",
-        "block_param_test.cc",
-        "block_test.cc",
-        "break_if_test.cc",
-        "constant_test.cc",
-        "construct_test.cc",
-        "continue_test.cc",
-        "convert_test.cc",
-        "core_builtin_call_test.cc",
-        "discard_test.cc",
-        "exit_if_test.cc",
-        "exit_loop_test.cc",
-        "exit_switch_test.cc",
-        "function_param_test.cc",
-        "function_test.cc",
-        "if_test.cc",
-        "instruction_result_test.cc",
-        "instruction_test.cc",
-        "intrinsic_call_test.cc",
-        "ir_helper_test.h",
-        "let_test.cc",
-        "load_test.cc",
-        "load_vector_element_test.cc",
-        "loop_test.cc",
-        "module_test.cc",
-        "multi_in_block_test.cc",
-        "next_iteration_test.cc",
-        "operand_instruction_test.cc",
-        "return_test.cc",
-        "store_test.cc",
-        "store_vector_element_test.cc",
-        "switch_test.cc",
-        "swizzle_test.cc",
-        "unary_test.cc",
-        "user_call_test.cc",
-        "validator_test.cc",
-        "value_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/type",
-        "${tint_src_dir}/utils/containers",
-        "${tint_src_dir}/utils/diagnostic",
-        "${tint_src_dir}/utils/ice",
-        "${tint_src_dir}/utils/id",
-        "${tint_src_dir}/utils/macros",
-        "${tint_src_dir}/utils/math",
-        "${tint_src_dir}/utils/memory",
-        "${tint_src_dir}/utils/reflection",
-        "${tint_src_dir}/utils/result",
-        "${tint_src_dir}/utils/rtti",
-        "${tint_src_dir}/utils/symbol",
-        "${tint_src_dir}/utils/text",
-        "${tint_src_dir}/utils/traits",
-      ]
-
-      if (tint_build_ir) {
-        deps += [ "${tint_src_dir}/lang/core/ir" ]
-      }
-    }
-  }
-}
diff --git a/src/tint/lang/core/ir/access.h b/src/tint/lang/core/ir/access.h
index 1afdceb..4c76121 100644
--- a/src/tint/lang/core/ir/access.h
+++ b/src/tint/lang/core/ir/access.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_ACCESS_H_
 #define SRC_TINT_LANG_CORE_IR_ACCESS_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -47,7 +49,7 @@
     tint::Slice<Value*> Indices() { return operands_.Slice().Offset(kIndicesOperandOffset); }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "access"; }
+    std::string FriendlyName() override { return "access"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/binary.h b/src/tint/lang/core/ir/binary.h
index f70b1a4..53ddf1a 100644
--- a/src/tint/lang/core/ir/binary.h
+++ b/src/tint/lang/core/ir/binary.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_BINARY_H_
 #define SRC_TINT_LANG_CORE_IR_BINARY_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -70,7 +72,7 @@
     Value* RHS() { return operands_[kRhsOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "binary"; }
+    std::string FriendlyName() override { return "binary"; }
 
   private:
     enum Kind kind_;
diff --git a/src/tint/lang/core/ir/bitcast.h b/src/tint/lang/core/ir/bitcast.h
index 57355c7..d5f7425 100644
--- a/src/tint/lang/core/ir/bitcast.h
+++ b/src/tint/lang/core/ir/bitcast.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_BITCAST_H_
 #define SRC_TINT_LANG_CORE_IR_BITCAST_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -36,7 +38,7 @@
     Value* Val() { return operands_[kValueOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "bitcast"; }
+    std::string FriendlyName() override { return "bitcast"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/break_if.h b/src/tint/lang/core/ir/break_if.h
index 341bc70..1f73c2f 100644
--- a/src/tint/lang/core/ir/break_if.h
+++ b/src/tint/lang/core/ir/break_if.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_BREAK_IF_H_
 #define SRC_TINT_LANG_CORE_IR_BREAK_IF_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/terminator.h"
 #include "src/tint/lang/core/ir/value.h"
 #include "src/tint/utils/rtti/castable.h"
@@ -54,7 +56,7 @@
     ir::Loop* Loop() { return loop_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "break-if"; }
+    std::string FriendlyName() override { return "break_if"; }
 
   private:
     ir::Loop* loop_ = nullptr;
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 2cc934e..ffa0347 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -620,17 +620,28 @@
             InstructionResult(type), func, Values(std::forward<ARGS>(args)...)));
     }
 
+    /// Creates a core builtin call instruction
+    /// @param type the return type of the call
+    /// @param func the builtin function to call
+    /// @param args the call arguments
+    /// @returns the instruction
+    template <typename KLASS, typename FUNC, typename... ARGS>
+    tint::traits::EnableIf<tint::traits::IsTypeOrDerived<KLASS, ir::BuiltinCall>, KLASS*>
+    Call(const core::type::Type* type, FUNC func, ARGS&&... args) {
+        return Append(ir.instructions.Create<KLASS>(InstructionResult(type), func,
+                                                    Values(std::forward<ARGS>(args)...)));
+    }
+
     /// Creates an intrinsic call instruction
     /// @param type the return type of the call
     /// @param kind the intrinsic function to call
     /// @param args the call arguments
     /// @returns the intrinsic call instruction
-    template <typename... ARGS>
-    ir::IntrinsicCall* Call(const core::type::Type* type,
-                            enum IntrinsicCall::Kind kind,
-                            ARGS&&... args) {
-        return Append(ir.instructions.Create<ir::IntrinsicCall>(
-            InstructionResult(type), kind, Values(std::forward<ARGS>(args)...)));
+    template <typename KLASS, typename KIND, typename... ARGS>
+    tint::traits::EnableIf<tint::traits::IsTypeOrDerived<KLASS, ir::IntrinsicCall>, KLASS*>
+    Call(const core::type::Type* type, KIND kind, ARGS&&... args) {
+        return Append(ir.instructions.Create<KLASS>(InstructionResult(type), kind,
+                                                    Values(std::forward<ARGS>(args)...)));
     }
 
     /// Creates a value conversion instruction
diff --git a/src/tint/lang/core/ir/builtin_call.h b/src/tint/lang/core/ir/builtin_call.h
index bc9d17d..f152f7d 100644
--- a/src/tint/lang/core/ir/builtin_call.h
+++ b/src/tint/lang/core/ir/builtin_call.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_LANG_CORE_IR_BUILTIN_CALL_H_
 #define SRC_TINT_LANG_CORE_IR_BUILTIN_CALL_H_
 
+#include "src/tint/lang/core/intrinsic/table_data.h"
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -31,6 +32,15 @@
     /// @param args the conversion arguments
     explicit BuiltinCall(InstructionResult* result, VectorRef<Value*> args = tint::Empty);
     ~BuiltinCall() override;
+
+    /// @returns the identifier for the function
+    virtual size_t FuncId() = 0;
+
+    /// @returns the intrinsic name
+    virtual const char* IntrinsicName() = 0;
+
+    /// @returns the table data to validate this builtin
+    virtual const core::intrinsic::TableData& TableData() = 0;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/construct.h b/src/tint/lang/core/ir/construct.h
index e56fd48..61c9a76 100644
--- a/src/tint/lang/core/ir/construct.h
+++ b/src/tint/lang/core/ir/construct.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_CONSTRUCT_H_
 #define SRC_TINT_LANG_CORE_IR_CONSTRUCT_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -33,7 +35,7 @@
     ~Construct() override;
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "construct"; }
+    std::string FriendlyName() override { return "construct"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/continue.h b/src/tint/lang/core/ir/continue.h
index 0c8f8b1..1c393e1 100644
--- a/src/tint/lang/core/ir/continue.h
+++ b/src/tint/lang/core/ir/continue.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_CONTINUE_H_
 #define SRC_TINT_LANG_CORE_IR_CONTINUE_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/terminator.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -41,7 +43,7 @@
     ir::Loop* Loop() { return loop_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "continue"; }
+    std::string FriendlyName() override { return "continue"; }
 
   private:
     ir::Loop* loop_ = nullptr;
diff --git a/src/tint/lang/core/ir/convert.h b/src/tint/lang/core/ir/convert.h
index e9bfa44..1c58228 100644
--- a/src/tint/lang/core/ir/convert.h
+++ b/src/tint/lang/core/ir/convert.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_CONVERT_H_
 #define SRC_TINT_LANG_CORE_IR_CONVERT_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/lang/core/type/type.h"
 #include "src/tint/utils/rtti/castable.h"
@@ -34,7 +36,7 @@
     ~Convert() override;
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "convert"; }
+    std::string FriendlyName() override { return "convert"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/core_builtin_call.h b/src/tint/lang/core/ir/core_builtin_call.h
index b9e1f4b..bc1907b 100644
--- a/src/tint/lang/core/ir/core_builtin_call.h
+++ b/src/tint/lang/core/ir/core_builtin_call.h
@@ -15,7 +15,11 @@
 #ifndef SRC_TINT_LANG_CORE_IR_CORE_BUILTIN_CALL_H_
 #define SRC_TINT_LANG_CORE_IR_CORE_BUILTIN_CALL_H_
 
+#include <string>
+
 #include "src/tint/lang/core/function.h"
+#include "src/tint/lang/core/intrinsic/data/data.h"
+#include "src/tint/lang/core/intrinsic/table_data.h"
 #include "src/tint/lang/core/ir/builtin_call.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -36,8 +40,17 @@
     /// @returns the builtin function
     core::Function Func() { return func_; }
 
+    /// @returns the identifier for the function
+    size_t FuncId() override { return static_cast<size_t>(func_); }
+
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "core-builtin-call"; }
+    std::string FriendlyName() override { return core::str(func_); }
+
+    /// @returns the intrinsic name
+    const char* IntrinsicName() override { return core::str(func_); }
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() override { return core::intrinsic::data::kData; }
 
   private:
     core::Function func_;
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 9ec2d35..5a9d29c 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -423,9 +423,9 @@
         });
 }
 
-void Disassembler::EmitInstructionName(std::string_view name, Instruction* inst) {
+void Disassembler::EmitInstructionName(Instruction* inst) {
     SourceMarker sm(this);
-    out_ << name;
+    out_ << inst->FriendlyName();
     sm.Store(inst);
 }
 
@@ -445,74 +445,23 @@
         [&](Loop* l) { EmitLoop(l); },      //
         [&](Binary* b) { EmitBinary(b); },  //
         [&](Unary* u) { EmitUnary(u); },    //
-        [&](Bitcast* b) {
-            EmitValueWithType(b);
-            out_ << " = ";
-            EmitInstructionName("bitcast", b);
-            out_ << " ";
-            EmitOperandList(b);
-        },
-        [&](Discard* d) { EmitInstructionName("discard", d); },
-        [&](CoreBuiltinCall* b) {
-            EmitValueWithType(b);
-            out_ << " = ";
-            EmitInstructionName(core::str(b->Func()), b);
-            out_ << " ";
-            EmitOperandList(b);
-        },
-        [&](Construct* c) {
-            EmitValueWithType(c);
-            out_ << " = ";
-            EmitInstructionName("construct", c);
-            if (!c->Operands().IsEmpty()) {
-                out_ << " ";
-                EmitOperandList(c);
-            }
-        },
-        [&](Convert* c) {
-            EmitValueWithType(c);
-            out_ << " = ";
-            EmitInstructionName("convert", c);
-            out_ << " ";
-            EmitOperandList(c);
-        },
-        [&](IntrinsicCall* i) {
-            EmitValueWithType(i);
-            out_ << " = ";
-            EmitInstructionName(tint::ToString(i->Kind()), i);
-            out_ << " ";
-            EmitOperandList(i);
-        },
-        [&](Load* l) {
-            EmitValueWithType(l);
-            out_ << " = ";
-            EmitInstructionName("load", l);
-            out_ << " ";
-            EmitValue(l->From());
-        },
+        [&](Discard* d) { EmitInstructionName(d); },
         [&](Store* s) {
-            EmitInstructionName("store", s);
+            EmitInstructionName(s);
             out_ << " ";
             EmitValue(s->To());
             out_ << ", ";
             EmitValue(s->From());
         },
-        [&](LoadVectorElement* l) {
-            EmitValueWithType(l);
-            out_ << " = ";
-            EmitInstructionName("load_vector_element", l);
-            out_ << " ";
-            EmitOperandList(l);
-        },
         [&](StoreVectorElement* s) {
-            EmitInstructionName("store_vector_element", s);
+            EmitInstructionName(s);
             out_ << " ";
             EmitOperandList(s);
         },
         [&](UserCall* uc) {
             EmitValueWithType(uc);
             out_ << " = ";
-            EmitInstructionName("call", uc);
+            EmitInstructionName(uc);
             out_ << " %" << IdOf(uc->Func());
             if (!uc->Args().IsEmpty()) {
                 out_ << ", ";
@@ -522,7 +471,7 @@
         [&](Var* v) {
             EmitValueWithType(v);
             out_ << " = ";
-            EmitInstructionName("var", v);
+            EmitInstructionName(v);
             if (v->Initializer()) {
                 out_ << ", ";
                 EmitOperand(v, Var::kInitializerOperandOffset);
@@ -532,24 +481,10 @@
                 EmitBindingPoint(v->BindingPoint().value());
             }
         },
-        [&](Let* l) {
-            EmitValueWithType(l);
-            out_ << " = ";
-            EmitInstructionName("let", l);
-            out_ << " ";
-            EmitOperandList(l);
-        },
-        [&](Access* a) {
-            EmitValueWithType(a);
-            out_ << " = ";
-            EmitInstructionName("access", a);
-            out_ << " ";
-            EmitOperandList(a);
-        },
         [&](Swizzle* s) {
             EmitValueWithType(s);
             out_ << " = ";
-            EmitInstructionName("swizzle", s);
+            EmitInstructionName(s);
             out_ << " ";
             EmitValue(s->Object());
             out_ << ", ";
@@ -571,7 +506,15 @@
             }
         },
         [&](Terminator* b) { EmitTerminator(b); },
-        [&](Default) { out_ << "Unknown instruction: " << inst->TypeInfo().name; });
+        [&](Default) {
+            EmitValueWithType(inst);
+            out_ << " = ";
+            EmitInstructionName(inst);
+            if (!inst->Operands().IsEmpty()) {
+                out_ << " ";
+                EmitOperandList(inst);
+            }
+        });
 
     {  // Add a comment if the result IDs don't match their names
         Vector<std::string, 4> names;
diff --git a/src/tint/lang/core/ir/disassembler.h b/src/tint/lang/core/ir/disassembler.h
index e29a857..bb0bd44 100644
--- a/src/tint/lang/core/ir/disassembler.h
+++ b/src/tint/lang/core/ir/disassembler.h
@@ -144,7 +144,7 @@
     void EmitLine();
     void EmitOperand(Instruction* inst, size_t index);
     void EmitOperandList(Instruction* inst, size_t start_index = 0);
-    void EmitInstructionName(std::string_view name, Instruction* inst);
+    void EmitInstructionName(Instruction* inst);
 
     Module& mod_;
     StringStream out_;
diff --git a/src/tint/lang/core/ir/discard.h b/src/tint/lang/core/ir/discard.h
index 2fb03e0..b98c753 100644
--- a/src/tint/lang/core/ir/discard.h
+++ b/src/tint/lang/core/ir/discard.h
@@ -15,8 +15,9 @@
 #ifndef SRC_TINT_LANG_CORE_IR_DISCARD_H_
 #define SRC_TINT_LANG_CORE_IR_DISCARD_H_
 
-#include "src/tint/lang/core/ir/call.h"
+#include <string>
 
+#include "src/tint/lang/core/ir/call.h"
 #include "src/tint/utils/rtti/castable.h"
 
 namespace tint::core::ir {
@@ -29,7 +30,7 @@
     ~Discard() override;
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "discard"; }
+    std::string FriendlyName() override { return "discard"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_if.h b/src/tint/lang/core/ir/exit_if.h
index 24f97b3..31e6d55 100644
--- a/src/tint/lang/core/ir/exit_if.h
+++ b/src/tint/lang/core/ir/exit_if.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_EXIT_IF_H_
 #define SRC_TINT_LANG_CORE_IR_EXIT_IF_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/exit.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -45,7 +47,7 @@
     ir::If* If();
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "exit-if"; }
+    std::string FriendlyName() override { return "exit_if"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_loop.h b/src/tint/lang/core/ir/exit_loop.h
index 5212262..6e173df 100644
--- a/src/tint/lang/core/ir/exit_loop.h
+++ b/src/tint/lang/core/ir/exit_loop.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_EXIT_LOOP_H_
 #define SRC_TINT_LANG_CORE_IR_EXIT_LOOP_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/exit.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -45,7 +47,7 @@
     ir::Loop* Loop();
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "exit-loop"; }
+    std::string FriendlyName() override { return "exit_loop"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_switch.h b/src/tint/lang/core/ir/exit_switch.h
index 80e2a54..45a2733 100644
--- a/src/tint/lang/core/ir/exit_switch.h
+++ b/src/tint/lang/core/ir/exit_switch.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_EXIT_SWITCH_H_
 #define SRC_TINT_LANG_CORE_IR_EXIT_SWITCH_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/exit.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -45,7 +47,7 @@
     ir::Switch* Switch();
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "exit-switch"; }
+    std::string FriendlyName() override { return "exit_switch"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/if.h b/src/tint/lang/core/ir/if.h
index 1c852df..aa68508 100644
--- a/src/tint/lang/core/ir/if.h
+++ b/src/tint/lang/core/ir/if.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_IF_H_
 #define SRC_TINT_LANG_CORE_IR_IF_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/control_instruction.h"
 
 // Forward declarations
@@ -65,7 +67,7 @@
     ir::Block* False() { return false_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "if"; }
+    std::string FriendlyName() override { return "if"; }
 
   private:
     ir::Block* true_ = nullptr;
diff --git a/src/tint/lang/core/ir/instruction.h b/src/tint/lang/core/ir/instruction.h
index 192b4d7..d2503d2 100644
--- a/src/tint/lang/core/ir/instruction.h
+++ b/src/tint/lang/core/ir/instruction.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_INSTRUCTION_H_
 #define SRC_TINT_LANG_CORE_IR_INSTRUCTION_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/instruction_result.h"
 #include "src/tint/lang/core/ir/value.h"
 #include "src/tint/utils/containers/enum_set.h"
@@ -58,7 +60,7 @@
     virtual void Destroy();
 
     /// @returns the friendly name for the instruction
-    virtual std::string_view FriendlyName() = 0;
+    virtual std::string FriendlyName() = 0;
 
     /// @returns true if the Instruction has not been destroyed with Destroy()
     bool Alive() const { return !flags_.Contains(Flag::kDead); }
diff --git a/src/tint/lang/core/ir/intrinsic_call.cc b/src/tint/lang/core/ir/intrinsic_call.cc
index c95227e..a31ab1d 100644
--- a/src/tint/lang/core/ir/intrinsic_call.cc
+++ b/src/tint/lang/core/ir/intrinsic_call.cc
@@ -20,82 +20,11 @@
 
 namespace tint::core::ir {
 
-IntrinsicCall::IntrinsicCall(InstructionResult* result, enum Kind kind, VectorRef<Value*> arguments)
-    : kind_(kind) {
+IntrinsicCall::IntrinsicCall(InstructionResult* result, VectorRef<Value*> arguments) {
     AddOperands(IntrinsicCall::kArgsOperandOffset, std::move(arguments));
     AddResult(result);
 }
 
 IntrinsicCall::~IntrinsicCall() = default;
 
-std::string_view ToString(enum IntrinsicCall::Kind kind) {
-    switch (kind) {
-        case IntrinsicCall::Kind::kSpirvArrayLength:
-            return "spirv.array_length";
-        case IntrinsicCall::Kind::kSpirvAtomicIAdd:
-            return "spirv.atomic_iadd";
-        case IntrinsicCall::Kind::kSpirvAtomicISub:
-            return "spirv.atomic_isub";
-        case IntrinsicCall::Kind::kSpirvAtomicAnd:
-            return "spirv.atomic_and";
-        case IntrinsicCall::Kind::kSpirvAtomicCompareExchange:
-            return "spirv.atomic_compare_exchange";
-        case IntrinsicCall::Kind::kSpirvAtomicExchange:
-            return "spirv.atomic_exchange";
-        case IntrinsicCall::Kind::kSpirvAtomicLoad:
-            return "spirv.atomic_load";
-        case IntrinsicCall::Kind::kSpirvAtomicOr:
-            return "spirv.atomic_or";
-        case IntrinsicCall::Kind::kSpirvAtomicSMax:
-            return "spirv.atomic_smax";
-        case IntrinsicCall::Kind::kSpirvAtomicSMin:
-            return "spirv.atomic_smin";
-        case IntrinsicCall::Kind::kSpirvAtomicStore:
-            return "spirv.atomic_store";
-        case IntrinsicCall::Kind::kSpirvAtomicUMax:
-            return "spirv.atomic_umax";
-        case IntrinsicCall::Kind::kSpirvAtomicUMin:
-            return "spirv.atomic_umin";
-        case IntrinsicCall::Kind::kSpirvAtomicXor:
-            return "spirv.atomic_xor";
-        case IntrinsicCall::Kind::kSpirvDot:
-            return "spirv.dot";
-        case IntrinsicCall::Kind::kSpirvImageFetch:
-            return "spirv.image_fetch";
-        case IntrinsicCall::Kind::kSpirvImageGather:
-            return "spirv.image_gather";
-        case IntrinsicCall::Kind::kSpirvImageDrefGather:
-            return "spirv.image_dref_gather";
-        case IntrinsicCall::Kind::kSpirvImageQuerySize:
-            return "spirv.image_query_size";
-        case IntrinsicCall::Kind::kSpirvImageQuerySizeLod:
-            return "spirv.image_query_size_lod";
-        case IntrinsicCall::Kind::kSpirvImageSampleImplicitLod:
-            return "spirv.image_sample_implicit_lod";
-        case IntrinsicCall::Kind::kSpirvImageSampleExplicitLod:
-            return "spirv.image_sample_explicit_lod";
-        case IntrinsicCall::Kind::kSpirvImageSampleDrefImplicitLod:
-            return "spirv.image_sample_dref_implicit_lod";
-        case IntrinsicCall::Kind::kSpirvImageSampleDrefExplicitLod:
-            return "spirv.image_sample_dref_implicit_lod";
-        case IntrinsicCall::Kind::kSpirvImageWrite:
-            return "spirv.image_write";
-        case IntrinsicCall::Kind::kSpirvMatrixTimesMatrix:
-            return "spirv.matrix_times_matrix";
-        case IntrinsicCall::Kind::kSpirvMatrixTimesScalar:
-            return "spirv.matrix_times_scalar";
-        case IntrinsicCall::Kind::kSpirvMatrixTimesVector:
-            return "spirv.matrix_times_vector";
-        case IntrinsicCall::Kind::kSpirvSampledImage:
-            return "spirv.sampled_image";
-        case IntrinsicCall::Kind::kSpirvSelect:
-            return "spirv.select";
-        case IntrinsicCall::Kind::kSpirvVectorTimesScalar:
-            return "spirv.vector_times_scalar";
-        case IntrinsicCall::Kind::kSpirvVectorTimesMatrix:
-            return "spirv.vector_times_matrix";
-    }
-    return "<unknown>";
-}
-
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/intrinsic_call.h b/src/tint/lang/core/ir/intrinsic_call.h
index 3c9b0a9..175d902 100644
--- a/src/tint/lang/core/ir/intrinsic_call.h
+++ b/src/tint/lang/core/ir/intrinsic_call.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_INTRINSIC_CALL_H_
 #define SRC_TINT_LANG_CORE_IR_INTRINSIC_CALL_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -26,70 +28,13 @@
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
 
-    /// The kind of instruction.
-    enum class Kind {
-        // SPIR-V backend intrinsics.
-        kSpirvArrayLength,
-        kSpirvAtomicAnd,
-        kSpirvAtomicCompareExchange,
-        kSpirvAtomicExchange,
-        kSpirvAtomicIAdd,
-        kSpirvAtomicISub,
-        kSpirvAtomicLoad,
-        kSpirvAtomicOr,
-        kSpirvAtomicSMax,
-        kSpirvAtomicSMin,
-        kSpirvAtomicStore,
-        kSpirvAtomicUMax,
-        kSpirvAtomicUMin,
-        kSpirvAtomicXor,
-        kSpirvDot,
-        kSpirvImageFetch,
-        kSpirvImageGather,
-        kSpirvImageDrefGather,
-        kSpirvImageQuerySize,
-        kSpirvImageQuerySizeLod,
-        kSpirvImageSampleImplicitLod,
-        kSpirvImageSampleExplicitLod,
-        kSpirvImageSampleDrefImplicitLod,
-        kSpirvImageSampleDrefExplicitLod,
-        kSpirvImageWrite,
-        kSpirvMatrixTimesMatrix,
-        kSpirvMatrixTimesScalar,
-        kSpirvMatrixTimesVector,
-        kSpirvSampledImage,
-        kSpirvSelect,
-        kSpirvVectorTimesMatrix,
-        kSpirvVectorTimesScalar,
-    };
-
     /// Constructor
     /// @param result the result value
-    /// @param kind the intrinsic kind
     /// @param args the intrinsic call arguments
-    IntrinsicCall(InstructionResult* result, enum Kind kind, VectorRef<Value*> args = tint::Empty);
+    explicit IntrinsicCall(InstructionResult* result, VectorRef<Value*> args = tint::Empty);
     ~IntrinsicCall() override;
-
-    /// @returns the builtin function
-    enum Kind Kind() { return kind_; }
-
-    /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "intrinsic-call"; }
-
-  private:
-    enum Kind kind_;
 };
 
-/// @param kind the enum value
-/// @returns the string for the given enum value
-std::string_view ToString(enum IntrinsicCall::Kind kind);
-
-/// Emits the name of the intrinsic type.
-template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, enum IntrinsicCall::Kind kind) {
-    return out << ToString(kind);
-}
-
 }  // namespace tint::core::ir
 
 #endif  // SRC_TINT_LANG_CORE_IR_INTRINSIC_CALL_H_
diff --git a/src/tint/lang/core/ir/intrinsic_call_test.cc b/src/tint/lang/core/ir/intrinsic_call_test.cc
deleted file mode 100644
index 9753b2f..0000000
--- a/src/tint/lang/core/ir/intrinsic_call_test.cc
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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 "gmock/gmock.h"
-#include "gtest/gtest-spi.h"
-#include "src/tint/lang/core/ir/ir_helper_test.h"
-
-namespace tint::core::ir {
-namespace {
-
-using namespace tint::core::number_suffixes;  // NOLINT
-using IR_IntrinsicCallTest = IRTestHelper;
-
-TEST_F(IR_IntrinsicCallTest, Usage) {
-    auto* arg1 = b.Constant(1_u);
-    auto* arg2 = b.Constant(2_u);
-    auto* intrinsic = b.Call(mod.Types().f32(), IntrinsicCall::Kind::kSpirvSelect, arg1, arg2);
-
-    ASSERT_TRUE(intrinsic->Is<IntrinsicCall>());
-    EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{intrinsic, 0u}));
-    EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{intrinsic, 1u}));
-}
-
-TEST_F(IR_IntrinsicCallTest, Result) {
-    auto* arg1 = b.Constant(1_u);
-    auto* arg2 = b.Constant(2_u);
-    auto* intrinsic = b.Call(mod.Types().f32(), IntrinsicCall::Kind::kSpirvSelect, arg1, arg2);
-
-    ASSERT_TRUE(intrinsic->Is<IntrinsicCall>());
-    EXPECT_TRUE(intrinsic->HasResults());
-    EXPECT_FALSE(intrinsic->HasMultiResults());
-    EXPECT_TRUE(intrinsic->Result()->Is<InstructionResult>());
-    EXPECT_EQ(intrinsic->Result()->Source(), intrinsic);
-}
-
-TEST_F(IR_IntrinsicCallTest, Fail_NullType) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Call(nullptr, IntrinsicCall::Kind::kSpirvSelect);
-        },
-        "");
-}
-
-}  // namespace
-}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/let.h b/src/tint/lang/core/ir/let.h
index 51fd71b..5c0355b 100644
--- a/src/tint/lang/core/ir/let.h
+++ b/src/tint/lang/core/ir/let.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_LET_H_
 #define SRC_TINT_LANG_CORE_IR_LET_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 
 namespace tint::core::ir {
@@ -35,7 +37,7 @@
     ir::Value* Value() { return operands_[kValueOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "let"; }
+    std::string FriendlyName() override { return "let"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load.h b/src/tint/lang/core/ir/load.h
index ec969d1..55fb983 100644
--- a/src/tint/lang/core/ir/load.h
+++ b/src/tint/lang/core/ir/load.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_LOAD_H_
 #define SRC_TINT_LANG_CORE_IR_LOAD_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -37,7 +39,7 @@
     Value* From() { return operands_[kFromOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "load"; }
+    std::string FriendlyName() override { return "load"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load_vector_element.h b/src/tint/lang/core/ir/load_vector_element.h
index f66018e..cd7e206 100644
--- a/src/tint/lang/core/ir/load_vector_element.h
+++ b/src/tint/lang/core/ir/load_vector_element.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_LOAD_VECTOR_ELEMENT_H_
 #define SRC_TINT_LANG_CORE_IR_LOAD_VECTOR_ELEMENT_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -43,7 +45,7 @@
     ir::Value* Index() { return operands_[kIndexOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "load-vector-element"; }
+    std::string FriendlyName() override { return "load_vector_element"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/loop.h b/src/tint/lang/core/ir/loop.h
index 79547b6..689b9e2 100644
--- a/src/tint/lang/core/ir/loop.h
+++ b/src/tint/lang/core/ir/loop.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_LOOP_H_
 #define SRC_TINT_LANG_CORE_IR_LOOP_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/control_instruction.h"
 
 // Forward declarations
@@ -80,7 +82,7 @@
     ir::MultiInBlock* Continuing() { return continuing_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "loop"; }
+    std::string FriendlyName() override { return "loop"; }
 
   private:
     ir::Block* initializer_ = nullptr;
diff --git a/src/tint/lang/core/ir/next_iteration.h b/src/tint/lang/core/ir/next_iteration.h
index 2489232..87de579 100644
--- a/src/tint/lang/core/ir/next_iteration.h
+++ b/src/tint/lang/core/ir/next_iteration.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_NEXT_ITERATION_H_
 #define SRC_TINT_LANG_CORE_IR_NEXT_ITERATION_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/terminator.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -41,7 +43,7 @@
     ir::Loop* Loop() { return loop_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "next-iteration"; }
+    std::string FriendlyName() override { return "next_iteration"; }
 
   private:
     ir::Loop* loop_ = nullptr;
diff --git a/src/tint/lang/core/ir/return.cc b/src/tint/lang/core/ir/return.cc
index aac03dc..ab6e841 100644
--- a/src/tint/lang/core/ir/return.cc
+++ b/src/tint/lang/core/ir/return.cc
@@ -33,4 +33,8 @@
 
 Return::~Return() = default;
 
+Function* Return::Func() const {
+    return tint::As<Function>(operands_[kFunctionOperandOffset]);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/return.h b/src/tint/lang/core/ir/return.h
index 883f427..f5b8c8a 100644
--- a/src/tint/lang/core/ir/return.h
+++ b/src/tint/lang/core/ir/return.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_RETURN_H_
 #define SRC_TINT_LANG_CORE_IR_RETURN_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/terminator.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -46,7 +48,7 @@
     ~Return() override;
 
     /// @returns the function being returned
-    Function* Func() { return tint::As<Function>(operands_[kFunctionOperandOffset]); }
+    Function* Func() const;
 
     /// @returns the return value, or nullptr
     ir::Value* Value() const {
@@ -63,7 +65,7 @@
     }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "return"; }
+    std::string FriendlyName() override { return "return"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store.h b/src/tint/lang/core/ir/store.h
index 7da1d7e..924a6b8 100644
--- a/src/tint/lang/core/ir/store.h
+++ b/src/tint/lang/core/ir/store.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_STORE_H_
 #define SRC_TINT_LANG_CORE_IR_STORE_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -42,7 +44,7 @@
     Value* From() { return operands_[kFromOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "store"; }
+    std::string FriendlyName() override { return "store"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store_vector_element.h b/src/tint/lang/core/ir/store_vector_element.h
index 7e8f4fc..a4b31a8 100644
--- a/src/tint/lang/core/ir/store_vector_element.h
+++ b/src/tint/lang/core/ir/store_vector_element.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_STORE_VECTOR_ELEMENT_H_
 #define SRC_TINT_LANG_CORE_IR_STORE_VECTOR_ELEMENT_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -49,7 +51,7 @@
     ir::Value* Value() { return operands_[kValueOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "store-vector-element"; }
+    std::string FriendlyName() override { return "store_vector_element"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/switch.h b/src/tint/lang/core/ir/switch.h
index 120c860..1b450ac 100644
--- a/src/tint/lang/core/ir/switch.h
+++ b/src/tint/lang/core/ir/switch.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_SWITCH_H_
 #define SRC_TINT_LANG_CORE_IR_SWITCH_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/control_instruction.h"
 
 // Forward declarations
@@ -81,7 +83,7 @@
     Value* Condition() { return operands_[kConditionOperandOffset]; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "switch"; }
+    std::string FriendlyName() override { return "switch"; }
 
   private:
     Vector<Case, 4> cases_;
diff --git a/src/tint/lang/core/ir/swizzle.h b/src/tint/lang/core/ir/swizzle.h
index 6632515..48a838e 100644
--- a/src/tint/lang/core/ir/swizzle.h
+++ b/src/tint/lang/core/ir/swizzle.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_SWIZZLE_H_
 #define SRC_TINT_LANG_CORE_IR_SWIZZLE_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -40,7 +42,7 @@
     VectorRef<uint32_t> Indices() { return indices_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "swizzle"; }
+    std::string FriendlyName() override { return "swizzle"; }
 
   private:
     Vector<uint32_t, 4> indices_;
diff --git a/src/tint/lang/core/ir/terminate_invocation.h b/src/tint/lang/core/ir/terminate_invocation.h
index 60fa964..8edf03e 100644
--- a/src/tint/lang/core/ir/terminate_invocation.h
+++ b/src/tint/lang/core/ir/terminate_invocation.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_TERMINATE_INVOCATION_H_
 #define SRC_TINT_LANG_CORE_IR_TERMINATE_INVOCATION_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/terminator.h"
 
 namespace tint::core::ir {
@@ -25,7 +27,7 @@
     ~TerminateInvocation() override;
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "terminate-invocation"; }
+    std::string FriendlyName() override { return "terminate_invocation"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
new file mode 100644
index 0000000..7216985
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -0,0 +1,124 @@
+# 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 = "transform",
+  srcs = [
+    "add_empty_entry_point.cc",
+    "bgra8unorm_polyfill.cc",
+    "binary_polyfill.cc",
+    "binding_remapper.cc",
+    "block_decorated_structs.cc",
+    "builtin_polyfill.cc",
+    "demote_to_helper.cc",
+    "multiplanar_external_texture.cc",
+    "robustness.cc",
+    "shader_io.cc",
+    "std140.cc",
+  ],
+  hdrs = [
+    "add_empty_entry_point.h",
+    "bgra8unorm_polyfill.h",
+    "binary_polyfill.h",
+    "binding_remapper.h",
+    "block_decorated_structs.h",
+    "builtin_polyfill.h",
+    "demote_to_helper.h",
+    "multiplanar_external_texture.h",
+    "robustness.h",
+    "shader_io.h",
+    "std140.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "add_empty_entry_point_test.cc",
+    "bgra8unorm_polyfill_test.cc",
+    "binary_polyfill_test.cc",
+    "binding_remapper_test.cc",
+    "block_decorated_structs_test.cc",
+    "builtin_polyfill_test.cc",
+    "demote_to_helper_test.cc",
+    "helper_test.h",
+    "multiplanar_external_texture_test.cc",
+    "robustness_test.cc",
+    "std140_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir/transform",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/core/ir/transform/BUILD.cfg b/src/tint/lang/core/ir/transform/BUILD.cfg
deleted file mode 100644
index 0ca3ed3..0000000
--- a/src/tint/lang/core/ir/transform/BUILD.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "condition": "tint_build_ir"
-}
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 6bff32a..f474b48 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -21,11 +21,9 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_core_ir_transform
 # Kind:      lib
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_core_ir_transform lib
   lang/core/ir/transform/add_empty_entry_point.cc
@@ -44,6 +42,8 @@
   lang/core/ir/transform/demote_to_helper.h
   lang/core/ir/transform/multiplanar_external_texture.cc
   lang/core/ir/transform/multiplanar_external_texture.h
+  lang/core/ir/transform/robustness.cc
+  lang/core/ir/transform/robustness.h
   lang/core/ir/transform/shader_io.cc
   lang/core/ir/transform/shader_io.h
   lang/core/ir/transform/std140.cc
@@ -55,6 +55,9 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
   tint_utils_containers
   tint_utils_diagnostic
@@ -71,18 +74,9 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_core_ir_transform lib
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_core_ir_transform_test
 # Kind:      test
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_core_ir_transform_test test
   lang/core/ir/transform/add_empty_entry_point_test.cc
@@ -94,6 +88,7 @@
   lang/core/ir/transform/demote_to_helper_test.cc
   lang/core/ir/transform/helper_test.h
   lang/core/ir/transform/multiplanar_external_texture_test.cc
+  lang/core/ir/transform/robustness_test.cc
   lang/core/ir/transform/std140_test.cc
 )
 
@@ -102,6 +97,10 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
+  tint_lang_core_ir_transform
   tint_lang_core_type
   tint_utils_containers
   tint_utils_diagnostic
@@ -121,12 +120,3 @@
 tint_target_add_external_dependencies(tint_lang_core_ir_transform_test test
   "gtest"
 )
-
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_core_ir_transform_test test
-    tint_lang_core_ir
-    tint_lang_core_ir_transform
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
\ No newline at end of file
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index b9a27f8..82e462b 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -28,35 +28,82 @@
 if (tint_build_unittests) {
   import("//testing/test.gni")
 }
-if (tint_build_ir) {
-  libtint_source_set("transform") {
+
+libtint_source_set("transform") {
+  sources = [
+    "add_empty_entry_point.cc",
+    "add_empty_entry_point.h",
+    "bgra8unorm_polyfill.cc",
+    "bgra8unorm_polyfill.h",
+    "binary_polyfill.cc",
+    "binary_polyfill.h",
+    "binding_remapper.cc",
+    "binding_remapper.h",
+    "block_decorated_structs.cc",
+    "block_decorated_structs.h",
+    "builtin_polyfill.cc",
+    "builtin_polyfill.h",
+    "demote_to_helper.cc",
+    "demote_to_helper.h",
+    "multiplanar_external_texture.cc",
+    "multiplanar_external_texture.h",
+    "robustness.cc",
+    "robustness.h",
+    "shader_io.cc",
+    "shader_io.h",
+    "std140.cc",
+    "std140.h",
+  ]
+  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",
+    "${tint_src_dir}/lang/core/intrinsic/data",
+    "${tint_src_dir}/lang/core/ir",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
+if (tint_build_unittests) {
+  tint_unittests_source_set("unittests") {
+    testonly = true
     sources = [
-      "add_empty_entry_point.cc",
-      "add_empty_entry_point.h",
-      "bgra8unorm_polyfill.cc",
-      "bgra8unorm_polyfill.h",
-      "binary_polyfill.cc",
-      "binary_polyfill.h",
-      "binding_remapper.cc",
-      "binding_remapper.h",
-      "block_decorated_structs.cc",
-      "block_decorated_structs.h",
-      "builtin_polyfill.cc",
-      "builtin_polyfill.h",
-      "demote_to_helper.cc",
-      "demote_to_helper.h",
-      "multiplanar_external_texture.cc",
-      "multiplanar_external_texture.h",
-      "shader_io.cc",
-      "shader_io.h",
-      "std140.cc",
-      "std140.h",
+      "add_empty_entry_point_test.cc",
+      "bgra8unorm_polyfill_test.cc",
+      "binary_polyfill_test.cc",
+      "binding_remapper_test.cc",
+      "block_decorated_structs_test.cc",
+      "builtin_polyfill_test.cc",
+      "demote_to_helper_test.cc",
+      "helper_test.h",
+      "multiplanar_external_texture_test.cc",
+      "robustness_test.cc",
+      "std140_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/intrinsic",
+      "${tint_src_dir}/lang/core/intrinsic/data",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/ir/transform",
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -72,56 +119,5 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
-
-    if (tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/core/ir" ]
-    }
-  }
-}
-if (tint_build_unittests) {
-  if (tint_build_ir) {
-    tint_unittests_source_set("unittests") {
-      testonly = true
-      sources = [
-        "add_empty_entry_point_test.cc",
-        "bgra8unorm_polyfill_test.cc",
-        "binary_polyfill_test.cc",
-        "binding_remapper_test.cc",
-        "block_decorated_structs_test.cc",
-        "builtin_polyfill_test.cc",
-        "demote_to_helper_test.cc",
-        "helper_test.h",
-        "multiplanar_external_texture_test.cc",
-        "std140_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/type",
-        "${tint_src_dir}/utils/containers",
-        "${tint_src_dir}/utils/diagnostic",
-        "${tint_src_dir}/utils/ice",
-        "${tint_src_dir}/utils/id",
-        "${tint_src_dir}/utils/macros",
-        "${tint_src_dir}/utils/math",
-        "${tint_src_dir}/utils/memory",
-        "${tint_src_dir}/utils/reflection",
-        "${tint_src_dir}/utils/result",
-        "${tint_src_dir}/utils/rtti",
-        "${tint_src_dir}/utils/symbol",
-        "${tint_src_dir}/utils/text",
-        "${tint_src_dir}/utils/traits",
-      ]
-
-      if (tint_build_ir) {
-        deps += [
-          "${tint_src_dir}/lang/core/ir",
-          "${tint_src_dir}/lang/core/ir/transform",
-        ]
-      }
-    }
   }
 }
diff --git a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
index 1c414d7..d4fcbf2 100644
--- a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
@@ -149,7 +149,12 @@
                         swizzle->InsertBefore(call);
                         call->SetOperand(index, swizzle->Result());
                     } else if (call->Func() == core::Function::kTextureLoad) {
-                        TINT_ICE() << "unhandled bgra8unorm texture load";
+                        // Swizzle the result of a `textureLoad()` builtin.
+                        auto* swizzle =
+                            b.Swizzle(call->Result()->Type(), nullptr, Vector{2u, 1u, 0u, 3u});
+                        call->Result()->ReplaceAllUsesWith(swizzle->Result());
+                        swizzle->InsertAfter(call);
+                        swizzle->SetOperand(Swizzle::kObjectOperandOffset, call->Result());
                     }
                 },
                 [&](UserCall* call) {
diff --git a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill_test.cc b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill_test.cc
index a7974d9..e8d19a3 100644
--- a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill_test.cc
+++ b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill_test.cc
@@ -604,5 +604,117 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_Bgra8UnormPolyfillTest, TextureLoad) {
+    auto format = core::TexelFormat::kBgra8Unorm;
+    auto* texture_ty =
+        ty.Get<core::type::StorageTexture>(core::type::TextureDimension::k2d, format, read,
+                                           core::type::StorageTexture::SubtypeFor(format, ty));
+
+    auto* var = b.Var("texture", ty.ptr(handle, texture_ty));
+    var->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(var);
+
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+    func->SetParams({coords});
+    b.Append(func->Block(), [&] {
+        auto* load = b.Load(var->Result());
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, load, coords);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<bgra8unorm, read>, read_write> = var @binding_point(1, 2)
+}
+
+%foo = func(%coords:vec2<u32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<bgra8unorm, read> = load %texture
+    %result:vec4<f32> = textureLoad %4, %coords
+    ret %result
+  }
+}
+)";
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, read>, read_write> = var @binding_point(1, 2)
+}
+
+%foo = func(%coords:vec2<u32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<rgba8unorm, read> = load %texture
+    %result:vec4<f32> = textureLoad %4, %coords
+    %6:vec4<f32> = swizzle %result, zyxw
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(Bgra8UnormPolyfill);
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_Bgra8UnormPolyfillTest, TextureLoadAndStore) {
+    auto format = core::TexelFormat::kBgra8Unorm;
+    auto* texture_ty =
+        ty.Get<core::type::StorageTexture>(core::type::TextureDimension::k2d, format, read_write,
+                                           core::type::StorageTexture::SubtypeFor(format, ty));
+
+    auto* var = b.Var("texture", ty.ptr(handle, texture_ty));
+    var->SetBindingPoint(1, 2);
+    b.RootBlock()->Append(var);
+
+    auto* func = b.Function("foo", ty.void_());
+    auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+    func->SetParams({coords});
+    b.Append(func->Block(), [&] {
+        auto* load = b.Load(var->Result());
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, load, coords);
+        b.Call(ty.void_(), core::Function::kTextureStore, load, coords, result);
+        b.Return(func);
+        mod.SetName(result, "result");
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<bgra8unorm, read_write>, read_write> = var @binding_point(1, 2)
+}
+
+%foo = func(%coords:vec2<u32>):void -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<bgra8unorm, read_write> = load %texture
+    %result:vec4<f32> = textureLoad %4, %coords
+    %6:void = textureStore %4, %coords, %result
+    ret
+  }
+}
+)";
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, read_write>, read_write> = var @binding_point(1, 2)
+}
+
+%foo = func(%coords:vec2<u32>):void -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %result:vec4<f32> = textureLoad %4, %coords
+    %6:vec4<f32> = swizzle %result, zyxw
+    %7:vec4<f32> = swizzle %6, zyxw
+    %8:void = textureStore %4, %coords, %7
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    Run(Bgra8UnormPolyfill);
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::core::ir::transform
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 d7ebbb2..655c714 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
@@ -536,7 +536,8 @@
             b.Discard();
             b.ExitIf(ifelse);
         });
-        b.Call(ty.void_(), core::Function::kTextureStore, texture, coord, 0.5_f);
+        b.Call(ty.void_(), core::Function::kTextureStore, b.Load(texture), coord,
+               b.Splat(b.ir.Types().vec4<f32>(), 0.5_f, 4));
         b.Return(ep, 0.5_f);
     });
 
@@ -553,7 +554,8 @@
         exit_if  # if_1
       }
     }
-    %5:void = textureStore %texture, %coord, 0.5f
+    %5:texture_storage_2d<r32float, write> = load %texture
+    %6:void = textureStore %5, %coord, vec4<f32>(0.5f)
     ret 0.5f
   }
 }
@@ -574,16 +576,17 @@
         exit_if  # if_1
       }
     }
-    %6:bool = load %continue_execution
-    if %6 [t: %b4] {  # if_2
+    %6:texture_storage_2d<r32float, write> = load %texture
+    %7:bool = load %continue_execution
+    if %7 [t: %b4] {  # if_2
       %b4 = block {  # true
-        %7:void = textureStore %texture, %coord, 0.5f
+        %8:void = textureStore %6, %coord, vec4<f32>(0.5f)
         exit_if  # if_2
       }
     }
-    %8:bool = load %continue_execution
-    %9:bool = eq %8, false
-    if %9 [t: %b5] {  # if_3
+    %9:bool = load %continue_execution
+    %10:bool = eq %9, false
+    if %10 [t: %b5] {  # if_3
       %b5 = block {  # true
         terminate_invocation
       }
diff --git a/src/tint/lang/core/ir/transform/robustness.cc b/src/tint/lang/core/ir/transform/robustness.cc
new file mode 100644
index 0000000..6eef036
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/robustness.cc
@@ -0,0 +1,349 @@
+// 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/robustness.h"
+
+#include <algorithm>
+#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"
+#include "src/tint/lang/core/type/depth_texture.h"
+#include "src/tint/lang/core/type/sampled_texture.h"
+#include "src/tint/lang/core/type/texture.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 robustness config.
+    const RobustnessConfig& config;
+
+    /// The IR module.
+    Module* ir = nullptr;
+
+    /// The IR builder.
+    Builder b{*ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir->Types()};
+
+    /// Process the module.
+    void Process() {
+        // Find the access instructions that may need to be clamped.
+        Vector<ir::Access*, 64> accesses;
+        Vector<ir::LoadVectorElement*, 64> vector_loads;
+        Vector<ir::StoreVectorElement*, 64> vector_stores;
+        Vector<ir::CoreBuiltinCall*, 64> texture_calls;
+        for (auto* inst : ir->instructions.Objects()) {
+            if (inst->Alive()) {
+                tint::Switch(
+                    inst,  //
+                    [&](ir::Access* access) {
+                        // Check if accesses into this object should be clamped.
+                        auto* ptr = access->Object()->Type()->As<type::Pointer>();
+                        if (ptr) {
+                            if (ShouldClamp(ptr->AddressSpace())) {
+                                accesses.Push(access);
+                            }
+                        } else {
+                            if (config.clamp_value) {
+                                accesses.Push(access);
+                            }
+                        }
+                    },
+                    [&](ir::LoadVectorElement* lve) {
+                        // Check if loads from this address space should be clamped.
+                        auto* ptr = lve->From()->Type()->As<type::Pointer>();
+                        if (ShouldClamp(ptr->AddressSpace())) {
+                            vector_loads.Push(lve);
+                        }
+                    },
+                    [&](ir::StoreVectorElement* sve) {
+                        // Check if stores to this address space should be clamped.
+                        auto* ptr = sve->To()->Type()->As<type::Pointer>();
+                        if (ShouldClamp(ptr->AddressSpace())) {
+                            vector_stores.Push(sve);
+                        }
+                    },
+                    [&](ir::CoreBuiltinCall* call) {
+                        // Check if this is a texture builtin that needs to be clamped.
+                        if (config.clamp_texture) {
+                            if (call->Func() == core::Function::kTextureDimensions ||
+                                call->Func() == core::Function::kTextureLoad ||
+                                call->Func() == core::Function::kTextureStore) {
+                                texture_calls.Push(call);
+                            }
+                        }
+                    });
+            }
+        }
+
+        // Clamp access indices.
+        for (auto* access : accesses) {
+            b.InsertBefore(access, [&] {  //
+                ClampAccessIndices(access);
+            });
+        }
+
+        // Clamp load-vector-element instructions.
+        for (auto* lve : vector_loads) {
+            auto* vec = lve->From()->Type()->UnwrapPtr()->As<type::Vector>();
+            b.InsertBefore(lve, [&] {  //
+                ClampOperand(lve, LoadVectorElement::kIndexOperandOffset,
+                             b.Constant(u32(vec->Width() - 1u)));
+            });
+        }
+
+        // Clamp store-vector-element instructions.
+        for (auto* sve : vector_stores) {
+            auto* vec = sve->To()->Type()->UnwrapPtr()->As<type::Vector>();
+            b.InsertBefore(sve, [&] {  //
+                ClampOperand(sve, StoreVectorElement::kIndexOperandOffset,
+                             b.Constant(u32(vec->Width() - 1u)));
+            });
+        }
+
+        // Clamp indices and coordinates for texture builtins calls.
+        for (auto* call : texture_calls) {
+            b.InsertBefore(call, [&] {  //
+                ClampTextureCallArgs(call);
+            });
+        }
+
+        // TODO(jrprice): Handle config.bindings_ignored.
+        if (!config.bindings_ignored.empty()) {
+            TINT_UNIMPLEMENTED();
+        }
+    }
+
+    /// Check if clamping should be applied to a particular address space.
+    /// @param addrspace the address space to check
+    /// @returns true if pointer accesses in @p param addrspace should be clamped
+    bool ShouldClamp(AddressSpace addrspace) {
+        switch (addrspace) {
+            case AddressSpace::kFunction:
+                return config.clamp_function;
+            case AddressSpace::kPrivate:
+                return config.clamp_private;
+            case AddressSpace::kPushConstant:
+                return config.clamp_push_constant;
+            case AddressSpace::kStorage:
+                return config.clamp_storage;
+            case AddressSpace::kUniform:
+                return config.clamp_uniform;
+            case AddressSpace::kWorkgroup:
+                return config.clamp_workgroup;
+            case AddressSpace::kUndefined:
+            case AddressSpace::kPixelLocal:
+            case AddressSpace::kHandle:
+            case AddressSpace::kIn:
+            case AddressSpace::kOut:
+                return false;
+        }
+        return false;
+    }
+
+    /// Convert a value to a u32 if needed.
+    /// @param value the value to convert
+    /// @returns the converted value, or @p value if it is already a u32
+    ir::Value* CastToU32(ir::Value* value) {
+        if (value->Type()->is_unsigned_integer_scalar_or_vector()) {
+            return value;
+        }
+
+        const type::Type* type = ty.u32();
+        if (auto* vec = value->Type()->As<type::Vector>()) {
+            type = ty.vec(type, vec->Width());
+        }
+        return b.Convert(type, value)->Result();
+    }
+
+    /// Clamp operand @p op_idx of @p inst to ensure it is within @p limit.
+    /// @param inst the instruction
+    /// @param op_idx the index of the operand that should be clamped
+    /// @param limit the limit to clamp to
+    void ClampOperand(ir::Instruction* inst, size_t op_idx, ir::Value* limit) {
+        auto* idx = inst->Operands()[op_idx];
+        auto* const_idx = idx->As<ir::Constant>();
+        auto* const_limit = limit->As<ir::Constant>();
+
+        ir::Value* clamped_idx = nullptr;
+        if (const_idx && const_limit) {
+            // Generate a new constant index that is clamped to the limit.
+            clamped_idx = b.Constant(u32(std::min(const_idx->Value()->ValueAs<uint32_t>(),
+                                                  const_limit->Value()->ValueAs<uint32_t>())));
+        } else {
+            // Clamp it to the dynamic limit.
+            clamped_idx = b.Call(ty.u32(), core::Function::kMin, CastToU32(idx), limit)->Result();
+        }
+
+        // Replace the index operand with the clamped version.
+        inst->SetOperand(op_idx, clamped_idx);
+    }
+
+    /// Clamp the indices of an access instruction to ensure they are within the limits of the types
+    /// that they are indexing into.
+    /// @param access the access instruction
+    void ClampAccessIndices(ir::Access* access) {
+        auto* type = access->Object()->Type()->UnwrapPtr();
+        auto indices = access->Indices();
+        for (size_t i = 0; i < indices.Length(); i++) {
+            auto* idx = indices[i];
+            auto* const_idx = idx->As<ir::Constant>();
+
+            // Determine the limit of the type being indexed into.
+            auto limit = tint::Switch(
+                type,  //
+                [&](const type::Vector* vec) -> ir::Value* {
+                    return b.Constant(u32(vec->Width() - 1u));
+                },
+                [&](const type::Matrix* mat) -> ir::Value* {
+                    return b.Constant(u32(mat->columns() - 1u));
+                },
+                [&](const type::Array* arr) -> ir::Value* {
+                    if (arr->ConstantCount()) {
+                        return b.Constant(u32(arr->ConstantCount().value() - 1u));
+                    }
+                    TINT_ASSERT_OR_RETURN_VALUE(arr->Count()->Is<type::RuntimeArrayCount>(),
+                                                nullptr);
+
+                    // Skip clamping runtime-sized array indices if requested.
+                    if (config.disable_runtime_sized_array_index_clamping) {
+                        return nullptr;
+                    }
+
+                    auto* object = access->Object();
+                    if (i > 0) {
+                        // Generate a pointer to the runtime-sized array if it isn't the base of
+                        // this access instruction.
+                        auto* base_ptr = object->Type()->As<type::Pointer>();
+                        TINT_ASSERT_OR_RETURN_VALUE(base_ptr != nullptr, nullptr);
+                        TINT_ASSERT_OR_RETURN_VALUE(i == 1, nullptr);
+                        auto* arr_ptr = ty.ptr(base_ptr->AddressSpace(), arr, base_ptr->Access());
+                        object = b.Access(arr_ptr, object, indices[0])->Result();
+                    }
+
+                    // Use the `arrayLength` builtin to get the limit of a runtime-sized array.
+                    auto* length = b.Call(ty.u32(), core::Function::kArrayLength, object);
+                    return b.Subtract(ty.u32(), length, b.Constant(1_u))->Result();
+                });
+
+            // If there's a dynamic limit that needs enforced, clamp the index operand.
+            if (limit) {
+                ClampOperand(access, ir::Access::kIndicesOperandOffset + i, limit);
+            }
+
+            // Get the type that this index produces.
+            type = const_idx ? type->Element(const_idx->Value()->ValueAs<u32>())
+                             : type->Elements().type;
+        }
+    }
+
+    /// Clamp the indices and coordinates of a texture builtin call instruction to ensure they are
+    /// within the limits of the texture that they are accessing.
+    /// @param call the texture builtin call instruction
+    void ClampTextureCallArgs(ir::CoreBuiltinCall* call) {
+        const auto& args = call->Args();
+        auto* texture = args[0]->Type()->As<type::Texture>();
+
+        // Helper for clamping the level argument.
+        // Keep hold of the clamped value to use for clamping the coordinates.
+        Value* clamped_level = nullptr;
+        auto clamp_level = [&](uint32_t idx) {
+            auto* num_levels = b.Call(ty.u32(), core::Function::kTextureNumLevels, args[0]);
+            auto* limit = b.Subtract(ty.u32(), num_levels, 1_u);
+            clamped_level =
+                b.Call(ty.u32(), core::Function::kMin, CastToU32(args[idx]), limit)->Result();
+            call->SetOperand(CoreBuiltinCall::kArgsOperandOffset + idx, clamped_level);
+        };
+
+        // Helper for clamping the coordinates.
+        auto clamp_coords = [&](uint32_t idx) {
+            const type::Type* type = ty.u32();
+            auto* one = b.Constant(1_u);
+            if (auto* vec = args[idx]->Type()->As<type::Vector>()) {
+                type = ty.vec(type, vec->Width());
+                one = b.Splat(type, one, vec->Width());
+            }
+            auto* dims = clamped_level ? b.Call(type, core::Function::kTextureDimensions, args[0],
+                                                clamped_level)
+                                       : b.Call(type, core::Function::kTextureDimensions, args[0]);
+            auto* limit = b.Subtract(type, dims, one);
+            call->SetOperand(
+                CoreBuiltinCall::kArgsOperandOffset + idx,
+                b.Call(type, core::Function::kMin, CastToU32(args[idx]), limit)->Result());
+        };
+
+        // Helper for clamping the array index.
+        auto clamp_array_index = [&](uint32_t idx) {
+            auto* num_layers = b.Call(ty.u32(), core::Function::kTextureNumLayers, args[0]);
+            auto* limit = b.Subtract(ty.u32(), num_layers, 1_u);
+            call->SetOperand(
+                CoreBuiltinCall::kArgsOperandOffset + idx,
+                b.Call(ty.u32(), core::Function::kMin, CastToU32(args[idx]), limit)->Result());
+        };
+
+        // Select which arguments to clamp based on the function overload.
+        switch (call->Func()) {
+            case core::Function::kTextureDimensions: {
+                if (args.Length() > 1) {
+                    clamp_level(1u);
+                }
+                break;
+            }
+            case core::Function::kTextureLoad: {
+                clamp_coords(1u);
+                uint32_t next_arg = 2u;
+                if (type::IsTextureArray(texture->dim())) {
+                    clamp_array_index(next_arg++);
+                }
+                if (texture->IsAnyOf<type::SampledTexture, type::DepthTexture>()) {
+                    clamp_level(next_arg++);
+                }
+                break;
+            }
+            case core::Function::kTextureStore: {
+                clamp_coords(1u);
+                if (type::IsTextureArray(texture->dim())) {
+                    clamp_array_index(2u);
+                }
+                break;
+            }
+            default:
+                break;
+        }
+    }
+};
+
+}  // namespace
+
+Result<SuccessType, std::string> Robustness(Module* ir, const RobustnessConfig& config) {
+    auto result = ValidateAndDumpIfNeeded(*ir, "Robustness 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/robustness.h b/src/tint/lang/core/ir/transform/robustness.h
new file mode 100644
index 0000000..82d0452
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/robustness.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_CORE_IR_TRANSFORM_ROBUSTNESS_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_ROBUSTNESS_H_
+
+#include <string>
+#include <unordered_set>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// Configuration options that control when to clamp accesses.
+struct RobustnessConfig {
+    /// Should non-pointer accesses be clamped?
+    bool clamp_value = true;
+
+    /// Should texture accesses be clamped?
+    bool clamp_texture = true;
+
+    /// Should accesses to pointers with the 'function' address space be clamped?
+    bool clamp_function = true;
+    /// Should accesses to pointers with the 'private' address space be clamped?
+    bool clamp_private = true;
+    /// Should accesses to pointers with the 'push_constant' address space be clamped?
+    bool clamp_push_constant = true;
+    /// Should accesses to pointers with the 'storage' address space be clamped?
+    bool clamp_storage = true;
+    /// Should accesses to pointers with the 'uniform' address space be clamped?
+    bool clamp_uniform = true;
+    /// Should accesses to pointers with the 'workgroup' address space be clamped?
+    bool clamp_workgroup = true;
+
+    /// Bindings that should always be ignored.
+    std::unordered_set<tint::BindingPoint> bindings_ignored;
+
+    /// Should the transform skip index clamping on runtime-sized arrays?
+    bool disable_runtime_sized_array_index_clamping = false;
+};
+
+/// Robustness is a transform that prevents out-of-bounds memory accesses.
+/// @param module the module to transform
+/// @param config the robustness configuration
+/// @returns an error string on failure
+Result<SuccessType, std::string> Robustness(Module* module, const RobustnessConfig& config);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_ROBUSTNESS_H_
diff --git a/src/tint/lang/core/ir/transform/robustness_test.cc b/src/tint/lang/core/ir/transform/robustness_test.cc
new file mode 100644
index 0000000..e55a5de
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/robustness_test.cc
@@ -0,0 +1,3640 @@
+// 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/robustness.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/depth_multisampled_texture.h"
+#include "src/tint/lang/core/type/depth_texture.h"
+#include "src/tint/lang/core/type/external_texture.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/multisampled_texture.h"
+#include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/sampled_texture.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/core/type/struct.h"
+#include "src/tint/lang/core/type/vector.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using IR_RobustnessTest = TransformTestWithParam<bool>;
+
+////////////////////////////////////////////////////////////////
+// These tests use the function address space.
+// Test clamping of vectors, matrices, and fixed-size arrays.
+// Test indices that are const, const-via-let, and dynamic.
+// Test signed vs unsigned indices.
+////////////////////////////////////////////////////////////////
+
+TEST_P(IR_RobustnessTest, VectorLoad_ConstIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        auto* load = b.LoadVectorElement(vec, b.Constant(5_u));
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:u32 = load_vector_element %vec, 5u
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %3:u32 = load_vector_element %vec, 3u
+    ret %3
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorLoad_ConstIndexViaLet) {
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        auto* idx = b.Let("idx", b.Constant(5_u));
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %idx:u32 = let 5u
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %idx:u32 = let 5u
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorLoad_DynamicIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorLoad_DynamicIndex_Signed) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.i32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:i32):u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:i32):u32 -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %4:u32 = convert %idx
+    %5:u32 = min %4, 3u
+    %6:u32 = load_vector_element %vec, %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorStore_ConstIndex) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        b.StoreVectorElement(vec, b.Constant(5_u), b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %vec, 5u, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %vec, 3u, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorStore_ConstIndexViaLet) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        auto* idx = b.Let("idx", b.Constant(5_u));
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %idx:u32 = let 5u
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %idx:u32 = let 5u
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorStore_DynamicIndex) {
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:u32):void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:u32):void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, VectorStore_DynamicIndex_Signed) {
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.i32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* vec = b.Var("vec", ty.ptr(function, ty.vec4<u32>()));
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:i32):void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:i32):void -> %b1 {
+  %b1 = block {
+    %vec:ptr<function, vec4<u32>, read_write> = var
+    %4:u32 = convert %idx
+    %5:u32 = min %4, 3u
+    store_vector_element %vec, %5, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Matrix_ConstIndex) {
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    b.Append(func->Block(), [&] {
+        auto* mat = b.Var("mat", ty.ptr(function, ty.mat4x4<f32>()));
+        auto* access = b.Access(ty.ptr(function, ty.vec4<f32>()), mat, b.Constant(2_u));
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func():vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %mat, 2u
+    %4:vec4<f32> = load %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_P(IR_RobustnessTest, Matrix_ConstIndexViaLet) {
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    b.Append(func->Block(), [&] {
+        auto* mat = b.Var("mat", ty.ptr(function, ty.mat4x4<f32>()));
+        auto* idx = b.Let("idx", b.Constant(2_u));
+        auto* access = b.Access(ty.ptr(function, ty.vec4<f32>()), mat, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func():vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %idx:u32 = let 2u
+    %4:ptr<function, vec4<f32>, read_write> = access %mat, %idx
+    %5:vec4<f32> = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %idx:u32 = let 2u
+    %4:u32 = min %idx, 3u
+    %5:ptr<function, vec4<f32>, read_write> = access %mat, %4
+    %6:vec4<f32> = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Matrix_DynamicIndex) {
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* mat = b.Var("mat", ty.ptr(function, ty.mat4x4<f32>()));
+        auto* access = b.Access(ty.ptr(function, ty.vec4<f32>()), mat, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:u32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %4:ptr<function, vec4<f32>, read_write> = access %mat, %idx
+    %5:vec4<f32> = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:u32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %4:u32 = min %idx, 3u
+    %5:ptr<function, vec4<f32>, read_write> = access %mat, %4
+    %6:vec4<f32> = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Matrix_DynamicIndex_Signed) {
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    auto* idx = b.FunctionParam("idx", ty.i32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* mat = b.Var("mat", ty.ptr(function, ty.mat4x4<f32>()));
+        auto* access = b.Access(ty.ptr(function, ty.vec4<f32>()), mat, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %4:ptr<function, vec4<f32>, read_write> = access %mat, %idx
+    %5:vec4<f32> = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:i32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %mat:ptr<function, mat4x4<f32>, read_write> = var
+    %4:u32 = convert %idx
+    %5:u32 = min %4, 3u
+    %6:ptr<function, vec4<f32>, read_write> = access %mat, %5
+    %7:vec4<f32> = load %6
+    ret %7
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Array_ConstSize_ConstIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 4>()));
+        auto* access = b.Access(ty.ptr<function, u32>(), arr, b.Constant(2_u));
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %arr, 2u
+    %4:u32 = load %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_P(IR_RobustnessTest, Array_ConstSize_ConstIndexViaLet) {
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 4>()));
+        auto* idx = b.Let("idx", b.Constant(2_u));
+        auto* access = b.Access(ty.ptr<function, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %idx:u32 = let 2u
+    %4:ptr<function, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %idx:u32 = let 2u
+    %4:u32 = min %idx, 3u
+    %5:ptr<function, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Array_ConstSize_DynamicIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 4>()));
+        auto* access = b.Access(ty.ptr<function, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %4:ptr<function, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %4:u32 = min %idx, 3u
+    %5:ptr<function, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Array_ConstSize_DynamicIndex_Signed) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.i32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array<u32, 4>()));
+        auto* access = b.Access(ty.ptr<function, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:i32):u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %4:ptr<function, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:i32):u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<u32, 4>, read_write> = var
+    %4:u32 = convert %idx
+    %5:u32 = min %4, 3u
+    %6:ptr<function, u32, read_write> = access %arr, %5
+    %7:u32 = load %6
+    ret %7
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, NestedArrays) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx1 = b.FunctionParam("idx1", ty.u32());
+    auto* idx2 = b.FunctionParam("idx2", ty.u32());
+    auto* idx3 = b.FunctionParam("idx3", ty.u32());
+    auto* idx4 = b.FunctionParam("idx4", ty.u32());
+    func->SetParams({idx1, idx2, idx3, idx4});
+    b.Append(func->Block(), [&] {
+        auto* arr = b.Var(
+            "arr", ty.ptr(function, ty.array(ty.array(ty.array(ty.array(ty.u32(), 4), 5), 6), 7)));
+        auto* access = b.Access(ty.ptr<function, u32>(), arr, idx1, idx2, idx3, idx4);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%foo = func(%idx1:u32, %idx2:u32, %idx3:u32, %idx4:u32):u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<array<array<array<u32, 4>, 5>, 6>, 7>, read_write> = var
+    %7:ptr<function, u32, read_write> = access %arr, %idx1, %idx2, %idx3, %idx4
+    %8:u32 = load %7
+    ret %8
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx1:u32, %idx2:u32, %idx3:u32, %idx4:u32):u32 -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<array<array<array<u32, 4>, 5>, 6>, 7>, read_write> = var
+    %7:u32 = min %idx1, 6u
+    %8:u32 = min %idx2, 5u
+    %9:u32 = min %idx3, 4u
+    %10:u32 = min %idx4, 3u
+    %11:ptr<function, u32, read_write> = access %arr, %7, %8, %9, %10
+    %12:u32 = load %11
+    ret %12
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, NestedMixedTypes) {
+    auto* structure = ty.Struct(mod.symbols.Register("structure"),
+                                {
+                                    {mod.symbols.Register("arr"), ty.array(ty.mat3x4<f32>(), 4)},
+                                });
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    auto* idx1 = b.FunctionParam("idx1", ty.u32());
+    auto* idx2 = b.FunctionParam("idx2", ty.u32());
+    auto* idx3 = b.FunctionParam("idx3", ty.u32());
+    func->SetParams({idx1, idx2, idx3});
+    b.Append(func->Block(), [&] {
+        auto* arr = b.Var("arr", ty.ptr(function, ty.array(structure, 8)));
+        auto* access =
+            b.Access(ty.ptr<function, vec4<f32>>(), arr, idx1, b.Constant(0_u), idx2, idx3);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+structure = struct @align(16) {
+  arr:array<mat3x4<f32>, 4> @offset(0)
+}
+
+%foo = func(%idx1:u32, %idx2:u32, %idx3:u32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<structure, 8>, read_write> = var
+    %6:ptr<function, vec4<f32>, read_write> = access %arr, %idx1, 0u, %idx2, %idx3
+    %7:vec4<f32> = load %6
+    ret %7
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+structure = struct @align(16) {
+  arr:array<mat3x4<f32>, 4> @offset(0)
+}
+
+%foo = func(%idx1:u32, %idx2:u32, %idx3:u32):vec4<f32> -> %b1 {
+  %b1 = block {
+    %arr:ptr<function, array<structure, 8>, read_write> = var
+    %6:u32 = min %idx1, 7u
+    %7:u32 = min %idx2, 3u
+    %8:u32 = min %idx3, 2u
+    %9:ptr<function, vec4<f32>, read_write> = access %arr, %6, 0u, %7, %8
+    %10:vec4<f32> = load %9
+    ret %10
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_function = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+////////////////////////////////////////////////////////////////
+// Test the clamp toggles for every other address space.
+////////////////////////////////////////////////////////////////
+
+TEST_P(IR_RobustnessTest, Private_LoadVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(private_, ty.vec4<u32>()));
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<private, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<private, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_private = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Private_StoreVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(private_, ty.vec4<u32>()));
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<private, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<private, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_private = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Private_Access) {
+    auto* arr = b.Var("arr", ty.ptr(private_, ty.array<u32, 4>()));
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<private_, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<private, array<u32, 4>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<private, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<private, array<u32, 4>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:ptr<private, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_private = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, PushConstant_LoadVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(push_constant, ty.vec4<u32>()));
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<push_constant, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<push_constant, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_push_constant = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, PushConstant_StoreVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(push_constant, ty.vec4<u32>()));
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<push_constant, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<push_constant, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_push_constant = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, PushConstant_Access) {
+    auto* arr = b.Var("arr", ty.ptr(push_constant, ty.array<u32, 4>()));
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<push_constant, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<push_constant, array<u32, 4>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<push_constant, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<push_constant, array<u32, 4>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:ptr<push_constant, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_push_constant = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Storage_LoadVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(storage, ty.vec4<u32>()));
+    vec->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<storage, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<storage, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Storage_StoreVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(storage, ty.vec4<u32>()));
+    vec->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<storage, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<storage, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Storage_Access) {
+    auto* arr = b.Var("arr", ty.ptr(storage, ty.array<u32, 4>()));
+    arr->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:ptr<storage, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Unifom_LoadVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(uniform, ty.vec4<u32>()));
+    vec->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<uniform, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<uniform, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_uniform = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Unifom_StoreVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(uniform, ty.vec4<u32>()));
+    vec->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<uniform, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<uniform, vec4<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_uniform = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Unifom_Access) {
+    auto* arr = b.Var("arr", ty.ptr(uniform, ty.array<u32, 4>()));
+    arr->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<uniform, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<uniform, array<u32, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<uniform, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<uniform, array<u32, 4>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:ptr<uniform, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_uniform = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Workgroup_LoadVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(workgroup, ty.vec4<u32>()));
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* load = b.LoadVectorElement(vec, idx);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<workgroup, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = load_vector_element %vec, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<workgroup, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:u32 = load_vector_element %vec, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_workgroup = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Workgroup_StoreVectorElement) {
+    auto* vec = b.Var("vec", ty.ptr(workgroup, ty.vec4<u32>()));
+    b.RootBlock()->Append(vec);
+
+    auto* func = b.Function("foo", ty.void_());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        b.StoreVectorElement(vec, idx, b.Constant(0_u));
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %vec:ptr<workgroup, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    store_vector_element %vec, %idx, 0u
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %vec:ptr<workgroup, vec4<u32>, read_write> = var
+}
+
+%foo = func(%idx:u32):void -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    store_vector_element %vec, %4, 0u
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_workgroup = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, Workgroup_Access) {
+    auto* arr = b.Var("arr", ty.ptr(workgroup, ty.array<u32, 4>()));
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<workgroup, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<workgroup, array<u32, 4>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<workgroup, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<workgroup, array<u32, 4>, read_write> = var
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = min %idx, 3u
+    %5:ptr<workgroup, u32, read_write> = access %arr, %4
+    %6:u32 = load %5
+    ret %6
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_workgroup = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+////////////////////////////////////////////////////////////////
+// Test clamping non-pointer values.
+////////////////////////////////////////////////////////////////
+
+TEST_P(IR_RobustnessTest, ConstantVector_DynamicIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* vec = mod.constant_values.Composite(ty.vec4<u32>(), Vector{
+                                                                      mod.constant_values.Get(1_u),
+                                                                      mod.constant_values.Get(2_u),
+                                                                      mod.constant_values.Get(3_u),
+                                                                      mod.constant_values.Get(4_u),
+                                                                  });
+        auto* element = b.Access(ty.u32(), b.Constant(vec), idx);
+        b.Return(func, element);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %3:u32 = access vec4<u32>(1u, 2u, 3u, 4u), %idx
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %3:u32 = min %idx, 3u
+    %4:u32 = access vec4<u32>(1u, 2u, 3u, 4u), %3
+    ret %4
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_value = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, ConstantArray_DynamicIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* arr =
+            mod.constant_values.Composite(ty.array<u32, 4>(), Vector{
+                                                                  mod.constant_values.Get(1_u),
+                                                                  mod.constant_values.Get(2_u),
+                                                                  mod.constant_values.Get(3_u),
+                                                                  mod.constant_values.Get(4_u),
+                                                              });
+        auto* element = b.Access(ty.u32(), b.Constant(arr), idx);
+        b.Return(func, element);
+    });
+
+    auto* src = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %3:u32 = access array<u32, 4>(1u, 2u, 3u, 4u), %idx
+    ret %3
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %3:u32 = min %idx, 3u
+    %4:u32 = access array<u32, 4>(1u, 2u, 3u, 4u), %3
+    ret %4
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_value = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, ParamValueArray_DynamicIndex) {
+    auto* func = b.Function("foo", ty.u32());
+    auto* arr = b.FunctionParam("arr", ty.array<u32, 4>());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({arr, idx});
+    b.Append(func->Block(), [&] {
+        auto* element = b.Access(ty.u32(), arr, idx);
+        b.Return(func, element);
+    });
+
+    auto* src = R"(
+%foo = func(%arr:array<u32, 4>, %idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %4:u32 = access %arr, %idx
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%arr:array<u32, 4>, %idx:u32):u32 -> %b1 {
+  %b1 = block {
+    %4:u32 = min %idx, 3u
+    %5:u32 = access %arr, %4
+    ret %5
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_value = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+INSTANTIATE_TEST_SUITE_P(, IR_RobustnessTest, testing::Values(false, true));
+
+////////////////////////////////////////////////////////////////
+// Test clamping runtime-sized arrays.
+////////////////////////////////////////////////////////////////
+
+TEST_P(IR_RobustnessTest, RuntimeSizedArray_ConstIndex) {
+    auto* arr = b.Var("arr", ty.ptr(storage, ty.array<u32>()));
+    arr->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, u32>(), arr, b.Constant(42_u));
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():u32 -> %b2 {
+  %b2 = block {
+    %3:ptr<storage, u32, read_write> = access %arr, 42u
+    %4:u32 = load %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():u32 -> %b2 {
+  %b2 = block {
+    %3:u32 = arrayLength %arr
+    %4:u32 = sub %3, 1u
+    %5:u32 = min 42u, %4
+    %6:ptr<storage, u32, read_write> = access %arr, %5
+    %7:u32 = load %6
+    ret %7
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, RuntimeSizedArray_DynamicIndex) {
+    auto* arr = b.Var("arr", ty.ptr(storage, ty.array<u32>()));
+    arr->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = arrayLength %arr
+    %5:u32 = sub %4, 1u
+    %6:u32 = min %idx, %5
+    %7:ptr<storage, u32, read_write> = access %arr, %6
+    %8:u32 = load %7
+    ret %8
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, RuntimeSizedArray_InStruct_ConstIndex) {
+    auto* structure = ty.Struct(mod.symbols.Register("structure"),
+                                {
+                                    {mod.symbols.Register("arr"), ty.array<u32>()},
+                                });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(buffer);
+
+    auto* func = b.Function("foo", ty.u32());
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, u32>(), buffer, b.Constant(0_u), b.Constant(42_u));
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+structure = struct @align(4) {
+  arr:array<u32> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, structure, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():u32 -> %b2 {
+  %b2 = block {
+    %3:ptr<storage, u32, read_write> = access %buffer, 0u, 42u
+    %4:u32 = load %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+structure = struct @align(4) {
+  arr:array<u32> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, structure, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func():u32 -> %b2 {
+  %b2 = block {
+    %3:ptr<storage, array<u32>, read_write> = access %buffer, 0u
+    %4:u32 = arrayLength %3
+    %5:u32 = sub %4, 1u
+    %6:u32 = min 42u, %5
+    %7:ptr<storage, u32, read_write> = access %buffer, 0u, %6
+    %8:u32 = load %7
+    ret %8
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, RuntimeSizedArray_InStruct_DynamicIndex) {
+    auto* structure = ty.Struct(mod.symbols.Register("structure"),
+                                {
+                                    {mod.symbols.Register("arr"), ty.array<u32>()},
+                                });
+
+    auto* buffer = b.Var("buffer", ty.ptr(storage, structure));
+    buffer->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(buffer);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, u32>(), buffer, b.Constant(0_u), idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+structure = struct @align(4) {
+  arr:array<u32> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, structure, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, u32, read_write> = access %buffer, 0u, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+structure = struct @align(4) {
+  arr:array<u32> @offset(0)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<storage, structure, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, array<u32>, read_write> = access %buffer, 0u
+    %5:u32 = arrayLength %4
+    %6:u32 = sub %5, 1u
+    %7:u32 = min %idx, %6
+    %8:ptr<storage, u32, read_write> = access %buffer, 0u, %7
+    %9:u32 = load %8
+    ret %9
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, RuntimeSizedArray_DisableClamping) {
+    auto* arr = b.Var("arr", ty.ptr(storage, ty.array<u32>()));
+    arr->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(arr);
+
+    auto* func = b.Function("foo", ty.u32());
+    auto* idx = b.FunctionParam("idx", ty.u32());
+    func->SetParams({idx});
+    b.Append(func->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, u32>(), arr, idx);
+        auto* load = b.Load(access);
+        b.Return(func, load);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, u32, read_write> = access %arr, %idx
+    %5:u32 = load %4
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %arr:ptr<storage, array<u32>, read_write> = var @binding_point(0, 0)
+}
+
+%foo = func(%idx:u32):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = arrayLength %arr
+    %5:u32 = sub %4, 1u
+    %6:u32 = min %idx, %5
+    %7:ptr<storage, u32, read_write> = access %arr, %6
+    %8:u32 = load %7
+    ret %8
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_storage = true;
+    cfg.disable_runtime_sized_array_index_clamping = !GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+////////////////////////////////////////////////////////////////
+// Test clamping texture builtin calls.
+////////////////////////////////////////////////////////////////
+
+TEST_P(IR_RobustnessTest, TextureDimensions) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    auto* func = b.Function("foo", ty.vec2<u32>());
+    b.Append(func->Block(), [&] {
+        auto* handle = b.Load(texture);
+        auto* dims = b.Call(ty.vec2<u32>(), core::Function::kTextureDimensions, handle);
+        b.Return(func, dims);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%foo = func():vec2<u32> -> %b2 {
+  %b2 = block {
+    %3:texture_2d<f32> = load %texture
+    %4:vec2<u32> = textureDimensions %3
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureDimensions_WithLevel) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    auto* func = b.Function("foo", ty.vec2<u32>());
+    auto* level = b.FunctionParam("level", ty.u32());
+    func->SetParams({level});
+    b.Append(func->Block(), [&] {
+        auto* handle = b.Load(texture);
+        auto* dims = b.Call(ty.vec2<u32>(), core::Function::kTextureDimensions, handle, level);
+        b.Return(func, dims);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%foo = func(%level:u32):vec2<u32> -> %b2 {
+  %b2 = block {
+    %4:texture_2d<f32> = load %texture
+    %5:vec2<u32> = textureDimensions %4, %level
+    ret %5
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%foo = func(%level:u32):vec2<u32> -> %b2 {
+  %b2 = block {
+    %4:texture_2d<f32> = load %texture
+    %5:u32 = textureNumLevels %4
+    %6:u32 = sub %5, 1u
+    %7:u32 = min %level, %6
+    %8:vec2<u32> = textureDimensions %4, %7
+    ret %8
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled1D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k1d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.i32());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.u32());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_1d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_1d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_1d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_1d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_1d<f32> = load %texture
+    %6:u32 = textureDimensions %5
+    %7:u32 = sub %6, 1u
+    %8:u32 = convert %coords
+    %9:u32 = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_1d<f32> = load %texture
+    %19:u32 = textureDimensions %18
+    %20:u32 = sub %19, 1u
+    %21:u32 = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled2D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_2d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_2d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_2d<f32> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_2d<f32> = load %texture
+    %19:vec2<u32> = textureDimensions %18
+    %20:vec2<u32> = sub %19, vec2<u32>(1u)
+    %21:vec2<u32> = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled2DArray) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k2dArray, ty.f32()),
+               read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d_array<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %6:texture_2d_array<f32> = load %texture
+    %7:vec4<f32> = textureLoad %6, %coords, %layer, %level
+    ret %7
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %12:texture_2d_array<f32> = load %texture
+    %13:vec4<f32> = textureLoad %12, %coords_1, %layer_1, %level_1
+    ret %13
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_2d_array<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %6:texture_2d_array<f32> = load %texture
+    %7:vec2<u32> = textureDimensions %6
+    %8:vec2<u32> = sub %7, vec2<u32>(1u)
+    %9:vec2<u32> = convert %coords
+    %10:vec2<u32> = min %9, %8
+    %11:u32 = textureNumLayers %6
+    %12:u32 = sub %11, 1u
+    %13:u32 = convert %layer
+    %14:u32 = min %13, %12
+    %15:u32 = textureNumLevels %6
+    %16:u32 = sub %15, 1u
+    %17:u32 = convert %level
+    %18:u32 = min %17, %16
+    %19:vec4<f32> = textureLoad %6, %10, %14, %18
+    ret %19
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %24:texture_2d_array<f32> = load %texture
+    %25:vec2<u32> = textureDimensions %24
+    %26:vec2<u32> = sub %25, vec2<u32>(1u)
+    %27:vec2<u32> = min %coords_1, %26
+    %28:u32 = textureNumLayers %24
+    %29:u32 = sub %28, 1u
+    %30:u32 = min %layer_1, %29
+    %31:u32 = textureNumLevels %24
+    %32:u32 = sub %31, 1u
+    %33:u32 = min %level_1, %32
+    %34:vec4<f32> = textureLoad %24, %27, %30, %33
+    ret %34
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Sampled3D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::SampledTexture>(type::TextureDimension::k3d, ty.f32()), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_3d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_3d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_3d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_3d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_3d<f32> = load %texture
+    %6:vec3<u32> = textureDimensions %5
+    %7:vec3<u32> = sub %6, vec3<u32>(1u)
+    %8:vec3<u32> = convert %coords
+    %9:vec3<u32> = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_3d<f32> = load %texture
+    %19:vec3<u32> = textureDimensions %18
+    %20:vec3<u32> = sub %19, vec3<u32>(1u)
+    %21:vec3<u32> = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Multisampled2D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::MultisampledTexture>(type::TextureDimension::k2d, ty.f32()),
+               read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_multisampled_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_multisampled_2d<f32> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_multisampled_2d<f32> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_multisampled_2d<f32>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_multisampled_2d<f32> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:vec4<f32> = textureLoad %5, %9, %level
+    ret %10
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %14:texture_multisampled_2d<f32> = load %texture
+    %15:vec2<u32> = textureDimensions %14
+    %16:vec2<u32> = sub %15, vec2<u32>(1u)
+    %17:vec2<u32> = min %coords_1, %16
+    %18:vec4<f32> = textureLoad %14, %17, %level_1
+    ret %18
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Depth2D) {
+    auto* texture = b.Var(
+        "texture", ty.ptr(handle, ty.Get<type::DepthTexture>(type::TextureDimension::k2d), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_2d = load %texture
+    %6:f32 = textureLoad %5, %coords, %level
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %10:texture_depth_2d = load %texture
+    %11:f32 = textureLoad %10, %coords_1, %level_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_2d = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:u32 = textureNumLevels %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %level
+    %13:u32 = min %12, %11
+    %14:f32 = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %level_1: 'level'
+  %b3 = block {
+    %18:texture_depth_2d = load %texture
+    %19:vec2<u32> = textureDimensions %18
+    %20:vec2<u32> = sub %19, vec2<u32>(1u)
+    %21:vec2<u32> = min %coords_1, %20
+    %22:u32 = textureNumLevels %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %level_1, %23
+    %25:f32 = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Depth2DArray) {
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle, ty.Get<type::DepthTexture>(type::TextureDimension::k2dArray), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        auto* level = b.FunctionParam("level", ty.i32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        auto* level = b.FunctionParam("level", ty.u32());
+        func->SetParams({coords, layer, level});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, layer, level);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d_array, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %6:texture_depth_2d_array = load %texture
+    %7:f32 = textureLoad %6, %coords, %layer, %level
+    ret %7
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %12:texture_depth_2d_array = load %texture
+    %13:f32 = textureLoad %12, %coords_1, %layer_1, %level_1
+    ret %13
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_2d_array, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %level:i32):f32 -> %b2 {
+  %b2 = block {
+    %6:texture_depth_2d_array = load %texture
+    %7:vec2<u32> = textureDimensions %6
+    %8:vec2<u32> = sub %7, vec2<u32>(1u)
+    %9:vec2<u32> = convert %coords
+    %10:vec2<u32> = min %9, %8
+    %11:u32 = textureNumLayers %6
+    %12:u32 = sub %11, 1u
+    %13:u32 = convert %layer
+    %14:u32 = min %13, %12
+    %15:u32 = textureNumLevels %6
+    %16:u32 = sub %15, 1u
+    %17:u32 = convert %level
+    %18:u32 = min %17, %16
+    %19:f32 = textureLoad %6, %10, %14, %18
+    ret %19
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %level_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %level_1: 'level'
+  %b3 = block {
+    %24:texture_depth_2d_array = load %texture
+    %25:vec2<u32> = textureDimensions %24
+    %26:vec2<u32> = sub %25, vec2<u32>(1u)
+    %27:vec2<u32> = min %coords_1, %26
+    %28:u32 = textureNumLayers %24
+    %29:u32 = sub %28, 1u
+    %30:u32 = min %layer_1, %29
+    %31:u32 = textureNumLevels %24
+    %32:u32 = sub %31, 1u
+    %33:u32 = min %level_1, %32
+    %34:f32 = textureLoad %24, %27, %30, %33
+    ret %34
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_DepthMultisampled2D) {
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle, ty.Get<type::DepthMultisampledTexture>(type::TextureDimension::k2d), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* index = b.FunctionParam("index", ty.i32());
+        func->SetParams({coords, index});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, index);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.f32());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* index = b.FunctionParam("index", ty.u32());
+        func->SetParams({coords, index});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.f32(), core::Function::kTextureLoad, handle, coords, index);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_multisampled_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %index:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_multisampled_2d = load %texture
+    %6:f32 = textureLoad %5, %coords, %index
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %index_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %index_1: 'index'
+  %b3 = block {
+    %10:texture_depth_multisampled_2d = load %texture
+    %11:f32 = textureLoad %10, %coords_1, %index_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_depth_multisampled_2d, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %index:i32):f32 -> %b2 {
+  %b2 = block {
+    %5:texture_depth_multisampled_2d = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:f32 = textureLoad %5, %9, %index
+    ret %10
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %index_1:u32):f32 -> %b3 {  # %coords_1: 'coords', %index_1: 'index'
+  %b3 = block {
+    %14:texture_depth_multisampled_2d = load %texture
+    %15:vec2<u32> = textureDimensions %14
+    %16:vec2<u32> = sub %15, vec2<u32>(1u)
+    %17:vec2<u32> = min %coords_1, %16
+    %18:f32 = textureLoad %14, %17, %index_1
+    ret %18
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_External) {
+    auto* texture = b.Var("texture", ty.ptr(handle, ty.Get<type::ExternalTexture>(), read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_external, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_external = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_external = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_external, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_external = load %texture
+    %5:vec2<u32> = textureDimensions %4
+    %6:vec2<u32> = sub %5, vec2<u32>(1u)
+    %7:vec2<u32> = convert %coords
+    %8:vec2<u32> = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_external = load %texture
+    %13:vec2<u32> = textureDimensions %12
+    %14:vec2<u32> = sub %13, vec2<u32>(1u)
+    %15:vec2<u32> = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage1D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k1d, format, read_write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.i32());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.u32());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %5:u32 = textureDimensions %4
+    %6:u32 = sub %5, 1u
+    %7:u32 = convert %coords
+    %8:u32 = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_storage_1d<rgba8unorm, read_write> = load %texture
+    %13:u32 = textureDimensions %12
+    %14:u32 = sub %13, 1u
+    %15:u32 = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage2D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k2d, format, read_write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %5:vec2<u32> = textureDimensions %4
+    %6:vec2<u32> = sub %5, vec2<u32>(1u)
+    %7:vec2<u32> = convert %coords
+    %8:vec2<u32> = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_storage_2d<rgba8unorm, read_write> = load %texture
+    %13:vec2<u32> = textureDimensions %12
+    %14:vec2<u32> = sub %13, vec2<u32>(1u)
+    %15:vec2<u32> = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage2DArray) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture = b.Var(
+        "texture",
+        ty.ptr(handle,
+               ty.Get<type::StorageTexture>(type::TextureDimension::k2dArray, format, read_write,
+                                            type::StorageTexture::SubtypeFor(format, ty)),
+               read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        func->SetParams({coords, layer});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        func->SetParams({coords, layer});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel =
+                b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords, layer);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %6:vec4<f32> = textureLoad %5, %coords, %layer
+    ret %6
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer'
+  %b3 = block {
+    %10:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %11:vec4<f32> = textureLoad %10, %coords_1, %layer_1
+    ret %11
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32):vec4<f32> -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:u32 = textureNumLayers %5
+    %11:u32 = sub %10, 1u
+    %12:u32 = convert %layer
+    %13:u32 = min %12, %11
+    %14:vec4<f32> = textureLoad %5, %9, %13
+    ret %14
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32):vec4<f32> -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer'
+  %b3 = block {
+    %18:texture_storage_2d_array<rgba8unorm, read_write> = load %texture
+    %19:vec2<u32> = textureDimensions %18
+    %20:vec2<u32> = sub %19, vec2<u32>(1u)
+    %21:vec2<u32> = min %coords_1, %20
+    %22:u32 = textureNumLayers %18
+    %23:u32 = sub %22, 1u
+    %24:u32 = min %layer_1, %23
+    %25:vec4<f32> = textureLoad %18, %21, %24
+    ret %25
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureLoad_Storage3D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k3d, format, read_write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<i32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.vec4<f32>());
+        auto* coords = b.FunctionParam("coords", ty.vec3<u32>());
+        func->SetParams({coords});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            auto* texel = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, handle, coords);
+            b.Return(func, texel);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %5:vec4<f32> = textureLoad %4, %coords
+    ret %5
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %8:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %9:vec4<f32> = textureLoad %8, %coords_1
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, read_write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>):vec4<f32> -> %b2 {
+  %b2 = block {
+    %4:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %5:vec3<u32> = textureDimensions %4
+    %6:vec3<u32> = sub %5, vec3<u32>(1u)
+    %7:vec3<u32> = convert %coords
+    %8:vec3<u32> = min %7, %6
+    %9:vec4<f32> = textureLoad %4, %8
+    ret %9
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>):vec4<f32> -> %b3 {  # %coords_1: 'coords'
+  %b3 = block {
+    %12:texture_storage_3d<rgba8unorm, read_write> = load %texture
+    %13:vec3<u32> = textureDimensions %12
+    %14:vec3<u32> = sub %13, vec3<u32>(1u)
+    %15:vec3<u32> = min %coords_1, %14
+    %16:vec4<f32> = textureLoad %12, %15
+    ret %16
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage1D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k1d, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.i32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.u32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_1d<rgba8unorm, write> = load %texture
+    %6:void = textureStore %5, %coords, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %10:texture_storage_1d<rgba8unorm, write> = load %texture
+    %11:void = textureStore %10, %coords_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_1d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_1d<rgba8unorm, write> = load %texture
+    %6:u32 = textureDimensions %5
+    %7:u32 = sub %6, 1u
+    %8:u32 = convert %coords
+    %9:u32 = min %8, %7
+    %10:void = textureStore %5, %9, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %14:texture_storage_1d<rgba8unorm, write> = load %texture
+    %15:u32 = textureDimensions %14
+    %16:u32 = sub %15, 1u
+    %17:u32 = min %coords_1, %16
+    %18:void = textureStore %14, %17, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage2D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k2d, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d<rgba8unorm, write> = load %texture
+    %6:void = textureStore %5, %coords, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %10:texture_storage_2d<rgba8unorm, write> = load %texture
+    %11:void = textureStore %10, %coords_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_2d<rgba8unorm, write> = load %texture
+    %6:vec2<u32> = textureDimensions %5
+    %7:vec2<u32> = sub %6, vec2<u32>(1u)
+    %8:vec2<u32> = convert %coords
+    %9:vec2<u32> = min %8, %7
+    %10:void = textureStore %5, %9, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %14:texture_storage_2d<rgba8unorm, write> = load %texture
+    %15:vec2<u32> = textureDimensions %14
+    %16:vec2<u32> = sub %15, vec2<u32>(1u)
+    %17:vec2<u32> = min %coords_1, %16
+    %18:void = textureStore %14, %17, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage2DArray) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k2dArray, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
+        auto* layer = b.FunctionParam("layer", ty.i32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, layer, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, layer, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+        auto* layer = b.FunctionParam("layer", ty.u32());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, layer, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, layer, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %6:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %7:void = textureStore %6, %coords, %layer, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %value_1: 'value'
+  %b3 = block {
+    %12:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %13:void = textureStore %12, %coords_1, %layer_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_2d_array<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec2<i32>, %layer:i32, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %6:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %7:vec2<u32> = textureDimensions %6
+    %8:vec2<u32> = sub %7, vec2<u32>(1u)
+    %9:vec2<u32> = convert %coords
+    %10:vec2<u32> = min %9, %8
+    %11:u32 = textureNumLayers %6
+    %12:u32 = sub %11, 1u
+    %13:u32 = convert %layer
+    %14:u32 = min %13, %12
+    %15:void = textureStore %6, %10, %14, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec2<u32>, %layer_1:u32, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %layer_1: 'layer', %value_1: 'value'
+  %b3 = block {
+    %20:texture_storage_2d_array<rgba8unorm, write> = load %texture
+    %21:vec2<u32> = textureDimensions %20
+    %22:vec2<u32> = sub %21, vec2<u32>(1u)
+    %23:vec2<u32> = min %coords_1, %22
+    %24:u32 = textureNumLayers %20
+    %25:u32 = sub %24, 1u
+    %26:u32 = min %layer_1, %25
+    %27:void = textureStore %20, %23, %26, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+TEST_P(IR_RobustnessTest, TextureStore_Storage3D) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture =
+        b.Var("texture",
+              ty.ptr(handle,
+                     ty.Get<type::StorageTexture>(type::TextureDimension::k3d, format, write,
+                                                  type::StorageTexture::SubtypeFor(format, ty)),
+                     read));
+    texture->SetBindingPoint(0, 0);
+    b.RootBlock()->Append(texture);
+
+    {
+        auto* func = b.Function("load_signed", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec3<i32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_unsigned", ty.void_());
+        auto* coords = b.FunctionParam("coords", ty.vec3<u32>());
+        auto* value = b.FunctionParam("value", ty.vec4<f32>());
+        func->SetParams({coords, value});
+        b.Append(func->Block(), [&] {
+            auto* handle = b.Load(texture);
+            b.Call(ty.void_(), core::Function::kTextureStore, handle, coords, value);
+            b.Return(func);
+        });
+    }
+
+    auto* src = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_3d<rgba8unorm, write> = load %texture
+    %6:void = textureStore %5, %coords, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %10:texture_storage_3d<rgba8unorm, write> = load %texture
+    %11:void = textureStore %10, %coords_1, %value_1
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %texture:ptr<handle, texture_storage_3d<rgba8unorm, write>, read> = var @binding_point(0, 0)
+}
+
+%load_signed = func(%coords:vec3<i32>, %value:vec4<f32>):void -> %b2 {
+  %b2 = block {
+    %5:texture_storage_3d<rgba8unorm, write> = load %texture
+    %6:vec3<u32> = textureDimensions %5
+    %7:vec3<u32> = sub %6, vec3<u32>(1u)
+    %8:vec3<u32> = convert %coords
+    %9:vec3<u32> = min %8, %7
+    %10:void = textureStore %5, %9, %value
+    ret
+  }
+}
+%load_unsigned = func(%coords_1:vec3<u32>, %value_1:vec4<f32>):void -> %b3 {  # %coords_1: 'coords', %value_1: 'value'
+  %b3 = block {
+    %14:texture_storage_3d<rgba8unorm, write> = load %texture
+    %15:vec3<u32> = textureDimensions %14
+    %16:vec3<u32> = sub %15, vec3<u32>(1u)
+    %17:vec3<u32> = min %coords_1, %16
+    %18:void = textureStore %14, %17, %value_1
+    ret
+  }
+}
+)";
+
+    RobustnessConfig cfg;
+    cfg.clamp_texture = GetParam();
+    Run(Robustness, cfg);
+
+    EXPECT_EQ(GetParam() ? expect : src, str());
+}
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/unary.h b/src/tint/lang/core/ir/unary.h
index 4013c47..2e55cc3 100644
--- a/src/tint/lang/core/ir/unary.h
+++ b/src/tint/lang/core/ir/unary.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_UNARY_H_
 #define SRC_TINT_LANG_CORE_IR_UNARY_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -46,7 +48,7 @@
     enum Kind Kind() { return kind_; }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "unary"; }
+    std::string FriendlyName() override { return "unary"; }
 
   private:
     enum Kind kind_;
diff --git a/src/tint/lang/core/ir/unreachable.h b/src/tint/lang/core/ir/unreachable.h
index 97d052e..fd92538 100644
--- a/src/tint/lang/core/ir/unreachable.h
+++ b/src/tint/lang/core/ir/unreachable.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_UNREACHABLE_H_
 #define SRC_TINT_LANG_CORE_IR_UNREACHABLE_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/terminator.h"
 
 namespace tint::core::ir {
@@ -25,7 +27,7 @@
     ~Unreachable() override;
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "unreachable"; }
+    std::string FriendlyName() override { return "unreachable"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/user_call.h b/src/tint/lang/core/ir/user_call.h
index ed0f9eb..58a381f 100644
--- a/src/tint/lang/core/ir/user_call.h
+++ b/src/tint/lang/core/ir/user_call.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_USER_CALL_H_
 #define SRC_TINT_LANG_CORE_IR_USER_CALL_H_
 
+#include <string>
+
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/lang/core/ir/function.h"
 #include "src/tint/utils/rtti/castable.h"
@@ -44,7 +46,7 @@
     Function* Func() { return operands_[kFunctionOperandOffset]->As<ir::Function>(); }
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "user-call"; }
+    std::string FriendlyName() override { return "call"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index e5f4c72..c2ffd81 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/core/intrinsic/table.h"
 #include "src/tint/lang/core/ir/access.h"
 #include "src/tint/lang/core/ir/binary.h"
 #include "src/tint/lang/core/ir/bitcast.h"
@@ -56,6 +57,7 @@
 #include "src/tint/lang/core/type/vector.h"
 #include "src/tint/lang/core/type/void.h"
 #include "src/tint/utils/containers/reverse.h"
+#include "src/tint/utils/containers/transform.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 #include "src/tint/utils/rtti/switch.h"
 
@@ -69,6 +71,205 @@
 
 namespace tint::core::ir {
 
+namespace {
+
+/// The core IR validator.
+class Validator {
+  public:
+    /// Create a core validator
+    /// @param mod the module to be validated
+    explicit Validator(Module& mod);
+
+    /// Destructor
+    ~Validator();
+
+    /// Runs the validator over the module provided during construction
+    /// @returns the results of validation, either a success result object or the diagnostics of
+    /// validation failures.
+    Result<SuccessType, diag::List> IsValid();
+
+  protected:
+    /// @param inst the instruction
+    /// @param err the error message
+    /// @returns a string with the instruction name name and error message formatted
+    std::string InstError(Instruction* inst, std::string err);
+
+    /// Adds an error for the @p inst and highlights the instruction in the disassembly
+    /// @param inst the instruction
+    /// @param err the error string
+    void AddError(Instruction* inst, std::string err);
+
+    /// Adds an error for the @p inst operand at @p idx and highlights the operand in the
+    /// disassembly
+    /// @param inst the instaruction
+    /// @param idx the operand index
+    /// @param err the error string
+    void AddError(Instruction* inst, size_t idx, std::string err);
+
+    /// Adds an error for the @p inst result at @p idx and highlgihts the result in the disassembly
+    /// @param inst the instruction
+    /// @param idx the result index
+    /// @param err the error string
+    void AddResultError(Instruction* inst, size_t idx, std::string err);
+
+    /// Adds an error the @p block and highlights the block header in the disassembly
+    /// @param blk the block
+    /// @param err the error string
+    void AddError(Block* blk, std::string err);
+
+    /// Adds a note to @p inst and highlights the instruction in the disassembly
+    /// @param inst the instruction
+    /// @param err the message to emit
+    void AddNote(Instruction* inst, std::string err);
+
+    /// Adds a note to @p inst for operand @p idx and highlights the operand in the
+    /// disassembly
+    /// @param inst the instruction
+    /// @param idx the operand index
+    /// @param err the message string
+    void AddNote(Instruction* inst, size_t idx, std::string err);
+
+    /// Adds a note to @p blk and highlights the block in the disassembly
+    /// @param blk the block
+    /// @param err the message to emit
+    void AddNote(Block* blk, std::string err);
+
+    /// Adds an error to the diagnostics
+    /// @param err the message to emit
+    /// @param src the source lines to highlight
+    void AddError(std::string err, Source src = {});
+
+    /// Adds a note to the diagnostics
+    /// @param note the note to emit
+    /// @param src the source lines to highlight
+    void AddNote(std::string note, Source src = {});
+
+    /// @param v the value to get the name for
+    /// @returns the name for the given value
+    std::string Name(Value* v);
+
+    /// Checks the given operand is not null
+    /// @param inst the instruciton
+    /// @param operand the operand
+    /// @param idx the operand index
+    void CheckOperandNotNull(ir::Instruction* inst, ir::Value* operand, size_t idx);
+
+    /// Checks all operands in the given range (inclusive) for @p inst are not null
+    /// @param inst the instruction
+    /// @param start_operand the first operand to check
+    /// @param end_operand the last operand to check
+    void CheckOperandsNotNull(ir::Instruction* inst, size_t start_operand, size_t end_operand);
+
+    /// Validates the root block
+    /// @param blk the block
+    void CheckRootBlock(Block* blk);
+
+    /// Validates the given function
+    /// @param func the function validate
+    void CheckFunction(Function* func);
+
+    /// Validates the given block
+    /// @param blk the block to validate
+    void CheckBlock(Block* blk);
+
+    /// Validates the given instruction
+    /// @param inst the instruction to validate
+    void CheckInstruction(Instruction* inst);
+
+    /// Validates the given var
+    /// @param var the var to validate
+    void CheckVar(Var* var);
+
+    /// Validates the given let
+    /// @param let the let to validate
+    void CheckLet(Let* let);
+
+    /// Validates the given call
+    /// @param call the call to validate
+    void CheckCall(Call* call);
+
+    /// Validates the given builtin call
+    /// @param call the call to validate
+    void CheckBuiltinCall(BuiltinCall* call);
+
+    /// Validates the given access
+    /// @param a the access to validate
+    void CheckAccess(ir::Access* a);
+
+    /// Validates the given binary
+    /// @param b the binary to validate
+    void CheckBinary(ir::Binary* b);
+
+    /// Validates the given unary
+    /// @param u the unary to validate
+    void CheckUnary(ir::Unary* u);
+
+    /// Validates the given if
+    /// @param if_ the if to validate
+    void CheckIf(If* if_);
+
+    /// Validates the given loop
+    /// @param l the loop to validate
+    void CheckLoop(Loop* l);
+
+    /// Validates the given switch
+    /// @param s the switch to validate
+    void CheckSwitch(Switch* s);
+
+    /// Validates the given terminator
+    /// @param b the terminator to validate
+    void CheckTerminator(ir::Terminator* b);
+
+    /// Validates the given exit
+    /// @param e the exit to validate
+    void CheckExit(ir::Exit* e);
+
+    /// Validates the given exit if
+    /// @param e the exit if to validate
+    void CheckExitIf(ExitIf* e);
+
+    /// Validates the given return
+    /// @param r the return to validate
+    void CheckReturn(Return* r);
+
+    /// Validates the @p exit targets a valid @p control instruction where the instruction may jump
+    /// over if control instructions.
+    /// @param exit the exit to validate
+    /// @param control the control instruction targeted
+    void CheckControlsAllowingIf(Exit* exit, Instruction* control);
+
+    /// Validates the given exit switch
+    /// @param s the exit switch to validate
+    void CheckExitSwitch(ExitSwitch* s);
+
+    /// Validates the given exit loop
+    /// @param l the exit loop to validate
+    void CheckExitLoop(ExitLoop* l);
+
+    /// Validates the given load vector element
+    /// @param l the load vector element to validate
+    void CheckLoadVectorElement(LoadVectorElement* l);
+
+    /// Validates the given store vector element
+    /// @param s the store vector element to validate
+    void CheckStoreVectorElement(StoreVectorElement* s);
+
+    /// @param inst the instruction
+    /// @param idx the operand index
+    /// @returns the vector pointer type for the given instruction operand
+    const core::type::Type* GetVectorPtrElementType(Instruction* inst, size_t idx);
+
+  private:
+    Module& mod_;
+    diag::List diagnostics_;
+    Disassembler dis_{mod_};
+    Block* current_block_ = nullptr;
+    Hashset<Function*, 4> seen_functions_;
+    Vector<ControlInstruction*, 8> control_stack_;
+
+    void DisassembleIfNeeded();
+};
+
 Validator::Validator(Module& mod) : mod_(mod) {}
 
 Validator::~Validator() = default;
@@ -312,15 +513,30 @@
 
 void Validator::CheckCall(Call* call) {
     tint::Switch(
-        call,                      //
-        [&](Bitcast*) {},          //
-        [&](CoreBuiltinCall*) {},  //
-        [&](IntrinsicCall*) {},    //
-        [&](Construct*) {},        //
-        [&](Convert*) {},          //
-        [&](Discard*) {},          //
-        [&](UserCall*) {},         //
-        [&](Default) { AddError(call, InstError(call, "missing validation")); });
+        call,                                          //
+        [&](Bitcast*) {},                              //
+        [&](BuiltinCall* c) { CheckBuiltinCall(c); },  //
+        [&](IntrinsicCall*) {},                        //
+        [&](Construct*) {},                            //
+        [&](Convert*) {},                              //
+        [&](Discard*) {},                              //
+        [&](UserCall*) {},                             //
+        [&](Default) {
+            // Validation of custom IR instructions
+        });
+}
+
+void Validator::CheckBuiltinCall(BuiltinCall* call) {
+    auto args = Transform<8>(call->Args(), [&](ir::Value* v) { return v->Type(); });
+    intrinsic::Context context{call->TableData(), mod_.Types(), mod_.symbols, diagnostics_};
+
+    auto result = core::intrinsic::Lookup(context, call->IntrinsicName(), call->FuncId(), args,
+                                          core::EvaluationStage::kRuntime, Source{});
+    if (result) {
+        if (result->return_type != call->Result()->Type()) {
+            AddError(call, InstError(call, "call result type does not match builtin return type"));
+        }
+    }
 }
 
 void Validator::CheckAccess(ir::Access* a) {
@@ -630,6 +846,8 @@
     return nullptr;
 }
 
+}  // namespace
+
 Result<SuccessType, diag::List> Validate(Module& mod) {
     Validator v(mod);
     return v.IsValid();
diff --git a/src/tint/lang/core/ir/validator.h b/src/tint/lang/core/ir/validator.h
index 1723d65..497defa 100644
--- a/src/tint/lang/core/ir/validator.h
+++ b/src/tint/lang/core/ir/validator.h
@@ -17,22 +17,12 @@
 
 #include <string>
 
-#include "src/tint/lang/core/ir/disassembler.h"
-#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
 namespace tint::core::ir {
-class Access;
-class ExitIf;
-class ExitLoop;
-class ExitSwitch;
-class Let;
-class LoadVectorElement;
-class Return;
-class StoreVectorElement;
-class Var;
+class Module;
 }  // namespace tint::core::ir
 
 namespace tint::core::ir {
@@ -48,200 +38,6 @@
 /// @returns an error string if the module is not valid
 Result<SuccessType, std::string> ValidateAndDumpIfNeeded(Module& ir, const char* msg);
 
-/// The core IR validator.
-class Validator {
-  public:
-    /// Create a core validator
-    /// @param mod the module to be validated
-    explicit Validator(Module& mod);
-
-    /// Destructor
-    ~Validator();
-
-    /// Runs the validator over the module provided during construction
-    /// @returns the results of validation, either a success result object or the diagnostics of
-    /// validation failures.
-    Result<SuccessType, diag::List> IsValid();
-
-  protected:
-    /// @param inst the instruction
-    /// @param err the error message
-    /// @returns a string with the instruction name name and error message formatted
-    std::string InstError(Instruction* inst, std::string err);
-
-    /// Adds an error for the @p inst and highlights the instruction in the disassembly
-    /// @param inst the instruction
-    /// @param err the error string
-    void AddError(Instruction* inst, std::string err);
-
-    /// Adds an error for the @p inst operand at @p idx and highlights the operand in the
-    /// disassembly
-    /// @param inst the instaruction
-    /// @param idx the operand index
-    /// @param err the error string
-    void AddError(Instruction* inst, size_t idx, std::string err);
-
-    /// Adds an error for the @p inst result at @p idx and highlgihts the result in the disassembly
-    /// @param inst the instruction
-    /// @param idx the result index
-    /// @param err the error string
-    void AddResultError(Instruction* inst, size_t idx, std::string err);
-
-    /// Adds an error the @p block and highlights the block header in the disassembly
-    /// @param blk the block
-    /// @param err the error string
-    void AddError(Block* blk, std::string err);
-
-    /// Adds a note to @p inst and highlights the instruction in the disassembly
-    /// @param inst the instruction
-    /// @param err the message to emit
-    void AddNote(Instruction* inst, std::string err);
-
-    /// Adds a note to @p inst for operand @p idx and highlights the operand in the
-    /// disassembly
-    /// @param inst the instruction
-    /// @param idx the operand index
-    /// @param err the message string
-    void AddNote(Instruction* inst, size_t idx, std::string err);
-
-    /// Adds a note to @p blk and highlights the block in the disassembly
-    /// @param blk the block
-    /// @param err the message to emit
-    void AddNote(Block* blk, std::string err);
-
-    /// Adds an error to the diagnostics
-    /// @param err the message to emit
-    /// @param src the source lines to highlight
-    void AddError(std::string err, Source src = {});
-
-    /// Adds a note to the diagnostics
-    /// @param note the note to emit
-    /// @param src the source lines to highlight
-    void AddNote(std::string note, Source src = {});
-
-    /// @param v the value to get the name for
-    /// @returns the name for the given value
-    std::string Name(Value* v);
-
-    /// Checks the given operand is not null
-    /// @param inst the instruciton
-    /// @param operand the operand
-    /// @param idx the operand index
-    void CheckOperandNotNull(ir::Instruction* inst, ir::Value* operand, size_t idx);
-
-    /// Checks all operands in the given range (inclusive) for @p inst are not null
-    /// @param inst the instruction
-    /// @param start_operand the first operand to check
-    /// @param end_operand the last operand to check
-    void CheckOperandsNotNull(ir::Instruction* inst, size_t start_operand, size_t end_operand);
-
-    /// Validates the root block
-    /// @param blk the block
-    void CheckRootBlock(Block* blk);
-
-    /// Validates the given function
-    /// @param func the function validate
-    void CheckFunction(Function* func);
-
-    /// Validates the given block
-    /// @param blk the block to validate
-    void CheckBlock(Block* blk);
-
-    /// Validates the given instruction
-    /// @param inst the instruction to validate
-    void CheckInstruction(Instruction* inst);
-
-    /// Validates the given var
-    /// @param var the var to validate
-    void CheckVar(Var* var);
-
-    /// Validates the given let
-    /// @param let the let to validate
-    void CheckLet(Let* let);
-
-    /// Validates the given call
-    /// @param call the call to validate
-    void CheckCall(Call* call);
-
-    /// Validates the given access
-    /// @param a the access to validate
-    void CheckAccess(ir::Access* a);
-
-    /// Validates the given binary
-    /// @param b the binary to validate
-    void CheckBinary(ir::Binary* b);
-
-    /// Validates the given unary
-    /// @param u the unary to validate
-    void CheckUnary(ir::Unary* u);
-
-    /// Validates the given if
-    /// @param if_ the if to validate
-    void CheckIf(If* if_);
-
-    /// Validates the given loop
-    /// @param l the loop to validate
-    void CheckLoop(Loop* l);
-
-    /// Validates the given switch
-    /// @param s the switch to validate
-    void CheckSwitch(Switch* s);
-
-    /// Validates the given terminator
-    /// @param b the terminator to validate
-    void CheckTerminator(ir::Terminator* b);
-
-    /// Validates the given exit
-    /// @param e the exit to validate
-    void CheckExit(ir::Exit* e);
-
-    /// Validates the given exit if
-    /// @param e the exit if to validate
-    void CheckExitIf(ExitIf* e);
-
-    /// Validates the given return
-    /// @param r the return to validate
-    void CheckReturn(Return* r);
-
-    /// Validates the @p exit targets a valid @p control instruction where the instruction may jump
-    /// over if control instructions.
-    /// @param exit the exit to validate
-    /// @param control the control instruction targeted
-    void CheckControlsAllowingIf(Exit* exit, Instruction* control);
-
-    /// Validates the given exit switch
-    /// @param s the exit switch to validate
-    void CheckExitSwitch(ExitSwitch* s);
-
-    /// Validates the given exit loop
-    /// @param l the exit loop to validate
-    void CheckExitLoop(ExitLoop* l);
-
-    /// Validates the given load vector element
-    /// @param l the load vector element to validate
-    void CheckLoadVectorElement(LoadVectorElement* l);
-
-    /// Validates the given store vector element
-    /// @param s the store vector element to validate
-    void CheckStoreVectorElement(StoreVectorElement* s);
-
-    /// @param inst the instruction
-    /// @param idx the operand index
-    /// @returns the vector pointer type for the given instruction operand
-    const core::type::Type* GetVectorPtrElementType(Instruction* inst, size_t idx);
-
-  private:
-    Module& mod_;
-    diag::List diagnostics_;
-    Disassembler dis_{mod_};
-
-    Block* current_block_ = nullptr;
-    Hashset<Function*, 4> seen_functions_;
-    Vector<ControlInstruction*, 8> control_stack_;
-
-    void DisassembleIfNeeded();
-};
-
 }  // namespace tint::core::ir
 
 #endif  // SRC_TINT_LANG_CORE_IR_VALIDATOR_H_
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 7d09e7b..52aad79 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -1267,7 +1267,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit-if: has no parent control instruction
+    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit_if: has no parent control instruction
         exit_if  # undef
         ^^^^^^^
 
@@ -1306,7 +1306,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:9 error: exit-if: args count (1) does not match control instruction result count (2)
+        R"(:5:9 error: exit_if: args count (1) does not match control instruction result count (2)
         exit_if 1i  # if_1
         ^^^^^^^^^^
 
@@ -1350,7 +1350,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:9 error: exit-if: args count (3) does not match control instruction result count (2)
+        R"(:5:9 error: exit_if: args count (3) does not match control instruction result count (2)
         exit_if 1i, 2.0f, 3i  # if_1
         ^^^^^^^^^^^^^^^^^^^^
 
@@ -1411,7 +1411,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:21 error: exit-if: argument type (f32) does not match control instruction type (i32)
+        R"(:5:21 error: exit_if: argument type (f32) does not match control instruction type (i32)
         exit_if 1i, 2i  # if_1
                     ^^
 
@@ -1451,7 +1451,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:8:5 error: exit-if: found outside all control instructions
+              R"(:8:5 error: exit_if: found outside all control instructions
     exit_if  # if_1
     ^^^^^^^
 
@@ -1494,7 +1494,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-if: if target jumps over other control instructions
+              R"(:7:13 error: exit_if: if target jumps over other control instructions
             exit_if  # if_1
             ^^^^^^^
 
@@ -1547,7 +1547,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-if: if target jumps over other control instructions
+              R"(:7:13 error: exit_if: if target jumps over other control instructions
             exit_if  # if_1
             ^^^^^^^
 
@@ -1599,7 +1599,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-if: if target jumps over other control instructions
+              R"(:7:13 error: exit_if: if target jumps over other control instructions
             exit_if  # if_1
             ^^^^^^^
 
@@ -1658,7 +1658,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit-switch: has no parent control instruction
+    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit_switch: has no parent control instruction
         exit_switch  # undef
         ^^^^^^^^^^^
 
@@ -1699,7 +1699,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:9 error: exit-switch: args count (1) does not match control instruction result count (2)
+        R"(:5:9 error: exit_switch: args count (1) does not match control instruction result count (2)
         exit_switch 1i  # switch_1
         ^^^^^^^^^^^^^^
 
@@ -1743,7 +1743,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:9 error: exit-switch: args count (3) does not match control instruction result count (2)
+        R"(:5:9 error: exit_switch: args count (3) does not match control instruction result count (2)
         exit_switch 1i, 2.0f, 3i  # switch_1
         ^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -1805,7 +1805,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:25 error: exit-switch: argument type (f32) does not match control instruction type (i32)
+        R"(:5:25 error: exit_switch: argument type (f32) does not match control instruction type (i32)
         exit_switch 1i, 2i  # switch_1
                         ^^
 
@@ -1849,7 +1849,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:10:9 error: exit-switch: switch not found in parent control instructions
+              R"(:10:9 error: exit_switch: switch not found in parent control instructions
         exit_switch  # switch_1
         ^^^^^^^^^^^
 
@@ -1931,7 +1931,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-switch: switch target jumps over other control instructions
+              R"(:7:13 error: exit_switch: switch target jumps over other control instructions
             exit_switch  # switch_1
             ^^^^^^^^^^^
 
@@ -1982,7 +1982,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-switch: switch target jumps over other control instructions
+              R"(:7:13 error: exit_switch: switch target jumps over other control instructions
             exit_switch  # switch_1
             ^^^^^^^^^^^
 
@@ -2039,7 +2039,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit-loop: has no parent control instruction
+    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit_loop: has no parent control instruction
         exit_loop  # undef
         ^^^^^^^^^
 
@@ -2082,7 +2082,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:9 error: exit-loop: args count (1) does not match control instruction result count (2)
+        R"(:5:9 error: exit_loop: args count (1) does not match control instruction result count (2)
         exit_loop 1i  # loop_1
         ^^^^^^^^^^^^
 
@@ -2129,7 +2129,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:9 error: exit-loop: args count (3) does not match control instruction result count (2)
+        R"(:5:9 error: exit_loop: args count (3) does not match control instruction result count (2)
         exit_loop 1i, 2.0f, 3i  # loop_1
         ^^^^^^^^^^^^^^^^^^^^^^
 
@@ -2194,7 +2194,7 @@
     ASSERT_FALSE(res);
     EXPECT_EQ(
         res.Failure().str(),
-        R"(:5:23 error: exit-loop: argument type (f32) does not match control instruction type (i32)
+        R"(:5:23 error: exit_loop: argument type (f32) does not match control instruction type (i32)
         exit_loop 1i, 2i  # loop_1
                       ^^
 
@@ -2240,7 +2240,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:13:9 error: exit-loop: loop not found in parent control instructions
+              R"(:13:9 error: exit_loop: loop not found in parent control instructions
         exit_loop  # loop_1
         ^^^^^^^^^
 
@@ -2324,7 +2324,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-loop: loop target jumps over other control instructions
+              R"(:7:13 error: exit_loop: loop target jumps over other control instructions
             exit_loop  # loop_1
             ^^^^^^^^^
 
@@ -2379,7 +2379,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-loop: loop target jumps over other control instructions
+              R"(:7:13 error: exit_loop: loop target jumps over other control instructions
             exit_loop  # loop_1
             ^^^^^^^^^
 
@@ -2429,7 +2429,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:8:9 error: exit-loop: loop exit jumps out of continuing block
+              R"(:8:9 error: exit_loop: loop exit jumps out of continuing block
         exit_loop  # loop_1
         ^^^^^^^^^
 
@@ -2475,7 +2475,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:10:13 error: exit-loop: loop exit jumps out of continuing block
+              R"(:10:13 error: exit_loop: loop exit jumps out of continuing block
             exit_loop  # loop_1
             ^^^^^^^^^
 
@@ -2527,7 +2527,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:5:9 error: exit-loop: loop exit not permitted in loop initializer
+              R"(:5:9 error: exit_loop: loop exit not permitted in loop initializer
         exit_loop  # loop_1
         ^^^^^^^^^
 
@@ -2577,7 +2577,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:7:13 error: exit-loop: loop exit not permitted in loop initializer
+              R"(:7:13 error: exit_loop: loop exit not permitted in loop initializer
             exit_loop  # loop_1
             ^^^^^^^^^
 
@@ -2748,7 +2748,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().str(),
-              R"(:4:5 error: load-vector-element: instruction result is undefined
+              R"(:4:5 error: load_vector_element: instruction result is undefined
     undef = load_vector_element %2, 1i
     ^^^^^
 
@@ -2778,7 +2778,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:34 error: load-vector-element: operand is undefined
+    EXPECT_EQ(res.Failure().str(), R"(:3:34 error: load_vector_element: operand is undefined
     %2:f32 = load_vector_element undef, 1i
                                  ^^^^^
 
@@ -2808,7 +2808,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:38 error: load-vector-element: operand is undefined
+    EXPECT_EQ(res.Failure().str(), R"(:4:38 error: load_vector_element: operand is undefined
     %3:f32 = load_vector_element %2, undef
                                      ^^^^^
 
@@ -2838,7 +2838,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:26 error: store-vector-element: operand is undefined
+    EXPECT_EQ(res.Failure().str(), R"(:3:26 error: store_vector_element: operand is undefined
     store_vector_element undef, 1i, 2i
                          ^^^^^
 
@@ -2868,7 +2868,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:30 error: store-vector-element: operand is undefined
+    EXPECT_EQ(res.Failure().str(), R"(:4:30 error: store_vector_element: operand is undefined
     store_vector_element %2, undef, 2i
                              ^^^^^
 
@@ -2907,7 +2907,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:34 error: store-vector-element: operand is undefined
+    EXPECT_EQ(res.Failure().str(), R"(:4:34 error: store_vector_element: operand is undefined
     store_vector_element %2, 1i, undef
                                  ^^^^^
 
diff --git a/src/tint/lang/core/ir/var.h b/src/tint/lang/core/ir/var.h
index 8349be5..c10adf3 100644
--- a/src/tint/lang/core/ir/var.h
+++ b/src/tint/lang/core/ir/var.h
@@ -15,6 +15,8 @@
 #ifndef SRC_TINT_LANG_CORE_IR_VAR_H_
 #define SRC_TINT_LANG_CORE_IR_VAR_H_
 
+#include <string>
+
 #include "src/tint/api/common/binding_point.h"
 #include "src/tint/lang/core/access.h"
 #include "src/tint/lang/core/address_space.h"
@@ -53,7 +55,7 @@
     void DestroyIfOnlyAssigned();
 
     /// @returns the friendly name for the instruction
-    std::string_view FriendlyName() override { return "var"; }
+    std::string FriendlyName() override { return "var"; }
 
   private:
     std::optional<struct BindingPoint> binding_point_;
diff --git a/src/tint/lang/core/type/BUILD.bazel b/src/tint/lang/core/type/BUILD.bazel
new file mode 100644
index 0000000..4103661
--- /dev/null
+++ b/src/tint/lang/core/type/BUILD.bazel
@@ -0,0 +1,175 @@
+# 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 = "type",
+  srcs = [
+    "abstract_float.cc",
+    "abstract_int.cc",
+    "abstract_numeric.cc",
+    "array.cc",
+    "array_count.cc",
+    "atomic.cc",
+    "bool.cc",
+    "builtin_structs.cc",
+    "depth_multisampled_texture.cc",
+    "depth_texture.cc",
+    "external_texture.cc",
+    "f16.cc",
+    "f32.cc",
+    "i32.cc",
+    "manager.cc",
+    "matrix.cc",
+    "multisampled_texture.cc",
+    "node.cc",
+    "numeric_scalar.cc",
+    "pointer.cc",
+    "reference.cc",
+    "sampled_texture.cc",
+    "sampler.cc",
+    "sampler_kind.cc",
+    "scalar.cc",
+    "storage_texture.cc",
+    "struct.cc",
+    "texture.cc",
+    "texture_dimension.cc",
+    "type.cc",
+    "u32.cc",
+    "unique_node.cc",
+    "vector.cc",
+    "void.cc",
+  ],
+  hdrs = [
+    "abstract_float.h",
+    "abstract_int.h",
+    "abstract_numeric.h",
+    "array.h",
+    "array_count.h",
+    "atomic.h",
+    "bool.h",
+    "builtin_structs.h",
+    "clone_context.h",
+    "depth_multisampled_texture.h",
+    "depth_texture.h",
+    "external_texture.h",
+    "f16.h",
+    "f32.h",
+    "i32.h",
+    "manager.h",
+    "matrix.h",
+    "multisampled_texture.h",
+    "node.h",
+    "numeric_scalar.h",
+    "pointer.h",
+    "reference.h",
+    "sampled_texture.h",
+    "sampler.h",
+    "sampler_kind.h",
+    "scalar.h",
+    "storage_texture.h",
+    "struct.h",
+    "texture.h",
+    "texture_dimension.h",
+    "type.h",
+    "u32.h",
+    "unique_node.h",
+    "vector.h",
+    "void.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//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_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "array_test.cc",
+    "atomic_test.cc",
+    "bool_test.cc",
+    "builtin_structs_test.cc",
+    "depth_multisampled_texture_test.cc",
+    "depth_texture_test.cc",
+    "external_texture_test.cc",
+    "f16_test.cc",
+    "f32_test.cc",
+    "helper_test.h",
+    "i32_test.cc",
+    "manager_test.cc",
+    "matrix_test.cc",
+    "multisampled_texture_test.cc",
+    "pointer_test.cc",
+    "reference_test.cc",
+    "sampled_texture_test.cc",
+    "sampler_test.cc",
+    "storage_texture_test.cc",
+    "struct_test.cc",
+    "texture_test.cc",
+    "type_test.cc",
+    "u32_test.cc",
+    "vector_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/glsl/BUILD.bazel b/src/tint/lang/glsl/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/lang/glsl/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/lang/glsl/writer/BUILD.bazel b/src/tint/lang/glsl/writer/BUILD.bazel
new file mode 100644
index 0000000..00cd0bd
--- /dev/null
+++ b/src/tint/lang/glsl/writer/BUILD.bazel
@@ -0,0 +1,113 @@
+# 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 = "writer",
+  srcs = [
+    "output.cc",
+    "writer.cc",
+  ],
+  hdrs = [
+    "output.h",
+    "writer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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_glsl_writer": [
+      "//src/tint/lang/glsl/writer/ast_printer",
+      "//src/tint/lang/glsl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "writer_bench.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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_glsl_writer": [
+      "//src/tint/lang/glsl/writer",
+      "//src/tint/lang/glsl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
diff --git a/src/tint/lang/glsl/writer/BUILD.cmake b/src/tint/lang/glsl/writer/BUILD.cmake
index e86bde9..3a28fff 100644
--- a/src/tint/lang/glsl/writer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/BUILD.cmake
@@ -22,6 +22,7 @@
 ################################################################################
 
 include(lang/glsl/writer/ast_printer/BUILD.cmake)
+include(lang/glsl/writer/ast_raise/BUILD.cmake)
 include(lang/glsl/writer/common/BUILD.cmake)
 
 if(TINT_BUILD_GLSL_WRITER)
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
new file mode 100644
index 0000000..35217ed
--- /dev/null
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
@@ -0,0 +1,160 @@
+# 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 = "ast_printer",
+  srcs = [
+    "ast_printer.cc",
+  ],
+  hdrs = [
+    "ast_printer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/helpers",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_glsl_writer": [
+      "//src/tint/lang/glsl/writer/ast_raise",
+      "//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"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "array_accessor_test.cc",
+    "assign_test.cc",
+    "ast_printer_test.cc",
+    "binary_test.cc",
+    "bitcast_test.cc",
+    "block_test.cc",
+    "break_test.cc",
+    "builtin_test.cc",
+    "builtin_texture_test.cc",
+    "call_test.cc",
+    "case_test.cc",
+    "cast_test.cc",
+    "constructor_test.cc",
+    "continue_test.cc",
+    "discard_test.cc",
+    "function_test.cc",
+    "helper_test.h",
+    "identifier_test.cc",
+    "if_test.cc",
+    "import_test.cc",
+    "loop_test.cc",
+    "member_accessor_test.cc",
+    "module_constant_test.cc",
+    "return_test.cc",
+    "sanitizer_test.cc",
+    "storage_buffer_test.cc",
+    "switch_test.cc",
+    "type_test.cc",
+    "unary_op_test.cc",
+    "uniform_buffer_test.cc",
+    "variable_decl_statement_test.cc",
+    "workgroup_var_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_glsl_writer": [
+      "//src/tint/lang/glsl/writer",
+      "//src/tint/lang/glsl/writer/ast_printer",
+      "//src/tint/lang/glsl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  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 f8f380e..2addfbb 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
@@ -62,10 +62,17 @@
 
 if(TINT_BUILD_GLSL_WRITER)
   tint_target_add_dependencies(tint_lang_glsl_writer_ast_printer lib
+    tint_lang_glsl_writer_ast_raise
     tint_lang_glsl_writer_common
   )
 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 f07d9ca..690ceb2 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
@@ -63,7 +63,14 @@
     ]
 
     if (tint_build_glsl_writer) {
-      deps += [ "${tint_src_dir}/lang/glsl/writer/common" ]
+      deps += [
+        "${tint_src_dir}/lang/glsl/writer/ast_raise",
+        "${tint_src_dir}/lang/glsl/writer/common",
+      ]
+    }
+
+    if (tint_build_hlsl_writer) {
+      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_raise" ]
     }
   }
 }
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 cbb5ed4..b99b1bd 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -33,7 +33,12 @@
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/glsl/writer/ast_raise/combine_samplers.h"
+#include "src/tint/lang/glsl/writer/ast_raise/pad_structs.h"
+#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"
@@ -43,15 +48,12 @@
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
 #include "src/tint/lang/wgsl/ast/transform/builtin_polyfill.h"
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
-#include "src/tint/lang/wgsl/ast/transform/combine_samplers.h"
-#include "src/tint/lang/wgsl/ast/transform/decompose_memory_access.h"
 #include "src/tint/lang/wgsl/ast/transform/demote_to_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
-#include "src/tint/lang/wgsl/ast/transform/pad_structs.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
@@ -61,12 +63,11 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/single_entry_point.h"
 #include "src/tint/lang/wgsl/ast/transform/std140.h"
-#include "src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h"
-#include "src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/lang/wgsl/helpers/append_vector.h"
+#include "src/tint/lang/wgsl/helpers/check_supported_extensions.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -223,7 +224,7 @@
     manager.Add<ast::transform::CanonicalizeEntryPointIO>();
 
     // PadStructs must come after CanonicalizeEntryPointIO
-    manager.Add<ast::transform::PadStructs>();
+    manager.Add<PadStructs>();
 
     // DemoteToHelper must come after PromoteSideEffectsToDecl and ExpandCompoundAssignment.
     manager.Add<ast::transform::DemoteToHelper>();
@@ -234,14 +235,13 @@
     // info, instead of combined sampler binding point. As a result, TextureBuiltinsFromUniform also
     // comes before BindingRemapper so the binding point info it reflects is before remapping.
     if (options.texture_builtins_from_uniform) {
-        manager.Add<ast::transform::TextureBuiltinsFromUniform>();
-        data.Add<ast::transform::TextureBuiltinsFromUniform::Config>(
+        manager.Add<TextureBuiltinsFromUniform>();
+        data.Add<TextureBuiltinsFromUniform::Config>(
             options.texture_builtins_from_uniform->ubo_binding);
     }
 
-    data.Add<ast::transform::CombineSamplers::BindingInfo>(options.binding_map,
-                                                           options.placeholder_binding_point);
-    manager.Add<ast::transform::CombineSamplers>();
+    data.Add<CombineSamplers::BindingInfo>(options.binding_map, options.placeholder_binding_point);
+    manager.Add<CombineSamplers>();
 
     data.Add<ast::transform::BindingRemapper::Remappings>(
         options.binding_points, options.access_controls, options.allow_collisions);
@@ -254,7 +254,7 @@
     // Std140 must come after PromoteSideEffectsToDecl and before SimplifyPointers.
     manager.Add<ast::transform::Std140>();
 
-    manager.Add<ast::transform::Texture1DTo2D>();
+    manager.Add<Texture1DTo2D>();
 
     manager.Add<ast::transform::SimplifyPointers>();
 
@@ -264,7 +264,7 @@
     SanitizedResult result;
     ast::transform::DataMap outputs;
     result.program = manager.Run(in, data, outputs);
-    if (auto* res = outputs.Get<ast::transform::TextureBuiltinsFromUniform::Result>()) {
+    if (auto* res = outputs.Get<TextureBuiltinsFromUniform::Result>()) {
         result.needs_internal_uniform_buffer = true;
         result.bindpoint_to_data = std::move(res->bindpoint_to_data);
     }
@@ -277,6 +277,20 @@
 ASTPrinter::~ASTPrinter() = default;
 
 bool ASTPrinter::Generate() {
+    if (!tint::writer::CheckSupportedExtensions(
+            "GLSL", builder_.AST(), diagnostics_,
+            Vector{
+                core::Extension::kChromiumDisableUniformityAnalysis,
+                core::Extension::kChromiumExperimentalDp4A,
+                core::Extension::kChromiumExperimentalFullPtrParameters,
+                core::Extension::kChromiumInternalDualSourceBlending,
+                core::Extension::kChromiumExperimentalReadWriteStorageTexture,
+                core::Extension::kChromiumExperimentalPushConstant,
+                core::Extension::kF16,
+            })) {
+        return false;
+    }
+
     {
         auto out = Line();
         out << "#version " << version_.major_version << version_.minor_version << "0";
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
index 9fc56e4..e699eea 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
@@ -108,5 +108,15 @@
 )");
 }
 
+TEST_F(GlslASTPrinterTest, UnsupportedExtension) {
+    Enable(Source{{12, 34}}, core::Extension::kUndefined);
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_FALSE(gen.Generate());
+    EXPECT_EQ(gen.Diagnostics().str(),
+              R"(12:34 error: GLSL backend does not support extension 'undefined')");
+}
+
 }  // namespace
 }  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
new file mode 100644
index 0000000..ad9c02e
--- /dev/null
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
@@ -0,0 +1,118 @@
+# 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 = "ast_raise",
+  srcs = [
+    "combine_samplers.cc",
+    "pad_structs.cc",
+    "texture_1d_to_2d.cc",
+    "texture_builtins_from_uniform.cc",
+  ],
+  hdrs = [
+    "combine_samplers.h",
+    "pad_structs.h",
+    "texture_1d_to_2d.h",
+    "texture_builtins_from_uniform.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "combine_samplers_test.cc",
+    "pad_structs_test.cc",
+    "texture_1d_to_2d_test.cc",
+    "texture_builtins_from_uniform_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_glsl_writer": [
+      "//src/tint/lang/glsl/writer/ast_raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg b/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg
new file mode 100644
index 0000000..7459430
--- /dev/null
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_glsl_writer"
+}
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
new file mode 100644
index 0000000..8f3c78a
--- /dev/null
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
@@ -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.
+
+################################################################################
+# 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_WRITER)
+################################################################################
+# Target:    tint_lang_glsl_writer_ast_raise
+# Kind:      lib
+# Condition: TINT_BUILD_GLSL_WRITER
+################################################################################
+tint_add_target(tint_lang_glsl_writer_ast_raise lib
+  lang/glsl/writer/ast_raise/combine_samplers.cc
+  lang/glsl/writer/ast_raise/combine_samplers.h
+  lang/glsl/writer/ast_raise/pad_structs.cc
+  lang/glsl/writer/ast_raise/pad_structs.h
+  lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
+  lang/glsl/writer/ast_raise/texture_1d_to_2d.h
+  lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
+  lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
+)
+
+tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise lib
+  tint_api_common
+  tint_api_options
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_program
+  tint_lang_wgsl_resolver
+  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
+)
+
+endif(TINT_BUILD_GLSL_WRITER)
+if(TINT_BUILD_GLSL_WRITER)
+################################################################################
+# Target:    tint_lang_glsl_writer_ast_raise_test
+# Kind:      test
+# Condition: TINT_BUILD_GLSL_WRITER
+################################################################################
+tint_add_target(tint_lang_glsl_writer_ast_raise_test test
+  lang/glsl/writer/ast_raise/combine_samplers_test.cc
+  lang/glsl/writer/ast_raise/pad_structs_test.cc
+  lang/glsl/writer/ast_raise/texture_1d_to_2d_test.cc
+  lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise_test test
+  tint_api_common
+  tint_api_options
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  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
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_glsl_writer_ast_raise_test test
+  "gtest"
+)
+
+if(TINT_BUILD_GLSL_WRITER)
+  tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise_test test
+    tint_lang_glsl_writer_ast_raise
+  )
+endif(TINT_BUILD_GLSL_WRITER)
+
+endif(TINT_BUILD_GLSL_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
new file mode 100644
index 0000000..c0bc3b3
--- /dev/null
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
@@ -0,0 +1,114 @@
+# 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_unittests) {
+  import("//testing/test.gni")
+}
+if (tint_build_glsl_writer) {
+  libtint_source_set("ast_raise") {
+    sources = [
+      "combine_samplers.cc",
+      "combine_samplers.h",
+      "pad_structs.cc",
+      "pad_structs.h",
+      "texture_1d_to_2d.cc",
+      "texture_1d_to_2d.h",
+      "texture_builtins_from_uniform.cc",
+      "texture_builtins_from_uniform.h",
+    ]
+    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/type",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${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_unittests) {
+  if (tint_build_glsl_writer) {
+    tint_unittests_source_set("unittests") {
+      testonly = true
+      sources = [
+        "combine_samplers_test.cc",
+        "pad_structs_test.cc",
+        "texture_1d_to_2d_test.cc",
+        "texture_builtins_from_uniform_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/type",
+        "${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",
+        "${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/ast_raise" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/ast/transform/combine_samplers.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
similarity index 85%
rename from src/tint/lang/wgsl/ast/transform/combine_samplers.cc
rename to src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
index 70db874..84647d0 100644
--- a/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/combine_samplers.h"
+#include "src/tint/lang/glsl/writer/ast_raise/combine_samplers.h"
 
 #include <string>
 #include <unordered_map>
@@ -27,8 +27,8 @@
 
 #include "src/tint/utils/containers/map.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::CombineSamplers);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::CombineSamplers::BindingInfo);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::CombineSamplers);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::CombineSamplers::BindingInfo);
 
 namespace {
 
@@ -39,7 +39,7 @@
 
 }  // namespace
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
@@ -62,7 +62,7 @@
 
     /// Map from a texture/sampler pair to the corresponding combined sampler
     /// variable
-    using CombinedTextureSamplerMap = std::unordered_map<sem::VariablePair, const Variable*>;
+    using CombinedTextureSamplerMap = std::unordered_map<sem::VariablePair, const ast::Variable*>;
 
     /// A map of all global texture/sampler variable pairs to the global
     /// combined sampler variable that will replace it.
@@ -77,14 +77,14 @@
     /// references (one comparison sampler, one regular). These are also used as
     /// temporary sampler parameters to the texture builtins to satisfy the WGSL
     /// resolver, but are then ignored and removed by the GLSL writer.
-    const Variable* placeholder_samplers_[2] = {};
+    const ast::Variable* placeholder_samplers_[2] = {};
 
     /// Group and binding attributes used by all combined sampler globals.
     /// Group 0 and binding 0 are used, with collisions disabled.
     /// @returns the newly-created attribute list
     auto Attributes() const {
-        tint::Vector<const Attribute*, 3> attributes{ctx.dst->Group(0_a), ctx.dst->Binding(0_a)};
-        attributes.Push(ctx.dst->Disable(DisabledValidation::kBindingPointCollision));
+        Vector<const ast::Attribute*, 3> attributes{ctx.dst->Group(0_a), ctx.dst->Binding(0_a)};
+        attributes.Push(ctx.dst->Disable(ast::DisabledValidation::kBindingPointCollision));
         return attributes;
     }
 
@@ -100,9 +100,9 @@
     /// @param sampler_var the sampler (global) variable
     /// @param name the default name to use (may be overridden by map lookup)
     /// @returns the newly-created global variable
-    const Variable* CreateCombinedGlobal(const sem::Variable* texture_var,
-                                         const sem::Variable* sampler_var,
-                                         std::string name) {
+    const ast::Variable* CreateCombinedGlobal(const sem::Variable* texture_var,
+                                              const sem::Variable* sampler_var,
+                                              std::string name) {
         SamplerTexturePair bp_pair;
         bp_pair.texture_binding_point = *texture_var->As<sem::GlobalVariable>()->BindingPoint();
         bp_pair.sampler_binding_point =
@@ -112,7 +112,7 @@
         if (it != binding_info->binding_map.end()) {
             name = it->second;
         }
-        Type type = CreateCombinedASTTypeFor(texture_var, sampler_var);
+        ast::Type type = CreateCombinedASTTypeFor(texture_var, sampler_var);
         Symbol symbol = ctx.dst->Symbols().New(name);
         return ctx.dst->GlobalVar(symbol, type, Attributes());
     }
@@ -120,8 +120,8 @@
     /// Creates placeholder global sampler variables.
     /// @param kind the sampler kind to create for
     /// @returns the newly-created global variable
-    const Variable* CreatePlaceholder(core::type::SamplerKind kind) {
-        Type type = ctx.dst->ty.sampler(kind);
+    const ast::Variable* CreatePlaceholder(core::type::SamplerKind kind) {
+        ast::Type type = ctx.dst->ty.sampler(kind);
         const char* name = kind == core::type::SamplerKind::kComparisonSampler
                                ? "placeholder_comparison_sampler"
                                : "placeholder_sampler";
@@ -135,7 +135,7 @@
     /// @param texture the texture variable of interest
     /// @param sampler the texture variable of interest
     /// @returns the newly-created type
-    Type CreateCombinedASTTypeFor(const sem::Variable* texture, const sem::Variable* sampler) {
+    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) {
@@ -160,7 +160,8 @@
                 ctx.Remove(ctx.src->AST().GlobalDeclarations(), global);
             } else if (auto binding_point = global_sem->BindingPoint()) {
                 if (binding_point->group == 0 && binding_point->binding == 0) {
-                    auto* attribute = ctx.dst->Disable(DisabledValidation::kBindingPointCollision);
+                    auto* attribute =
+                        ctx.dst->Disable(ast::DisabledValidation::kBindingPointCollision);
                     ctx.InsertFront(global->attributes, attribute);
                 }
             }
@@ -168,13 +169,13 @@
 
         // Rewrite all function signatures to use combined samplers, and remove
         // separate textures & samplers. Create new combined globals where found.
-        ctx.ReplaceAll([&](const Function* ast_fn) -> const Function* {
+        ctx.ReplaceAll([&](const ast::Function* ast_fn) -> const ast::Function* {
             if (auto* fn = sem.Get(ast_fn)) {
                 auto pairs = fn->TextureSamplerPairs();
                 if (pairs.IsEmpty()) {
                     return nullptr;
                 }
-                tint::Vector<const Parameter*, 8> params;
+                Vector<const ast::Parameter*, 8> params;
                 for (auto pair : fn->TextureSamplerPairs()) {
                     const sem::Variable* texture_var = pair.first;
                     const sem::Variable* sampler_var = pair.second;
@@ -185,13 +186,13 @@
                     if (IsGlobal(pair)) {
                         // Both texture and sampler are global; add a new global variable
                         // to represent the combined sampler (if not already created).
-                        tint::GetOrCreate(global_combined_texture_samplers_, pair, [&] {
+                        GetOrCreate(global_combined_texture_samplers_, pair, [&] {
                             return CreateCombinedGlobal(texture_var, sampler_var, name);
                         });
                     } else {
                         // Either texture or sampler (or both) is a function parameter;
                         // add a new function parameter to represent the combined sampler.
-                        Type type = CreateCombinedASTTypeFor(texture_var, sampler_var);
+                        ast::Type type = CreateCombinedASTTypeFor(texture_var, sampler_var);
                         auto* var = ctx.dst->Param(ctx.dst->Symbols().New(name), type);
                         params.Push(var);
                         function_combined_texture_samplers_[fn][pair] = var;
@@ -211,9 +212,9 @@
                 auto* body = ctx.Clone(ast_fn->body);
                 auto attributes = ctx.Clone(ast_fn->attributes);
                 auto return_type_attributes = ctx.Clone(ast_fn->return_type_attributes);
-                return ctx.dst->create<Function>(name, params, return_type, body,
-                                                 std::move(attributes),
-                                                 std::move(return_type_attributes));
+                return ctx.dst->create<ast::Function>(name, params, return_type, body,
+                                                      std::move(attributes),
+                                                      std::move(return_type_attributes));
             }
             return nullptr;
         });
@@ -221,9 +222,9 @@
         // Replace all function call expressions containing texture or
         // sampler parameters to use the current function's combined samplers or
         // the combined global samplers, as appropriate.
-        ctx.ReplaceAll([&](const CallExpression* expr) -> const Expression* {
+        ctx.ReplaceAll([&](const ast::CallExpression* expr) -> const ast::Expression* {
             if (auto* call = sem.Get(expr)->UnwrapMaterialize()->As<sem::Call>()) {
-                tint::Vector<const Expression*, 8> args;
+                Vector<const ast::Expression*, 8> args;
                 // Replace all texture builtin calls.
                 if (auto* builtin = call->Target()->As<sem::Builtin>()) {
                     const auto& signature = builtin->Signature();
@@ -250,7 +251,7 @@
                     for (auto* arg : expr->args) {
                         auto* type = ctx.src->TypeOf(arg)->UnwrapRef();
                         if (type->Is<core::type::Texture>()) {
-                            const Variable* var =
+                            const ast::Variable* var =
                                 IsGlobal(new_pair)
                                     ? global_combined_texture_samplers_[new_pair]
                                     : function_combined_texture_samplers_[call->Stmt()->Function()]
@@ -259,7 +260,7 @@
                         } else if (auto* sampler_type = type->As<core::type::Sampler>()) {
                             core::type::SamplerKind kind = sampler_type->kind();
                             int index = (kind == core::type::SamplerKind::kSampler) ? 0 : 1;
-                            const Variable*& p = placeholder_samplers_[index];
+                            const ast::Variable*& p = placeholder_samplers_[index];
                             if (!p) {
                                 p = CreatePlaceholder(kind);
                             }
@@ -268,10 +269,10 @@
                             args.Push(ctx.Clone(arg));
                         }
                     }
-                    const Expression* value = ctx.dst->Call(ctx.Clone(expr->target), args);
+                    const ast::Expression* value = ctx.dst->Call(ctx.Clone(expr->target), args);
                     if (builtin->Type() == core::Function::kTextureLoad &&
                         texture_var->Type()->UnwrapRef()->Is<core::type::DepthTexture>() &&
-                        !call->Stmt()->Declaration()->Is<CallStatement>()) {
+                        !call->Stmt()->Declaration()->Is<ast::CallStatement>()) {
                         value = ctx.dst->MemberAccessor(value, "x");
                     }
                     return value;
@@ -303,7 +304,7 @@
                         // If both texture and sampler are (now) global, pass that
                         // global variable to the callee. Otherwise use the caller's
                         // function parameter for this pair.
-                        const Variable* var =
+                        const ast::Variable* var =
                             IsGlobal(new_pair)
                                 ? global_combined_texture_samplers_[new_pair]
                                 : function_combined_texture_samplers_[call->Stmt()->Function()]
@@ -335,9 +336,9 @@
 
 CombineSamplers::~CombineSamplers() = default;
 
-Transform::ApplyResult CombineSamplers::Apply(const Program* src,
-                                              const DataMap& inputs,
-                                              DataMap&) const {
+ast::transform::Transform::ApplyResult CombineSamplers::Apply(const Program* src,
+                                                              const ast::transform::DataMap& inputs,
+                                                              ast::transform::DataMap&) const {
     auto* binding_info = inputs.Get<BindingInfo>();
     if (!binding_info) {
         ProgramBuilder b;
@@ -349,4 +350,4 @@
     return State(src, binding_info).Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/combine_samplers.h b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.h
similarity index 85%
rename from src/tint/lang/wgsl/ast/transform/combine_samplers.h
rename to src/tint/lang/glsl/writer/ast_raise/combine_samplers.h
index 67fbcb5..725e66d 100644
--- a/src/tint/lang/wgsl/ast/transform/combine_samplers.h
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_COMBINE_SAMPLERS_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_COMBINE_SAMPLERS_H_
+#ifndef SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_COMBINE_SAMPLERS_H_
+#define SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_COMBINE_SAMPLERS_H_
 
 #include <string>
 #include <unordered_map>
@@ -21,7 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 #include "src/tint/lang/wgsl/sem/sampler_texture_pair.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 /// This transform converts all separate texture/sampler refences in a
 /// program into combined texture/samplers. This is required for GLSL,
@@ -52,7 +52,7 @@
 /// information needed to represent a combined sampler in GLSL
 /// (dimensionality, component type, etc). The GLSL writer outputs such
 /// (Tint) Textures as (GLSL) Samplers.
-class CombineSamplers final : public Castable<CombineSamplers, Transform> {
+class CombineSamplers final : public Castable<CombineSamplers, ast::transform::Transform> {
   public:
     /// A pair of binding points.
     using SamplerTexturePair = sem::SamplerTexturePair;
@@ -62,7 +62,7 @@
 
     /// The client-provided mapping from separate texture and sampler binding
     /// points to combined sampler binding point.
-    struct BindingInfo final : public Castable<BindingInfo, Data> {
+    struct BindingInfo final : public Castable<BindingInfo, ast::transform::Data> {
         /// Constructor
         /// @param map the map of all (texture, sampler) -> (combined) pairs
         /// @param placeholder the binding point to use for placeholder samplers.
@@ -88,15 +88,15 @@
     /// Destructor
     ~CombineSamplers() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_COMBINE_SAMPLERS_H_
+#endif  // SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_COMBINE_SAMPLERS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/combine_samplers_test.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc
similarity index 96%
rename from src/tint/lang/wgsl/ast/transform/combine_samplers_test.cc
rename to src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc
index 1d8afe5..28f8c74 100644
--- a/src/tint/lang/wgsl/ast/transform/combine_samplers_test.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers_test.cc
@@ -12,23 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/combine_samplers.h"
+#include "src/tint/lang/glsl/writer/ast_raise/combine_samplers.h"
 
 #include <memory>
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 namespace {
 
-using CombineSamplersTest = TransformTest;
+using CombineSamplersTest = ast::transform::TransformTest;
 
 TEST_F(CombineSamplersTest, EmptyModule) {
     auto* src = "";
     auto* expect = "";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -55,7 +55,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -82,7 +82,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -117,7 +117,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -152,7 +152,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -179,7 +179,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     CombineSamplers::BindingMap map;
     sem::SamplerTexturePair pair;
     pair.texture_binding_point.group = 0;
@@ -214,7 +214,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     CombineSamplers::BindingMap map;
     sem::SamplerTexturePair pair;
     pair.texture_binding_point.group = 3;
@@ -262,7 +262,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -300,7 +300,7 @@
 alias Tex2d = texture_2d<f32>;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -343,7 +343,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -385,7 +385,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -428,7 +428,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -491,7 +491,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -538,7 +538,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -585,7 +585,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -624,7 +624,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -661,7 +661,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -700,7 +700,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -737,7 +737,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -776,7 +776,7 @@
     pair.sampler_binding_point.binding = placeholder.binding;
     CombineSamplers::BindingMap map;
     map[pair] = "fred";
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(map, placeholder);
     auto got = Run<CombineSamplers>(src, data);
 
@@ -820,7 +820,7 @@
     CombineSamplers::BindingMap map;
     map[pair] = "barney";
     map[placeholder_pair] = "fred";
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(map, placeholder);
     auto got = Run<CombineSamplers>(src, data);
 
@@ -847,7 +847,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -882,7 +882,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -916,7 +916,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -947,7 +947,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -977,7 +977,7 @@
 @internal(disable_validation__binding_point_collision) @group(0) @binding(0) var<uniform> gcoords : vec2<f32>;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -985,4 +985,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/pad_structs.cc b/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
similarity index 76%
rename from src/tint/lang/wgsl/ast/transform/pad_structs.cc
rename to src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
index 7b47952..aa35141 100644
--- a/src/tint/lang/wgsl/ast/transform/pad_structs.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/pad_structs.h"
+#include "src/tint/lang/glsl/writer/ast_raise/pad_structs.h"
 
 #include <string>
 #include <unordered_map>
@@ -29,14 +29,14 @@
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::PadStructs);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::PadStructs);
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 namespace {
 
-void CreatePadding(Vector<const StructMember*, 8>* new_members,
-                   Hashset<const StructMember*, 8>* padding_members,
+void CreatePadding(Vector<const ast::StructMember*, 8>* new_members,
+                   Hashset<const ast::StructMember*, 8>* padding_members,
                    ast::Builder* b,
                    uint32_t bytes) {
     const size_t count = bytes / 4u;
@@ -56,22 +56,24 @@
 
 PadStructs::~PadStructs() = default;
 
-Transform::ApplyResult PadStructs::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult PadStructs::Apply(const Program* src,
+                                                         const ast::transform::DataMap&,
+                                                         ast::transform::DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     auto& sem = src->Sem();
 
-    std::unordered_map<const Struct*, const Struct*> replaced_structs;
-    Hashset<const StructMember*, 8> padding_members;
+    std::unordered_map<const ast::Struct*, const ast::Struct*> replaced_structs;
+    Hashset<const ast::StructMember*, 8> padding_members;
 
-    ctx.ReplaceAll([&](const Struct* ast_str) -> const Struct* {
+    ctx.ReplaceAll([&](const ast::Struct* ast_str) -> const ast::Struct* {
         auto* str = sem.Get(ast_str);
         if (!str || !str->IsHostShareable()) {
             return nullptr;
         }
         uint32_t offset = 0;
         bool has_runtime_sized_array = false;
-        tint::Vector<const StructMember*, 8> new_members;
+        tint::Vector<const ast::StructMember*, 8> new_members;
         for (auto* mem : str->Members()) {
             auto name = mem->Name().Name();
 
@@ -106,18 +108,19 @@
             CreatePadding(&new_members, &padding_members, ctx.dst, struct_size - offset);
         }
 
-        tint::Vector<const Attribute*, 1> struct_attribs;
+        tint::Vector<const ast::Attribute*, 1> struct_attribs;
         if (!padding_members.IsEmpty()) {
-            struct_attribs = tint::Vector{b.Disable(DisabledValidation::kIgnoreStructMemberLimit)};
+            struct_attribs =
+                tint::Vector{b.Disable(ast::DisabledValidation::kIgnoreStructMemberLimit)};
         }
 
-        auto* new_struct = b.create<Struct>(ctx.Clone(ast_str->name), std::move(new_members),
-                                            std::move(struct_attribs));
+        auto* new_struct = b.create<ast::Struct>(ctx.Clone(ast_str->name), std::move(new_members),
+                                                 std::move(struct_attribs));
         replaced_structs[ast_str] = new_struct;
         return new_struct;
     });
 
-    ctx.ReplaceAll([&](const CallExpression* ast_call) -> const CallExpression* {
+    ctx.ReplaceAll([&](const ast::CallExpression* ast_call) -> const ast::CallExpression* {
         if (ast_call->args.Length() == 0) {
             return nullptr;
         }
@@ -140,7 +143,7 @@
             return nullptr;
         }
 
-        tint::Vector<const Expression*, 8> new_args;
+        tint::Vector<const ast::Expression*, 8> new_args;
 
         auto* arg = ast_call->args.begin();
         for (auto* member : new_struct->members) {
@@ -158,4 +161,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/pad_structs.h b/src/tint/lang/glsl/writer/ast_raise/pad_structs.h
similarity index 67%
rename from src/tint/lang/wgsl/ast/transform/pad_structs.h
rename to src/tint/lang/glsl/writer/ast_raise/pad_structs.h
index f8f50f4..33fe20d 100644
--- a/src/tint/lang/wgsl/ast/transform/pad_structs.h
+++ b/src/tint/lang/glsl/writer/ast_raise/pad_structs.h
@@ -12,19 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_PAD_STRUCTS_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_PAD_STRUCTS_H_
+#ifndef SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_PAD_STRUCTS_H_
+#define SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_PAD_STRUCTS_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 /// This transform turns all explicit alignment and sizing into padding
 /// members of structs. This is required for GLSL ES, since it not support
 /// the offset= decoration.
 ///
 /// @note This transform requires the CanonicalizeEntryPointIO transform to have been run first.
-class PadStructs final : public Castable<PadStructs, Transform> {
+class PadStructs final : public Castable<PadStructs, ast::transform::Transform> {
   public:
     /// Constructor
     PadStructs();
@@ -32,12 +32,12 @@
     /// Destructor
     ~PadStructs() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_PAD_STRUCTS_H_
+#endif  // SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_PAD_STRUCTS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/pad_structs_test.cc b/src/tint/lang/glsl/writer/ast_raise/pad_structs_test.cc
similarity index 92%
rename from src/tint/lang/wgsl/ast/transform/pad_structs_test.cc
rename to src/tint/lang/glsl/writer/ast_raise/pad_structs_test.cc
index 212fad2..4f2e26f 100644
--- a/src/tint/lang/wgsl/ast/transform/pad_structs_test.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/pad_structs_test.cc
@@ -12,23 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/pad_structs.h"
+#include "src/tint/lang/glsl/writer/ast_raise/pad_structs.h"
 
 #include <memory>
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 namespace {
 
-using PadStructsTest = TransformTest;
+using PadStructsTest = ast::transform::TransformTest;
 
 TEST_F(PadStructsTest, EmptyModule) {
     auto* src = "";
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -61,7 +61,7 @@
   let x = u.x;
 }
 )";
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -97,7 +97,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -137,7 +137,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -179,7 +179,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -219,7 +219,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -254,7 +254,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -300,7 +300,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -350,7 +350,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -396,7 +396,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -433,7 +433,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -477,7 +477,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -524,7 +524,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -561,7 +561,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -598,11 +598,11 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
similarity index 86%
rename from src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
rename to src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
index c47dabf..aa23d0b 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h"
+#include "src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h"
 
 #include <utility>
 
@@ -25,11 +25,11 @@
 #include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/utils/rtti/switch.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::Texture1DTo2D);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::Texture1DTo2D);
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 namespace {
 
@@ -87,18 +87,18 @@
             return SkipTransform;
         }
 
-        auto create_var = [&](const Variable* v, Type type) -> const Variable* {
-            if (v->As<Parameter>()) {
+        auto create_var = [&](const ast::Variable* v, ast::Type type) -> const ast::Variable* {
+            if (v->As<ast::Parameter>()) {
                 return ctx.dst->Param(ctx.Clone(v->name->symbol), type, ctx.Clone(v->attributes));
             } else {
                 return ctx.dst->Var(ctx.Clone(v->name->symbol), type, ctx.Clone(v->attributes));
             }
         };
 
-        ctx.ReplaceAll([&](const Variable* v) -> const Variable* {
-            const Variable* r = Switch(
+        ctx.ReplaceAll([&](const ast::Variable* v) -> const ast::Variable* {
+            const ast::Variable* r = Switch(
                 sem.Get(v)->Type()->UnwrapRef(),
-                [&](const core::type::SampledTexture* tex) -> const Variable* {
+                [&](const core::type::SampledTexture* tex) -> const ast::Variable* {
                     if (tex->dim() == core::type::TextureDimension::k1d) {
                         auto type = ctx.dst->ty.sampled_texture(core::type::TextureDimension::k2d,
                                                                 CreateASTTypeFor(ctx, tex->type()));
@@ -107,7 +107,7 @@
                         return nullptr;
                     }
                 },
-                [&](const core::type::StorageTexture* storage_tex) -> const Variable* {
+                [&](const core::type::StorageTexture* storage_tex) -> const ast::Variable* {
                     if (storage_tex->dim() == core::type::TextureDimension::k1d) {
                         auto type = ctx.dst->ty.storage_texture(core::type::TextureDimension::k2d,
                                                                 storage_tex->texel_format(),
@@ -121,7 +121,7 @@
             return r;
         });
 
-        ctx.ReplaceAll([&](const CallExpression* c) -> const Expression* {
+        ctx.ReplaceAll([&](const ast::CallExpression* c) -> const ast::Expression* {
             auto* call = sem.Get(c)->UnwrapMaterialize()->As<sem::Call>();
             if (!call) {
                 return nullptr;
@@ -143,7 +143,7 @@
             if (builtin->Type() == core::Function::kTextureDimensions) {
                 // If this textureDimensions() call is in a CallStatement, we can leave it
                 // unmodified since the return value will be dropped on the floor anyway.
-                if (call->Stmt()->Declaration()->Is<CallStatement>()) {
+                if (call->Stmt()->Declaration()->Is<ast::CallStatement>()) {
                     return nullptr;
                 }
                 auto* new_call = ctx.CloneWithoutTransform(c);
@@ -155,14 +155,14 @@
                 return nullptr;
             }
 
-            tint::Vector<const Expression*, 8> args;
+            tint::Vector<const ast::Expression*, 8> args;
             int index = 0;
             for (auto* arg : c->args) {
                 if (index == coords_index) {
                     auto* ctype = call->Arguments()[static_cast<size_t>(coords_index)]->Type();
                     auto* coords = c->args[static_cast<size_t>(coords_index)];
 
-                    const LiteralExpression* half = nullptr;
+                    const ast::LiteralExpression* half = nullptr;
                     if (ctype->is_integer_scalar()) {
                         half = ctx.dst->Expr(0_a);
                     } else {
@@ -187,8 +187,10 @@
 
 Texture1DTo2D::~Texture1DTo2D() = default;
 
-Transform::ApplyResult Texture1DTo2D::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult Texture1DTo2D::Apply(const Program* src,
+                                                            const ast::transform::DataMap&,
+                                                            ast::transform::DataMap&) const {
     return State(src).Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h
similarity index 64%
rename from src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h
rename to src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h
index f7b7cd7..bd43aea 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_1D_TO_2D_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_1D_TO_2D_H_
+#ifndef SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_1D_TO_2D_H_
+#define SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_1D_TO_2D_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 /// This transform converts all 1D texture types and accesses to 2D.
 /// This is required for GLSL ES, which does not support 1D textures.
-class Texture1DTo2D final : public Castable<Texture1DTo2D, Transform> {
+class Texture1DTo2D final : public Castable<Texture1DTo2D, ast::transform::Transform> {
   public:
     /// Constructor
     Texture1DTo2D();
@@ -29,15 +29,15 @@
     /// Destructor
     ~Texture1DTo2D() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_1D_TO_2D_H_
+#endif  // SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_1D_TO_2D_H_
diff --git a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d_test.cc b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d_test.cc
similarity index 89%
rename from src/tint/lang/wgsl/ast/transform/texture_1d_to_2d_test.cc
rename to src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d_test.cc
index cdddfbd..c4257c3 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d_test.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d_test.cc
@@ -12,18 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h"
+#include "src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h"
+
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 namespace {
 
-using Texture1DTo2DTest = TransformTest;
+using Texture1DTo2DTest = ast::transform::TransformTest;
 
 TEST_F(Texture1DTo2DTest, EmptyModule) {
     auto* src = "";
 
-    DataMap data;
+    ast::transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -39,7 +40,7 @@
 @group(0) @binding(1) var s : sampler;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -65,7 +66,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -87,7 +88,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -109,7 +110,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -131,7 +132,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -153,7 +154,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -167,7 +168,7 @@
 @group(0) @binding(0) var t : texture_storage_2d<r32float, write>;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -184,7 +185,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -193,7 +194,7 @@
 var<private> i : i32;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -202,7 +203,7 @@
 @group(0) @binding(0) var<uniform> m : mat2x2<f32>;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -234,7 +235,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -265,7 +266,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -290,11 +291,11 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
similarity index 94%
rename from src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.cc
rename to src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
index 4c4baee..02c056d 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h"
+#include "src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h"
 
 #include <memory>
 #include <queue>
@@ -34,11 +34,11 @@
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/rtti/switch.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TextureBuiltinsFromUniform);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TextureBuiltinsFromUniform::Config);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TextureBuiltinsFromUniform::Result);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::TextureBuiltinsFromUniform);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::TextureBuiltinsFromUniform::Config);
+TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::TextureBuiltinsFromUniform::Result);
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 namespace {
 
@@ -74,7 +74,9 @@
     /// @param program the source program
     /// @param in the input transform data
     /// @param out the output transform data
-    explicit State(const Program* program, const DataMap& in, DataMap& out)
+    explicit State(const Program* program,
+                   const ast::transform::DataMap& in,
+                   ast::transform::DataMap& out)
         : src(program), inputs(in), outputs(out) {}
 
     /// Runs the transform
@@ -114,7 +116,7 @@
                                 return;
                             }
                             if (auto* call_stmt =
-                                    call->Stmt()->Declaration()->As<CallStatement>()) {
+                                    call->Stmt()->Declaration()->As<ast::CallStatement>()) {
                                 if (call_stmt->expr == call->Declaration()) {
                                     // textureNumLevels() / textureNumSamples() is used as a
                                     // statement. The argument expression must be side-effect free,
@@ -262,9 +264,9 @@
     /// The source program
     const Program* const src;
     /// The transform inputs
-    const DataMap& inputs;
+    const ast::transform::DataMap& inputs;
     /// The transform outputs
-    DataMap& outputs;
+    ast::transform::DataMap& outputs;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
@@ -311,11 +313,13 @@
     /// f(tex, internal_uniform.texture_builtin_value), if tex is from a global
     /// variable, store the BindingPoint. or f(tex, extra_param_tex), if tex is from a function
     /// param, store the texture function parameter pointer.
-    Hashmap<const CallExpression*, Vector<std::variant<BindingPoint, const ast::Parameter*>, 4>, 8>
+    Hashmap<const ast::CallExpression*,
+            Vector<std::variant<BindingPoint, const ast::Parameter*>, 4>,
+            8>
         call_to_data;
 
     /// Texture builtin calls to be replaced by either uniform values or function parameters.
-    Hashmap<const CallExpression*, std::variant<BindingPoint, const ast::Parameter*>, 8>
+    Hashmap<const ast::CallExpression*, std::variant<BindingPoint, const ast::Parameter*>, 8>
         builtin_to_replace;
 
     /// A map from global texture bindpoint to the symbol storing its builtin value in the uniform
@@ -323,7 +327,7 @@
     Hashmap<BindingPoint, Symbol, 16> bindpoint_to_syms;
 
     /// The internal uniform buffer
-    const Variable* ubo = nullptr;
+    const ast::Variable* ubo = nullptr;
     /// Get or create a UBO including u32 scalars for texture builtin values.
     /// @returns the symbol of the uniform buffer variable.
     Symbol GetUboSym() {
@@ -348,7 +352,7 @@
         }
 
         // Find if there's any existing global variable using the same cfg->ubo_binding
-        for (auto* var : src->AST().Globals<Var>()) {
+        for (auto* var : src->AST().Globals<ast::Var>()) {
             if (var->HasBindingPoint()) {
                 auto* global_sem = sem.Get<sem::GlobalVariable>(var);
 
@@ -361,7 +365,7 @@
                     // Replace it with a new struct including the new_member.
                     // Then remove the old structure global declaration.
 
-                    ubo = var->As<Variable>();
+                    ubo = var->As<ast::Variable>();
 
                     auto* ty = global_sem->Type()->UnwrapRef();
                     auto* str = ty->As<sem::Struct>();
@@ -437,7 +441,7 @@
 
         const ast::Parameter* param = nullptr;
         for (auto p : fn->Declaration()->params) {
-            if (p->As<Variable>() == var->Declaration()) {
+            if (p->As<ast::Variable>() == var->Declaration()) {
                 param = p;
                 break;
             }
@@ -471,9 +475,10 @@
     }
 };
 
-Transform::ApplyResult TextureBuiltinsFromUniform::Apply(const Program* src,
-                                                         const DataMap& inputs,
-                                                         DataMap& outputs) const {
+ast::transform::Transform::ApplyResult TextureBuiltinsFromUniform::Apply(
+    const Program* src,
+    const ast::transform::DataMap& inputs,
+    ast::transform::DataMap& outputs) const {
     return State{src, inputs, outputs}.Run();
 }
 
@@ -488,4 +493,4 @@
 TextureBuiltinsFromUniform::Result::Result(const Result&) = default;
 TextureBuiltinsFromUniform::Result::~Result() = default;
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
similarity index 84%
rename from src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h
rename to src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
index 0db043e..53ba1e3 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_BUILTINS_FROM_UNIFORM_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+#ifndef SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+#define SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_BUILTINS_FROM_UNIFORM_H_
 
 #include <unordered_map>
 #include <unordered_set>
@@ -27,7 +27,7 @@
 class CloneContext;
 }  // namespace tint
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 
 /// TextureBuiltinsFromUniform is a transform that implements calls to textureNumLevels() and
 /// textureNumSamples() by retrieving the texture information from a uniform buffer, as those
@@ -50,7 +50,8 @@
 ///
 /// This transform must run before `CombineSamplers` transform so that the binding point of the
 /// original texture object can be preserved.
-class TextureBuiltinsFromUniform final : public Castable<TextureBuiltinsFromUniform, Transform> {
+class TextureBuiltinsFromUniform final
+    : public Castable<TextureBuiltinsFromUniform, ast::transform::Transform> {
   public:
     /// Constructor
     TextureBuiltinsFromUniform();
@@ -58,7 +59,7 @@
     ~TextureBuiltinsFromUniform() override;
 
     /// Configuration options for the TextureBuiltinsFromUniform transform.
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public Castable<Config, ast::transform::Data> {
         /// Constructor
         /// @param ubo_bp the binding point to use for the generated uniform buffer.
         explicit Config(BindingPoint ubo_bp);
@@ -80,7 +81,7 @@
     /// Information produced about what the transform did.
     /// If there were no calls to the textureNumLevels() or textureNumSamples() builtin, then no
     /// Result will be emitted.
-    struct Result final : public Castable<Result, Data> {
+    struct Result final : public Castable<Result, ast::transform::Data> {
         /// Using for shorter names
         /// Records the field and the byte offset of the data to push in the internal uniform
         /// buffer.
@@ -105,15 +106,15 @@
         BindingPointToFieldAndOffset bindpoint_to_data;
     };
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+#endif  // SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_BUILTINS_FROM_UNIFORM_H_
diff --git a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
similarity index 95%
rename from src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc
rename to src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
index 21185e8..7e04856 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
@@ -12,23 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h"
+#include "src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h"
 
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::glsl::writer {
 namespace {
 
-using TextureBuiltinsFromUniformTest = TransformTest;
+using TextureBuiltinsFromUniformTest = ast::transform::TransformTest;
 
 TEST_F(TextureBuiltinsFromUniformTest, ShouldRunEmptyModule) {
     auto* src = R"()";
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     EXPECT_FALSE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
@@ -46,7 +46,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     EXPECT_FALSE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
@@ -64,7 +64,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     EXPECT_TRUE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
@@ -81,7 +81,7 @@
 )";
 
     auto* expect =
-        "error: missing transform data for tint::ast::transform::TextureBuiltinsFromUniform";
+        "error: missing transform data for tint::glsl::writer::TextureBuiltinsFromUniform";
 
     auto got = Run<TextureBuiltinsFromUniform>(src);
 
@@ -115,7 +115,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -161,7 +161,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -203,7 +203,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -245,7 +245,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -293,7 +293,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -345,7 +345,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -401,7 +401,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -461,7 +461,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -538,7 +538,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -614,7 +614,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -671,7 +671,7 @@
 
     TextureBuiltinsFromUniform::Config cfg({0, 30u});
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
@@ -684,4 +684,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/common/BUILD.bazel b/src/tint/lang/glsl/writer/common/BUILD.bazel
new file mode 100644
index 0000000..8010c3e
--- /dev/null
+++ b/src/tint/lang/glsl/writer/common/BUILD.bazel
@@ -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.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 = "common",
+  srcs = [
+    "options.cc",
+  ],
+  hdrs = [
+    "options.h",
+    "version.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
diff --git a/src/tint/lang/glsl/writer/common/options.h b/src/tint/lang/glsl/writer/common/options.h
index 2f55c4b..59d3208 100644
--- a/src/tint/lang/glsl/writer/common/options.h
+++ b/src/tint/lang/glsl/writer/common/options.h
@@ -77,7 +77,11 @@
     Version version;
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(disable_robustness,
+    TINT_REFLECT(binding_map,
+                 placeholder_binding_point,
+                 binding_points,
+                 access_controls,
+                 disable_robustness,
                  allow_collisions,
                  disable_workgroup_init,
                  external_texture_options,
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index e02c6bb..2fd23d8 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -19,7 +19,6 @@
 
 #include "src/tint/lang/glsl/writer/ast_printer/ast_printer.h"
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
-#include "src/tint/lang/wgsl/ast/transform/combine_samplers.h"
 
 namespace tint::glsl::writer {
 
diff --git a/src/tint/lang/hlsl/BUILD.bazel b/src/tint/lang/hlsl/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/lang/hlsl/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/lang/hlsl/validate/BUILD.bazel b/src/tint/lang/hlsl/validate/BUILD.bazel
new file mode 100644
index 0000000..b62a6a6
--- /dev/null
+++ b/src/tint/lang/hlsl/validate/BUILD.bazel
@@ -0,0 +1,55 @@
+# 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 = [
+    "hlsl.cc",
+  ],
+  hdrs = [
+    "val.h",
+  ],
+  deps = [
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/utils/command",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/file",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
diff --git a/src/tint/lang/hlsl/validate/hlsl.cc b/src/tint/lang/hlsl/validate/hlsl.cc
index bbcde35..af19505 100644
--- a/src/tint/lang/hlsl/validate/hlsl.cc
+++ b/src/tint/lang/hlsl/validate/hlsl.cc
@@ -74,7 +74,7 @@
         // and dawn_native\d3d12\ShaderModuleD3D12.cpp (GetDXCArguments)
         auto res = dxc(
             "-T " + std::string(stage_prefix) + "_" + std::string(shader_model_version),  // Profile
-            "-HV 2018 ",                                       // Use HLSL 2018
+            "-HV 2018",                                        // Use HLSL 2018
             "-E " + ep.first,                                  // Entry point
             "/Zpr",                                            // D3DCOMPILE_PACK_MATRIX_ROW_MAJOR
             "/Gis",                                            // D3DCOMPILE_IEEE_STRICTNESS
diff --git a/src/tint/lang/hlsl/writer/BUILD.bazel b/src/tint/lang/hlsl/writer/BUILD.bazel
new file mode 100644
index 0000000..0c9ed78
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/BUILD.bazel
@@ -0,0 +1,114 @@
+# 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 = "writer",
+  srcs = [
+    "output.cc",
+    "writer.cc",
+  ],
+  hdrs = [
+    "output.h",
+    "writer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer/ast_printer",
+      "//src/tint/lang/hlsl/writer/ast_raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "writer_bench.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//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_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
diff --git a/src/tint/lang/hlsl/writer/BUILD.cmake b/src/tint/lang/hlsl/writer/BUILD.cmake
index f82c2ea..df4dc70 100644
--- a/src/tint/lang/hlsl/writer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/BUILD.cmake
@@ -22,6 +22,7 @@
 ################################################################################
 
 include(lang/hlsl/writer/ast_printer/BUILD.cmake)
+include(lang/hlsl/writer/ast_raise/BUILD.cmake)
 include(lang/hlsl/writer/common/BUILD.cmake)
 
 if(TINT_BUILD_HLSL_WRITER)
@@ -67,6 +68,7 @@
 if(TINT_BUILD_HLSL_WRITER)
   tint_target_add_dependencies(tint_lang_hlsl_writer lib
     tint_lang_hlsl_writer_ast_printer
+    tint_lang_hlsl_writer_ast_raise
   )
 endif(TINT_BUILD_HLSL_WRITER)
 
diff --git a/src/tint/lang/hlsl/writer/BUILD.gn b/src/tint/lang/hlsl/writer/BUILD.gn
index c0435f6..62cf889 100644
--- a/src/tint/lang/hlsl/writer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/BUILD.gn
@@ -60,7 +60,10 @@
     ]
 
     if (tint_build_hlsl_writer) {
-      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_printer" ]
+      deps += [
+        "${tint_src_dir}/lang/hlsl/writer/ast_printer",
+        "${tint_src_dir}/lang/hlsl/writer/ast_raise",
+      ]
     }
   }
 }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel
new file mode 100644
index 0000000..ad2940c
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel
@@ -0,0 +1,150 @@
+# 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 = "ast_printer",
+  srcs = [
+    "ast_printer.cc",
+  ],
+  hdrs = [
+    "ast_printer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/helpers",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer/ast_raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "array_accessor_test.cc",
+    "assign_test.cc",
+    "ast_printer_test.cc",
+    "binary_test.cc",
+    "bitcast_test.cc",
+    "block_test.cc",
+    "break_test.cc",
+    "builtin_test.cc",
+    "builtin_texture_test.cc",
+    "call_test.cc",
+    "case_test.cc",
+    "cast_test.cc",
+    "const_assert_test.cc",
+    "constructor_test.cc",
+    "continue_test.cc",
+    "discard_test.cc",
+    "function_test.cc",
+    "helper_test.h",
+    "identifier_test.cc",
+    "if_test.cc",
+    "import_test.cc",
+    "loop_test.cc",
+    "member_accessor_test.cc",
+    "module_constant_test.cc",
+    "return_test.cc",
+    "sanitizer_test.cc",
+    "switch_test.cc",
+    "type_test.cc",
+    "unary_op_test.cc",
+    "variable_decl_statement_test.cc",
+    "workgroup_var_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/hlsl/writer/common",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer",
+      "//src/tint/lang/hlsl/writer/ast_printer",
+      "//src/tint/lang/hlsl/writer/ast_raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
diff --git a/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake
index 8a43885..5c13dec 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake
@@ -61,6 +61,12 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_HLSL_WRITER)
+  tint_target_add_dependencies(tint_lang_hlsl_writer_ast_printer lib
+    tint_lang_hlsl_writer_ast_raise
+  )
+endif(TINT_BUILD_HLSL_WRITER)
+
 endif(TINT_BUILD_HLSL_WRITER)
 if(TINT_BUILD_HLSL_WRITER)
 ################################################################################
@@ -139,6 +145,7 @@
   tint_target_add_dependencies(tint_lang_hlsl_writer_ast_printer_test test
     tint_lang_hlsl_writer
     tint_lang_hlsl_writer_ast_printer
+    tint_lang_hlsl_writer_ast_raise
   )
 endif(TINT_BUILD_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 88e7e42..de41c15 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
@@ -62,6 +62,10 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_hlsl_writer) {
+      deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_raise" ]
+    }
   }
 }
 if (tint_build_unittests) {
@@ -135,6 +139,7 @@
         deps += [
           "${tint_src_dir}/lang/hlsl/writer",
           "${tint_src_dir}/lang/hlsl/writer/ast_printer",
+          "${tint_src_dir}/lang/hlsl/writer/ast_raise",
         ]
       }
     }
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 1bd104c..1c077b1 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -33,6 +33,12 @@
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.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"
@@ -41,24 +47,18 @@
 #include "src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
 #include "src/tint/lang/wgsl/ast/transform/builtin_polyfill.h"
-#include "src/tint/lang/wgsl/ast/transform/calculate_array_length.h"
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
-#include "src/tint/lang/wgsl/ast/transform/decompose_memory_access.h"
 #include "src/tint/lang/wgsl/ast/transform/demote_to_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
-#include "src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
-#include "src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
-#include "src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.h"
 #include "src/tint/lang/wgsl/ast/transform/remove_phonies.h"
 #include "src/tint/lang/wgsl/ast/transform/robustness.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
-#include "src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h"
 #include "src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h"
@@ -186,7 +186,7 @@
     // SimplifyPointers transform. Can't do it right now because
     // LocalizeStructArrayAssignment introduces pointers.
     manager.Add<ast::transform::SimplifyPointers>();
-    manager.Add<ast::transform::LocalizeStructArrayAssignment>();
+    manager.Add<LocalizeStructArrayAssignment>();
 
     manager.Add<ast::transform::PromoteSideEffectsToDecl>();
 
@@ -267,18 +267,17 @@
         // with the current stage output.
 
         // Build the config for internal TruncateInterstageVariables transform.
-        ast::transform::TruncateInterstageVariables::Config truncate_interstage_variables_cfg;
+        TruncateInterstageVariables::Config truncate_interstage_variables_cfg;
         truncate_interstage_variables_cfg.interstage_locations =
             std::move(options.interstage_locations);
-        manager.Add<ast::transform::TruncateInterstageVariables>();
-        data.Add<ast::transform::TruncateInterstageVariables::Config>(
-            std::move(truncate_interstage_variables_cfg));
+        manager.Add<TruncateInterstageVariables>();
+        data.Add<TruncateInterstageVariables::Config>(std::move(truncate_interstage_variables_cfg));
     }
 
     // NumWorkgroupsFromUniform must come after CanonicalizeEntryPointIO, as it
     // assumes that num_workgroups builtins only appear as struct members and are
     // only accessed directly via member accessors.
-    manager.Add<ast::transform::NumWorkgroupsFromUniform>();
+    manager.Add<NumWorkgroupsFromUniform>();
     manager.Add<ast::transform::VectorizeScalarMatrixInitializers>();
     manager.Add<ast::transform::SimplifyPointers>();
     manager.Add<ast::transform::RemovePhonies>();
@@ -306,20 +305,20 @@
     //   of `*(&(intrinsic_load()))` expressions.
     // * RemovePhonies, as phonies can be assigned a pointer to a
     //   non-constructible buffer, or dynamic array, which DMA cannot cope with.
-    manager.Add<ast::transform::DecomposeMemoryAccess>();
+    manager.Add<DecomposeMemoryAccess>();
     // CalculateArrayLength must come after DecomposeMemoryAccess, as
     // DecomposeMemoryAccess special-cases the arrayLength() intrinsic, which
     // will be transformed by CalculateArrayLength
-    manager.Add<ast::transform::CalculateArrayLength>();
+    manager.Add<CalculateArrayLength>();
     manager.Add<ast::transform::PromoteInitializersToLet>();
 
-    manager.Add<ast::transform::RemoveContinueInSwitch>();
+    manager.Add<RemoveContinueInSwitch>();
 
     manager.Add<ast::transform::AddEmptyEntryPoint>();
 
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
         ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
-    data.Add<ast::transform::NumWorkgroupsFromUniform::Config>(options.root_constant_binding_point);
+    data.Add<NumWorkgroupsFromUniform::Config>(options.root_constant_binding_point);
 
     SanitizedResult result;
     ast::transform::DataMap outputs;
@@ -1102,7 +1101,7 @@
                                   const sem::Function* func) {
     auto* expr = call->Declaration();
 
-    if (ast::HasAttribute<ast::transform::CalculateArrayLength::BufferSizeIntrinsic>(
+    if (ast::HasAttribute<CalculateArrayLength::BufferSizeIntrinsic>(
             func->Declaration()->attributes)) {
         // Special function generated by the CalculateArrayLength transform for
         // calling X.GetDimensions(Y)
@@ -1117,8 +1116,8 @@
         return true;
     }
 
-    if (auto* intrinsic = ast::GetAttribute<ast::transform::DecomposeMemoryAccess::Intrinsic>(
-            func->Declaration()->attributes)) {
+    if (auto* intrinsic =
+            ast::GetAttribute<DecomposeMemoryAccess::Intrinsic>(func->Declaration()->attributes)) {
         switch (intrinsic->address_space) {
             case core::AddressSpace::kUniform:
                 return EmitUniformBufferAccess(out, expr, intrinsic);
@@ -1335,10 +1334,9 @@
     return true;
 }
 
-bool ASTPrinter::EmitUniformBufferAccess(
-    StringStream& out,
-    const ast::CallExpression* expr,
-    const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
+bool ASTPrinter::EmitUniformBufferAccess(StringStream& out,
+                                         const ast::CallExpression* expr,
+                                         const DecomposeMemoryAccess::Intrinsic* intrinsic) {
     auto const buffer = intrinsic->Buffer()->identifier->symbol.Name();
     auto* const offset = expr->args[0];
 
@@ -1367,7 +1365,7 @@
     // scalar_offset_index or scalar_offset_index_unified_expr. Currently only loading f16 scalar
     // require using offset in bytes.
     const bool need_offset_in_bytes =
-        intrinsic->type == ast::transform::DecomposeMemoryAccess::Intrinsic::DataType::kF16;
+        intrinsic->type == DecomposeMemoryAccess::Intrinsic::DataType::kF16;
 
     if (!scalar_offset_constant) {
         // UBO offset not compile-time known.
@@ -1398,8 +1396,8 @@
 
     const char swizzle[] = {'x', 'y', 'z', 'w'};
 
-    using Op = ast::transform::DecomposeMemoryAccess::Intrinsic::Op;
-    using DataType = ast::transform::DecomposeMemoryAccess::Intrinsic::DataType;
+    using Op = DecomposeMemoryAccess::Intrinsic::Op;
+    using DataType = DecomposeMemoryAccess::Intrinsic::DataType;
     switch (intrinsic->op) {
         case Op::kLoad: {
             auto cast = [&](const char* to, auto&& load) {
@@ -1622,16 +1620,15 @@
     return false;
 }
 
-bool ASTPrinter::EmitStorageBufferAccess(
-    StringStream& out,
-    const ast::CallExpression* expr,
-    const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
+bool ASTPrinter::EmitStorageBufferAccess(StringStream& out,
+                                         const ast::CallExpression* expr,
+                                         const DecomposeMemoryAccess::Intrinsic* intrinsic) {
     auto const buffer = intrinsic->Buffer()->identifier->symbol.Name();
     auto* const offset = expr->args[0];
     auto* const value = expr->args.Length() > 1 ? expr->args[1] : nullptr;
 
-    using Op = ast::transform::DecomposeMemoryAccess::Intrinsic::Op;
-    using DataType = ast::transform::DecomposeMemoryAccess::Intrinsic::DataType;
+    using Op = DecomposeMemoryAccess::Intrinsic::Op;
+    using DataType = DecomposeMemoryAccess::Intrinsic::DataType;
     switch (intrinsic->op) {
         case Op::kLoad: {
             auto load = [&](const char* cast, int n) {
@@ -1713,7 +1710,7 @@
                 }
                 out << ", asuint";
                 ScopedParen sp2(out);
-                if (!EmitExpression(out, value)) {
+                if (!EmitTextureOrStorageBufferCallArgExpression(out, value)) {
                     return false;
                 }
                 return true;
@@ -1782,10 +1779,9 @@
     return false;
 }
 
-bool ASTPrinter::EmitStorageAtomicIntrinsic(
-    const ast::Function* func,
-    const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
-    using Op = ast::transform::DecomposeMemoryAccess::Intrinsic::Op;
+bool ASTPrinter::EmitStorageAtomicIntrinsic(const ast::Function* func,
+                                            const DecomposeMemoryAccess::Intrinsic* intrinsic) {
+    using Op = DecomposeMemoryAccess::Intrinsic::Op;
 
     const sem::Function* sem_func = builder_.Sem().Get(func);
     auto* result_ty = sem_func->ReturnType();
@@ -2517,6 +2513,37 @@
     return true;
 }
 
+bool ASTPrinter::EmitTextureOrStorageBufferCallArgExpression(StringStream& out,
+                                                             const ast::Expression* expr) {
+    // TODO(crbug.com/tint/1976): Workaround DXC bug that fails to compile texture/storage function
+    // calls with signed integer splatted constants. DXC fails to convert the coord arg, for e.g.
+    // `0.xxx`, from a vector of 64-bit ints to a vector of 32-bit ints to match the texture load
+    // parameter type. We work around this for now by explicitly casting the splatted constant to
+    // the right type, for e.g. `int3(0.xxx)`.
+    bool emitted_cast = false;
+    if (auto* sem = builder_.Sem().GetVal(expr)) {
+        if (auto* constant = sem->ConstantValue()) {
+            if (auto* splat = constant->As<core::constant::Splat>()) {
+                if (splat->Type()->is_signed_integer_vector()) {
+                    if (!EmitType(out, constant->Type(), core::AddressSpace::kUndefined,
+                                  core::Access::kUndefined, "")) {
+                        return false;
+                    }
+                    out << "(";
+                    emitted_cast = true;
+                }
+            }
+        }
+    }
+    if (!EmitExpression(out, expr)) {
+        return false;
+    }
+    if (emitted_cast) {
+        out << ")";
+    }
+    return true;
+}
+
 bool ASTPrinter::EmitTextureCall(StringStream& out,
                                  const sem::Call* call,
                                  const sem::Builtin* builtin) {
@@ -2819,36 +2846,6 @@
         return emit_vector_appended_with_i32_zero(vector);
     };
 
-    auto emit_arg_expression = [&](const ast::Expression* arg_expr) -> bool {
-        // TODO(crbug.com/tint/1976): Workaround DXC bug that fails to compile texture loads with
-        // splatted constants. DXC fails to convert the coord arg, for e.g. `0.xxx`, from a vector
-        // of 64-bit ints to a vector of 32-bit ints to match the texture load parameter type. We
-        // work around this for now by explicitly casting the splatted constant to the right type,
-        // for e.g. `int3(0.xxx)`.
-        bool emitted_cast = false;
-        if (auto* sem = builder_.Sem().GetVal(arg_expr)) {
-            if (auto* constant = sem->ConstantValue()) {
-                if (auto* splat = constant->As<core::constant::Splat>()) {
-                    if (splat->Type()->is_signed_integer_vector()) {
-                        if (!EmitType(out, constant->Type(), core::AddressSpace::kUndefined,
-                                      core::Access::kUndefined, "")) {
-                            return false;
-                        }
-                        out << "(";
-                        emitted_cast = true;
-                    }
-                }
-            }
-        }
-        if (!EmitExpression(out, arg_expr)) {
-            return false;
-        }
-        if (emitted_cast) {
-            out << ")";
-        }
-        return true;
-    };
-
     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);
@@ -2872,7 +2869,7 @@
         if (!EmitExpression(out, param_coords)) {
             return false;
         }
-    } else if (!emit_arg_expression(param_coords)) {
+    } else if (!EmitTextureOrStorageBufferCallArgExpression(out, param_coords)) {
         return false;
     }
 
@@ -2883,7 +2880,7 @@
         }
         if (auto* e = arg(usage)) {
             out << ", ";
-            if (!emit_arg_expression(e)) {
+            if (!EmitTextureOrStorageBufferCallArgExpression(out, e)) {
                 return false;
             }
         }
@@ -3124,8 +3121,7 @@
     auto* sem = builder_.Sem().Get(func);
 
     // Emit storage atomic helpers
-    if (auto* intrinsic =
-            ast::GetAttribute<ast::transform::DecomposeMemoryAccess::Intrinsic>(func->attributes)) {
+    if (auto* intrinsic = ast::GetAttribute<DecomposeMemoryAccess::Intrinsic>(func->attributes)) {
         if (intrinsic->address_space == core::AddressSpace::kStorage && intrinsic->IsAtomic()) {
             if (!EmitStorageAtomicIntrinsic(func, intrinsic)) {
                 return false;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
index f7fc52a..7b7966b 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
@@ -24,8 +24,8 @@
 #include "src/tint/api/common/binding_point.h"
 #include "src/tint/api/options/array_length_from_uniform.h"
 #include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h"
 #include "src/tint/lang/hlsl/writer/common/options.h"
-#include "src/tint/lang/wgsl/ast/transform/decompose_memory_access.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/utils/containers/scope_stack.h"
 #include "src/tint/utils/generator/text_generator.h"
@@ -75,7 +75,7 @@
     bool Generate();
 
     /// Handles an index accessor expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the expression to emit
     /// @returns true if the index accessor was emitted
     bool EmitIndexAccessor(StringStream& out, const ast::IndexAccessorExpression* expr);
@@ -84,12 +84,12 @@
     /// @returns true if the statement was emitted successfully
     bool EmitAssign(const ast::AssignmentStatement* stmt);
     /// Handles generating a binary expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
     bool EmitBinary(StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a bitcast expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the as expression
     /// @returns true if the bitcast was emitted
     bool EmitBitcast(StringStream& out, const ast::BitcastExpression* expr);
@@ -114,24 +114,24 @@
     /// @returns true if the statement was emitted successfully
     bool EmitBreakIf(const ast::BreakIfStatement* stmt);
     /// Handles generating a call expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
     bool EmitCall(StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a function call expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call expression
     /// @param function the function being called
     /// @returns true if the expression is emitted
     bool EmitFunctionCall(StringStream& out, const sem::Call* call, const sem::Function* function);
     /// Handles generating a builtin call expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call expression
     /// @param builtin the builtin being called
     /// @returns true if the expression is emitted
     bool EmitBuiltinCall(StringStream& out, const sem::Call* call, const sem::Builtin* builtin);
     /// Handles generating a value conversion expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call expression
     /// @param conv the value conversion
     /// @returns true if the expression is emitted
@@ -139,7 +139,7 @@
                              const sem::Call* call,
                              const sem::ValueConversion* conv);
     /// Handles generating a value constructor expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call expression
     /// @param ctor the value constructor
     /// @returns true if the expression is emitted
@@ -147,45 +147,44 @@
                               const sem::Call* call,
                               const sem::ValueConstructor* ctor);
     /// Handles generating a call expression to a
-    /// ast::transform::DecomposeMemoryAccess::Intrinsic for a uniform buffer
-    /// @param out the output of the expression stream
+    /// DecomposeMemoryAccess::Intrinsic for a uniform buffer
+    /// @param out the output stream
     /// @param expr the call expression
-    /// @param intrinsic the ast::transform::DecomposeMemoryAccess::Intrinsic
+    /// @param intrinsic the DecomposeMemoryAccess::Intrinsic
     /// @returns true if the call expression is emitted
     bool EmitUniformBufferAccess(StringStream& out,
                                  const ast::CallExpression* expr,
-                                 const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
+                                 const DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating a call expression to a
-    /// ast::transform::DecomposeMemoryAccess::Intrinsic for a storage buffer
-    /// @param out the output of the expression stream
+    /// DecomposeMemoryAccess::Intrinsic for a storage buffer
+    /// @param out the output stream
     /// @param expr the call expression
-    /// @param intrinsic the ast::transform::DecomposeMemoryAccess::Intrinsic
+    /// @param intrinsic the DecomposeMemoryAccess::Intrinsic
     /// @returns true if the call expression is emitted
     bool EmitStorageBufferAccess(StringStream& out,
                                  const ast::CallExpression* expr,
-                                 const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
+                                 const DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating a barrier intrinsic call
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param builtin the semantic information for the barrier builtin
     /// @returns true if the call expression is emitted
     bool EmitBarrierCall(StringStream& out, const sem::Builtin* builtin);
     /// Handles generating an atomic intrinsic call for a storage buffer variable
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param intrinsic the atomic intrinsic
     /// @returns true if the call expression is emitted
     bool EmitStorageAtomicCall(StringStream& out,
                                const ast::CallExpression* expr,
-                               const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
+                               const DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating the helper function for the atomic intrinsic function
     /// @param func the function
     /// @param intrinsic the atomic intrinsic
     /// @returns true if the function is emitted
-    bool EmitStorageAtomicIntrinsic(
-        const ast::Function* func,
-        const ast::transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
+    bool EmitStorageAtomicIntrinsic(const ast::Function* func,
+                                    const DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating an atomic intrinsic call for a workgroup variable
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the atomic builtin
     /// @returns true if the call expression is emitted
@@ -194,18 +193,18 @@
                                  const sem::Builtin* builtin);
     /// Handles generating a call to a texture function (`textureSample`,
     /// `textureSampleGrad`, etc)
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call expression
     /// @param builtin the semantic information for the texture builtin
     /// @returns true if the call expression is emitted
     bool EmitTextureCall(StringStream& out, const sem::Call* call, const sem::Builtin* builtin);
     /// Handles generating a call to the `select()` builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
     bool EmitSelectCall(StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to the `modf()` builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -213,7 +212,7 @@
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles generating a call to the `frexp()` builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -221,7 +220,7 @@
                        const ast::CallExpression* expr,
                        const sem::Builtin* builtin);
     /// Handles generating a call to the `degrees()` builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -229,7 +228,7 @@
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `radians()` builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -237,13 +236,13 @@
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `sign()` builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call semantic node
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
     bool EmitSignCall(StringStream& out, const sem::Call* call, const sem::Builtin* builtin);
     /// Handles generating a call to data packing builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -251,7 +250,7 @@
                              const ast::CallExpression* expr,
                              const sem::Builtin* builtin);
     /// Handles generating a call to data unpacking builtin
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -259,7 +258,7 @@
                                const ast::CallExpression* expr,
                                const sem::Builtin* builtin);
     /// Handles generating a call to the `quantizeToF16()` intrinsic
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -267,7 +266,7 @@
                                const ast::CallExpression* expr,
                                const sem::Builtin* builtin);
     /// Handles generating a call to the `trunc()` intrinsic
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -275,7 +274,7 @@
                        const ast::CallExpression* expr,
                        const sem::Builtin* builtin);
     /// Handles generating a call to DP4a builtins (dot4I8Packed and dot4U8Packed)
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -283,7 +282,7 @@
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles generating a call to subgroup builtins.
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
@@ -303,11 +302,19 @@
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
     bool EmitContinue(const ast::ContinueStatement* stmt);
-    /// Handles generate an Expression
-    /// @param out the output of the expression stream
+    /// Handles generating an Expression
+    /// @param out the output stream
     /// @param expr the expression
     /// @returns true if the expression was emitted
     bool EmitExpression(StringStream& out, const ast::Expression* expr);
+    /// Handles generating an Expression for texture or storage buffer call arguments. This is
+    /// specifically to work around a DXC bug around passing signed integer splatted constants as
+    /// args to these functions (see crbug.com/tint/1976)
+    /// @param out the output stream
+    /// @param expr the expression
+    /// @returns true if the expression was emitted
+    bool EmitTextureOrStorageBufferCallArgExpression(StringStream& out,
+                                                     const ast::Expression* expr);
     /// Handles generating a function
     /// @param func the function to generate
     /// @returns true if the function was emitted
@@ -385,12 +392,12 @@
     /// @returns true if the statement was emitted
     bool EmitWhile(const ast::WhileStatement* stmt);
     /// Handles generating an identifier expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the identifier expression
     /// @returns true if the identifeir was emitted
     bool EmitIdentifier(StringStream& out, const ast::IdentifierExpression* expr);
     /// Handles a member accessor expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the member accessor expression
     /// @returns true if the member accessor was emitted
     bool EmitMemberAccessor(StringStream& out, const ast::MemberAccessorExpression* expr);
@@ -444,7 +451,7 @@
     /// @returns true if the struct is emitted
     bool EmitStructType(TextBuffer* buffer, const core::type::Struct* ty);
     /// Handles a unary op expression
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param expr the expression to emit
     /// @returns true if the expression was emitted
     bool EmitUnaryOp(StringStream& out, const ast::UnaryOpExpression* expr);
@@ -522,8 +529,8 @@
     };
 
     struct DMAIntrinsic {
-        ast::transform::DecomposeMemoryAccess::Intrinsic::Op op;
-        ast::transform::DecomposeMemoryAccess::Intrinsic::DataType type;
+        DecomposeMemoryAccess::Intrinsic::Op op;
+        DecomposeMemoryAccess::Intrinsic::DataType type;
         bool operator==(const DMAIntrinsic& rhs) const { return op == rhs.op && type == rhs.type; }
         /// Hasher is a std::hash function for DMAIntrinsic
         struct Hasher {
@@ -543,7 +550,7 @@
     /// if it hasn't been built already. If the builtin needs to be built then
     /// CallBuiltinHelper will generate the function signature and will call
     /// `build` to emit the body of the function.
-    /// @param out the output of the expression stream
+    /// @param out the output stream
     /// @param call the call expression
     /// @param builtin the semantic information for the builtin
     /// @param build a function with the signature:
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
new file mode 100644
index 0000000..87be9ba
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
@@ -0,0 +1,122 @@
+# 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 = "ast_raise",
+  srcs = [
+    "calculate_array_length.cc",
+    "decompose_memory_access.cc",
+    "localize_struct_array_assignment.cc",
+    "num_workgroups_from_uniform.cc",
+    "remove_continue_in_switch.cc",
+    "truncate_interstage_variables.cc",
+  ],
+  hdrs = [
+    "calculate_array_length.h",
+    "decompose_memory_access.h",
+    "localize_struct_array_assignment.h",
+    "num_workgroups_from_uniform.h",
+    "remove_continue_in_switch.h",
+    "truncate_interstage_variables.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "calculate_array_length_test.cc",
+    "decompose_memory_access_test.cc",
+    "localize_struct_array_assignment_test.cc",
+    "num_workgroups_from_uniform_test.cc",
+    "remove_continue_in_switch_test.cc",
+    "truncate_interstage_variables_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_hlsl_writer": [
+      "//src/tint/lang/hlsl/writer/ast_raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg
new file mode 100644
index 0000000..31b4636
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_hlsl_writer"
+}
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
new file mode 100644
index 0000000..6e3e1f0
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
@@ -0,0 +1,123 @@
+# 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_HLSL_WRITER)
+################################################################################
+# Target:    tint_lang_hlsl_writer_ast_raise
+# Kind:      lib
+# Condition: TINT_BUILD_HLSL_WRITER
+################################################################################
+tint_add_target(tint_lang_hlsl_writer_ast_raise lib
+  lang/hlsl/writer/ast_raise/calculate_array_length.cc
+  lang/hlsl/writer/ast_raise/calculate_array_length.h
+  lang/hlsl/writer/ast_raise/decompose_memory_access.cc
+  lang/hlsl/writer/ast_raise/decompose_memory_access.h
+  lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
+  lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
+  lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
+  lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
+  lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc
+  lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
+  lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
+  lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
+)
+
+tint_target_add_dependencies(tint_lang_hlsl_writer_ast_raise lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_program
+  tint_lang_wgsl_resolver
+  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
+)
+
+endif(TINT_BUILD_HLSL_WRITER)
+if(TINT_BUILD_HLSL_WRITER)
+################################################################################
+# Target:    tint_lang_hlsl_writer_ast_raise_test
+# Kind:      test
+# Condition: TINT_BUILD_HLSL_WRITER
+################################################################################
+tint_add_target(tint_lang_hlsl_writer_ast_raise_test test
+  lang/hlsl/writer/ast_raise/calculate_array_length_test.cc
+  lang/hlsl/writer/ast_raise/decompose_memory_access_test.cc
+  lang/hlsl/writer/ast_raise/localize_struct_array_assignment_test.cc
+  lang/hlsl/writer/ast_raise/num_workgroups_from_uniform_test.cc
+  lang/hlsl/writer/ast_raise/remove_continue_in_switch_test.cc
+  lang/hlsl/writer/ast_raise/truncate_interstage_variables_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_hlsl_writer_ast_raise_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  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
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_hlsl_writer_ast_raise_test test
+  "gtest"
+)
+
+if(TINT_BUILD_HLSL_WRITER)
+  tint_target_add_dependencies(tint_lang_hlsl_writer_ast_raise_test test
+    tint_lang_hlsl_writer_ast_raise
+  )
+endif(TINT_BUILD_HLSL_WRITER)
+
+endif(TINT_BUILD_HLSL_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
new file mode 100644
index 0000000..89f2e4b
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
@@ -0,0 +1,118 @@
+# 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_unittests) {
+  import("//testing/test.gni")
+}
+if (tint_build_hlsl_writer) {
+  libtint_source_set("ast_raise") {
+    sources = [
+      "calculate_array_length.cc",
+      "calculate_array_length.h",
+      "decompose_memory_access.cc",
+      "decompose_memory_access.h",
+      "localize_struct_array_assignment.cc",
+      "localize_struct_array_assignment.h",
+      "num_workgroups_from_uniform.cc",
+      "num_workgroups_from_uniform.h",
+      "remove_continue_in_switch.cc",
+      "remove_continue_in_switch.h",
+      "truncate_interstage_variables.cc",
+      "truncate_interstage_variables.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/ast",
+      "${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_unittests) {
+  if (tint_build_hlsl_writer) {
+    tint_unittests_source_set("unittests") {
+      testonly = true
+      sources = [
+        "calculate_array_length_test.cc",
+        "decompose_memory_access_test.cc",
+        "localize_struct_array_assignment_test.cc",
+        "num_workgroups_from_uniform_test.cc",
+        "remove_continue_in_switch_test.cc",
+        "truncate_interstage_variables_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/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",
+        "${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/ast_raise" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc
similarity index 89%
rename from src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
rename to src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc
index b36c275..604700f 100644
--- a/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/calculate_array_length.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h"
 
 #include <unordered_map>
 #include <utility>
@@ -34,10 +34,10 @@
 #include "src/tint/utils/math/hash.h"
 #include "src/tint/utils/rtti/switch.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::CalculateArrayLength);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::CalculateArrayLength::BufferSizeIntrinsic);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::CalculateArrayLength);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::CalculateArrayLength::BufferSizeIntrinsic);
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
 using namespace tint::core::fluent_types;     // NOLINT
@@ -59,7 +59,7 @@
 /// ArrayUsage describes a runtime array usage.
 /// It is used as a key by the array_length_by_usage map.
 struct ArrayUsage {
-    BlockStatement const* const block;
+    ast::BlockStatement const* const block;
     sem::Variable const* const buffer;
     bool operator==(const ArrayUsage& rhs) const {
         return block == rhs.block && buffer == rhs.buffer;
@@ -71,7 +71,7 @@
 
 }  // namespace
 
-CalculateArrayLength::BufferSizeIntrinsic::BufferSizeIntrinsic(GenerationID pid, NodeID nid)
+CalculateArrayLength::BufferSizeIntrinsic::BufferSizeIntrinsic(GenerationID pid, ast::NodeID nid)
     : Base(pid, nid, tint::Empty) {}
 CalculateArrayLength::BufferSizeIntrinsic::~BufferSizeIntrinsic() = default;
 std::string CalculateArrayLength::BufferSizeIntrinsic::InternalName() const {
@@ -87,9 +87,9 @@
 CalculateArrayLength::CalculateArrayLength() = default;
 CalculateArrayLength::~CalculateArrayLength() = default;
 
-Transform::ApplyResult CalculateArrayLength::Apply(const Program* src,
-                                                   const DataMap&,
-                                                   DataMap&) const {
+ast::transform::Transform::ApplyResult CalculateArrayLength::Apply(const Program* src,
+                                                                   const ast::transform::DataMap&,
+                                                                   ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -106,16 +106,16 @@
         return tint::GetOrCreate(buffer_size_intrinsics, buffer_type, [&] {
             auto name = b.Sym();
             auto type = CreateASTTypeFor(ctx, buffer_type);
-            auto* disable_validation = b.Disable(DisabledValidation::kFunctionParameter);
+            auto* disable_validation = b.Disable(ast::DisabledValidation::kFunctionParameter);
             b.Func(name,
-                   tint::Vector{
+                   Vector{
                        b.Param("buffer",
                                b.ty.ptr(buffer_type->AddressSpace(), type, buffer_type->Access()),
-                               tint::Vector{disable_validation}),
+                               Vector{disable_validation}),
                        b.Param("result", b.ty.ptr<function, u32>()),
                    },
                    b.ty.void_(), nullptr,
-                   tint::Vector{
+                   Vector{
                        b.ASTNodes().Create<BufferSizeIntrinsic>(b.ID(), b.AllocateNodeID()),
                    });
 
@@ -127,13 +127,13 @@
 
     // Find all the arrayLength() calls...
     for (auto* node : src->ASTNodes().Objects()) {
-        if (auto* call_expr = node->As<CallExpression>()) {
+        if (auto* call_expr = node->As<ast::CallExpression>()) {
             auto* call = sem.Get(call_expr)->UnwrapMaterialize()->As<sem::Call>();
             if (auto* builtin = call->Target()->As<sem::Builtin>()) {
                 if (builtin->Type() == core::Function::kArrayLength) {
                     // We're dealing with an arrayLength() call
 
-                    if (auto* call_stmt = call->Stmt()->Declaration()->As<CallStatement>()) {
+                    if (auto* call_stmt = call->Stmt()->Declaration()->As<ast::CallStatement>()) {
                         if (call_stmt->expr == call_expr) {
                             // arrayLength() is used as a statement.
                             // The argument expression must be side-effect free, so just drop the
@@ -150,13 +150,13 @@
                     //   arrayLength(&struct_var.array_member)
                     //   arrayLength(&array_var)
                     auto* arg = call_expr->args[0];
-                    auto* address_of = arg->As<UnaryOpExpression>();
+                    auto* address_of = arg->As<ast::UnaryOpExpression>();
                     if (TINT_UNLIKELY(!address_of || address_of->op != core::UnaryOp::kAddressOf)) {
                         TINT_ICE()
                             << "arrayLength() expected address-of, got " << arg->TypeInfo().name;
                     }
                     auto* storage_buffer_expr = address_of->expr;
-                    if (auto* accessor = storage_buffer_expr->As<MemberAccessorExpression>()) {
+                    if (auto* accessor = storage_buffer_expr->As<ast::MemberAccessorExpression>()) {
                         storage_buffer_expr = accessor->object;
                     }
                     auto* storage_buffer_sem = sem.Get<sem::VariableUser>(storage_buffer_expr);
@@ -198,7 +198,8 @@
                             // array_length = ----------------------------------------
                             //                             array_stride
                             auto name = b.Sym();
-                            const Expression* total_size = b.Expr(buffer_size_result->variable);
+                            const ast::Expression* total_size =
+                                b.Expr(buffer_size_result->variable);
 
                             const core::type::Array* array_type = Switch(
                                 storage_buffer_type->StoreType(),
@@ -242,4 +243,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/calculate_array_length.h b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h
similarity index 71%
rename from src/tint/lang/wgsl/ast/transform/calculate_array_length.h
rename to src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h
index f492e90..e798a77 100644
--- a/src/tint/lang/wgsl/ast/transform/calculate_array_length.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h
@@ -12,31 +12,32 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_CALCULATE_ARRAY_LENGTH_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_CALCULATE_ARRAY_LENGTH_H_
+#ifndef SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_CALCULATE_ARRAY_LENGTH_H_
+#define SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_CALCULATE_ARRAY_LENGTH_H_
 
 #include <string>
 
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// CalculateArrayLength is a transform used to replace calls to arrayLength()
 /// with a value calculated from the size of the storage buffer.
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class CalculateArrayLength final : public Castable<CalculateArrayLength, Transform> {
+class CalculateArrayLength final
+    : public Castable<CalculateArrayLength, ast::transform::Transform> {
   public:
     /// BufferSizeIntrinsic is an InternalAttribute that's applied to intrinsic
     /// functions used to obtain the runtime size of a storage buffer.
-    class BufferSizeIntrinsic final : public Castable<BufferSizeIntrinsic, InternalAttribute> {
+    class BufferSizeIntrinsic final : public Castable<BufferSizeIntrinsic, ast::InternalAttribute> {
       public:
         /// Constructor
         /// @param generation_id the identifier of the program that owns this node
         /// @param nid the unique node identifier
-        BufferSizeIntrinsic(GenerationID generation_id, NodeID nid);
+        BufferSizeIntrinsic(GenerationID generation_id, ast::NodeID nid);
         /// Destructor
         ~BufferSizeIntrinsic() override;
 
@@ -46,7 +47,7 @@
         /// Performs a deep clone of this object using the program::CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const BufferSizeIntrinsic* Clone(CloneContext& ctx) const override;
+        const BufferSizeIntrinsic* Clone(ast::CloneContext& ctx) const override;
     };
 
     /// Constructor
@@ -54,12 +55,12 @@
     /// Destructor
     ~CalculateArrayLength() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_CALCULATE_ARRAY_LENGTH_H_
+#endif  // SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_CALCULATE_ARRAY_LENGTH_H_
diff --git a/src/tint/lang/wgsl/ast/transform/calculate_array_length_test.cc b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length_test.cc
similarity index 97%
rename from src/tint/lang/wgsl/ast/transform/calculate_array_length_test.cc
rename to src/tint/lang/hlsl/writer/ast_raise/calculate_array_length_test.cc
index 41f99cf..11ae0fc 100644
--- a/src/tint/lang/wgsl/ast/transform/calculate_array_length_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length_test.cc
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/calculate_array_length.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
-using CalculateArrayLengthTest = TransformTest;
+using CalculateArrayLengthTest = ast::transform::TransformTest;
+using Unshadow = ast::transform::Unshadow;
+using SimplifyPointers = ast::transform::SimplifyPointers;
 
 TEST_F(CalculateArrayLengthTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -548,4 +550,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc
similarity index 91%
rename from src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
rename to src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc
index b60adb1..9cff4ce 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/decompose_memory_access.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h"
 
 #include <memory>
 #include <string>
@@ -45,10 +45,10 @@
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::DecomposeMemoryAccess);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::DecomposeMemoryAccess::Intrinsic);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::DecomposeMemoryAccess);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::DecomposeMemoryAccess::Intrinsic);
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 namespace {
 
@@ -68,17 +68,17 @@
 /// offsets for storage and uniform buffer accesses.
 struct Offset : Castable<Offset> {
     /// @returns builds and returns the Expression in `ctx.dst`
-    virtual const Expression* Build(program::CloneContext& ctx) const = 0;
+    virtual const ast::Expression* Build(program::CloneContext& ctx) const = 0;
 };
 
 /// OffsetExpr is an implementation of Offset that clones and casts the given
 /// expression to `u32`.
 struct OffsetExpr : Offset {
-    const Expression* const expr = nullptr;
+    const ast::Expression* const expr = nullptr;
 
-    explicit OffsetExpr(const Expression* e) : expr(e) {}
+    explicit OffsetExpr(const ast::Expression* e) : expr(e) {}
 
-    const Expression* Build(program::CloneContext& ctx) const override {
+    const ast::Expression* Build(program::CloneContext& ctx) const override {
         auto* type = ctx.src->Sem().GetVal(expr)->Type()->UnwrapRef();
         auto* res = ctx.Clone(expr);
         if (!type->Is<core::type::U32>()) {
@@ -95,7 +95,7 @@
 
     explicit OffsetLiteral(uint32_t lit) : literal(lit) {}
 
-    const Expression* Build(program::CloneContext& ctx) const override {
+    const ast::Expression* Build(program::CloneContext& ctx) const override {
         return ctx.dst->Expr(u32(literal));
     }
 };
@@ -107,8 +107,8 @@
     Offset const* lhs = nullptr;
     Offset const* rhs = nullptr;
 
-    const Expression* Build(program::CloneContext& ctx) const override {
-        return ctx.dst->create<BinaryExpression>(op, lhs->Build(ctx), rhs->Build(ctx));
+    const ast::Expression* Build(program::CloneContext& ctx) const override {
+        return ctx.dst->create<ast::BinaryExpression>(op, lhs->Build(ctx), rhs->Build(ctx));
     }
 };
 
@@ -317,8 +317,8 @@
 
 /// Store describes a single storage or uniform buffer write
 struct Store {
-    const AssignmentStatement* assignment;  // The AST assignment statement
-    BufferAccess target;                    // The target for the write
+    const ast::AssignmentStatement* assignment;  // The AST assignment statement
+    BufferAccess target;                         // The target for the write
 };
 
 }  // namespace
@@ -334,9 +334,9 @@
     /// expressions chain the access.
     /// Subset of #expression_order, as expressions are not removed from
     /// #expression_order.
-    std::unordered_map<const Expression*, BufferAccess> accesses;
+    std::unordered_map<const ast::Expression*, BufferAccess> accesses;
     /// The visited order of AST expressions (superset of #accesses)
-    std::vector<const Expression*> expression_order;
+    std::vector<const ast::Expression*> expression_order;
     /// [buffer-type, element-type] -> load function name
     std::unordered_map<LoadStoreKey, Symbol, LoadStoreKey::Hasher> load_funcs;
     /// [buffer-type, element-type] -> store function name
@@ -358,8 +358,8 @@
 
     /// @param expr the expression to convert to an Offset
     /// @returns an Offset for the given Expression
-    const Offset* ToOffset(const Expression* expr) {
-        if (auto* lit = expr->As<IntLiteralExpression>()) {
+    const Offset* ToOffset(const ast::Expression* expr) {
+        if (auto* lit = expr->As<ast::IntLiteralExpression>()) {
             if (lit->value >= 0) {
                 return offsets_.Create<OffsetLiteral>(static_cast<uint32_t>(lit->value));
             }
@@ -436,7 +436,7 @@
     /// to #expression_order.
     /// @param expr the expression that performs the access
     /// @param access the access
-    void AddAccess(const Expression* expr, const BufferAccess& access) {
+    void AddAccess(const ast::Expression* expr, const BufferAccess& access) {
         TINT_ASSERT(access.type);
         accesses.emplace(expr, access);
         expression_order.emplace_back(expr);
@@ -447,7 +447,7 @@
     /// `node`, an invalid BufferAccess is returned.
     /// @param node the expression that performed an access
     /// @return the BufferAccess for the given expression
-    BufferAccess TakeAccess(const Expression* node) {
+    BufferAccess TakeAccess(const ast::Expression* node) {
         auto lhs_it = accesses.find(node);
         if (lhs_it == accesses.end()) {
             return {};
@@ -470,16 +470,16 @@
                     core::AddressSpace address_space,
                     const Symbol& buffer) {
         return tint::GetOrCreate(load_funcs, LoadStoreKey{el_ty, buffer}, [&] {
-            tint::Vector params{b.Param("offset", b.ty.u32())};
+            Vector params{b.Param("offset", b.ty.u32())};
 
             auto name = b.Symbols().New(buffer.Name() + "_load");
 
             if (auto* intrinsic = IntrinsicLoadFor(ctx.dst, el_ty, address_space, buffer)) {
                 auto el_ast_ty = CreateASTTypeFor(ctx, el_ty);
                 b.Func(name, params, el_ast_ty, nullptr,
-                       tint::Vector{
+                       Vector{
                            intrinsic,
-                           b.Disable(DisabledValidation::kFunctionHasNoBody),
+                           b.Disable(ast::DisabledValidation::kFunctionHasNoBody),
                        });
             } else if (auto* arr_ty = el_ty->As<core::type::Array>()) {
                 // fn load_func(buffer : buf_ty, offset : u32) -> array<T, N> {
@@ -502,8 +502,8 @@
                     TINT_ICE() << "unexpected non-constant array count";
                     arr_cnt = 1;
                 }
-                auto* for_cond = b.create<BinaryExpression>(core::BinaryOp::kLessThan, b.Expr(i),
-                                                            b.Expr(u32(arr_cnt.value())));
+                auto* for_cond = b.create<ast::BinaryExpression>(
+                    core::BinaryOp::kLessThan, b.Expr(i), b.Expr(u32(arr_cnt.value())));
                 auto* for_cont = b.Assign(i, b.Add(i, 1_u));
                 auto* arr_el = b.IndexAccessor(arr, i);
                 auto* el_offset = b.Add(b.Expr("offset"), b.Mul(i, u32(arr_ty->Stride())));
@@ -512,13 +512,13 @@
                     b.For(for_init, for_cond, for_cont, b.Block(b.Assign(arr_el, el_val)));
 
                 b.Func(name, params, CreateASTTypeFor(ctx, arr_ty),
-                       tint::Vector{
+                       Vector{
                            b.Decl(arr),
                            for_loop,
                            b.Return(arr),
                        });
             } else {
-                tint::Vector<const Expression*, 8> values;
+                Vector<const ast::Expression*, 8> values;
                 if (auto* mat_ty = el_ty->As<core::type::Matrix>()) {
                     auto* vec_ty = mat_ty->ColumnType();
                     Symbol load = LoadFunc(vec_ty, address_space, buffer);
@@ -534,7 +534,7 @@
                     }
                 }
                 b.Func(name, params, CreateASTTypeFor(ctx, el_ty),
-                       tint::Vector{
+                       Vector{
                            b.Return(b.Call(CreateASTTypeFor(ctx, el_ty), values)),
                        });
             }
@@ -550,7 +550,7 @@
     /// @return the name of the function that performs the store
     Symbol StoreFunc(const core::type::Type* el_ty, const Symbol& buffer) {
         return tint::GetOrCreate(store_funcs, LoadStoreKey{el_ty, buffer}, [&] {
-            tint::Vector params{
+            Vector params{
                 b.Param("offset", b.ty.u32()),
                 b.Param("value", CreateASTTypeFor(ctx, el_ty)),
             };
@@ -559,12 +559,12 @@
 
             if (auto* intrinsic = IntrinsicStoreFor(ctx.dst, el_ty, buffer)) {
                 b.Func(name, params, b.ty.void_(), nullptr,
-                       tint::Vector{
+                       Vector{
                            intrinsic,
-                           b.Disable(DisabledValidation::kFunctionHasNoBody),
+                           b.Disable(ast::DisabledValidation::kFunctionHasNoBody),
                        });
             } else {
-                auto body = Switch<tint::Vector<const Statement*, 8>>(
+                auto body = Switch<Vector<const ast::Statement*, 8>>(
                     el_ty,  //
                     [&](const core::type::Array* arr_ty) {
                         // fn store_func(buffer : buf_ty, offset : u32, value : el_ty) {
@@ -588,7 +588,7 @@
                             TINT_ICE() << "unexpected non-constant array count";
                             arr_cnt = 1;
                         }
-                        auto* for_cond = b.create<BinaryExpression>(
+                        auto* for_cond = b.create<ast::BinaryExpression>(
                             core::BinaryOp::kLessThan, b.Expr(i), b.Expr(u32(arr_cnt.value())));
                         auto* for_cont = b.Assign(i, b.Add(i, 1_u));
                         auto* arr_el = b.IndexAccessor(array, i);
@@ -596,12 +596,12 @@
                         auto* store_stmt = b.CallStmt(b.Call(store, el_offset, arr_el));
                         auto* for_loop = b.For(for_init, for_cond, for_cont, b.Block(store_stmt));
 
-                        return tint::Vector{b.Decl(array), for_loop};
+                        return Vector{b.Decl(array), for_loop};
                     },
                     [&](const core::type::Matrix* mat_ty) {
                         auto* vec_ty = mat_ty->ColumnType();
                         Symbol store = StoreFunc(vec_ty, buffer);
-                        tint::Vector<const Statement*, 4> stmts;
+                        Vector<const ast::Statement*, 4> stmts;
                         for (uint32_t i = 0; i < mat_ty->columns(); i++) {
                             auto* offset = b.Add("offset", u32(i * mat_ty->ColumnStride()));
                             auto* element = b.IndexAccessor("value", u32(i));
@@ -611,7 +611,7 @@
                         return stmts;
                     },
                     [&](const core::type::Struct* str) {
-                        tint::Vector<const Statement*, 8> stmts;
+                        Vector<const ast::Statement*, 8> stmts;
                         for (auto* member : str->Members()) {
                             auto* offset = b.Add("offset", u32(member->Offset()));
                             auto* element = b.MemberAccessor("value", ctx.Clone(member->Name()));
@@ -643,7 +643,7 @@
         return tint::GetOrCreate(atomic_funcs, AtomicKey{el_ty, op, buffer}, [&] {
             // The first parameter to all WGSL atomics is the expression to the
             // atomic. This is replaced with two parameters: the buffer and offset.
-            tint::Vector params{b.Param("offset", b.ty.u32())};
+            Vector params{b.Param("offset", b.ty.u32())};
 
             // Other parameters are copied as-is:
             for (size_t i = 1; i < intrinsic->Parameters().Length(); i++) {
@@ -658,13 +658,13 @@
                            << el_ty->TypeInfo().name;
             }
 
-            Type ret_ty = CreateASTTypeFor(ctx, intrinsic->ReturnType());
+            ast::Type ret_ty = CreateASTTypeFor(ctx, intrinsic->ReturnType());
 
             auto name = b.Symbols().New(buffer.Name() + intrinsic->str());
             b.Func(name, std::move(params), ret_ty, nullptr,
-                   tint::Vector{
+                   Vector{
                        atomic,
-                       b.Disable(DisabledValidation::kFunctionHasNoBody),
+                       b.Disable(ast::DisabledValidation::kFunctionHasNoBody),
                    });
             return name;
         });
@@ -672,12 +672,12 @@
 };
 
 DecomposeMemoryAccess::Intrinsic::Intrinsic(GenerationID pid,
-                                            NodeID nid,
+                                            ast::NodeID nid,
                                             Op o,
                                             DataType ty,
                                             core::AddressSpace as,
-                                            const IdentifierExpression* buf)
-    : Base(pid, nid, tint::Vector{buf}), op(o), type(ty), address_space(as) {}
+                                            const ast::IdentifierExpression* buf)
+    : Base(pid, nid, Vector{buf}), op(o), type(ty), address_space(as) {}
 DecomposeMemoryAccess::Intrinsic::~Intrinsic() = default;
 std::string DecomposeMemoryAccess::Intrinsic::InternalName() const {
     StringStream ss;
@@ -787,16 +787,17 @@
     return op != Op::kLoad && op != Op::kStore;
 }
 
-const IdentifierExpression* DecomposeMemoryAccess::Intrinsic::Buffer() const {
+const ast::IdentifierExpression* DecomposeMemoryAccess::Intrinsic::Buffer() const {
     return dependencies[0];
 }
 
 DecomposeMemoryAccess::DecomposeMemoryAccess() = default;
 DecomposeMemoryAccess::~DecomposeMemoryAccess() = default;
 
-Transform::ApplyResult DecomposeMemoryAccess::Apply(const Program* src,
-                                                    const DataMap&,
-                                                    DataMap&) const {
+ast::transform::Transform::ApplyResult DecomposeMemoryAccess::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -815,7 +816,7 @@
     // nodes are fully immutable and require their children to be constructed
     // first so their pointer can be passed to the parent's initializer.
     for (auto* node : src->ASTNodes().Objects()) {
-        if (auto* ident = node->As<IdentifierExpression>()) {
+        if (auto* ident = node->As<ast::IdentifierExpression>()) {
             // X
             if (auto* sem_ident = sem.GetVal(ident)) {
                 if (auto* user = sem_ident->UnwrapLoad()->As<sem::VariableUser>()) {
@@ -835,7 +836,7 @@
             continue;
         }
 
-        if (auto* accessor = node->As<MemberAccessorExpression>()) {
+        if (auto* accessor = node->As<ast::MemberAccessorExpression>()) {
             // X.Y
             auto* accessor_sem = sem.Get(accessor)->UnwrapLoad();
             if (auto* swizzle = accessor_sem->As<sem::Swizzle>()) {
@@ -865,7 +866,7 @@
             continue;
         }
 
-        if (auto* accessor = node->As<IndexAccessorExpression>()) {
+        if (auto* accessor = node->As<ast::IndexAccessorExpression>()) {
             if (auto access = state.TakeAccess(accessor->object)) {
                 // X[Y]
                 if (auto* arr = access.type->As<core::type::Array>()) {
@@ -898,7 +899,7 @@
             }
         }
 
-        if (auto* op = node->As<UnaryOpExpression>()) {
+        if (auto* op = node->As<ast::UnaryOpExpression>()) {
             if (op->op == core::UnaryOp::kAddressOf) {
                 // &X
                 if (auto access = state.TakeAccess(op->expr)) {
@@ -910,7 +911,7 @@
             }
         }
 
-        if (auto* assign = node->As<AssignmentStatement>()) {
+        if (auto* assign = node->As<ast::AssignmentStatement>()) {
             // X = Y
             // Move the LHS access to a store.
             if (auto lhs = state.TakeAccess(assign->lhs)) {
@@ -918,7 +919,7 @@
             }
         }
 
-        if (auto* call_expr = node->As<CallExpression>()) {
+        if (auto* call_expr = node->As<ast::CallExpression>()) {
             auto* call = sem.Get(call_expr)->UnwrapMaterialize()->As<sem::Call>();
             if (auto* builtin = call->Target()->As<sem::Builtin>()) {
                 if (builtin->Type() == core::Function::kArrayLength) {
@@ -937,7 +938,7 @@
                             auto buffer = ctx.Clone(access.var->Declaration()->name->symbol);
                             Symbol func = state.AtomicFunc(el_ty, builtin, buffer);
 
-                            tint::Vector<const Expression*, 8> args{offset};
+                            Vector<const ast::Expression*, 8> args{offset};
                             for (size_t i = 1; i < call_expr->args.Length(); i++) {
                                 auto* arg = call_expr->args[i];
                                 args.Push(ctx.Clone(arg));
@@ -985,7 +986,7 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::Offset);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::OffsetLiteral);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::Offset);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::OffsetLiteral);
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.h b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h
similarity index 79%
rename from src/tint/lang/wgsl/ast/transform/decompose_memory_access.h
rename to src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h
index 120bb26..cea9dd9 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h
@@ -12,25 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_MEMORY_ACCESS_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_MEMORY_ACCESS_H_
+#ifndef SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_DECOMPOSE_MEMORY_ACCESS_H_
+#define SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_DECOMPOSE_MEMORY_ACCESS_H_
 
 #include <string>
 
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// DecomposeMemoryAccess is a transform used to replace storage and uniform buffer accesses with a
 /// combination of load, store or atomic functions on primitive types.
-class DecomposeMemoryAccess final : public Castable<DecomposeMemoryAccess, Transform> {
+class DecomposeMemoryAccess final
+    : public Castable<DecomposeMemoryAccess, ast::transform::Transform> {
   public:
     /// Intrinsic is an InternalAttribute that's used to decorate a stub function so that the HLSL
     /// transforms this into calls to
     /// `[RW]ByteAddressBuffer.Load[N]()` or `[RW]ByteAddressBuffer.Store[N]()`,
     /// with a possible cast.
-    class Intrinsic final : public Castable<Intrinsic, InternalAttribute> {
+    class Intrinsic final : public Castable<Intrinsic, ast::InternalAttribute> {
       public:
         /// Intrinsic op
         enum class Op {
@@ -77,11 +78,11 @@
         /// @param address_space the address space of the buffer
         /// @param buffer the storage or uniform buffer identifier
         Intrinsic(GenerationID pid,
-                  NodeID nid,
+                  ast::NodeID nid,
                   Op o,
                   DataType type,
                   core::AddressSpace address_space,
-                  const IdentifierExpression* buffer);
+                  const ast::IdentifierExpression* buffer);
         /// Destructor
         ~Intrinsic() override;
 
@@ -92,13 +93,13 @@
         /// Performs a deep clone of this object using the program::CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const Intrinsic* Clone(CloneContext& ctx) const override;
+        const Intrinsic* Clone(ast::CloneContext& ctx) const override;
 
         /// @return true if op is atomic
         bool IsAtomic() const;
 
         /// @return the buffer that this intrinsic operates on
-        const IdentifierExpression* Buffer() const;
+        const ast::IdentifierExpression* Buffer() const;
 
         /// The op of the intrinsic
         const Op op;
@@ -115,15 +116,15 @@
     /// Destructor
     ~DecomposeMemoryAccess() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_MEMORY_ACCESS_H_
+#endif  // SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_DECOMPOSE_MEMORY_ACCESS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_memory_access_test.cc b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access_test.cc
similarity index 99%
rename from src/tint/lang/wgsl/ast/transform/decompose_memory_access_test.cc
rename to src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access_test.cc
index 9d07277..e2b900d 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_memory_access_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access_test.cc
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/decompose_memory_access.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
-using DecomposeMemoryAccessTest = TransformTest;
+using DecomposeMemoryAccessTest = ast::transform::TransformTest;
 
 TEST_F(DecomposeMemoryAccessTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -3942,4 +3942,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
similarity index 65%
rename from src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
rename to src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
index e4c1bb4..883c911 100644
--- a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h"
 
 #include <unordered_map>
 #include <utility>
@@ -30,9 +30,9 @@
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::LocalizeStructArrayAssignment);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::LocalizeStructArrayAssignment);
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// PIMPL state for the transform
 struct LocalizeStructArrayAssignment::State {
@@ -45,14 +45,14 @@
     ApplyResult Run() {
         struct Shared {
             bool process_nested_nodes = false;
-            tint::Vector<const Statement*, 4> insert_before_stmts;
-            tint::Vector<const Statement*, 4> insert_after_stmts;
+            Vector<const ast::Statement*, 4> insert_before_stmts;
+            Vector<const ast::Statement*, 4> insert_after_stmts;
         } s;
 
         bool made_changes = false;
 
         for (auto* node : ctx.src->ASTNodes().Objects()) {
-            if (auto* assign_stmt = node->As<AssignmentStatement>()) {
+            if (auto* assign_stmt = node->As<ast::AssignmentStatement>()) {
                 // Process if it's an assignment statement to a dynamically indexed array
                 // within a struct on a function or private storage variable. This
                 // specific use-case is what FXC fails to compile with:
@@ -72,7 +72,7 @@
                     // Reset shared state for this assignment statement
                     s = Shared{};
 
-                    const Expression* new_lhs = nullptr;
+                    const ast::Expression* new_lhs = nullptr;
                     {
                         TINT_SCOPED_ASSIGNMENT(s.process_nested_nodes, true);
                         new_lhs = ctx.Clone(assign_stmt->lhs);
@@ -100,52 +100,53 @@
             return SkipTransform;
         }
 
-        ctx.ReplaceAll([&](const IndexAccessorExpression* index_access) -> const Expression* {
-            if (!s.process_nested_nodes) {
-                return nullptr;
-            }
-
-            // Indexing a member access expr?
-            auto* mem_access = index_access->object->As<MemberAccessorExpression>();
-            if (!mem_access) {
-                return nullptr;
-            }
-
-            // Process any nested IndexAccessorExpressions
-            mem_access = ctx.Clone(mem_access);
-
-            // Store the address of the member access into a let as we need to read
-            // the value twice e.g. let tint_symbol = &(s.a1);
-            auto mem_access_ptr = b.Sym();
-            s.insert_before_stmts.Push(b.Decl(b.Let(mem_access_ptr, b.AddressOf(mem_access))));
-
-            // Disable further transforms when cloning
-            TINT_SCOPED_ASSIGNMENT(s.process_nested_nodes, false);
-
-            // Copy entire array out of struct into local temp var
-            // e.g. var tint_symbol_1 = *(tint_symbol);
-            auto tmp_var = b.Sym();
-            s.insert_before_stmts.Push(b.Decl(b.Var(tmp_var, b.Deref(mem_access_ptr))));
-
-            // Replace input index_access with a clone of itself, but with its
-            // .object replaced by the new temp var. This is returned from this
-            // function to modify the original assignment statement. e.g.
-            // tint_symbol_1[uniforms.i]
-            auto* new_index_access = b.IndexAccessor(tmp_var, ctx.Clone(index_access->index));
-
-            // Assign temp var back to array
-            // e.g. *(tint_symbol) = tint_symbol_1;
-            auto* assign_rhs_to_temp = b.Assign(b.Deref(mem_access_ptr), tmp_var);
-            {
-                tint::Vector<const Statement*, 8> stmts{assign_rhs_to_temp};
-                for (auto* stmt : s.insert_after_stmts) {
-                    stmts.Push(stmt);
+        ctx.ReplaceAll(
+            [&](const ast::IndexAccessorExpression* index_access) -> const ast::Expression* {
+                if (!s.process_nested_nodes) {
+                    return nullptr;
                 }
-                s.insert_after_stmts = std::move(stmts);
-            }
 
-            return new_index_access;
-        });
+                // Indexing a member access expr?
+                auto* mem_access = index_access->object->As<ast::MemberAccessorExpression>();
+                if (!mem_access) {
+                    return nullptr;
+                }
+
+                // Process any nested IndexAccessorExpressions
+                mem_access = ctx.Clone(mem_access);
+
+                // Store the address of the member access into a let as we need to read
+                // the value twice e.g. let tint_symbol = &(s.a1);
+                auto mem_access_ptr = b.Sym();
+                s.insert_before_stmts.Push(b.Decl(b.Let(mem_access_ptr, b.AddressOf(mem_access))));
+
+                // Disable further transforms when cloning
+                TINT_SCOPED_ASSIGNMENT(s.process_nested_nodes, false);
+
+                // Copy entire array out of struct into local temp var
+                // e.g. var tint_symbol_1 = *(tint_symbol);
+                auto tmp_var = b.Sym();
+                s.insert_before_stmts.Push(b.Decl(b.Var(tmp_var, b.Deref(mem_access_ptr))));
+
+                // Replace input index_access with a clone of itself, but with its
+                // .object replaced by the new temp var. This is returned from this
+                // function to modify the original assignment statement. e.g.
+                // tint_symbol_1[uniforms.i]
+                auto* new_index_access = b.IndexAccessor(tmp_var, ctx.Clone(index_access->index));
+
+                // Assign temp var back to array
+                // e.g. *(tint_symbol) = tint_symbol_1;
+                auto* assign_rhs_to_temp = b.Assign(b.Deref(mem_access_ptr), tmp_var);
+                {
+                    Vector<const ast::Statement*, 8> stmts{assign_rhs_to_temp};
+                    for (auto* stmt : s.insert_after_stmts) {
+                        stmts.Push(stmt);
+                    }
+                    s.insert_after_stmts = std::move(stmts);
+                }
+
+                return new_index_access;
+            });
 
         ctx.Clone();
         return resolver::Resolve(b);
@@ -161,22 +162,22 @@
 
     /// Returns true if `expr` contains an index accessor expression to a
     /// structure member of array type.
-    bool ContainsStructArrayIndex(const Expression* expr) {
+    bool ContainsStructArrayIndex(const ast::Expression* expr) {
         bool result = false;
-        TraverseExpressions(expr, [&](const IndexAccessorExpression* ia) {
+        TraverseExpressions(expr, [&](const ast::IndexAccessorExpression* ia) {
             // Indexing using a runtime value?
             auto* idx_sem = src->Sem().GetVal(ia->index);
             if (!idx_sem->ConstantValue()) {
                 // Indexing a member access expr?
-                if (auto* ma = ia->object->As<MemberAccessorExpression>()) {
+                if (auto* ma = ia->object->As<ast::MemberAccessorExpression>()) {
                     // That accesses an array?
                     if (src->TypeOf(ma)->UnwrapRef()->Is<core::type::Array>()) {
                         result = true;
-                        return TraverseAction::Stop;
+                        return ast::TraverseAction::Stop;
                     }
                 }
             }
-            return TraverseAction::Descend;
+            return ast::TraverseAction::Descend;
         });
 
         return result;
@@ -186,7 +187,7 @@
     // of the assignment statement.
     // See https://www.w3.org/TR/WGSL/#originating-variable-section
     std::pair<const core::type::Type*, core::AddressSpace> GetOriginatingTypeAndAddressSpace(
-        const AssignmentStatement* assign_stmt) {
+        const ast::AssignmentStatement* assign_stmt) {
         auto* root_ident = src->Sem().GetVal(assign_stmt->lhs)->RootIdentifier();
         if (TINT_UNLIKELY(!root_ident)) {
             TINT_ICE() << "Unable to determine originating variable for lhs of assignment "
@@ -214,10 +215,11 @@
 
 LocalizeStructArrayAssignment::~LocalizeStructArrayAssignment() = default;
 
-Transform::ApplyResult LocalizeStructArrayAssignment::Apply(const Program* src,
-                                                            const DataMap&,
-                                                            DataMap&) const {
+ast::transform::Transform::ApplyResult LocalizeStructArrayAssignment::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
     return State{src}.Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.h b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
similarity index 70%
rename from src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.h
rename to src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
index 32cf93e..d70e04e 100644
--- a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_LOCALIZE_STRUCT_ARRAY_ASSIGNMENT_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_LOCALIZE_STRUCT_ARRAY_ASSIGNMENT_H_
+#ifndef SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_LOCALIZE_STRUCT_ARRAY_ASSIGNMENT_H_
+#define SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_LOCALIZE_STRUCT_ARRAY_ASSIGNMENT_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// This transforms replaces assignment to dynamically-indexed fixed-size arrays
 /// in structs on shader-local variables with code that copies the arrays to a
@@ -28,7 +28,7 @@
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
 class LocalizeStructArrayAssignment final
-    : public Castable<LocalizeStructArrayAssignment, Transform> {
+    : public Castable<LocalizeStructArrayAssignment, ast::transform::Transform> {
   public:
     /// Constructor
     LocalizeStructArrayAssignment();
@@ -36,15 +36,15 @@
     /// Destructor
     ~LocalizeStructArrayAssignment() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_LOCALIZE_STRUCT_ARRAY_ASSIGNMENT_H_
+#endif  // SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_LOCALIZE_STRUCT_ARRAY_ASSIGNMENT_H_
diff --git a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment_test.cc b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment_test.cc
similarity index 97%
rename from src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment_test.cc
rename to src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment_test.cc
index 1ca28d1..f6441f8 100644
--- a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment_test.cc
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
-using LocalizeStructArrayAssignmentTest = TransformTest;
+using LocalizeStructArrayAssignmentTest = ast::transform::TransformTest;
+using Unshadow = ast::transform::Unshadow;
+using SimplifyPointers = ast::transform::SimplifyPointers;
 
 TEST_F(LocalizeStructArrayAssignmentTest, EmptyModule) {
     auto* src = R"()";
@@ -844,4 +846,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
similarity index 88%
rename from src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
rename to src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
index d337198..3bb5a40 100644
--- a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h"
 
 #include <memory>
 #include <string>
@@ -30,15 +30,15 @@
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::NumWorkgroupsFromUniform);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::NumWorkgroupsFromUniform::Config);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::NumWorkgroupsFromUniform);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::NumWorkgroupsFromUniform::Config);
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
 bool ShouldRun(const Program* program) {
     for (auto* node : program->ASTNodes().Objects()) {
-        if (auto* attr = node->As<BuiltinAttribute>()) {
+        if (auto* attr = node->As<ast::BuiltinAttribute>()) {
             if (program->Sem().Get(attr)->Value() == core::BuiltinValue::kNumWorkgroups) {
                 return true;
             }
@@ -68,9 +68,10 @@
 NumWorkgroupsFromUniform::NumWorkgroupsFromUniform() = default;
 NumWorkgroupsFromUniform::~NumWorkgroupsFromUniform() = default;
 
-Transform::ApplyResult NumWorkgroupsFromUniform::Apply(const Program* src,
-                                                       const DataMap& inputs,
-                                                       DataMap&) const {
+ast::transform::Transform::ApplyResult NumWorkgroupsFromUniform::Apply(
+    const Program* src,
+    const ast::transform::DataMap& inputs,
+    ast::transform::DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
@@ -91,7 +92,7 @@
     std::unordered_set<Accessor, Accessor::Hasher> to_replace;
     for (auto* func : src->AST().Functions()) {
         // num_workgroups is only valid for compute stages.
-        if (func->PipelineStage() != PipelineStage::kCompute) {
+        if (func->PipelineStage() != ast::PipelineStage::kCompute) {
             continue;
         }
 
@@ -131,7 +132,7 @@
 
     // Get (or create, on first call) the uniform buffer that will receive the
     // number of workgroups.
-    const Variable* num_workgroups_ubo = nullptr;
+    const ast::Variable* num_workgroups_ubo = nullptr;
     auto get_ubo = [&] {
         if (!num_workgroups_ubo) {
             auto* num_workgroups_struct =
@@ -171,11 +172,11 @@
     // Now replace all the places where the builtins are accessed with the value
     // loaded from the uniform buffer.
     for (auto* node : src->ASTNodes().Objects()) {
-        auto* accessor = node->As<MemberAccessorExpression>();
+        auto* accessor = node->As<ast::MemberAccessorExpression>();
         if (!accessor) {
             continue;
         }
-        auto* ident = accessor->object->As<IdentifierExpression>();
+        auto* ident = accessor->object->As<ast::IdentifierExpression>();
         if (!ident) {
             continue;
         }
@@ -195,4 +196,4 @@
 NumWorkgroupsFromUniform::Config::Config(const Config&) = default;
 NumWorkgroupsFromUniform::Config::~Config() = default;
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
similarity index 78%
rename from src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h
rename to src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
index 50407e4..4b26e33 100644
--- a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_NUM_WORKGROUPS_FROM_UNIFORM_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_NUM_WORKGROUPS_FROM_UNIFORM_H_
+#ifndef SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_NUM_WORKGROUPS_FROM_UNIFORM_H_
+#define SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_NUM_WORKGROUPS_FROM_UNIFORM_H_
 
 #include <optional>
 
 #include "src/tint/api/common/binding_point.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// NumWorkgroupsFromUniform is a transform that implements the `num_workgroups`
 /// builtin by loading it from a uniform buffer.
@@ -39,7 +39,8 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * CanonicalizeEntryPointIO
-class NumWorkgroupsFromUniform final : public Castable<NumWorkgroupsFromUniform, Transform> {
+class NumWorkgroupsFromUniform final
+    : public Castable<NumWorkgroupsFromUniform, ast::transform::Transform> {
   public:
     /// Constructor
     NumWorkgroupsFromUniform();
@@ -47,7 +48,7 @@
     ~NumWorkgroupsFromUniform() override;
 
     /// Configuration options for the NumWorkgroupsFromUniform transform.
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public Castable<Config, ast::transform::Data> {
         /// Constructor
         /// @param ubo_bp the binding point to use for the generated uniform buffer. If ubo_bp
         /// contains no value, a free binding point will be used to ensure the generated program is
@@ -67,12 +68,12 @@
         std::optional<BindingPoint> ubo_binding;
     };
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_NUM_WORKGROUPS_FROM_UNIFORM_H_
+#endif  // SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_NUM_WORKGROUPS_FROM_UNIFORM_H_
diff --git a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform_test.cc b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform_test.cc
similarity index 94%
rename from src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform_test.cc
rename to src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform_test.cc
index a9651e0..e29ec8c 100644
--- a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h"
 
 #include <utility>
 
@@ -20,15 +20,17 @@
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
-using NumWorkgroupsFromUniformTest = TransformTest;
+using NumWorkgroupsFromUniformTest = ast::transform::TransformTest;
+using CanonicalizeEntryPointIO = ast::transform::CanonicalizeEntryPointIO;
+using Unshadow = ast::transform::Unshadow;
 
 TEST_F(NumWorkgroupsFromUniformTest, ShouldRunEmptyModule) {
     auto* src = R"()";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     EXPECT_FALSE(ShouldRun<NumWorkgroupsFromUniform>(src, data));
 }
@@ -40,7 +42,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     EXPECT_TRUE(ShouldRun<NumWorkgroupsFromUniform>(src, data));
 }
@@ -52,10 +54,9 @@
 }
 )";
 
-    auto* expect =
-        "error: missing transform data for tint::ast::transform::NumWorkgroupsFromUniform";
+    auto* expect = "error: missing transform data for tint::hlsl::writer::NumWorkgroupsFromUniform";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
     EXPECT_EQ(expect, str(got));
@@ -90,7 +91,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -134,7 +135,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -178,7 +179,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -233,7 +234,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -289,7 +290,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -388,7 +389,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -429,7 +430,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -530,7 +531,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     // Make binding point unspecified.
     data.Add<NumWorkgroupsFromUniform::Config>(std::nullopt);
@@ -684,7 +685,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     // Make binding point unspecified.
     data.Add<NumWorkgroupsFromUniform::Config>(std::nullopt);
@@ -693,4 +694,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc
new file mode 100644
index 0000000..4f1808b
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc
@@ -0,0 +1,187 @@
+// Copyright 2022 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/hlsl/writer/ast_raise/remove_continue_in_switch.h"
+
+#include <string>
+#include <utility>
+
+#include "src/tint/lang/wgsl/ast/continue_statement.h"
+#include "src/tint/lang/wgsl/ast/switch_statement.h"
+#include "src/tint/lang/wgsl/ast/transform/get_insertion_point.h"
+#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/block_statement.h"
+#include "src/tint/lang/wgsl/sem/loop_statement.h"
+#include "src/tint/lang/wgsl/sem/switch_statement.h"
+#include "src/tint/utils/containers/hashmap.h"
+#include "src/tint/utils/containers/hashset.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::RemoveContinueInSwitch);
+
+namespace tint::hlsl::writer {
+
+/// PIMPL state for the transform
+struct RemoveContinueInSwitch::State {
+    /// Constructor
+    /// @param program the source program
+    explicit State(const Program* program) : src(program) {}
+
+    /// Runs the transform
+    /// @returns the new program or SkipTransform if the transform is not required
+    ApplyResult Run() {
+        // First collect all switch statements within loops that contain a continue statement.
+        for (auto* node : src->ASTNodes().Objects()) {
+            auto* cont = node->As<ast::ContinueStatement>();
+            if (!cont) {
+                continue;
+            }
+
+            // If first parent is not a switch within a loop, skip
+            auto* switch_stmt = GetParentSwitchInLoop(sem, cont);
+            if (!switch_stmt) {
+                continue;
+            }
+
+            auto& info = switch_infos.GetOrCreate(switch_stmt, [&] {
+                switch_stmts.Push(switch_stmt);
+                auto* block = sem.Get(switch_stmt)->FindFirstParent<sem::LoopBlockStatement>();
+                return SwitchInfo{/* loop_block */ block, /* continues */ Empty};
+            });
+            info.continues.Push(cont);
+        }
+
+        if (switch_stmts.IsEmpty()) {
+            return SkipTransform;
+        }
+
+        // For each switch statement:
+        // 1. Declare a 'tint_continue' var just before the parent loop, and reset it to false at
+        // the top of the loop body.
+        // 2. Replace 'continue' with 'tint_continue = true; break;'
+        // 3. Insert 'if (tint_continue) { break; }' after switch, and all parent switches, except
+        // for the parent-most, for which we insert 'if (tint_continue) { continue; }'
+        for (auto* switch_stmt : switch_stmts) {
+            const auto& info = switch_infos.Get(switch_stmt);
+
+            auto var_name = loop_to_var.GetOrCreate(info->loop_block, [&] {
+                // Create and insert 'var tint_continue : bool;' before loop
+                auto var = b.Symbols().New("tint_continue");
+                auto* decl = b.Decl(b.Var(var, b.ty.bool_()));
+                auto ip = ast::transform::utils::GetInsertionPoint(
+                    ctx, info->loop_block->Parent()->Declaration());
+                ctx.InsertBefore(ip.first->Declaration()->statements, ip.second, decl);
+
+                // Insert 'tint_continue = false' at top of loop body
+                auto assign_false = b.Assign(var, false);
+                ctx.InsertFront(info->loop_block->Declaration()->statements, assign_false);
+
+                return var;
+            });
+
+            for (auto& c : info->continues) {
+                // Replace 'continue;' with 'tint_continue = true; break;'
+                ctx.Replace(c, b.Assign(b.Expr(var_name), true));
+                ctx.InsertAfter(sem.Get(c)->Block()->Declaration()->statements, c, b.Break());
+            }
+
+            // Insert 'if (tint_continue) { break; }' after switch, and all parent switches,
+            // except for the parent-most, for which we insert 'if (tint_continue) { continue; }'
+            auto* curr_switch = switch_stmt;
+            while (curr_switch) {
+                auto* curr_switch_sem = sem.Get(curr_switch);
+                auto* parent = curr_switch_sem->Parent()->Declaration();
+                auto* next_switch = GetParentSwitchInLoop(sem, parent);
+
+                if (switch_handles_continue.Add(curr_switch)) {
+                    const ast::IfStatement* if_stmt = nullptr;
+                    if (next_switch) {
+                        if_stmt = b.If(b.Expr(var_name), b.Block(b.Break()));
+                    } else {
+                        if_stmt = b.If(b.Expr(var_name), b.Block(b.Continue()));
+                    }
+                    ctx.InsertAfter(curr_switch_sem->Block()->Declaration()->statements,
+                                    curr_switch, if_stmt);
+                }
+
+                curr_switch = next_switch;
+            }
+        }
+
+        ctx.Clone();
+        return resolver::Resolve(b);
+    }
+
+  private:
+    /// The source program
+    const Program* const src;
+    /// The target program builder
+    ProgramBuilder b;
+    /// The clone context
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    /// Alias to src->sem
+    const sem::Info& sem = src->Sem();
+
+    // Vector of switch statements within a loop that contains at least one continue statement.
+    Vector<const ast::SwitchStatement*, 4> switch_stmts;
+
+    // Info for each switch statement within a loop that contains at least one continue statement.
+    struct SwitchInfo {
+        // Loop block containing this switch
+        const sem::LoopBlockStatement* loop_block;
+        // Continue statements within this switch
+        Vector<const ast::ContinueStatement*, 4> continues;
+    };
+
+    // Map of switch statements to per-switch info for switch statements within a loop that contains
+    // at least one continue statement.
+    Hashmap<const ast::SwitchStatement*, SwitchInfo, 4> switch_infos;
+
+    // Map of loop block statement to the single 'tint_continue' variable used to replace 'continue'
+    // control flow.
+    Hashmap<const sem::LoopBlockStatement*, Symbol, 4> loop_to_var;
+
+    // Set used to avoid duplicating 'if (tint_continue) { break/continue; }' after each switch
+    // within a loop.
+    Hashset<const ast::SwitchStatement*, 4> switch_handles_continue;
+
+    // If `stmt` is within a switch statement within a loop, returns a pointer to
+    // that switch statement.
+    static const ast::SwitchStatement* GetParentSwitchInLoop(const sem::Info& sem,
+                                                             const ast::Statement* stmt) {
+        // Find whether first parent is a switch or a loop
+        auto* sem_stmt = sem.Get(stmt);
+        auto* sem_parent =
+            sem_stmt->FindFirstParent<sem::SwitchStatement, sem::LoopBlockStatement>();
+
+        if (!sem_parent) {
+            return nullptr;
+        }
+        return sem_parent->Declaration()->As<ast::SwitchStatement>();
+    }
+};
+
+RemoveContinueInSwitch::RemoveContinueInSwitch() = default;
+RemoveContinueInSwitch::~RemoveContinueInSwitch() = default;
+
+ast::transform::Transform::ApplyResult RemoveContinueInSwitch::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
+    State state(src);
+    return state.Run();
+}
+
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.h b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
similarity index 66%
rename from src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.h
rename to src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
index 8775c63..4ad747e 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
@@ -12,18 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_REMOVE_CONTINUE_IN_SWITCH_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_REMOVE_CONTINUE_IN_SWITCH_H_
+#ifndef SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_REMOVE_CONTINUE_IN_SWITCH_H_
+#define SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_REMOVE_CONTINUE_IN_SWITCH_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// This transform replaces continue statements in switch cases with setting a
 /// bool variable, and checking if the variable is set after the switch to
 /// continue. It is necessary to work around FXC "error X3708: continue cannot
 /// be used in a switch". See crbug.com/tint/1080.
-class RemoveContinueInSwitch final : public Castable<RemoveContinueInSwitch, Transform> {
+class RemoveContinueInSwitch final
+    : public Castable<RemoveContinueInSwitch, ast::transform::Transform> {
   public:
     /// Constructor
     RemoveContinueInSwitch();
@@ -31,15 +32,15 @@
     /// Destructor
     ~RemoveContinueInSwitch() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_REMOVE_CONTINUE_IN_SWITCH_H_
+#endif  // SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_REMOVE_CONTINUE_IN_SWITCH_H_
diff --git a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch_test.cc b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch_test.cc
similarity index 63%
rename from src/tint/lang/wgsl/ast/transform/remove_continue_in_switch_test.cc
rename to src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch_test.cc
index ea82a6c..e03e734 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch_test.cc
@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h"
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
-using RemoveContinueInSwitchTest = TransformTest;
+using RemoveContinueInSwitchTest = ast::transform::TransformTest;
 
 TEST_F(RemoveContinueInSwitchTest, ShouldRun_True) {
     auto* src = R"(
@@ -28,7 +28,6 @@
     switch(i) {
       case 0: {
         continue;
-        break;
       }
       default: {
         break;
@@ -101,7 +100,7 @@
     auto* src = "";
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -116,7 +115,6 @@
     switch(i) {
       case 0: {
         continue;
-        break;
       }
       default: {
         break;
@@ -135,15 +133,13 @@
     auto* expect = R"(
 fn f() {
   var i = 0;
+  var tint_continue : bool;
   loop {
+    tint_continue = false;
     let marker1 = 0;
-    var tint_continue : bool = false;
     switch(i) {
       case 0: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
         break;
       }
       default: {
@@ -163,7 +159,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -178,15 +174,12 @@
     switch(i) {
       case 0: {
         continue;
-        break;
       }
       case 1: {
         continue;
-        break;
       }
       case 2: {
         continue;
-        break;
       }
       default: {
         break;
@@ -205,29 +198,21 @@
     auto* expect = R"(
 fn f() {
   var i = 0;
+  var tint_continue : bool;
   loop {
+    tint_continue = false;
     let marker1 = 0;
-    var tint_continue : bool = false;
     switch(i) {
       case 0: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
         break;
       }
       case 1: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
         break;
       }
       case 2: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
         break;
       }
       default: {
@@ -247,7 +232,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -262,7 +247,6 @@
     switch(i) {
       case 0: {
         continue;
-        break;
       }
       default: {
         break;
@@ -274,7 +258,6 @@
     switch(i) {
       case 0: {
         continue;
-        break;
       }
       default: {
         break;
@@ -290,15 +273,13 @@
     auto* expect = R"(
 fn f() {
   var i = 0;
+  var tint_continue : bool;
   loop {
+    tint_continue = false;
     let marker1 = 0;
-    var tint_continue : bool = false;
     switch(i) {
       case 0: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
         break;
       }
       default: {
@@ -310,20 +291,16 @@
     }
     let marker2 = 0;
     let marker3 = 0;
-    var tint_continue_1 : bool = false;
     switch(i) {
       case 0: {
-        {
-          tint_continue_1 = true;
-          break;
-        }
+        tint_continue = true;
         break;
       }
       default: {
         break;
       }
     }
-    if (tint_continue_1) {
+    if (tint_continue) {
       continue;
     }
     let marker4 = 0;
@@ -332,98 +309,264 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(RemoveContinueInSwitchTest, NestedLoopSwitch) {
+TEST_F(RemoveContinueInSwitchTest, NestedLoopSwitchSwitch) {
     auto* src = R"(
 fn f() {
-  var i = 0;
-  loop {
-    let marker1 = 0;
+  var j = 0;
+  for (var i = 0; i < 2; i += 2) {
     switch(i) {
       case 0: {
-        var j = 0;
-        loop {
-          let marker3 = 0;
-          switch(j) {
-            case 0: {
-              continue;
-              break;
-            }
-            default: {
-              break;
-            }
+        switch(j) {
+          case 0: {
+            continue;
           }
-          let marker4 = 0;
-          break;
+          default: {
+          }
         }
-        continue;
-        break;
       }
       default: {
-        break;
       }
     }
-    let marker2 = 0;
-    break;
   }
 }
 )";
 
     auto* expect = R"(
 fn f() {
-  var i = 0;
-  loop {
-    let marker1 = 0;
-    var tint_continue_1 : bool = false;
+  var j = 0;
+  var tint_continue : bool;
+  for(var i = 0; (i < 2); i += 2) {
+    tint_continue = false;
     switch(i) {
       case 0: {
-        var j = 0;
-        loop {
-          let marker3 = 0;
-          var tint_continue : bool = false;
+        switch(j) {
+          case 0: {
+            tint_continue = true;
+            break;
+          }
+          default: {
+          }
+        }
+        if (tint_continue) {
+          break;
+        }
+      }
+      default: {
+      }
+    }
+    if (tint_continue) {
+      continue;
+    }
+  }
+}
+)";
+
+    ast::transform::DataMap data;
+    auto got = Run<RemoveContinueInSwitch>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(RemoveContinueInSwitchTest, NestedLoopLoopSwitch) {
+    auto* src = R"(
+fn f() {
+  for (var i = 0; i < 2; i += 2) {
+    for (var j = 0; j < 2; j += 2) {
+      switch(i) {
+        case 0: {
+          continue;
+        }
+        default: {
+        }
+      }
+    }
+  }
+}
+)";
+
+    auto* expect = R"(
+fn f() {
+  for(var i = 0; (i < 2); i += 2) {
+    var tint_continue : bool;
+    for(var j = 0; (j < 2); j += 2) {
+      tint_continue = false;
+      switch(i) {
+        case 0: {
+          tint_continue = true;
+          break;
+        }
+        default: {
+        }
+      }
+      if (tint_continue) {
+        continue;
+      }
+    }
+  }
+}
+)";
+
+    ast::transform::DataMap data;
+    auto got = Run<RemoveContinueInSwitch>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(RemoveContinueInSwitchTest, NestedLoopSwitchLoopSwitch) {
+    auto* src = R"(
+fn f() {
+  for (var i = 0; i < 2; i += 2) {
+    switch(i) {
+      case 0: {
+        for (var j = 0; j < 2; j += 2) {
           switch(j) {
             case 0: {
-              {
-                tint_continue = true;
-                break;
-              }
+              continue; // j loop
+            }
+            default: {
+            }
+          }
+        }
+        continue; // i loop
+      }
+      default: {
+      }
+    }
+  }
+}
+)";
+
+    auto* expect = R"(
+fn f() {
+  var tint_continue_1 : bool;
+  for(var i = 0; (i < 2); i += 2) {
+    tint_continue_1 = false;
+    switch(i) {
+      case 0: {
+        var tint_continue : bool;
+        for(var j = 0; (j < 2); j += 2) {
+          tint_continue = false;
+          switch(j) {
+            case 0: {
+              tint_continue = true;
               break;
             }
             default: {
-              break;
             }
           }
           if (tint_continue) {
             continue;
           }
-          let marker4 = 0;
-          break;
         }
-        {
-          tint_continue_1 = true;
-          break;
-        }
+        tint_continue_1 = true;
         break;
       }
       default: {
-        break;
       }
     }
     if (tint_continue_1) {
       continue;
     }
-    let marker2 = 0;
-    break;
   }
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
+    auto got = Run<RemoveContinueInSwitch>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(RemoveContinueInSwitchTest, NestedLoopSwitchLoopSwitchSwitch) {
+    auto* src = R"(
+fn f() {
+  var k = 0;
+  for (var i = 0; i < 2; i += 2) {
+    switch(i) {
+      case 0: {
+        for (var j = 0; j < 2; j += 2) {
+          switch(j) {
+            case 0: {
+              continue; // j loop
+            }
+            case 1: {
+              switch (k) {
+                case 0: {
+                  continue; // j loop
+                }
+                default: {
+                }
+              }
+            }
+            default: {
+            }
+          }
+        }
+        continue; // i loop
+      }
+      default: {
+      }
+    }
+  }
+}
+)";
+
+    auto* expect = R"(
+fn f() {
+  var k = 0;
+  var tint_continue_1 : bool;
+  for(var i = 0; (i < 2); i += 2) {
+    tint_continue_1 = false;
+    switch(i) {
+      case 0: {
+        var tint_continue : bool;
+        for(var j = 0; (j < 2); j += 2) {
+          tint_continue = false;
+          switch(j) {
+            case 0: {
+              tint_continue = true;
+              break;
+            }
+            case 1: {
+              switch(k) {
+                case 0: {
+                  tint_continue = true;
+                  break;
+                }
+                default: {
+                }
+              }
+              if (tint_continue) {
+                break;
+              }
+            }
+            default: {
+            }
+          }
+          if (tint_continue) {
+            continue;
+          }
+        }
+        tint_continue_1 = true;
+        break;
+      }
+      default: {
+      }
+    }
+    if (tint_continue_1) {
+      continue;
+    }
+  }
+}
+)";
+
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -469,19 +612,18 @@
   var b = true;
   var c = true;
   var d = true;
+  var tint_continue : bool;
   loop {
+    tint_continue = false;
     if (a) {
       if (b) {
         let marker1 = 0;
-        var tint_continue : bool = false;
         switch(i) {
           case 0: {
             if (c) {
               if (d) {
-                {
-                  tint_continue = true;
-                  break;
-                }
+                tint_continue = true;
+                break;
               }
             }
             break;
@@ -501,7 +643,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -529,15 +671,14 @@
 
     auto* expect = R"(
 fn f() {
+  var tint_continue : bool;
   for(var i = 0; (i < 4); i = (i + 1)) {
+    tint_continue = false;
     let marker1 = 0;
-    var tint_continue : bool = false;
     switch(i) {
       case 0: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
+        break;
         break;
       }
       default: {
@@ -553,7 +694,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -583,15 +724,14 @@
     auto* expect = R"(
 fn f() {
   var i = 0;
+  var tint_continue : bool;
   while((i < 4)) {
+    tint_continue = false;
     let marker1 = 0;
-    var tint_continue : bool = false;
     switch(i) {
       case 0: {
-        {
-          tint_continue = true;
-          break;
-        }
+        tint_continue = true;
+        break;
         break;
       }
       default: {
@@ -607,11 +747,11 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
similarity index 78%
rename from src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
rename to src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
index 7b9bcf6..240da0a 100644
--- a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h"
 
 #include <memory>
 #include <string>
@@ -28,10 +28,10 @@
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/text/unicode.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TruncateInterstageVariables);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TruncateInterstageVariables::Config);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::TruncateInterstageVariables);
+TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::TruncateInterstageVariables::Config);
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 namespace {
 
@@ -48,9 +48,10 @@
 TruncateInterstageVariables::TruncateInterstageVariables() = default;
 TruncateInterstageVariables::~TruncateInterstageVariables() = default;
 
-Transform::ApplyResult TruncateInterstageVariables::Apply(const Program* src,
-                                                          const DataMap& config,
-                                                          DataMap&) const {
+ast::transform::Transform::ApplyResult TruncateInterstageVariables::Apply(
+    const Program* src,
+    const ast::transform::DataMap& config,
+    ast::transform::DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
@@ -76,7 +77,7 @@
             continue;
         }
 
-        if (func_ast->PipelineStage() != PipelineStage::kVertex) {
+        if (func_ast->PipelineStage() != ast::PipelineStage::kVertex) {
             // Currently only vertex stage could have interstage output variables that need
             // truncated.
             continue;
@@ -119,8 +120,8 @@
             old_shader_io_structs_to_new_struct_and_truncate_functions.GetOrCreate(str, [&] {
                 auto new_struct_sym = b.Symbols().New();
 
-                tint::Vector<const StructMember*, 20> truncated_members;
-                tint::Vector<const Expression*, 20> initializer_exprs;
+                Vector<const ast::StructMember*, 20> truncated_members;
+                Vector<const ast::Expression*, 20> initializer_exprs;
 
                 for (auto* member : str->Members()) {
                     if (omit_members.Contains(member)) {
@@ -136,10 +137,9 @@
 
                 // Create the mapping function to truncate the shader io.
                 auto mapping_fn_sym = b.Symbols().New("truncate_shader_output");
-                b.Func(mapping_fn_sym,
-                       tint::Vector{b.Param("io", ctx.Clone(func_ast->return_type))},
+                b.Func(mapping_fn_sym, Vector{b.Param("io", ctx.Clone(func_ast->return_type))},
                        b.ty(new_struct_sym),
-                       tint::Vector{
+                       Vector{
                            b.Return(b.Call(new_struct_sym, std::move(initializer_exprs))),
                        });
                 return TruncatedStructAndConverter{new_struct_sym, mapping_fn_sym};
@@ -155,24 +155,25 @@
     }
 
     // Replace return statements with new truncated shader IO struct
-    ctx.ReplaceAll([&](const ReturnStatement* return_statement) -> const ReturnStatement* {
-        auto* return_sem = sem.Get(return_statement);
-        if (auto mapping_fn_sym =
-                entry_point_functions_to_truncate_functions.Find(return_sem->Function())) {
-            return b.Return(return_statement->source,
-                            b.Call(*mapping_fn_sym, ctx.Clone(return_statement->value)));
-        }
-        return nullptr;
-    });
+    ctx.ReplaceAll(
+        [&](const ast::ReturnStatement* return_statement) -> const ast::ReturnStatement* {
+            auto* return_sem = sem.Get(return_statement);
+            if (auto mapping_fn_sym =
+                    entry_point_functions_to_truncate_functions.Find(return_sem->Function())) {
+                return b.Return(return_statement->source,
+                                b.Call(*mapping_fn_sym, ctx.Clone(return_statement->value)));
+            }
+            return nullptr;
+        });
 
     // Remove IO attributes from old shader IO struct which is not used as entry point output
     // anymore.
     for (auto it : old_shader_io_structs_to_new_struct_and_truncate_functions) {
-        const Struct* struct_ty = it.key->Declaration();
+        const ast::Struct* struct_ty = it.key->Declaration();
         for (auto* member : struct_ty->members) {
             for (auto* attr : member->attributes) {
-                if (attr->IsAnyOf<BuiltinAttribute, LocationAttribute, InterpolateAttribute,
-                                  InvariantAttribute>()) {
+                if (attr->IsAnyOf<ast::BuiltinAttribute, ast::LocationAttribute,
+                                  ast::InterpolateAttribute, ast::InvariantAttribute>()) {
                     ctx.Remove(member->attributes, attr);
                 }
             }
@@ -192,4 +193,4 @@
 TruncateInterstageVariables::Config& TruncateInterstageVariables::Config::operator=(const Config&) =
     default;
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.h b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
similarity index 84%
rename from src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.h
rename to src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
index 7c45cd9..8fe3250 100644
--- a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_TRUNCATE_INTERSTAGE_VARIABLES_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_TRUNCATE_INTERSTAGE_VARIABLES_H_
+#ifndef SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_TRUNCATE_INTERSTAGE_VARIABLES_H_
+#define SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_TRUNCATE_INTERSTAGE_VARIABLES_H_
 
 #include <bitset>
 
 #include "src/tint/api/common/binding_point.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 
 /// TruncateInterstageVariables is a transform that truncate interstage variables.
 /// It must be run after CanonicalizeEntryPointIO which guarantees all interstage variables of
@@ -88,10 +88,11 @@
 ///  }
 /// ```
 ///
-class TruncateInterstageVariables final : public Castable<TruncateInterstageVariables, Transform> {
+class TruncateInterstageVariables final
+    : public Castable<TruncateInterstageVariables, ast::transform::Transform> {
   public:
     /// Configuration options for the transform
-    struct Config final : public Castable<Config, Data> {
+    struct Config final : public Castable<Config, ast::transform::Data> {
         /// Constructor
         Config();
 
@@ -119,12 +120,12 @@
     /// Destructor
     ~TruncateInterstageVariables() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_TRUNCATE_INTERSTAGE_VARIABLES_H_
+#endif  // SRC_TINT_LANG_HLSL_WRITER_AST_RAISE_TRUNCATE_INTERSTAGE_VARIABLES_H_
diff --git a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables_test.cc b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables_test.cc
similarity index 92%
rename from src/tint/lang/wgsl/ast/transform/truncate_interstage_variables_test.cc
rename to src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables_test.cc
index 8884792..b0b0dee 100644
--- a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables_test.cc
@@ -12,18 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.h"
+#include "src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h"
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
 
 #include "gmock/gmock.h"
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::hlsl::writer {
 namespace {
 
 using ::testing::ContainerEq;
 
-using TruncateInterstageVariablesTest = TransformTest;
+using TruncateInterstageVariablesTest = ast::transform::TransformTest;
 
 TEST_F(TruncateInterstageVariablesTest, ShouldRunVertex) {
     auto* src = R"(
@@ -43,7 +43,7 @@
 
     {
         auto* expect =
-            "error: missing transform data for tint::ast::transform::TruncateInterstageVariables";
+            "error: missing transform data for tint::hlsl::writer::TruncateInterstageVariables";
         auto got = Run<TruncateInterstageVariables>(src);
         EXPECT_EQ(expect, str(got));
     }
@@ -51,7 +51,7 @@
     {
         // Empty interstage_locations: truncate all interstage variables, should run
         TruncateInterstageVariables::Config cfg;
-        DataMap data;
+        ast::transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
         EXPECT_TRUE(ShouldRun<TruncateInterstageVariables>(src, data));
     }
@@ -61,7 +61,7 @@
         TruncateInterstageVariables::Config cfg;
         cfg.interstage_locations[0] = true;
         cfg.interstage_locations[2] = true;
-        DataMap data;
+        ast::transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
         EXPECT_FALSE(ShouldRun<TruncateInterstageVariables>(src, data));
     }
@@ -70,7 +70,7 @@
         // Partial interstage_locations are marked: should run
         TruncateInterstageVariables::Config cfg;
         cfg.interstage_locations[2] = true;
-        DataMap data;
+        ast::transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
         EXPECT_TRUE(ShouldRun<TruncateInterstageVariables>(src, data));
     }
@@ -91,7 +91,7 @@
     TruncateInterstageVariables::Config cfg;
     cfg.interstage_locations[2] = true;
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     EXPECT_FALSE(ShouldRun<TruncateInterstageVariables>(src, data));
@@ -109,11 +109,12 @@
 
     TruncateInterstageVariables::Config cfg;
     cfg.interstage_locations[0] = true;
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
-    data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
-    auto got = Run<CanonicalizeEntryPointIO>(src, data);
+    data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
+        ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
+    auto got = Run<ast::transform::CanonicalizeEntryPointIO>(src, data);
 
     // Inevitably entry point can write only one variable if not using struct
     // So the truncate won't run.
@@ -169,7 +170,7 @@
     // fragment has input at @location(1)
     cfg.interstage_locations[1] = true;
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -226,7 +227,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -279,7 +280,7 @@
 )";
 
         TruncateInterstageVariables::Config cfg;
-        DataMap data;
+        ast::transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
 
         auto got = Run<TruncateInterstageVariables>(src, data);
@@ -349,7 +350,7 @@
         cfg.interstage_locations[3] = true;
         cfg.interstage_locations[5] = true;
 
-        DataMap data;
+        ast::transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
 
         auto got = Run<TruncateInterstageVariables>(src, data);
@@ -426,7 +427,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -521,7 +522,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -586,7 +587,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    ast::transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -595,4 +596,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.bazel b/src/tint/lang/hlsl/writer/common/BUILD.bazel
new file mode 100644
index 0000000..efc1eb0
--- /dev/null
+++ b/src/tint/lang/hlsl/writer/common/BUILD.bazel
@@ -0,0 +1,47 @@
+# 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 = "common",
+  srcs = [
+    "options.cc",
+  ],
+  hdrs = [
+    "options.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index 4540a35..e35e8bd 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -76,7 +76,12 @@
                  root_constant_binding_point,
                  disable_workgroup_init,
                  external_texture_options,
-                 array_length_from_uniform);
+                 array_length_from_uniform,
+                 binding_remapper_options,
+                 interstage_locations,
+                 truncate_interstage_variables,
+                 polyfill_reflect_vec2_f32,
+                 binding_points_ignored_in_robustness_transform);
 };
 
 }  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/msl/BUILD.bazel b/src/tint/lang/msl/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/lang/msl/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/lang/msl/validate/BUILD.bazel b/src/tint/lang/msl/validate/BUILD.bazel
new file mode 100644
index 0000000..cd91a04
--- /dev/null
+++ b/src/tint/lang/msl/validate/BUILD.bazel
@@ -0,0 +1,79 @@
+# 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 = [
+    "msl.cc",
+  ] + select({
+    ":is_mac": [
+      "msl_metal.mm",
+    ],
+    "//conditions:default": [],
+  }),
+  hdrs = [
+    "val.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/command",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/file",
+    "//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",
+  ] + select({
+    ":is_mac": [
+      
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "is_mac",
+  actual = "//src/tint:is_mac_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/validate/msl.cc b/src/tint/lang/msl/validate/msl.cc
index fce70d6..b0c10ad 100644
--- a/src/tint/lang/msl/validate/msl.cc
+++ b/src/tint/lang/msl/validate/msl.cc
@@ -42,6 +42,9 @@
         case MslVersion::kMsl_2_1:
             version_str = "-std=macos-metal2.1";
             break;
+        case MslVersion::kMsl_2_3:
+            version_str = "-std=macos-metal2.3";
+            break;
     }
 
 #ifdef _WIN32
diff --git a/src/tint/lang/msl/validate/msl_metal.mm b/src/tint/lang/msl/validate/msl_metal.mm
index 8fd3ee9..cdd34ad 100644
--- a/src/tint/lang/msl/validate/msl_metal.mm
+++ b/src/tint/lang/msl/validate/msl_metal.mm
@@ -43,6 +43,11 @@
         case MslVersion::kMsl_2_1:
             compileOptions.languageVersion = MTLLanguageVersion2_1;
             break;
+        case MslVersion::kMsl_2_3:
+            if (@available(macOS 11.0, *)) {
+                compileOptions.languageVersion = MTLLanguageVersion2_3;
+            }
+            break;
     }
 
     id<MTLLibrary> library = [device newLibraryWithSource:source
diff --git a/src/tint/lang/msl/validate/val.h b/src/tint/lang/msl/validate/val.h
index ca32763..e9f4037 100644
--- a/src/tint/lang/msl/validate/val.h
+++ b/src/tint/lang/msl/validate/val.h
@@ -30,12 +30,19 @@
 
 using EntryPointList = std::vector<std::pair<std::string, ast::PipelineStage>>;
 
-// The version of MSL to validate against.
+/// The version of MSL to validate against.
+/// Note: these must kept be in ascending order
 enum class MslVersion {
     kMsl_1_2,
     kMsl_2_1,
+    kMsl_2_3,
 };
 
+/// MslVersion less-than operator
+inline bool operator<(MslVersion a, MslVersion b) {
+    return static_cast<int>(a) < static_cast<int>(b);
+}
+
 /// The return structure of Validate()
 struct Result {
     /// True if validation passed
diff --git a/src/tint/lang/msl/writer/BUILD.bazel b/src/tint/lang/msl/writer/BUILD.bazel
new file mode 100644
index 0000000..1d5f3c4
--- /dev/null
+++ b/src/tint/lang/msl/writer/BUILD.bazel
@@ -0,0 +1,116 @@
+# 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 = "writer",
+  srcs = [
+    "output.cc",
+    "writer.cc",
+  ],
+  hdrs = [
+    "output.h",
+    "writer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/msl/writer/raise",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/reader/program_to_ir",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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_msl_writer": [
+      "//src/tint/lang/msl/writer/ast_printer",
+      "//src/tint/lang/msl/writer/common",
+      "//src/tint/lang/msl/writer/printer",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "writer_bench.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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_msl_writer": [
+      "//src/tint/lang/msl/writer",
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/writer/BUILD.cmake b/src/tint/lang/msl/writer/BUILD.cmake
index 6ddd2cb..b8bbc1d 100644
--- a/src/tint/lang/msl/writer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/BUILD.cmake
@@ -22,6 +22,7 @@
 ################################################################################
 
 include(lang/msl/writer/ast_printer/BUILD.cmake)
+include(lang/msl/writer/ast_raise/BUILD.cmake)
 include(lang/msl/writer/common/BUILD.cmake)
 include(lang/msl/writer/printer/BUILD.cmake)
 include(lang/msl/writer/raise/BUILD.cmake)
@@ -44,10 +45,12 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_msl_writer_raise
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
+  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -65,25 +68,13 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_msl_writer lib
-    tint_lang_core_ir
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_lang_msl_writer lib
     tint_lang_msl_writer_ast_printer
     tint_lang_msl_writer_common
-  )
-endif(TINT_BUILD_MSL_WRITER)
-
-if(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_msl_writer lib
     tint_lang_msl_writer_printer
   )
-endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_MSL_WRITER)
 
 endif(TINT_BUILD_MSL_WRITER)
 if(TINT_BUILD_MSL_WRITER)
diff --git a/src/tint/lang/msl/writer/BUILD.gn b/src/tint/lang/msl/writer/BUILD.gn
index f05d114..695bc61 100644
--- a/src/tint/lang/msl/writer/BUILD.gn
+++ b/src/tint/lang/msl/writer/BUILD.gn
@@ -37,10 +37,12 @@
       "${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/msl/writer/raise",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
+      "${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",
@@ -58,22 +60,12 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (tint_build_ir) {
-      deps += [
-        "${tint_src_dir}/lang/core/ir",
-        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-      ]
-    }
-
     if (tint_build_msl_writer) {
       deps += [
         "${tint_src_dir}/lang/msl/writer/ast_printer",
         "${tint_src_dir}/lang/msl/writer/common",
+        "${tint_src_dir}/lang/msl/writer/printer",
       ]
     }
-
-    if (tint_build_msl_writer && tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/msl/writer/printer" ]
-    }
   }
 }
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.bazel b/src/tint/lang/msl/writer/ast_printer/BUILD.bazel
new file mode 100644
index 0000000..b061b09
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.bazel
@@ -0,0 +1,146 @@
+# 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 = "ast_printer",
+  srcs = [
+    "ast_printer.cc",
+  ],
+  hdrs = [
+    "ast_printer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/helpers",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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_msl_writer": [
+      "//src/tint/lang/msl/writer/ast_raise",
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "array_accessor_test.cc",
+    "assign_test.cc",
+    "ast_function_test.cc",
+    "ast_printer_test.cc",
+    "ast_type_test.cc",
+    "binary_test.cc",
+    "bitcast_test.cc",
+    "block_test.cc",
+    "break_test.cc",
+    "builtin_test.cc",
+    "builtin_texture_test.cc",
+    "call_test.cc",
+    "case_test.cc",
+    "cast_test.cc",
+    "const_assert_test.cc",
+    "constructor_test.cc",
+    "continue_test.cc",
+    "discard_test.cc",
+    "helper_test.h",
+    "identifier_test.cc",
+    "if_test.cc",
+    "import_test.cc",
+    "loop_test.cc",
+    "member_accessor_test.cc",
+    "module_constant_test.cc",
+    "return_test.cc",
+    "sanitizer_test.cc",
+    "switch_test.cc",
+    "unary_op_test.cc",
+    "variable_decl_statement_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer",
+      "//src/tint/lang/msl/writer/ast_printer",
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.cmake b/src/tint/lang/msl/writer/ast_printer/BUILD.cmake
index afce85a..4db100e 100644
--- a/src/tint/lang/msl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.cmake
@@ -61,6 +61,7 @@
 
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_lang_msl_writer_ast_printer lib
+    tint_lang_msl_writer_ast_raise
     tint_lang_msl_writer_common
   )
 endif(TINT_BUILD_MSL_WRITER)
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.gn b/src/tint/lang/msl/writer/ast_printer/BUILD.gn
index dfa139a..6622f41 100644
--- a/src/tint/lang/msl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.gn
@@ -62,7 +62,10 @@
     ]
 
     if (tint_build_msl_writer) {
-      deps += [ "${tint_src_dir}/lang/msl/writer/common" ]
+      deps += [
+        "${tint_src_dir}/lang/msl/writer/ast_raise",
+        "${tint_src_dir}/lang/msl/writer/common",
+      ]
     }
   }
 }
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 3e6588c..22fd118 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -42,6 +42,10 @@
 #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/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h"
+#include "src/tint/lang/msl/writer/ast_raise/packed_vec3.h"
+#include "src/tint/lang/msl/writer/ast_raise/pixel_local.h"
+#include "src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h"
 #include "src/tint/lang/msl/writer/common/printer_support.h"
 #include "src/tint/lang/wgsl/ast/alias.h"
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
@@ -59,10 +63,7 @@
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
-#include "src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h"
-#include "src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
-#include "src/tint/lang/wgsl/ast/transform/packed_vec3.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
@@ -209,8 +210,17 @@
     manager.Add<ast::transform::RemovePhonies>();
     manager.Add<ast::transform::SimplifyPointers>();
 
-    // MslSubgroupBallot() must come after CanonicalizeEntryPointIO.
-    manager.Add<ast::transform::MslSubgroupBallot>();
+    // SubgroupBallot() must come after CanonicalizeEntryPointIO.
+    manager.Add<SubgroupBallot>();
+
+    {
+        PixelLocal::Config cfg;
+        for (auto it : options.pixel_local_options.attachments) {
+            cfg.attachments.Add(it.first, it.second);
+        }
+        data.Add<PixelLocal::Config>(cfg);
+        manager.Add<PixelLocal>();
+    }
 
     // ArrayLengthFromUniform must come after SimplifyPointers, as
     // it assumes that the form of the array length argument is &var.array.
@@ -223,8 +233,8 @@
     data.Add<ast::transform::ArrayLengthFromUniform::Config>(array_length_cfg);
 
     // PackedVec3 must come after ExpandCompoundAssignment.
-    manager.Add<ast::transform::PackedVec3>();
-    manager.Add<ast::transform::ModuleScopeVarToEntryPointParam>();
+    manager.Add<PackedVec3>();
+    manager.Add<ModuleScopeVarToEntryPointParam>();
 
     SanitizedResult result;
     ast::transform::DataMap outputs;
@@ -248,14 +258,15 @@
             "MSL", builder_.AST(), diagnostics_,
             Vector{
                 core::Extension::kChromiumDisableUniformityAnalysis,
+                core::Extension::kChromiumExperimentalDp4A,
                 core::Extension::kChromiumExperimentalFullPtrParameters,
+                core::Extension::kChromiumExperimentalPixelLocal,
                 core::Extension::kChromiumExperimentalPushConstant,
                 core::Extension::kChromiumExperimentalReadWriteStorageTexture,
                 core::Extension::kChromiumExperimentalSubgroups,
+                core::Extension::kChromiumInternalDualSourceBlending,
                 core::Extension::kChromiumInternalRelaxedUniformLayout,
                 core::Extension::kF16,
-                core::Extension::kChromiumInternalDualSourceBlending,
-                core::Extension::kChromiumExperimentalDp4A,
             })) {
         return false;
     }
@@ -631,8 +642,8 @@
 bool ASTPrinter::EmitFunctionCall(StringStream& out,
                                   const sem::Call* call,
                                   const sem::Function* fn) {
-    if (ast::GetAttribute<ast::transform::MslSubgroupBallot::SimdActiveThreadsMask>(
-            fn->Declaration()->attributes) != nullptr) {
+    if (ast::GetAttribute<SubgroupBallot::SimdActiveThreadsMask>(fn->Declaration()->attributes) !=
+        nullptr) {
         out << "as_type<uint2>((ulong)simd_active_threads_mask())";
         return true;
     }
@@ -1987,8 +1998,20 @@
 
             bool ok = Switch(
                 type,  //
-                [&](const core::type::Struct*) {
-                    out << " [[stage_in]]";
+                [&](const core::type::Struct* str) {
+                    bool is_pixel_local = false;
+                    if (auto* sem_str = str->As<sem::Struct>()) {
+                        for (auto* member : sem_str->Members()) {
+                            if (ast::HasAttribute<PixelLocal::Attachment>(
+                                    member->Declaration()->attributes)) {
+                                is_pixel_local = true;
+                                break;
+                            }
+                        }
+                    }
+                    if (!is_pixel_local) {
+                        out << " [[stage_in]]";
+                    }
                     return true;
                 },
                 [&](const core::type::Texture*) {
@@ -2801,7 +2824,7 @@
         if (auto location = attributes.location) {
             auto& pipeline_stage_uses = str->PipelineStageUses();
             if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
-                TINT_ICE() << "invalid entry point IO struct uses";
+                TINT_ICE() << "invalid entry point IO struct uses for " << str->Name().NameView();
                 return false;
             }
 
@@ -2839,6 +2862,13 @@
             out << " " << invariant_define_name_;
         }
 
+        if (auto* sem_mem = mem->As<sem::StructMember>()) {
+            if (auto* attachment =
+                    ast::GetAttribute<PixelLocal::Attachment>(sem_mem->Declaration()->attributes)) {
+                out << " [[color(" << attachment->index << ")]]";
+            }
+        }
+
         out << ";";
 
         if (is_host_shareable) {
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.bazel b/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
new file mode 100644
index 0000000..0b080b5
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
@@ -0,0 +1,117 @@
+# 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 = "ast_raise",
+  srcs = [
+    "module_scope_var_to_entry_point_param.cc",
+    "packed_vec3.cc",
+    "pixel_local.cc",
+    "subgroup_ballot.cc",
+  ],
+  hdrs = [
+    "module_scope_var_to_entry_point_param.h",
+    "packed_vec3.h",
+    "pixel_local.h",
+    "subgroup_ballot.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "module_scope_var_to_entry_point_param_test.cc",
+    "packed_vec3_test.cc",
+    "pixel_local_test.cc",
+    "subgroup_ballot_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer/ast_raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.cfg b/src/tint/lang/msl/writer/ast_raise/BUILD.cfg
new file mode 100644
index 0000000..1c7e256
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_msl_writer"
+}
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.cmake b/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
new file mode 100644
index 0000000..56fe8bd
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
@@ -0,0 +1,118 @@
+# 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_MSL_WRITER)
+################################################################################
+# Target:    tint_lang_msl_writer_ast_raise
+# Kind:      lib
+# Condition: TINT_BUILD_MSL_WRITER
+################################################################################
+tint_add_target(tint_lang_msl_writer_ast_raise lib
+  lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
+  lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
+  lang/msl/writer/ast_raise/packed_vec3.cc
+  lang/msl/writer/ast_raise/packed_vec3.h
+  lang/msl/writer/ast_raise/pixel_local.cc
+  lang/msl/writer/ast_raise/pixel_local.h
+  lang/msl/writer/ast_raise/subgroup_ballot.cc
+  lang/msl/writer/ast_raise/subgroup_ballot.h
+)
+
+tint_target_add_dependencies(tint_lang_msl_writer_ast_raise lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_program
+  tint_lang_wgsl_resolver
+  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
+)
+
+endif(TINT_BUILD_MSL_WRITER)
+if(TINT_BUILD_MSL_WRITER)
+################################################################################
+# Target:    tint_lang_msl_writer_ast_raise_test
+# Kind:      test
+# Condition: TINT_BUILD_MSL_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
+  lang/msl/writer/ast_raise/packed_vec3_test.cc
+  lang/msl/writer/ast_raise/pixel_local_test.cc
+  lang/msl/writer/ast_raise/subgroup_ballot_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_msl_writer_ast_raise_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  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
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_msl_writer_ast_raise_test test
+  "gtest"
+)
+
+if(TINT_BUILD_MSL_WRITER)
+  tint_target_add_dependencies(tint_lang_msl_writer_ast_raise_test test
+    tint_lang_msl_writer_ast_raise
+  )
+endif(TINT_BUILD_MSL_WRITER)
+
+endif(TINT_BUILD_MSL_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
new file mode 100644
index 0000000..f6890c2
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.gn
@@ -0,0 +1,113 @@
+# 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_unittests) {
+  import("//testing/test.gni")
+}
+if (tint_build_msl_writer) {
+  libtint_source_set("ast_raise") {
+    sources = [
+      "module_scope_var_to_entry_point_param.cc",
+      "module_scope_var_to_entry_point_param.h",
+      "packed_vec3.cc",
+      "packed_vec3.h",
+      "pixel_local.cc",
+      "pixel_local.h",
+      "subgroup_ballot.cc",
+      "subgroup_ballot.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/ast",
+      "${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_unittests) {
+  if (tint_build_msl_writer) {
+    tint_unittests_source_set("unittests") {
+      testonly = true
+      sources = [
+        "module_scope_var_to_entry_point_param_test.cc",
+        "packed_vec3_test.cc",
+        "pixel_local_test.cc",
+        "subgroup_ballot_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/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",
+        "${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/ast_raise" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
similarity index 87%
rename from src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
rename to src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
index 3a997be..4e529a2 100644
--- a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h"
+#include "src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h"
 
 #include <unordered_map>
 #include <unordered_set>
@@ -31,21 +31,21 @@
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/text/string.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ModuleScopeVarToEntryPointParam);
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::ModuleScopeVarToEntryPointParam);
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 namespace {
 
-using StructMemberList = tint::Vector<const StructMember*, 8>;
+using StructMemberList = tint::Vector<const ast::StructMember*, 8>;
 
 // The name of the struct member for arrays that are wrapped in structures.
 const char* kWrappedArrayMemberName = "arr";
 
 bool ShouldRun(const Program* program) {
     for (auto* decl : program->AST().GlobalDeclarations()) {
-        if (decl->Is<Variable>()) {
+        if (decl->Is<ast::Variable>()) {
             return true;
         }
     }
@@ -115,7 +115,7 @@
     /// @param workgroup_parameter_members reference to a list of a workgroup struct members
     /// @param is_pointer output signalling whether the replacement is a pointer
     /// @param is_wrapped output signalling whether the replacement is wrapped in a struct
-    void ProcessVariableInEntryPoint(const Function* func,
+    void ProcessVariableInEntryPoint(const ast::Function* func,
                                      const sem::Variable* var,
                                      Symbol new_var_symbol,
                                      std::function<Symbol()> workgroup_param,
@@ -133,7 +133,7 @@
                 // For a texture or sampler variable, redeclare it as an entry point parameter.
                 // Disable entry point parameter validation.
                 auto* disable_validation =
-                    ctx.dst->Disable(DisabledValidation::kEntryPointParameter);
+                    ctx.dst->Disable(ast::DisabledValidation::kEntryPointParameter);
                 auto attrs = ctx.Clone(var->Declaration()->attributes);
                 attrs.Push(disable_validation);
                 auto* param = ctx.dst->Param(new_var_symbol, store_type(), attrs);
@@ -146,8 +146,8 @@
                 // Variables into the Storage and Uniform address spaces are redeclared as entry
                 // point parameters with a pointer type.
                 auto attributes = ctx.Clone(var->Declaration()->attributes);
-                attributes.Push(ctx.dst->Disable(DisabledValidation::kEntryPointParameter));
-                attributes.Push(ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace));
+                attributes.Push(ctx.dst->Disable(ast::DisabledValidation::kEntryPointParameter));
+                attributes.Push(ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace));
 
                 auto param_type = store_type();
                 if (auto* arr = ty->As<core::type::Array>();
@@ -193,7 +193,7 @@
                     is_pointer = true;
                 } else {
                     auto* disable_validation =
-                        ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace);
+                        ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace);
                     auto* initializer = ctx.Clone(var->Declaration()->initializer);
                     auto* local_var = ctx.dst->Var(new_var_symbol, store_type(), sc, initializer,
                                                    tint::Vector{disable_validation});
@@ -201,12 +201,8 @@
                 }
                 break;
             }
-            case core::AddressSpace::kPushConstant: {
-                ctx.dst->Diagnostics().add_error(
-                    diag::System::Transform,
-                    "unhandled module-scope address space (" + tint::ToString(sc) + ")");
-                break;
-            }
+            case core::AddressSpace::kPixelLocal:
+                break;  // Ignore
             default: {
                 TINT_ICE() << "unhandled module-scope address space (" << sc << ")";
                 break;
@@ -220,7 +216,7 @@
     /// @param var the variable
     /// @param new_var_symbol the symbol to use for the replacement
     /// @param is_pointer output signalling whether the replacement is a pointer or not
-    void ProcessVariableInUserFunction(const Function* func,
+    void ProcessVariableInUserFunction(const ast::Function* func,
                                        const sem::Variable* var,
                                        Symbol new_var_symbol,
                                        bool& is_pointer) {
@@ -248,7 +244,7 @@
         }
 
         // Use a pointer for non-handle types.
-        tint::Vector<const Attribute*, 2> attributes;
+        tint::Vector<const ast::Attribute*, 2> attributes;
         if (!ty->is_handle()) {
             param_type = sc == core::AddressSpace::kStorage
                              ? ctx.dst->ty.ptr(sc, param_type, var->Access())
@@ -256,8 +252,9 @@
             is_pointer = true;
 
             // Disable validation of the parameter's address space and of arguments passed to it.
-            attributes.Push(ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace));
-            attributes.Push(ctx.dst->Disable(DisabledValidation::kIgnoreInvalidPointerArgument));
+            attributes.Push(ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace));
+            attributes.Push(
+                ctx.dst->Disable(ast::DisabledValidation::kIgnoreInvalidPointerArgument));
         }
 
         // Redeclare the variable as a parameter.
@@ -271,18 +268,18 @@
     /// @param new_var the symbol to use for replacement
     /// @param is_pointer true if `new_var` is a pointer to the new variable
     /// @param member_name if valid, the name of the struct member that holds this variable
-    void ReplaceUsesInFunction(const Function* func,
+    void ReplaceUsesInFunction(const ast::Function* func,
                                const sem::Variable* var,
                                Symbol new_var,
                                bool is_pointer,
                                Symbol member_name) {
         for (auto* user : var->Users()) {
             if (user->Stmt()->Function()->Declaration() == func) {
-                const Expression* expr = ctx.dst->Expr(new_var);
+                const ast::Expression* expr = ctx.dst->Expr(new_var);
                 if (is_pointer) {
                     // If this identifier is used by an address-of operator, just remove the
                     // address-of instead of adding a deref, since we already have a pointer.
-                    auto* ident = user->Declaration()->As<IdentifierExpression>();
+                    auto* ident = user->Declaration()->As<ast::IdentifierExpression>();
                     if (ident_to_address_of_.count(ident) && !member_name.IsValid()) {
                         ctx.Replace(ident_to_address_of_[ident], expr);
                         continue;
@@ -302,19 +299,19 @@
     /// Process the module.
     void Process() {
         // Predetermine the list of function calls that need to be replaced.
-        using CallList = tint::Vector<const CallExpression*, 8>;
-        std::unordered_map<const Function*, CallList> calls_to_replace;
+        using CallList = tint::Vector<const ast::CallExpression*, 8>;
+        std::unordered_map<const ast::Function*, CallList> calls_to_replace;
 
-        tint::Vector<const Function*, 8> functions_to_process;
+        tint::Vector<const ast::Function*, 8> functions_to_process;
 
         // Collect private variables into a single structure.
         StructMemberList private_struct_members;
-        tint::Vector<std::function<const AssignmentStatement*()>, 4> private_initializers;
-        std::unordered_set<const Function*> uses_privates;
+        tint::Vector<std::function<const ast::AssignmentStatement*()>, 4> private_initializers;
+        std::unordered_set<const ast::Function*> uses_privates;
 
         // Build a list of functions that transitively reference any module-scope variables.
         for (auto* decl : ctx.src->Sem().Module()->DependencyOrderedDeclarations()) {
-            if (auto* var = decl->As<Var>()) {
+            if (auto* var = decl->As<ast::Var>()) {
                 auto* sem_var = ctx.src->Sem().Get(var);
                 if (sem_var->AddressSpace() == core::AddressSpace::kPrivate) {
                     // Create a member in the private variable struct.
@@ -335,7 +332,7 @@
                 continue;
             }
 
-            auto* func_ast = decl->As<Function>();
+            auto* func_ast = decl->As<ast::Function>();
             if (!func_ast) {
                 continue;
             }
@@ -376,11 +373,11 @@
         // TODO(jrprice): We should add support for bidirectional SEM tree traversal so that we can
         // do this on the fly instead.
         for (auto* node : ctx.src->ASTNodes().Objects()) {
-            auto* address_of = node->As<UnaryOpExpression>();
+            auto* address_of = node->As<ast::UnaryOpExpression>();
             if (!address_of || address_of->op != core::UnaryOp::kAddressOf) {
                 continue;
             }
-            if (auto* ident = address_of->expr->As<IdentifierExpression>()) {
+            if (auto* ident = address_of->expr->As<ast::IdentifierExpression>()) {
                 ident_to_address_of_[ident] = address_of;
             }
         }
@@ -414,12 +411,12 @@
             if (uses_privates.count(func_ast)) {
                 if (is_entry_point) {
                     // Create a local declaration for the private variable struct.
-                    auto* var =
-                        ctx.dst->Var(PrivateStructVariableName(), ctx.dst->ty(PrivateStructName()),
-                                     core::AddressSpace::kPrivate,
-                                     tint::Vector{
-                                         ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace),
-                                     });
+                    auto* var = ctx.dst->Var(
+                        PrivateStructVariableName(), ctx.dst->ty(PrivateStructName()),
+                        core::AddressSpace::kPrivate,
+                        tint::Vector{
+                            ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace),
+                        });
                     ctx.InsertFront(func_ast->body->statements, ctx.dst->Decl(var));
 
                     // Initialize the members of that struct with the original initializers.
@@ -481,7 +478,7 @@
             // Allow pointer aliasing if needed.
             if (needs_pointer_aliasing) {
                 ctx.InsertBack(func_ast->attributes,
-                               ctx.dst->Disable(DisabledValidation::kIgnorePointerAliasing));
+                               ctx.dst->Disable(ast::DisabledValidation::kIgnorePointerAliasing));
             }
 
             if (!workgroup_parameter_members.IsEmpty()) {
@@ -490,12 +487,12 @@
                 auto* str =
                     ctx.dst->Structure(ctx.dst->Sym(), std::move(workgroup_parameter_members));
                 auto param_type = ctx.dst->ty.ptr(workgroup, ctx.dst->ty.Of(str));
-                auto* param =
-                    ctx.dst->Param(workgroup_param(), param_type,
-                                   tint::Vector{
-                                       ctx.dst->Disable(DisabledValidation::kEntryPointParameter),
-                                       ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace),
-                                   });
+                auto* param = ctx.dst->Param(
+                    workgroup_param(), param_type,
+                    tint::Vector{
+                        ctx.dst->Disable(ast::DisabledValidation::kEntryPointParameter),
+                        ctx.dst->Disable(ast::DisabledValidation::kIgnoreAddressSpace),
+                    });
                 ctx.InsertFront(func_ast->params, param);
             }
 
@@ -506,7 +503,7 @@
 
                 // Pass the private variable struct pointer if needed.
                 if (uses_privates.count(target_sem->Declaration())) {
-                    const Expression* arg = ctx.dst->Expr(PrivateStructVariableName());
+                    const ast::Expression* arg = ctx.dst->Expr(PrivateStructVariableName());
                     if (is_entry_point) {
                         arg = ctx.dst->AddressOf(arg);
                     }
@@ -529,7 +526,7 @@
 
                     auto new_var = it->second;
                     bool is_handle = target_var->Type()->UnwrapRef()->is_handle();
-                    const Expression* arg = ctx.dst->Expr(new_var.symbol);
+                    const ast::Expression* arg = ctx.dst->Expr(new_var.symbol);
                     if (new_var.is_wrapped) {
                         // The variable is wrapped in a struct, so we need to pass a pointer to the
                         // struct member instead.
@@ -575,7 +572,8 @@
     std::unordered_set<const sem::Struct*> cloned_structs_;
 
     // Map from identifier expression to the address-of expression that uses it.
-    std::unordered_map<const IdentifierExpression*, const UnaryOpExpression*> ident_to_address_of_;
+    std::unordered_map<const ast::IdentifierExpression*, const ast::UnaryOpExpression*>
+        ident_to_address_of_;
 
     // The name of the structure that contains all the module-scope private variables.
     Symbol private_struct_name;
@@ -588,9 +586,10 @@
 
 ModuleScopeVarToEntryPointParam::~ModuleScopeVarToEntryPointParam() = default;
 
-Transform::ApplyResult ModuleScopeVarToEntryPointParam::Apply(const Program* src,
-                                                              const DataMap&,
-                                                              DataMap&) const {
+ast::transform::Transform::ApplyResult ModuleScopeVarToEntryPointParam::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -604,4 +603,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
similarity index 77%
rename from src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h
rename to src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
index f4f4491..034b22a 100644
--- a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h
+++ b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_MODULE_SCOPE_VAR_TO_ENTRY_POINT_PARAM_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_MODULE_SCOPE_VAR_TO_ENTRY_POINT_PARAM_H_
+#ifndef SRC_TINT_LANG_MSL_WRITER_AST_RAISE_MODULE_SCOPE_VAR_TO_ENTRY_POINT_PARAM_H_
+#define SRC_TINT_LANG_MSL_WRITER_AST_RAISE_MODULE_SCOPE_VAR_TO_ENTRY_POINT_PARAM_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 
 /// Move module-scope variables into the entry point as parameters.
 ///
@@ -62,22 +62,22 @@
 /// }
 /// ```
 class ModuleScopeVarToEntryPointParam final
-    : public Castable<ModuleScopeVarToEntryPointParam, Transform> {
+    : public Castable<ModuleScopeVarToEntryPointParam, ast::transform::Transform> {
   public:
     /// Constructor
     ModuleScopeVarToEntryPointParam();
     /// Destructor
     ~ModuleScopeVarToEntryPointParam() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_MODULE_SCOPE_VAR_TO_ENTRY_POINT_PARAM_H_
+#endif  // SRC_TINT_LANG_MSL_WRITER_AST_RAISE_MODULE_SCOPE_VAR_TO_ENTRY_POINT_PARAM_H_
diff --git a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param_test.cc b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param_test.cc
similarity index 98%
rename from src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param_test.cc
rename to src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param_test.cc
index 70e3d91..f4dcb30 100644
--- a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param_test.cc
+++ b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param_test.cc
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h"
+#include "src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h"
 
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 namespace {
 
-using ModuleScopeVarToEntryPointParamTest = TransformTest;
+using ModuleScopeVarToEntryPointParamTest = ast::transform::TransformTest;
 
 TEST_F(ModuleScopeVarToEntryPointParamTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -1369,4 +1369,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/packed_vec3.cc b/src/tint/lang/msl/writer/ast_raise/packed_vec3.cc
similarity index 90%
rename from src/tint/lang/wgsl/ast/transform/packed_vec3.cc
rename to src/tint/lang/msl/writer/ast_raise/packed_vec3.cc
index c6f4780..cfe3c0e 100644
--- a/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
+++ b/src/tint/lang/msl/writer/ast_raise/packed_vec3.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/packed_vec3.h"
+#include "src/tint/lang/msl/writer/ast_raise/packed_vec3.h"
 
 #include <algorithm>
 #include <string>
@@ -38,12 +38,12 @@
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/rtti/switch.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::PackedVec3);
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PackedVec3);
 
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 
 /// PIMPL state for the transform
 struct PackedVec3::State {
@@ -98,7 +98,7 @@
     /// Create a `__packed_vec3` type with the same element type as `ty`.
     /// @param ty a three-element vector type
     /// @returns the new AST type
-    Type MakePackedVec3(const core::type::Type* ty) {
+    ast::Type MakePackedVec3(const core::type::Type* ty) {
         auto* vec = ty->As<core::type::Vector>();
         TINT_ASSERT(vec != nullptr && vec->Width() == 3);
         return b.ty(core::Builtin::kPackedVec3, CreateASTTypeFor(ctx, vec->type()));
@@ -113,10 +113,10 @@
     /// @param ty the type to rewrite
     /// @param array_element `true` if this is being called for the element of an array
     /// @returns the new AST type, or nullptr if rewriting was not necessary
-    Type RewriteType(const core::type::Type* ty, bool array_element = false) {
+    ast::Type RewriteType(const core::type::Type* ty, bool array_element = false) {
         return Switch(
             ty,
-            [&](const core::type::Vector* vec) -> Type {
+            [&](const core::type::Vector* vec) -> ast::Type {
                 if (IsVec3(vec)) {
                     if (array_element) {
                         // Create a struct with a single `__packed_vec3` member.
@@ -138,7 +138,7 @@
                 }
                 return {};
             },
-            [&](const core::type::Matrix* mat) -> Type {
+            [&](const core::type::Matrix* mat) -> ast::Type {
                 // Rewrite the matrix as an array of columns that use the aligned wrapper struct.
                 auto new_col_type = RewriteType(mat->ColumnType(), /* array_element */ true);
                 if (new_col_type) {
@@ -146,11 +146,11 @@
                 }
                 return {};
             },
-            [&](const core::type::Array* arr) -> Type {
+            [&](const core::type::Array* arr) -> ast::Type {
                 // Rewrite the array with the modified element type.
                 auto new_type = RewriteType(arr->ElemType(), /* array_element */ true);
                 if (new_type) {
-                    tint::Vector<const Attribute*, 1> attrs;
+                    tint::Vector<const ast::Attribute*, 1> attrs;
                     if (arr->Count()->Is<core::type::RuntimeArrayCount>()) {
                         return b.ty.array(new_type, std::move(attrs));
                     } else if (auto count = arr->ConstantCount()) {
@@ -162,21 +162,21 @@
                 }
                 return {};
             },
-            [&](const core::type::Struct* str) -> Type {
+            [&](const core::type::Struct* str) -> ast::Type {
                 if (ContainsVec3(str)) {
                     auto name = rewritten_structs.GetOrCreate(str, [&] {
-                        tint::Vector<const StructMember*, 4> members;
+                        tint::Vector<const ast::StructMember*, 4> members;
                         for (auto* member : str->Members()) {
                             // If the member type contains a vec3, rewrite it.
                             auto new_type = RewriteType(member->Type());
                             if (new_type) {
                                 // Copy the member attributes.
                                 bool needs_align = true;
-                                tint::Vector<const Attribute*, 4> attributes;
+                                tint::Vector<const ast::Attribute*, 4> attributes;
                                 if (auto* sem_mem = member->As<sem::StructMember>()) {
                                     for (auto* attr : sem_mem->Declaration()->attributes) {
-                                        if (attr->IsAnyOf<StructMemberAlignAttribute,
-                                                          StructMemberOffsetAttribute>()) {
+                                        if (attr->IsAnyOf<ast::StructMemberAlignAttribute,
+                                                          ast::StructMemberOffsetAttribute>()) {
                                             needs_align = false;
                                         }
                                         attributes.Push(ctx.Clone(attr));
@@ -222,12 +222,13 @@
     Symbol MakePackUnpackHelper(
         const char* name_prefix,
         const core::type::Type* ty,
-        const std::function<const Expression*(const Expression*, const core::type::Type*)>&
+        const std::function<const ast::Expression*(const ast::Expression*,
+                                                   const core::type::Type*)>&
             pack_or_unpack_element,
-        const std::function<Type()>& in_type,
-        const std::function<Type()>& out_type) {
+        const std::function<ast::Type()>& in_type,
+        const std::function<ast::Type()>& out_type) {
         // Allocate a variable to hold the return value of the function.
-        tint::Vector<const Statement*, 4> statements;
+        tint::Vector<const ast::Statement*, 4> statements;
         statements.Push(b.Decl(b.Var("result", out_type())));
 
         // Helper that generates a loop to copy and pack/unpack elements of an array to the result:
@@ -260,7 +261,7 @@
             [&](const core::type::Struct* str) {
                 // Copy the struct members over one at a time, packing/unpacking as necessary.
                 for (auto* member : str->Members()) {
-                    const Expression* element =
+                    const ast::Expression* element =
                         b.MemberAccessor("in", b.Ident(ctx.Clone(member->Name())));
                     if (ContainsVec3(member->Type())) {
                         element = pack_or_unpack_element(element, member->Type());
@@ -284,16 +285,17 @@
     /// @param expr the composite value expression to unpack
     /// @param ty the unpacked type
     /// @returns an expression that holds the unpacked value
-    const Expression* UnpackComposite(const Expression* expr, const core::type::Type* ty) {
+    const ast::Expression* UnpackComposite(const ast::Expression* expr,
+                                           const core::type::Type* ty) {
         auto helper = unpack_helpers.GetOrCreate(ty, [&] {
             return MakePackUnpackHelper(
                 "tint_unpack_vec3_in_composite", ty,
-                [&](const Expression* element,
-                    const core::type::Type* element_type) -> const Expression* {
+                [&](const ast::Expression* element,
+                    const core::type::Type* element_type) -> const ast::Expression* {
                     if (element_type->Is<core::type::Vector>()) {
                         // Unpack a `__packed_vec3` by casting it to a regular vec3.
                         // If it is an array element, extract the vector from the wrapper struct.
-                        if (element->Is<IndexAccessorExpression>()) {
+                        if (element->Is<ast::IndexAccessorExpression>()) {
                             element = b.MemberAccessor(element, kStructMemberName);
                         }
                         return b.Call(CreateASTTypeFor(ctx, element_type), element);
@@ -312,17 +314,17 @@
     /// @param expr the composite value expression to pack
     /// @param ty the unpacked type
     /// @returns an expression that holds the packed value
-    const Expression* PackComposite(const Expression* expr, const core::type::Type* ty) {
+    const ast::Expression* PackComposite(const ast::Expression* expr, const core::type::Type* ty) {
         auto helper = pack_helpers.GetOrCreate(ty, [&] {
             return MakePackUnpackHelper(
                 "tint_pack_vec3_in_composite", ty,
-                [&](const Expression* element,
-                    const core::type::Type* element_type) -> const Expression* {
+                [&](const ast::Expression* element,
+                    const core::type::Type* element_type) -> const ast::Expression* {
                     if (element_type->Is<core::type::Vector>()) {
                         // Pack a vector element by casting it to a packed_vec3.
                         // If it is an array element, construct a wrapper struct.
                         auto* packed = b.Call(MakePackedVec3(element_type), element);
-                        if (element->Is<IndexAccessorExpression>()) {
+                        if (element->Is<ast::IndexAccessorExpression>()) {
                             packed = b.Call(RewriteType(element_type, true), packed);
                         }
                         return packed;
@@ -404,7 +406,7 @@
                 },
                 [&](const sem::Statement* stmt) {
                     // Pack the RHS of assignment statements that are writing to packed types.
-                    if (auto* assign = stmt->Declaration()->As<AssignmentStatement>()) {
+                    if (auto* assign = stmt->Declaration()->As<ast::AssignmentStatement>()) {
                         auto* lhs = sem.GetVal(assign->lhs);
                         auto* rhs = sem.GetVal(assign->rhs);
                         if (!ContainsVec3(rhs->Type()) ||
@@ -467,7 +469,7 @@
         for (auto* expr : to_unpack_sorted) {
             TINT_ASSERT(ContainsVec3(expr->Type()));
             auto* packed = ctx.Clone(expr->Declaration());
-            const Expression* unpacked = nullptr;
+            const ast::Expression* unpacked = nullptr;
             if (IsVec3(expr->Type())) {
                 if (expr->UnwrapLoad()->Is<sem::IndexAccessorExpression>()) {
                     // If we are unpacking a vec3 from an array element, extract the vector from the
@@ -488,7 +490,7 @@
         for (auto* expr : to_pack_sorted) {
             TINT_ASSERT(ContainsVec3(expr->Type()));
             auto* unpacked = ctx.Clone(expr->Declaration());
-            const Expression* packed = nullptr;
+            const ast::Expression* packed = nullptr;
             if (IsVec3(expr->Type())) {
                 // Cast the regular vec3 to a packed vector type.
                 packed = b.Call(MakePackedVec3(expr->Type()), unpacked);
@@ -518,8 +520,10 @@
 PackedVec3::PackedVec3() = default;
 PackedVec3::~PackedVec3() = default;
 
-Transform::ApplyResult PackedVec3::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult PackedVec3::Apply(const Program* src,
+                                                         const ast::transform::DataMap&,
+                                                         ast::transform::DataMap&) const {
     return State{src}.Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/packed_vec3.h b/src/tint/lang/msl/writer/ast_raise/packed_vec3.h
similarity index 80%
rename from src/tint/lang/wgsl/ast/transform/packed_vec3.h
rename to src/tint/lang/msl/writer/ast_raise/packed_vec3.h
index 52b1ddb..4ef04a5 100644
--- a/src/tint/lang/wgsl/ast/transform/packed_vec3.h
+++ b/src/tint/lang/msl/writer/ast_raise/packed_vec3.h
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_PACKED_VEC3_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_PACKED_VEC3_H_
+#ifndef SRC_TINT_LANG_MSL_WRITER_AST_RAISE_PACKED_VEC3_H_
+#define SRC_TINT_LANG_MSL_WRITER_AST_RAISE_PACKED_VEC3_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 
 /// A transform to be used by the MSL backend which will:
 /// * Replace `vec3<T>` types with an internal `__packed_vec3` type when they are used in
@@ -38,22 +38,22 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * ExpandCompoundAssignment
-class PackedVec3 final : public Castable<PackedVec3, Transform> {
+class PackedVec3 final : public Castable<PackedVec3, ast::transform::Transform> {
   public:
     /// Constructor
     PackedVec3();
     /// Destructor
     ~PackedVec3() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_PACKED_VEC3_H_
+#endif  // SRC_TINT_LANG_MSL_WRITER_AST_RAISE_PACKED_VEC3_H_
diff --git a/src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc b/src/tint/lang/msl/writer/ast_raise/packed_vec3_test.cc
similarity index 96%
rename from src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc
rename to src/tint/lang/msl/writer/ast_raise/packed_vec3_test.cc
index 8866041..c2e53f9 100644
--- a/src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc
+++ b/src/tint/lang/msl/writer/ast_raise/packed_vec3_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/packed_vec3.h"
+#include "src/tint/lang/msl/writer/ast_raise/packed_vec3.h"
 
 #include <string>
 #include <utility>
@@ -30,10 +30,10 @@
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 namespace {
 
-using PackedVec3Test = TransformTest;
+using PackedVec3Test = ast::transform::TransformTest;
 
 TEST_F(PackedVec3Test, ShouldRun_EmptyModule) {
     auto* src = R"()";
@@ -222,7 +222,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -247,7 +247,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -272,7 +272,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -297,7 +297,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -325,7 +325,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -350,7 +350,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -375,7 +375,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -413,7 +413,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -443,7 +443,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -473,7 +473,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -503,7 +503,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -541,7 +541,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -574,7 +574,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -604,7 +604,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -642,7 +642,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -672,7 +672,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -702,7 +702,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -740,7 +740,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -770,7 +770,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -800,7 +800,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -830,7 +830,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -868,7 +868,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -901,7 +901,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -931,7 +931,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -969,7 +969,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -999,7 +999,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1029,7 +1029,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1075,7 +1075,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1113,7 +1113,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1143,7 +1143,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1173,7 +1173,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1203,7 +1203,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1249,7 +1249,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1282,7 +1282,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1320,7 +1320,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1358,7 +1358,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1388,7 +1388,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1431,7 +1431,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1461,7 +1461,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1491,7 +1491,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1535,7 +1535,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1573,7 +1573,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1611,7 +1611,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1649,7 +1649,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1693,7 +1693,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1734,7 +1734,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1772,7 +1772,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1822,7 +1822,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1860,7 +1860,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1898,7 +1898,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1955,7 +1955,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2006,7 +2006,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2049,7 +2049,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2092,7 +2092,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2135,7 +2135,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2193,7 +2193,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2239,7 +2239,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2290,7 +2290,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2345,7 +2345,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2388,7 +2388,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2444,7 +2444,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2487,7 +2487,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2530,7 +2530,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2587,7 +2587,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2638,7 +2638,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2681,7 +2681,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2724,7 +2724,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2767,7 +2767,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2825,7 +2825,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2871,7 +2871,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2922,7 +2922,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2977,7 +2977,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3020,7 +3020,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3076,7 +3076,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3119,7 +3119,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3162,7 +3162,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3227,7 +3227,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3286,7 +3286,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3337,7 +3337,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3380,7 +3380,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3423,7 +3423,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3466,7 +3466,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3532,7 +3532,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3578,7 +3578,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3637,7 +3637,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3692,7 +3692,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3743,7 +3743,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3799,7 +3799,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3842,7 +3842,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3903,7 +3903,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3946,7 +3946,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3989,7 +3989,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4043,7 +4043,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4093,7 +4093,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4148,7 +4148,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4215,7 +4215,7 @@
 @group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(std::move(src), data);
 
     EXPECT_EQ(expect, str(got));
@@ -4268,7 +4268,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4321,7 +4321,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     auto& vars = got.program.AST().GlobalVariables();
@@ -4440,7 +4440,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4489,7 +4489,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4521,7 +4521,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4582,7 +4582,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4675,7 +4675,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4795,7 +4795,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4889,7 +4889,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4962,7 +4962,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5051,7 +5051,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5176,7 +5176,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5292,7 +5292,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5394,7 +5394,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5481,7 +5481,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5584,7 +5584,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5726,7 +5726,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5794,7 +5794,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5885,7 +5885,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5982,7 +5982,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6088,7 +6088,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6194,7 +6194,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6267,7 +6267,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6340,7 +6340,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6418,7 +6418,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6486,7 +6486,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6554,7 +6554,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6651,7 +6651,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6676,7 +6676,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6701,7 +6701,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6747,7 +6747,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6799,7 +6799,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6864,7 +6864,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6941,7 +6941,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7006,7 +7006,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7079,7 +7079,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7201,7 +7201,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7324,7 +7324,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7470,7 +7470,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7517,7 +7517,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7563,7 +7563,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7623,7 +7623,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7687,7 +7687,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7752,7 +7752,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7792,7 +7792,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7874,7 +7874,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7948,7 +7948,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -8042,7 +8042,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -8089,11 +8089,11 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
new file mode 100644
index 0000000..53f6dfe
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
@@ -0,0 +1,287 @@
+// 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/msl/writer/ast_raise/pixel_local.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/module.h"
+#include "src/tint/lang/wgsl/sem/statement.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/utils/containers/transform.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal);
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal::Attachment);
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::PixelLocal::Config);
+
+using namespace tint::core::number_suffixes;  // NOLINT
+using namespace tint::core::fluent_types;     // NOLINT
+
+namespace tint::msl::writer {
+
+/// PIMPL state for the transform
+struct PixelLocal::State {
+    /// The source program
+    const Program* const src;
+    /// The target program builder
+    ProgramBuilder b;
+    /// The clone context
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    /// The transform config
+    const Config& cfg;
+
+    /// Constructor
+    /// @param program the source program
+    /// @param config the transform config
+    State(const Program* program, const Config& config) : src(program), cfg(config) {}
+
+    /// Runs the transform
+    /// @returns the new program or SkipTransform if the transform is not required
+    ApplyResult Run() {
+        auto& sem = src->Sem();
+
+        // If the pixel local extension isn't enabled, then there must be no use of pixel_local
+        // variables, and so there's nothing for this transform to do.
+        if (!sem.Module()->Extensions().Contains(
+                core::Extension::kChromiumExperimentalPixelLocal)) {
+            return SkipTransform;
+        }
+
+        bool made_changes = false;
+
+        // Change all module scope `var<pixel_local>` variables to `var<private>`.
+        // We need to do this even if the variable is not referenced by the entry point as later
+        // stages do not understand the pixel_local address space.
+        for (auto* global : src->AST().GlobalVariables()) {
+            if (auto* var = global->As<ast::Var>()) {
+                if (sem.Get(var)->AddressSpace() == core::AddressSpace::kPixelLocal) {
+                    // Change the 'var<pixel_local>' to 'var<private>'
+                    ctx.Replace(var->declared_address_space, b.Expr(core::AddressSpace::kPrivate));
+                    made_changes = true;
+                }
+            }
+        }
+
+        // Find the single entry point
+        const sem::Function* entry_point = nullptr;
+        for (auto* fn : src->AST().Functions()) {
+            if (fn->IsEntryPoint()) {
+                if (entry_point != nullptr) {
+                    TINT_ICE() << "PixelLocal transform requires that the SingleEntryPoint "
+                                  "transform has already been run";
+                    return SkipTransform;
+                }
+                entry_point = sem.Get(fn);
+
+                // Look for a `var<pixel_local>` used by the entry point...
+                for (auto* global : entry_point->TransitivelyReferencedGlobals()) {
+                    if (global->AddressSpace() != core::AddressSpace::kPixelLocal) {
+                        continue;
+                    }
+
+                    // Obtain struct of the pixel local.
+                    auto* pixel_local_str = global->Type()->UnwrapRef()->As<sem::Struct>();
+
+                    // Add an attachment decoration to each member of the pixel_local structure.
+                    for (auto* member : pixel_local_str->Members()) {
+                        ctx.InsertBack(member->Declaration()->attributes,
+                                       Attachment(AttachmentIndex(member->Index())));
+                        ctx.InsertBack(member->Declaration()->attributes,
+                                       b.Disable(ast::DisabledValidation::kEntryPointParameter));
+                    }
+
+                    TransformEntryPoint(entry_point, global, pixel_local_str);
+                    made_changes = true;
+
+                    break;  // Only a single `var<pixel_local>` can be used by an entry point.
+                }
+            }
+        }
+
+        if (!made_changes) {
+            return SkipTransform;
+        }
+
+        ctx.Clone();
+        return resolver::Resolve(b);
+    }
+
+    /// Transforms the entry point @p entry_point to handle the direct or transitive usage of the
+    /// `var<pixel_local>` @p pixel_local_var.
+    /// @param entry_point the entry point
+    /// @param pixel_local_var the `var<pixel_local>`
+    /// @param pixel_local_str the struct type of the var
+    void TransformEntryPoint(const sem::Function* entry_point,
+                             const sem::GlobalVariable* pixel_local_var,
+                             const sem::Struct* pixel_local_str) {
+        auto* fn = entry_point->Declaration();
+        auto fn_name = fn->name->symbol.Name();
+        auto pixel_local_str_name = ctx.Clone(pixel_local_str->Name());
+        auto pixel_local_var_name = ctx.Clone(pixel_local_var->Declaration()->name->symbol);
+
+        // Remove the @fragment attribute from the entry point
+        ctx.Remove(fn->attributes, ast::GetAttribute<ast::StageAttribute>(fn->attributes));
+        // Rename the entry point
+        auto inner_name = b.Symbols().New(fn_name + "_inner");
+        ctx.Replace(fn->name, b.Ident(inner_name));
+
+        // Create a new function that wraps the entry point.
+        // This function has all the existing entry point parameters and an additional
+        // parameter for the input pixel local structure.
+        auto params = ctx.Clone(fn->params);
+        auto pl_param = b.Symbols().New("pixel_local");
+        params.Push(b.Param(pl_param, b.ty(pixel_local_str_name)));
+
+        // Remove any entry-point attributes from the inner function.
+        // This must come after `ctx.Clone(fn->params)` as we want these attributes on the outer
+        // function.
+        for (auto* param : fn->params) {
+            for (auto* attr : param->attributes) {
+                if (attr->IsAnyOf<ast::BuiltinAttribute, ast::LocationAttribute,
+                                  ast::InterpolateAttribute, ast::InvariantAttribute>()) {
+                    ctx.Remove(param->attributes, attr);
+                }
+            }
+        }
+
+        // Build the outer function's statements, starting with an assignment of the pixel local
+        // parameter to the module scope var.
+        Vector<const ast::Statement*, 3> body{
+            b.Assign(pixel_local_var_name, pl_param),
+        };
+
+        // Build the arguments to call the inner function
+        auto call_args =
+            tint::Transform(fn->params, [&](auto* p) { return b.Expr(ctx.Clone(p->name)); });
+
+        // Create a structure to hold the combined flattened result of the entry point and the pixel
+        // local structure.
+        auto str_name = b.Symbols().New(fn_name + "_res");
+        Vector<const ast::StructMember*, 8> members;
+        Vector<const ast::Expression*, 8> return_args;  // arguments to the final `return` statement
+
+        auto add_member = [&](const core::type::Type* ty, VectorRef<const ast::Attribute*> attrs) {
+            members.Push(b.Member("output_" + std::to_string(members.Length()),
+                                  CreateASTTypeFor(ctx, ty), std::move(attrs)));
+        };
+        for (auto* member : pixel_local_str->Members()) {
+            add_member(member->Type(), Vector{
+                                           b.Location(AInt(AttachmentIndex(member->Index()))),
+                                       });
+            return_args.Push(b.MemberAccessor(pixel_local_var_name, ctx.Clone(member->Name())));
+        }
+        if (fn->return_type) {
+            Symbol call_result = b.Symbols().New("result");
+            if (auto* str = entry_point->ReturnType()->As<sem::Struct>()) {
+                // The entry point returned a structure.
+                for (auto* member : str->Members()) {
+                    auto& member_attrs = member->Declaration()->attributes;
+                    add_member(member->Type(), ctx.Clone(member_attrs));
+                    return_args.Push(b.MemberAccessor(call_result, ctx.Clone(member->Name())));
+                    if (auto* location = ast::GetAttribute<ast::LocationAttribute>(member_attrs)) {
+                        // Remove the @location attribute from the member of the inner function's
+                        // output structure.
+                        // Note: This will break other entry points that share the same output
+                        // structure, however this transform assumes that the SingleEntryPoint
+                        // transform will have already been run.
+                        ctx.Remove(member_attrs, location);
+                    }
+                }
+            } else {
+                // The entry point returned a non-structure
+                add_member(entry_point->ReturnType(), ctx.Clone(fn->return_type_attributes));
+                return_args.Push(b.Expr(call_result));
+
+                // Remove the @location from the inner function's return type attributes
+                ctx.Remove(fn->return_type_attributes,
+                           ast::GetAttribute<ast::LocationAttribute>(fn->return_type_attributes));
+            }
+            body.Push(b.Decl(b.Let(call_result, b.Call(inner_name, std::move(call_args)))));
+        } else {
+            body.Push(b.CallStmt(b.Call(inner_name, std::move(call_args))));
+        }
+
+        // Declare the output structure
+        b.Structure(str_name, std::move(members));
+
+        // Return the output structure
+        body.Push(b.Return(b.Call(str_name, std::move(return_args))));
+
+        // Declare the new entry point that calls the inner function
+        b.Func(fn_name, std::move(params), b.ty(str_name), body,
+               Vector{b.Stage(ast::PipelineStage::kFragment)});
+    }
+
+    /// @returns a new Attachment attribute
+    /// @param index the index of the attachment
+    PixelLocal::Attachment* Attachment(uint32_t index) {
+        return b.ASTNodes().Create<PixelLocal::Attachment>(b.ID(), b.AllocateNodeID(), index);
+    }
+
+    /// @returns the attachment index for the pixel local field with the given index
+    /// @param field_index the pixel local field index
+    uint32_t AttachmentIndex(uint32_t field_index) {
+        auto idx = cfg.attachments.Get(field_index);
+        if (TINT_UNLIKELY(!idx)) {
+            b.Diagnostics().add_error(diag::System::Transform,
+                                      "PixelLocal::Config::attachments missing entry for field " +
+                                          std::to_string(field_index));
+            return 0;
+        }
+        return *idx;
+    }
+};
+
+PixelLocal::PixelLocal() = default;
+
+PixelLocal::~PixelLocal() = default;
+
+ast::transform::Transform::ApplyResult PixelLocal::Apply(const Program* src,
+                                                         const ast::transform::DataMap& inputs,
+                                                         ast::transform::DataMap&) const {
+    auto* cfg = inputs.Get<Config>();
+    if (!cfg) {
+        ProgramBuilder b;
+        b.Diagnostics().add_error(diag::System::Transform,
+                                  "missing transform data for " + std::string(TypeInfo().name));
+        return resolver::Resolve(b);
+    }
+
+    return State(src, *cfg).Run();
+}
+
+PixelLocal::Config::Config() = default;
+
+PixelLocal::Config::Config(const Config&) = default;
+
+PixelLocal::Config::~Config() = default;
+
+PixelLocal::Attachment::Attachment(GenerationID pid, ast::NodeID nid, uint32_t idx)
+    : Base(pid, nid, Empty), index(idx) {}
+
+PixelLocal::Attachment::~Attachment() = default;
+
+std::string PixelLocal::Attachment::InternalName() const {
+    return "attachment(" + std::to_string(index) + ")";
+}
+
+const PixelLocal::Attachment* PixelLocal::Attachment::Clone(ast::CloneContext& ctx) const {
+    return ctx.dst->ASTNodes().Create<Attachment>(ctx.dst->ID(), ctx.dst->AllocateNodeID(), index);
+}
+
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.h b/src/tint/lang/msl/writer/ast_raise/pixel_local.h
new file mode 100644
index 0000000..4d1b51f
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.h
@@ -0,0 +1,96 @@
+// 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_MSL_WRITER_AST_RAISE_PIXEL_LOCAL_H_
+#define SRC_TINT_LANG_MSL_WRITER_AST_RAISE_PIXEL_LOCAL_H_
+
+#include <string>
+
+#include "src/tint/lang/wgsl/ast/internal_attribute.h"
+#include "src/tint/lang/wgsl/ast/transform/transform.h"
+#include "src/tint/utils/containers/hashmap.h"
+
+namespace tint::msl::writer {
+
+/// PixelLocal transforms module-scope `var<pixel_local>`s and fragment entry point functions that
+/// use them:
+/// * `var<pixel_local>` will be transformed to `var<private>`.
+/// * The entry point function will be wrapped with another function ('outer') that calls the
+///  'inner' function.
+/// * The outer function will have an additional parameter of the pixel local struct type, which is
+///   copied to the module-scope var before calling the 'inner' function.
+/// * The outer function will have a new struct return type which holds both the pixel local members
+///   and the returned value(s) of the 'inner' function.
+/// @note PixelLocal requires that the SingleEntryPoint transform has already been run
+class PixelLocal final : public Castable<PixelLocal, ast::transform::Transform> {
+  public:
+    /// Transform configuration options
+    struct Config final : public Castable<Config, ast::transform::Data> {
+        /// Constructor
+        Config();
+
+        /// Copy Constructor
+        Config(const Config&);
+
+        /// Destructor
+        ~Config() override;
+
+        /// Index of pixel_local structure member index to attachment index
+        Hashmap<uint32_t, uint32_t, 8> attachments;
+    };
+
+    /// Intrinsic is an InternalAttribute that's used to decorate a pixel local attachment
+    /// parameter, return value or structure member.
+    class Attachment final : public Castable<Attachment, ast::InternalAttribute> {
+      public:
+        /// Constructor
+        /// @param pid the identifier of the program that owns this node
+        /// @param nid the unique node identifier
+        /// @param idx the attachment index
+        Attachment(GenerationID pid, ast::NodeID nid, uint32_t idx);
+
+        /// Destructor
+        ~Attachment() override;
+
+        /// @return a short description of the internal attribute which will be
+        /// displayed as `@internal(<name>)`
+        std::string InternalName() const override;
+
+        /// Performs a deep clone of this object using the program::CloneContext `ctx`.
+        /// @param ctx the clone context
+        /// @return the newly cloned object
+        const Attachment* Clone(ast::CloneContext& ctx) const override;
+
+        /// The attachment index
+        const uint32_t index;
+    };
+
+    /// Constructor
+    PixelLocal();
+
+    /// Destructor
+    ~PixelLocal() override;
+
+    /// @copydoc ast::transform::Transform::Apply
+    ApplyResult Apply(const Program* program,
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
+
+  private:
+    struct State;
+};
+
+}  // namespace tint::msl::writer
+
+#endif  // SRC_TINT_LANG_MSL_WRITER_AST_RAISE_PIXEL_LOCAL_H_
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc b/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc
new file mode 100644
index 0000000..4b634f9
--- /dev/null
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local_test.cc
@@ -0,0 +1,799 @@
+// 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/msl/writer/ast_raise/pixel_local.h"
+
+#include <utility>
+
+#include "src/tint/lang/wgsl/ast/transform/helper_test.h"
+
+namespace tint::msl::writer {
+namespace {
+
+struct Binding {
+    uint32_t field_index;
+    uint32_t attachment_index;
+};
+
+ast::transform::DataMap Bindings(std::initializer_list<Binding> bindings) {
+    PixelLocal::Config cfg;
+    for (auto& binding : bindings) {
+        cfg.attachments.Add(binding.field_index, binding.attachment_index);
+    }
+    ast::transform::DataMap data;
+    data.Add<PixelLocal::Config>(std::move(cfg));
+    return data;
+}
+
+using PixelLocalTest = ast::transform::TransformTest;
+
+TEST_F(PixelLocalTest, EmptyModule) {
+    auto* src = "";
+
+    EXPECT_FALSE(ShouldRun<PixelLocal>(src, Bindings({})));
+}
+
+TEST_F(PixelLocalTest, Var) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : i32,
+};
+
+var<pixel_local> P : PixelLocal;
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : i32,
+}
+
+var<private> P : PixelLocal;
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, UseInEntryPoint) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F() {
+  P.a += 42;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner();
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner() {
+  P.a += 42;
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, UseInCallee) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+fn X() {
+  P.a += 42;
+}
+
+@fragment
+fn F() {
+  X();
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner();
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn X() {
+  P.a += 42;
+}
+
+fn F_inner() {
+  X();
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, MultipleAttachments) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+  b : i32,
+  c : f32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F() {
+  P.a = 42;
+  P.b = i32(P.c);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+  @location(0)
+  output_1 : i32,
+  @location(10)
+  output_2 : f32,
+}
+
+@fragment
+fn F(pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner();
+  return F_res(P.a, P.b, P.c);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+  @internal(attachment(0)) @internal(disable_validation__entry_point_parameter)
+  b : i32,
+  @internal(attachment(10)) @internal(disable_validation__entry_point_parameter)
+  c : f32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner() {
+  P.a = 42;
+  P.b = i32(P.c);
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}, {1, 0}, {2, 10}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithBuiltinInputParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F(@builtin(position) pos : vec4f) {
+  P.a += u32(pos.x);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(@builtin(position) pos : vec4f, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(pos);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner(pos : vec4f) {
+  P.a += u32(pos.x);
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithInvariantBuiltinInputParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F(@invariant @builtin(position) pos : vec4f) {
+  P.a += u32(pos.x);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(@invariant @builtin(position) pos : vec4f, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(pos);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner(pos : vec4f) {
+  P.a += u32(pos.x);
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithBuiltinInputStructParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+struct In {
+  @builtin(position) pos : vec4f,
+}
+
+@fragment
+fn F(in : In) {
+  P.a += u32(in.pos.x);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(in : In, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(in);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+struct In {
+  @builtin(position)
+  pos : vec4f,
+}
+
+fn F_inner(in : In) {
+  P.a += u32(in.pos.x);
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithInvariantBuiltinInputStructParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+struct In {
+  @invariant @builtin(position) pos : vec4f,
+}
+
+@fragment
+fn F(in : In) {
+  P.a += u32(in.pos.x);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(in : In, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(in);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+struct In {
+  @invariant @builtin(position)
+  pos : vec4f,
+}
+
+fn F_inner(in : In) {
+  P.a += u32(in.pos.x);
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithLocationInputParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F(@location(0) a : vec4f, @interpolate(flat) @location(1) b : vec4f) {
+  P.a += u32(a.x) + u32(b.y);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(@location(0) a : vec4f, @interpolate(flat) @location(1) b : vec4f, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(a, b);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner(a : vec4f, b : vec4f) {
+  P.a += (u32(a.x) + u32(b.y));
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithLocationInputStructParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+struct In {
+  @location(0) a : vec4f,
+  @interpolate(flat) @location(1) b : vec4f,
+}
+
+@fragment
+fn F(in : In) {
+  P.a += u32(in.a.x) + u32(in.b.y);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(in : In, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(in);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+struct In {
+  @location(0)
+  a : vec4f,
+  @interpolate(flat) @location(1)
+  b : vec4f,
+}
+
+fn F_inner(in : In) {
+  P.a += (u32(in.a.x) + u32(in.b.y));
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithBuiltinAndLocationInputParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F(@builtin(position) pos : vec4f, @location(0) uv : vec4f) {
+  P.a += u32(pos.x) + u32(uv.x);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(@builtin(position) pos : vec4f, @location(0) uv : vec4f, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(pos, uv);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner(pos : vec4f, uv : vec4f) {
+  P.a += (u32(pos.x) + u32(uv.x));
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithBuiltinAndLocationInputStructParameter) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+struct In {
+  @builtin(position) pos : vec4f,
+  @location(0) uv : vec4f,
+}
+
+@fragment
+fn F(in : In) {
+  P.a += u32(in.pos.x) + u32(in.uv.x);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+}
+
+@fragment
+fn F(in : In, pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  F_inner(in);
+  return F_res(P.a);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+struct In {
+  @builtin(position)
+  pos : vec4f,
+  @location(0)
+  uv : vec4f,
+}
+
+fn F_inner(in : In) {
+  P.a += (u32(in.pos.x) + u32(in.uv.x));
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithSingleFragmentOutput) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F() -> @location(0) vec4f {
+  P.a += 42;
+  return vec4f(1);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+  @location(0)
+  output_1 : vec4<f32>,
+}
+
+@fragment
+fn F(pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  let result = F_inner();
+  return F_res(P.a, result);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+}
+
+var<private> P : PixelLocal;
+
+fn F_inner() -> vec4f {
+  P.a += 42;
+  return vec4f(1);
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PixelLocalTest, WithMultipleFragmentOutputs) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+  b : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+struct Output {
+  @location(0) x : vec4f,
+  @location(2) y : vec4f,
+}
+
+@fragment
+fn F() -> Output {
+  P.a += 42;
+  return Output(vec4f(1), vec4f(9));
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_pixel_local;
+
+struct F_res {
+  @location(1)
+  output_0 : u32,
+  @location(5)
+  output_1 : u32,
+  @location(0)
+  output_2 : vec4<f32>,
+  @location(2)
+  output_3 : vec4<f32>,
+}
+
+@fragment
+fn F(pixel_local_1 : PixelLocal) -> F_res {
+  P = pixel_local_1;
+  let result = F_inner();
+  return F_res(P.a, P.b, result.x, result.y);
+}
+
+struct PixelLocal {
+  @internal(attachment(1)) @internal(disable_validation__entry_point_parameter)
+  a : u32,
+  @internal(attachment(5)) @internal(disable_validation__entry_point_parameter)
+  b : u32,
+}
+
+var<private> P : PixelLocal;
+
+struct Output {
+  x : vec4f,
+  y : vec4f,
+}
+
+fn F_inner() -> Output {
+  P.a += 42;
+  return Output(vec4f(1), vec4f(9));
+}
+)";
+
+    auto got = Run<PixelLocal>(src, Bindings({{0, 1}, {1, 5}}));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.cc b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
similarity index 87%
rename from src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.cc
rename to src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
index 696db80..e15bf24 100644
--- a/src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.cc
+++ b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.h"
+#include "src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h"
 
 #include <utility>
 
@@ -24,16 +24,16 @@
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::MslSubgroupBallot);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::MslSubgroupBallot::SimdActiveThreadsMask);
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::SubgroupBallot);
+TINT_INSTANTIATE_TYPEINFO(tint::msl::writer::SubgroupBallot::SimdActiveThreadsMask);
 
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 
 /// PIMPL state for the transform
-struct MslSubgroupBallot::State {
+struct SubgroupBallot::State {
     /// The source program
     const Program* const src;
     /// The target program builder
@@ -80,7 +80,7 @@
         // Set the subgroup size mask at the start of each entry point that transitively calls
         // `subgroupBallot()`.
         for (auto* global : src->AST().GlobalDeclarations()) {
-            auto* func = global->As<Function>();
+            auto* func = global->As<ast::Function>();
             if (func && func->IsEntryPoint() && TransitvelyCallsSubgroupBallot(sem.Get(func))) {
                 SetSubgroupSizeMask(func);
             }
@@ -102,7 +102,7 @@
             // `simd_active_threads_mask` function to return 64-bit vote.
             b.Func(intrinsic, Empty, b.ty.vec2<u32>(), nullptr,
                    Vector{b.ASTNodes().Create<SimdActiveThreadsMask>(b.ID(), b.AllocateNodeID()),
-                          b.Disable(DisabledValidation::kFunctionHasNoBody)});
+                          b.Disable(ast::DisabledValidation::kFunctionHasNoBody)});
 
             // Declare the `tint_subgroup_size_mask` variable.
             b.GlobalVar(subgroup_size_mask, core::AddressSpace::kPrivate, b.ty.vec4<u32>());
@@ -179,22 +179,22 @@
     }
 };
 
-MslSubgroupBallot::MslSubgroupBallot() = default;
+SubgroupBallot::SubgroupBallot() = default;
 
-MslSubgroupBallot::~MslSubgroupBallot() = default;
+SubgroupBallot::~SubgroupBallot() = default;
 
-Transform::ApplyResult MslSubgroupBallot::Apply(const Program* src,
-                                                const DataMap&,
-                                                DataMap&) const {
+ast::transform::Transform::ApplyResult SubgroupBallot::Apply(const Program* src,
+                                                             const ast::transform::DataMap&,
+                                                             ast::transform::DataMap&) const {
     return State(src).Run();
 }
 
-MslSubgroupBallot::SimdActiveThreadsMask::~SimdActiveThreadsMask() = default;
+SubgroupBallot::SimdActiveThreadsMask::~SimdActiveThreadsMask() = default;
 
-const MslSubgroupBallot::SimdActiveThreadsMask* MslSubgroupBallot::SimdActiveThreadsMask::Clone(
+const SubgroupBallot::SimdActiveThreadsMask* SubgroupBallot::SimdActiveThreadsMask::Clone(
     ast::CloneContext& ctx) const {
-    return ctx.dst->ASTNodes().Create<MslSubgroupBallot::SimdActiveThreadsMask>(
+    return ctx.dst->ASTNodes().Create<SubgroupBallot::SimdActiveThreadsMask>(
         ctx.dst->ID(), ctx.dst->AllocateNodeID());
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.h b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h
similarity index 62%
rename from src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.h
rename to src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h
index 982f3e2..cde3203 100644
--- a/src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.h
+++ b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h
@@ -12,59 +12,60 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_MSL_SUBGROUP_BALLOT_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_MSL_SUBGROUP_BALLOT_H_
+#ifndef SRC_TINT_LANG_MSL_WRITER_AST_RAISE_SUBGROUP_BALLOT_H_
+#define SRC_TINT_LANG_MSL_WRITER_AST_RAISE_SUBGROUP_BALLOT_H_
 
 #include <string>
 
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 
-/// MslSubgroupBallot is a transform that replaces calls to `subgroupBallot()` with an
+/// SubgroupBallot is a transform that replaces calls to `subgroupBallot()` with an
 /// implementation that uses MSL's `simd_active_threads_mask()`.
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * CanonicalizeEntryPointIO
-class MslSubgroupBallot final : public Castable<MslSubgroupBallot, Transform> {
+class SubgroupBallot final : public Castable<SubgroupBallot, ast::transform::Transform> {
   public:
     /// Constructor
-    MslSubgroupBallot();
+    SubgroupBallot();
 
     /// Destructor
-    ~MslSubgroupBallot() override;
+    ~SubgroupBallot() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
     /// SimdActiveThreadsMask is an InternalAttribute that is used to decorate a stub function so
     /// that the MSL backend transforms this into calls to the `simd_active_threads_mask` function.
-    class SimdActiveThreadsMask final : public Castable<SimdActiveThreadsMask, InternalAttribute> {
+    class SimdActiveThreadsMask final
+        : public Castable<SimdActiveThreadsMask, ast::InternalAttribute> {
       public:
         /// Constructor
         /// @param pid the identifier of the program that owns this node
         /// @param nid the unique node identifier
-        SimdActiveThreadsMask(GenerationID pid, NodeID nid) : Base(pid, nid, Empty) {}
+        SimdActiveThreadsMask(GenerationID pid, ast::NodeID nid) : Base(pid, nid, Empty) {}
 
         /// Destructor
         ~SimdActiveThreadsMask() override;
 
-        /// @copydoc InternalAttribute::InternalName
+        /// @copydoc ast::InternalAttribute::InternalName
         std::string InternalName() const override { return "simd_active_threads_mask"; }
 
         /// Performs a deep clone of this object using the program::CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const SimdActiveThreadsMask* Clone(CloneContext& ctx) const override;
+        const SimdActiveThreadsMask* Clone(ast::CloneContext& ctx) const override;
     };
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_MSL_SUBGROUP_BALLOT_H_
+#endif  // SRC_TINT_LANG_MSL_WRITER_AST_RAISE_SUBGROUP_BALLOT_H_
diff --git a/src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot_test.cc b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot_test.cc
similarity index 87%
rename from src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot_test.cc
rename to src/tint/lang/msl/writer/ast_raise/subgroup_ballot_test.cc
index 09a2917..1f6294a 100644
--- a/src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot_test.cc
+++ b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot_test.cc
@@ -12,22 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/msl_subgroup_ballot.h"
+#include "src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::msl::writer {
 namespace {
 
-using MslSubgroupBallotTest = TransformTest;
+using SubgroupBallotTest = ast::transform::TransformTest;
 
-TEST_F(MslSubgroupBallotTest, EmptyModule) {
+TEST_F(SubgroupBallotTest, EmptyModule) {
     auto* src = "";
 
-    EXPECT_FALSE(ShouldRun<MslSubgroupBallot>(src));
+    EXPECT_FALSE(ShouldRun<SubgroupBallot>(src));
 }
 
-TEST_F(MslSubgroupBallotTest, DirectUse) {
+TEST_F(SubgroupBallotTest, DirectUse) {
     auto* src = R"(
 enable chromium_experimental_subgroups;
 
@@ -62,12 +62,12 @@
 }
 )";
 
-    auto got = Run<MslSubgroupBallot>(src);
+    auto got = Run<SubgroupBallot>(src);
 
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(MslSubgroupBallotTest, IndirectUse) {
+TEST_F(SubgroupBallotTest, IndirectUse) {
     auto* src = R"(
 enable chromium_experimental_subgroups;
 
@@ -110,12 +110,12 @@
 }
 )";
 
-    auto got = Run<MslSubgroupBallot>(src);
+    auto got = Run<SubgroupBallot>(src);
 
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(MslSubgroupBallotTest, PreexistingSubgroupSizeBuiltin) {
+TEST_F(SubgroupBallotTest, PreexistingSubgroupSizeBuiltin) {
     auto* src = R"(
 enable chromium_experimental_subgroups;
 
@@ -154,10 +154,10 @@
 }
 )";
 
-    auto got = Run<MslSubgroupBallot>(src);
+    auto got = Run<SubgroupBallot>(src);
 
     EXPECT_EQ(expect, str(got));
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/common/BUILD.bazel b/src/tint/lang/msl/writer/common/BUILD.bazel
new file mode 100644
index 0000000..6ae1c91
--- /dev/null
+++ b/src/tint/lang/msl/writer/common/BUILD.bazel
@@ -0,0 +1,89 @@
+# 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 = "common",
+  srcs = [
+    "options.cc",
+    "printer_support.cc",
+  ],
+  hdrs = [
+    "options.h",
+    "printer_support.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//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/rtti",
+    "//src/tint/utils/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "printer_support_test.cc",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/writer/common/options.h b/src/tint/lang/msl/writer/common/options.h
index 16d2b34..763b1d3 100644
--- a/src/tint/lang/msl/writer/common/options.h
+++ b/src/tint/lang/msl/writer/common/options.h
@@ -18,6 +18,7 @@
 #include "src/tint/api/options/array_length_from_uniform.h"
 #include "src/tint/api/options/binding_remapper.h"
 #include "src/tint/api/options/external_texture.h"
+#include "src/tint/api/options/pixel_local.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::msl::writer {
@@ -52,6 +53,9 @@
     /// Set to `true` to disable workgroup memory zero initialization
     bool disable_workgroup_init = false;
 
+    /// Options used for dealing with pixel local storage
+    PixelLocalOptions pixel_local_options = {};
+
     /// Options used in the binding mappings for external textures
     ExternalTextureOptions external_texture_options = {};
 
@@ -62,10 +66,8 @@
     /// Options used in the bindings remapper
     BindingRemapperOptions binding_remapper_options = {};
 
-#if TINT_BUILD_IR
     /// Set to `true` to generate MSL via the Tint IR instead of from the AST.
     bool use_tint_ir = false;
-#endif
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
@@ -74,7 +76,9 @@
                  emit_vertex_point_size,
                  disable_workgroup_init,
                  external_texture_options,
-                 array_length_from_uniform);
+                 array_length_from_uniform,
+                 binding_remapper_options,
+                 use_tint_ir);
 };
 
 }  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/printer/BUILD.bazel b/src/tint/lang/msl/writer/printer/BUILD.bazel
new file mode 100644
index 0000000..b56908f
--- /dev/null
+++ b/src/tint/lang/msl/writer/printer/BUILD.bazel
@@ -0,0 +1,115 @@
+# 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 = "printer",
+  srcs = [
+    "printer.cc",
+  ],
+  hdrs = [
+    "printer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//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_msl_writer": [
+      "//src/tint/lang/msl/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "binary_test.cc",
+    "constant_test.cc",
+    "function_test.cc",
+    "helper_test.h",
+    "if_test.cc",
+    "let_test.cc",
+    "return_test.cc",
+    "type_test.cc",
+    "var_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/msl/writer/raise",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer/printer",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/writer/printer/BUILD.cfg b/src/tint/lang/msl/writer/printer/BUILD.cfg
index f3543df..1c7e256 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.cfg
+++ b/src/tint/lang/msl/writer/printer/BUILD.cfg
@@ -1,3 +1,3 @@
 {
-    "condition": "tint_build_msl_writer && tint_build_ir"
+    "condition": "tint_build_msl_writer"
 }
diff --git a/src/tint/lang/msl/writer/printer/BUILD.cmake b/src/tint/lang/msl/writer/printer/BUILD.cmake
index 1999e2a..b716984 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/printer/BUILD.cmake
@@ -21,11 +21,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
+if(TINT_BUILD_MSL_WRITER)
 ################################################################################
 # Target:    tint_lang_msl_writer_printer
 # Kind:      lib
-# Condition: TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR
+# Condition: TINT_BUILD_MSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_msl_writer_printer lib
   lang/msl/writer/printer/printer.cc
@@ -36,6 +36,7 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_utils_containers
   tint_utils_diagnostic
@@ -53,24 +54,18 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_msl_writer_printer lib
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_lang_msl_writer_printer lib
     tint_lang_msl_writer_common
   )
 endif(TINT_BUILD_MSL_WRITER)
 
-endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
-if(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_MSL_WRITER)
+if(TINT_BUILD_MSL_WRITER)
 ################################################################################
 # Target:    tint_lang_msl_writer_printer_test
 # Kind:      test
-# Condition: TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR
+# Condition: TINT_BUILD_MSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_msl_writer_printer_test test
   lang/msl/writer/printer/binary_test.cc
@@ -88,6 +83,9 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_msl_writer_raise
   tint_utils_containers
@@ -110,16 +108,10 @@
   "gtest"
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_msl_writer_printer_test test
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
-if(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
+if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_lang_msl_writer_printer_test test
     tint_lang_msl_writer_printer
   )
-endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_MSL_WRITER)
 
-endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_IR)
\ No newline at end of file
+endif(TINT_BUILD_MSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/msl/writer/printer/BUILD.gn b/src/tint/lang/msl/writer/printer/BUILD.gn
index 79e66b4..52f1d40 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/printer/BUILD.gn
@@ -28,7 +28,7 @@
 if (tint_build_unittests) {
   import("//testing/test.gni")
 }
-if (tint_build_msl_writer && tint_build_ir) {
+if (tint_build_msl_writer) {
   libtint_source_set("printer") {
     sources = [
       "printer.cc",
@@ -38,6 +38,7 @@
       "${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}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -55,17 +56,13 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/core/ir" ]
-    }
-
     if (tint_build_msl_writer) {
       deps += [ "${tint_src_dir}/lang/msl/writer/common" ]
     }
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_msl_writer && tint_build_ir) {
+  if (tint_build_msl_writer) {
     tint_unittests_source_set("unittests") {
       testonly = true
       sources = [
@@ -84,6 +81,9 @@
         "${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/intrinsic/data",
+        "${tint_src_dir}/lang/core/ir",
         "${tint_src_dir}/lang/core/type",
         "${tint_src_dir}/lang/msl/writer/raise",
         "${tint_src_dir}/utils/containers",
@@ -102,11 +102,7 @@
         "${tint_src_dir}/utils/traits",
       ]
 
-      if (tint_build_ir) {
-        deps += [ "${tint_src_dir}/lang/core/ir" ]
-      }
-
-      if (tint_build_msl_writer && tint_build_ir) {
+      if (tint_build_msl_writer) {
         deps += [ "${tint_src_dir}/lang/msl/writer/printer" ]
       }
     }
diff --git a/src/tint/lang/msl/writer/raise/BUILD.bazel b/src/tint/lang/msl/writer/raise/BUILD.bazel
new file mode 100644
index 0000000..ca4cf5b
--- /dev/null
+++ b/src/tint/lang/msl/writer/raise/BUILD.bazel
@@ -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.
+
+################################################################################
+# 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 = "raise",
+  srcs = [
+    "raise.cc",
+  ],
+  hdrs = [
+    "raise.h",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/result",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/msl/writer/writer.cc b/src/tint/lang/msl/writer/writer.cc
index 20d1df7..fcfc910 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -18,12 +18,9 @@
 #include <utility>
 
 #include "src/tint/lang/msl/writer/ast_printer/ast_printer.h"
-
-#if TINT_BUILD_IR
-#include "src/tint/lang/msl/writer/printer/printer.h"               // nogncheck
-#include "src/tint/lang/msl/writer/raise/raise.h"                   // nogncheck
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"  // nogncheck
-#endif                                                              // TINT_BUILD_IR
+#include "src/tint/lang/msl/writer/printer/printer.h"
+#include "src/tint/lang/msl/writer/raise/raise.h"
+#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 
 namespace tint::msl::writer {
 
@@ -33,7 +30,7 @@
     }
 
     Output output;
-#if TINT_BUILD_IR
+
     if (options.use_tint_ir) {
         // Convert the AST program to an IR module.
         auto converted = wgsl::reader::ProgramToIR(program);
@@ -56,9 +53,7 @@
             return result.Failure();
         }
         output.msl = impl->Result();
-    } else  // NOLINT(readability/braces)
-#endif
-    {
+    } else {
         // Sanitize the program.
         auto sanitized_result = Sanitize(program, options);
         if (!sanitized_result.program.IsValid()) {
diff --git a/src/tint/lang/spirv/BUILD.bazel b/src/tint/lang/spirv/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/lang/spirv/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/lang/spirv/BUILD.cmake b/src/tint/lang/spirv/BUILD.cmake
index 24b83a3..359a3c6 100644
--- a/src/tint/lang/spirv/BUILD.cmake
+++ b/src/tint/lang/spirv/BUILD.cmake
@@ -21,5 +21,7 @@
 #                       Do not modify this file directly
 ################################################################################
 
+include(lang/spirv/intrinsic/BUILD.cmake)
+include(lang/spirv/ir/BUILD.cmake)
 include(lang/spirv/reader/BUILD.cmake)
 include(lang/spirv/writer/BUILD.cmake)
diff --git a/src/tint/lang/spirv/intrinsic/BUILD.bazel b/src/tint/lang/spirv/intrinsic/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/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/lang/spirv/intrinsic/BUILD.cmake b/src/tint/lang/spirv/intrinsic/BUILD.cmake
new file mode 100644
index 0000000..af6fdc2
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/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(lang/spirv/intrinsic/data/BUILD.cmake)
diff --git a/src/tint/lang/spirv/intrinsic/BUILD.gn b/src/tint/lang/spirv/intrinsic/BUILD.gn
new file mode 100644
index 0000000..b502e46
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/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/lang/spirv/intrinsic/data/BUILD.bazel b/src/tint/lang/spirv/intrinsic/data/BUILD.bazel
new file mode 100644
index 0000000..e43a7f6
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/BUILD.bazel
@@ -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.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 = "data",
+  srcs = [
+    "data.cc",
+  ],
+  hdrs = [
+    "data.h",
+    "type_matchers.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//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"],
+)
+
diff --git a/src/tint/lang/spirv/intrinsic/data/BUILD.cmake b/src/tint/lang/spirv/intrinsic/data/BUILD.cmake
new file mode 100644
index 0000000..1f5e78f
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/BUILD.cmake
@@ -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.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_lang_spirv_intrinsic_data
+# Kind:      lib
+################################################################################
+tint_add_target(tint_lang_spirv_intrinsic_data lib
+  lang/spirv/intrinsic/data/data.cc
+  lang/spirv/intrinsic/data/data.h
+  lang/spirv/intrinsic/data/type_matchers.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_intrinsic_data lib
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_type
+  tint_utils_containers
+  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
+)
diff --git a/src/tint/lang/spirv/intrinsic/data/BUILD.gn b/src/tint/lang/spirv/intrinsic/data/BUILD.gn
new file mode 100644
index 0000000..4ae224e
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/BUILD.gn
@@ -0,0 +1,52 @@
+# 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")
+
+libtint_source_set("data") {
+  sources = [
+    "data.cc",
+    "data.h",
+    "type_matchers.h",
+  ]
+  deps = [
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/intrinsic",
+    "${tint_src_dir}/lang/core/intrinsic/data",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/utils/containers",
+    "${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",
+  ]
+}
diff --git a/src/tint/lang/spirv/intrinsic/data/data.cc b/src/tint/lang/spirv/intrinsic/data/data.cc
new file mode 100644
index 0000000..666a8b9
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/data.cc
@@ -0,0 +1,1274 @@
+// 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:
+//   src/tint/lang/spirv/intrinsic/data/data.cc.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include <limits>
+#include <string>
+
+#include "src/tint/lang/core/intrinsic/data/type_matchers.h"
+#include "src/tint/lang/spirv/intrinsic/data/data.h"
+#include "src/tint/lang/spirv/intrinsic/data/type_matchers.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace tint::spirv::intrinsic::data {
+
+using namespace tint::core::intrinsic::data;  // NOLINT(build/namespaces)
+
+namespace {
+
+using ConstEvalFunctionIndex = tint::core::intrinsic::ConstEvalFunctionIndex;
+using IntrinsicInfo = tint::core::intrinsic::IntrinsicInfo;
+using MatchState = tint::core::intrinsic::MatchState;
+using Number = tint::core::intrinsic::Number;
+using NumberMatcher = tint::core::intrinsic::NumberMatcher;
+using NumberMatcherIndex = tint::core::intrinsic::NumberMatcherIndex;
+using NumberMatcherIndicesIndex = tint::core::intrinsic::NumberMatcherIndicesIndex;
+using OverloadFlag = tint::core::intrinsic::OverloadFlag;
+using OverloadFlags = tint::core::intrinsic::OverloadFlags;
+using OverloadIndex = tint::core::intrinsic::OverloadIndex;
+using OverloadInfo = tint::core::intrinsic::OverloadInfo;
+using ParameterIndex = tint::core::intrinsic::ParameterIndex;
+using ParameterInfo = tint::core::intrinsic::ParameterInfo;
+using StringStream = tint::StringStream;
+using TemplateNumberIndex = tint::core::intrinsic::TemplateNumberIndex;
+using TemplateNumberInfo = tint::core::intrinsic::TemplateNumberInfo;
+using TemplateTypeIndex = tint::core::intrinsic::TemplateTypeIndex;
+using TemplateTypeInfo = tint::core::intrinsic::TemplateTypeInfo;
+using Type = tint::core::type::Type;
+using TypeMatcher = tint::core::intrinsic::TypeMatcher;
+using TypeMatcherIndex = tint::core::intrinsic::TypeMatcherIndex;
+using TypeMatcherIndicesIndex = tint::core::intrinsic::TypeMatcherIndicesIndex;
+
+template <size_t N>
+using TemplateNumberMatcher = tint::core::intrinsic::TemplateNumberMatcher<N>;
+
+template <size_t N>
+using TemplateTypeMatcher = tint::core::intrinsic::TemplateTypeMatcher<N>;
+
+// clang-format off
+
+/// TypeMatcher for 'type bool'
+constexpr TypeMatcher kBoolMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (!MatchBool(state, ty)) {
+      return nullptr;
+    }
+    return BuildBool(state, ty);
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "bool";
+  }
+};
+
+
+/// TypeMatcher for 'type f32'
+constexpr TypeMatcher kF32Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (!MatchF32(state, ty)) {
+      return nullptr;
+    }
+    return BuildF32(state, ty);
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "f32";
+  }
+};
+
+
+/// TypeMatcher for 'type f16'
+constexpr TypeMatcher kF16Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (!MatchF16(state, ty)) {
+      return nullptr;
+    }
+    return BuildF16(state, ty);
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "f16";
+  }
+};
+
+
+/// TypeMatcher for 'type i32'
+constexpr TypeMatcher kI32Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (!MatchI32(state, ty)) {
+      return nullptr;
+    }
+    return BuildI32(state, ty);
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "i32";
+  }
+};
+
+
+/// TypeMatcher for 'type u32'
+constexpr TypeMatcher kU32Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (!MatchU32(state, ty)) {
+      return nullptr;
+    }
+    return BuildU32(state, ty);
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "u32";
+  }
+};
+
+
+/// TypeMatcher for 'type vec2'
+constexpr TypeMatcher kVec2Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchVec2(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildVec2(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "vec2<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type vec3'
+constexpr TypeMatcher kVec3Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchVec3(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildVec3(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "vec3<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type vec4'
+constexpr TypeMatcher kVec4Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchVec4(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildVec4(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "vec4<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat2x2'
+constexpr TypeMatcher kMat2X2Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat2X2(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat2X2(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat2x2<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat2x3'
+constexpr TypeMatcher kMat2X3Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat2X3(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat2X3(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat2x3<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat2x4'
+constexpr TypeMatcher kMat2X4Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat2X4(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat2X4(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat2x4<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat3x2'
+constexpr TypeMatcher kMat3X2Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat3X2(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat3X2(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat3x2<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat3x3'
+constexpr TypeMatcher kMat3X3Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat3X3(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat3X3(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat3x3<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat3x4'
+constexpr TypeMatcher kMat3X4Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat3X4(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat3X4(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat3x4<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat4x2'
+constexpr TypeMatcher kMat4X2Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat4X2(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat4X2(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat4x2<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat4x3'
+constexpr TypeMatcher kMat4X3Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat4X3(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat4X3(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat4x3<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type mat4x4'
+constexpr TypeMatcher kMat4X4Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchMat4X4(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat4X4(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "mat4x4<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type vec'
+constexpr TypeMatcher kVecMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  Number N = Number::invalid;
+  const Type* T = nullptr;
+    if (!MatchVec(state, ty, N, T)) {
+      return nullptr;
+    }
+    N = state.Num(N);
+    if (!N.IsValid()) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildVec(state, ty, N, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string N = state->NumName();
+  const std::string T = state->TypeName();
+    StringStream ss;
+    ss << "vec" << N << "<" << T << ">";
+    return ss.str();
+  }
+};
+
+
+/// TypeMatcher for 'type mat'
+constexpr TypeMatcher kMatMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  Number N = Number::invalid;
+  Number M = Number::invalid;
+  const Type* T = nullptr;
+    if (!MatchMat(state, ty, N, M, T)) {
+      return nullptr;
+    }
+    N = state.Num(N);
+    if (!N.IsValid()) {
+      return nullptr;
+    }
+    M = state.Num(M);
+    if (!M.IsValid()) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildMat(state, ty, N, M, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string N = state->NumName();
+  const std::string M = state->NumName();
+  const std::string T = state->TypeName();
+    StringStream ss;
+    ss << "mat" << N << "x" << M << "<" << T << ">";
+    return ss.str();
+  }
+};
+
+
+/// TypeMatcher for 'type atomic'
+constexpr TypeMatcher kAtomicMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  const Type* T = nullptr;
+    if (!MatchAtomic(state, ty, T)) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    return BuildAtomic(state, ty, T);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string T = state->TypeName();
+    return "atomic<" + T + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type ptr'
+constexpr TypeMatcher kPtrMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+  Number S = Number::invalid;
+  const Type* T = nullptr;
+  Number A = Number::invalid;
+    if (!MatchPtr(state, ty, S, T, A)) {
+      return nullptr;
+    }
+    S = state.Num(S);
+    if (!S.IsValid()) {
+      return nullptr;
+    }
+    T = state.Type(T);
+    if (T == nullptr) {
+      return nullptr;
+    }
+    A = state.Num(A);
+    if (!A.IsValid()) {
+      return nullptr;
+    }
+    return BuildPtr(state, ty, S, T, A);
+  },
+/* string */ [](MatchState* state) -> std::string {
+  const std::string S = state->NumName();
+  const std::string T = state->TypeName();
+  const std::string A = state->NumName();
+    return "ptr<" + S + ", " + T + ", " + A + ">";
+  }
+};
+
+
+/// TypeMatcher for 'type struct_with_runtime_array'
+constexpr TypeMatcher kStructWithRuntimeArrayMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (!MatchStructWithRuntimeArray(state, ty)) {
+      return nullptr;
+    }
+    return BuildStructWithRuntimeArray(state, ty);
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "struct_with_runtime_array";
+  }
+};
+
+
+/// TypeMatcher for 'match f32_f16'
+constexpr TypeMatcher kF32F16Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (MatchF32(state, ty)) {
+      return BuildF32(state, ty);
+    }
+    if (MatchF16(state, ty)) {
+      return BuildF16(state, ty);
+    }
+    return nullptr;
+  },
+/* string */ [](MatchState*) -> std::string {
+    StringStream ss;
+    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+    // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
+    ss << kF32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
+    return ss.str();
+  }
+};
+
+/// TypeMatcher for 'match iu32'
+constexpr TypeMatcher kIu32Matcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (MatchI32(state, ty)) {
+      return BuildI32(state, ty);
+    }
+    if (MatchU32(state, ty)) {
+      return BuildU32(state, ty);
+    }
+    return nullptr;
+  },
+/* string */ [](MatchState*) -> std::string {
+    StringStream ss;
+    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+    // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
+    ss << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
+    return ss.str();
+  }
+};
+
+/// TypeMatcher for 'match scalar'
+constexpr TypeMatcher kScalarMatcher {
+/* match */ [](MatchState& state, const Type* ty) -> const Type* {
+    if (MatchF32(state, ty)) {
+      return BuildF32(state, ty);
+    }
+    if (MatchF16(state, ty)) {
+      return BuildF16(state, ty);
+    }
+    if (MatchI32(state, ty)) {
+      return BuildI32(state, ty);
+    }
+    if (MatchU32(state, ty)) {
+      return BuildU32(state, ty);
+    }
+    if (MatchBool(state, ty)) {
+      return BuildBool(state, ty);
+    }
+    return nullptr;
+  },
+/* string */ [](MatchState*) -> std::string {
+    StringStream ss;
+    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+    // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
+    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
+    return ss.str();
+  }
+};
+
+/// EnumMatcher for 'match read_write'
+constexpr NumberMatcher kReadWriteMatcher {
+/* match */ [](MatchState&, Number number) -> Number {
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::Access::kReadWrite)) {
+      return Number(static_cast<uint32_t>(core::Access::kReadWrite));
+    }
+    return Number::invalid;
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "read_write";
+  }
+};
+
+/// EnumMatcher for 'match storage'
+constexpr NumberMatcher kStorageMatcher {
+/* match */ [](MatchState&, Number number) -> Number {
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::AddressSpace::kStorage)) {
+      return Number(static_cast<uint32_t>(core::AddressSpace::kStorage));
+    }
+    return Number::invalid;
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "storage";
+  }
+};
+
+/// EnumMatcher for 'match workgroup_or_storage'
+constexpr NumberMatcher kWorkgroupOrStorageMatcher {
+/* match */ [](MatchState&, Number number) -> Number {
+    switch (static_cast<core::AddressSpace>(number.Value())) {
+      case core::AddressSpace::kWorkgroup:
+      case core::AddressSpace::kStorage:
+        return number;
+      default:
+        return Number::invalid;
+    }
+  },
+/* string */ [](MatchState*) -> std::string {
+    return "workgroup or storage";
+  }
+};
+
+/// Type and number matchers
+
+/// The template types, types, and type matchers
+constexpr TypeMatcher kTypeMatchers[] = {
+  /* [0] */ TemplateTypeMatcher<0>::matcher,
+  /* [1] */ TemplateTypeMatcher<1>::matcher,
+  /* [2] */ kBoolMatcher,
+  /* [3] */ kF32Matcher,
+  /* [4] */ kF16Matcher,
+  /* [5] */ kI32Matcher,
+  /* [6] */ kU32Matcher,
+  /* [7] */ kVec2Matcher,
+  /* [8] */ kVec3Matcher,
+  /* [9] */ kVec4Matcher,
+  /* [10] */ kMat2X2Matcher,
+  /* [11] */ kMat2X3Matcher,
+  /* [12] */ kMat2X4Matcher,
+  /* [13] */ kMat3X2Matcher,
+  /* [14] */ kMat3X3Matcher,
+  /* [15] */ kMat3X4Matcher,
+  /* [16] */ kMat4X2Matcher,
+  /* [17] */ kMat4X3Matcher,
+  /* [18] */ kMat4X4Matcher,
+  /* [19] */ kVecMatcher,
+  /* [20] */ kMatMatcher,
+  /* [21] */ kAtomicMatcher,
+  /* [22] */ kPtrMatcher,
+  /* [23] */ kStructWithRuntimeArrayMatcher,
+  /* [24] */ kF32F16Matcher,
+  /* [25] */ kIu32Matcher,
+  /* [26] */ kScalarMatcher,
+};
+
+/// The template numbers, and number matchers
+constexpr NumberMatcher kNumberMatchers[] = {
+  /* [0] */ TemplateNumberMatcher<0>::matcher,
+  /* [1] */ TemplateNumberMatcher<1>::matcher,
+  /* [2] */ TemplateNumberMatcher<2>::matcher,
+  /* [3] */ kReadWriteMatcher,
+  /* [4] */ kStorageMatcher,
+  /* [5] */ kWorkgroupOrStorageMatcher,
+};
+
+constexpr TypeMatcherIndex kTypeMatcherIndices[] = {
+  /* [0] */ TypeMatcherIndex(22),
+  /* [1] */ TypeMatcherIndex(21),
+  /* [2] */ TypeMatcherIndex(0),
+  /* [3] */ TypeMatcherIndex(22),
+  /* [4] */ TypeMatcherIndex(23),
+  /* [5] */ TypeMatcherIndex(19),
+  /* [6] */ TypeMatcherIndex(0),
+  /* [7] */ TypeMatcherIndex(20),
+  /* [8] */ TypeMatcherIndex(0),
+  /* [9] */ TypeMatcherIndex(19),
+  /* [10] */ TypeMatcherIndex(2),
+  /* [11] */ TypeMatcherIndex(6),
+  /* [12] */ TypeMatcherIndex(1),
+};
+
+static_assert(TypeMatcherIndex::CanIndex(kTypeMatcherIndices),
+              "TypeMatcherIndex is not large enough to index kTypeMatcherIndices");
+
+constexpr NumberMatcherIndex kNumberMatcherIndices[] = {
+  /* [0] */ NumberMatcherIndex(4),
+  /* [1] */ NumberMatcherIndex(0),
+  /* [2] */ NumberMatcherIndex(3),
+  /* [3] */ NumberMatcherIndex(1),
+  /* [4] */ NumberMatcherIndex(2),
+  /* [5] */ NumberMatcherIndex(0),
+  /* [6] */ NumberMatcherIndex(2),
+  /* [7] */ NumberMatcherIndex(1),
+  /* [8] */ NumberMatcherIndex(0),
+  /* [9] */ NumberMatcherIndex(1),
+};
+
+static_assert(NumberMatcherIndex::CanIndex(kNumberMatcherIndices),
+              "NumberMatcherIndex is not large enough to index kNumberMatcherIndices");
+
+constexpr ParameterInfo kParameters[] = {
+  {
+    /* [0] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(0),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
+  },
+  {
+    /* [1] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [2] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [3] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [4] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [5] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [6] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(0),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
+  },
+  {
+    /* [7] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [8] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(12),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [9] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [10] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(10),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [11] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [12] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [13] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(9),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
+  },
+  {
+    /* [14] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
+  },
+  {
+    /* [15] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
+  },
+  {
+    /* [16] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(7),
+  },
+  {
+    /* [17] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(3),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(0),
+  },
+  {
+    /* [18] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [19] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(5),
+  },
+  {
+    /* [20] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(7),
+  },
+  {
+    /* [21] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
+  },
+  {
+    /* [22] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+  {
+    /* [23] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(8),
+  },
+  {
+    /* [24] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(1),
+  },
+  {
+    /* [25] */
+    /* usage */ core::ParameterUsage::kNone,
+    /* type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+  },
+};
+
+static_assert(ParameterIndex::CanIndex(kParameters),
+              "ParameterIndex is not large enough to index kParameters");
+
+constexpr TemplateTypeInfo kTemplateTypes[] = {
+  {
+    /* [0] */
+    /* name */ "T",
+    /* matcher_index */ TypeMatcherIndex(25),
+  },
+  {
+    /* [1] */
+    /* name */ "U",
+    /* matcher_index */ TypeMatcherIndex(6),
+  },
+  {
+    /* [2] */
+    /* name */ "I",
+    /* matcher_index */ TypeMatcherIndex(6),
+  },
+  {
+    /* [3] */
+    /* name */ "T",
+    /* matcher_index */ TypeMatcherIndex(24),
+  },
+  {
+    /* [4] */
+    /* name */ "T",
+    /* matcher_index */ TypeMatcherIndex(26),
+  },
+};
+
+static_assert(TemplateTypeIndex::CanIndex(kTemplateTypes),
+              "TemplateTypeIndex is not large enough to index kTemplateTypes");
+
+constexpr TemplateNumberInfo kTemplateNumbers[] = {
+  {
+    /* [0] */
+    /* name */ "K",
+    /* matcher_index */ NumberMatcherIndex(/* invalid */),
+  },
+  {
+    /* [1] */
+    /* name */ "C",
+    /* matcher_index */ NumberMatcherIndex(/* invalid */),
+  },
+  {
+    /* [2] */
+    /* name */ "R",
+    /* matcher_index */ NumberMatcherIndex(/* invalid */),
+  },
+  {
+    /* [3] */
+    /* name */ "N",
+    /* matcher_index */ NumberMatcherIndex(/* invalid */),
+  },
+  {
+    /* [4] */
+    /* name */ "M",
+    /* matcher_index */ NumberMatcherIndex(/* invalid */),
+  },
+  {
+    /* [5] */
+    /* name */ "A",
+    /* matcher_index */ NumberMatcherIndex(/* invalid */),
+  },
+  {
+    /* [6] */
+    /* name */ "S",
+    /* matcher_index */ NumberMatcherIndex(5),
+  },
+};
+
+static_assert(TemplateNumberIndex::CanIndex(kTemplateNumbers),
+              "TemplateNumberIndex is not large enough to index kTemplateNumbers");
+
+constexpr OverloadInfo kOverloads[] = {
+  {
+    /* [0] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 3,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 0,
+    /* template_types */ TemplateTypeIndex(4),
+    /* template_numbers */ TemplateNumberIndex(/* invalid */),
+    /* parameters */ ParameterIndex(10),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [1] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 3,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(4),
+    /* template_numbers */ TemplateNumberIndex(3),
+    /* parameters */ ParameterIndex(13),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [2] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(2),
+    /* template_numbers */ TemplateNumberIndex(5),
+    /* parameters */ ParameterIndex(17),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(11),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [3] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 4,
+    /* num_template_types */ 2,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(0),
+    /* template_numbers */ TemplateNumberIndex(6),
+    /* parameters */ ParameterIndex(6),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [4] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 6,
+    /* num_template_types */ 2,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(0),
+    /* template_numbers */ TemplateNumberIndex(6),
+    /* parameters */ ParameterIndex(0),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [5] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 3,
+    /* num_template_types */ 2,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(0),
+    /* template_numbers */ TemplateNumberIndex(6),
+    /* parameters */ ParameterIndex(0),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [6] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 4,
+    /* num_template_types */ 2,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(0),
+    /* template_numbers */ TemplateNumberIndex(6),
+    /* parameters */ ParameterIndex(6),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(/* invalid */),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [7] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(3),
+    /* template_numbers */ TemplateNumberIndex(3),
+    /* parameters */ ParameterIndex(14),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [8] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 3,
+    /* template_types */ TemplateTypeIndex(3),
+    /* template_numbers */ TemplateNumberIndex(0),
+    /* parameters */ ParameterIndex(19),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(3),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [9] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 2,
+    /* template_types */ TemplateTypeIndex(3),
+    /* template_numbers */ TemplateNumberIndex(3),
+    /* parameters */ ParameterIndex(21),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(7),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(8),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [10] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 2,
+    /* template_types */ TemplateTypeIndex(3),
+    /* template_numbers */ TemplateNumberIndex(3),
+    /* parameters */ ParameterIndex(23),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(3),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [11] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 2,
+    /* template_types */ TemplateTypeIndex(3),
+    /* template_numbers */ TemplateNumberIndex(3),
+    /* parameters */ ParameterIndex(15),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(3),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+  {
+    /* [12] */
+    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
+    /* num_parameters */ 2,
+    /* num_template_types */ 1,
+    /* num_template_numbers */ 1,
+    /* template_types */ TemplateTypeIndex(3),
+    /* template_numbers */ TemplateNumberIndex(3),
+    /* parameters */ ParameterIndex(24),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(5),
+    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
+    /* const_eval_fn */ ConstEvalFunctionIndex(/* invalid */),
+  },
+};
+
+static_assert(OverloadIndex::CanIndex(kOverloads),
+              "OverloadIndex is not large enough to index kOverloads");
+
+constexpr IntrinsicInfo kBuiltins[] = {
+  {
+    /* [0] */
+    /* fn array_length<I : u32, A : access>(ptr<storage, struct_with_runtime_array, A>, I) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(2),
+  },
+  {
+    /* [1] */
+    /* fn atomic_and<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [2] */
+    /* fn atomic_compare_exchange<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, U, T, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(4),
+  },
+  {
+    /* [3] */
+    /* fn atomic_exchange<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [4] */
+    /* fn atomic_iadd<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [5] */
+    /* fn atomic_isub<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [6] */
+    /* fn atomic_load<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(5),
+  },
+  {
+    /* [7] */
+    /* fn atomic_or<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [8] */
+    /* fn atomic_smax<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [9] */
+    /* fn atomic_smin<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [10] */
+    /* fn atomic_store<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(6),
+  },
+  {
+    /* [11] */
+    /* fn atomic_umax<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [12] */
+    /* fn atomic_umin<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [13] */
+    /* fn atomic_xor<T : iu32, U : u32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(3),
+  },
+  {
+    /* [14] */
+    /* fn dot<N : num, T : f32_f16>(vec<N, T>, vec<N, T>) -> T */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(7),
+  },
+  {
+    /* [15] */
+    /* fn matrix_times_matrix<T : f32_f16, K : num, C : num, R : num>(mat<K, R, T>, mat<C, K, T>) -> mat<C, R, T> */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(8),
+  },
+  {
+    /* [16] */
+    /* fn matrix_times_scalar<T : f32_f16, N : num, M : num>(mat<N, M, T>, T) -> mat<N, M, T> */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(9),
+  },
+  {
+    /* [17] */
+    /* fn matrix_times_vector<T : f32_f16, N : num, M : num>(mat<N, M, T>, vec<N, T>) -> vec<M, T> */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(10),
+  },
+  {
+    /* [18] */
+    /* fn vector_times_matrix<T : f32_f16, N : num, M : num>(vec<N, T>, mat<M, N, T>) -> vec<M, T> */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(11),
+  },
+  {
+    /* [19] */
+    /* fn select<T : scalar>(bool, T, T) -> T */
+    /* fn select<N : num, T : scalar>(vec<N, bool>, vec<N, T>, vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ OverloadIndex(0),
+  },
+  {
+    /* [20] */
+    /* fn vector_times_scalar<T : f32_f16, N : num>(vec<N, T>, T) -> vec<N, T> */
+    /* num overloads */ 1,
+    /* overloads */ OverloadIndex(12),
+  },
+};
+
+// clang-format on
+
+}  // anonymous namespace
+
+const core::intrinsic::TableData kData{
+    /* template_types */ kTemplateTypes,
+    /* template_numbers */ kTemplateNumbers,
+    /* type_matcher_indices */ kTypeMatcherIndices,
+    /* number_matcher_indices */ kNumberMatcherIndices,
+    /* type_matchers */ kTypeMatchers,
+    /* number_matchers */ kNumberMatchers,
+    /* parameters */ kParameters,
+    /* overloads */ kOverloads,
+    /* const_eval_functions */ Empty,
+    /* ctor_conv */ Empty,
+    /* builtins */ kBuiltins,
+    /* binary '+' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '-' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '*' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '/' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '%' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '^' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '&' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '|' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '&&' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '||' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '==' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '!=' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '<' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '>' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '<=' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '>=' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '<<' */ tint::core::intrinsic::kNoOverloads,
+    /* binary '>>' */ tint::core::intrinsic::kNoOverloads,
+    /* unary '!' */ tint::core::intrinsic::kNoOverloads,
+    /* unary '~' */ tint::core::intrinsic::kNoOverloads,
+    /* unary '-' */ tint::core::intrinsic::kNoOverloads,
+};
+
+}  // namespace tint::spirv::intrinsic::data
diff --git a/src/tint/lang/spirv/intrinsic/data/data.cc.tmpl b/src/tint/lang/spirv/intrinsic/data/data.cc.tmpl
new file mode 100644
index 0000000..8130aea
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/data.cc.tmpl
@@ -0,0 +1,35 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate intrinsic_table.inl
+Used by BuiltinTable.cc for builtin overload resolution.
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/utils/templates/intrinsic_table_data.tmpl.inc" -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/spirv/spirv.def" -}}
+
+#include <limits>
+#include <string>
+
+#include "src/tint/lang/core/intrinsic/data/type_matchers.h"
+#include "src/tint/lang/spirv/intrinsic/data/type_matchers.h"
+#include "src/tint/lang/spirv/intrinsic/data/data.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace tint::spirv::intrinsic::data {
+
+using namespace tint::core::intrinsic::data;  // NOLINT(build/namespaces)
+
+{{ Eval "Data"
+  "Intrinsics" $I
+  "Name"       "kData" -}}
+
+}  // namespace tint::spirv::intrinsic::data
diff --git a/src/tint/lang/spirv/intrinsic/data/data.h b/src/tint/lang/spirv/intrinsic/data/data.h
new file mode 100644
index 0000000..60b0c2a
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/data.h
@@ -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.
+
+#ifndef SRC_TINT_LANG_SPIRV_INTRINSIC_DATA_DATA_H_
+#define SRC_TINT_LANG_SPIRV_INTRINSIC_DATA_DATA_H_
+
+#include "src/tint/lang/core/intrinsic/table_data.h"
+
+namespace tint::spirv::intrinsic::data {
+
+extern const core::intrinsic::TableData kData;
+
+}  // namespace tint::spirv::intrinsic::data
+
+#endif  // SRC_TINT_LANG_SPIRV_INTRINSIC_DATA_DATA_H_
diff --git a/src/tint/lang/spirv/intrinsic/data/type_matchers.h b/src/tint/lang/spirv/intrinsic/data/type_matchers.h
new file mode 100644
index 0000000..63c99cd
--- /dev/null
+++ b/src/tint/lang/spirv/intrinsic/data/type_matchers.h
@@ -0,0 +1,45 @@
+// 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_INTRINSIC_DATA_TYPE_MATCHERS_H_
+#define SRC_TINT_LANG_SPIRV_INTRINSIC_DATA_TYPE_MATCHERS_H_
+
+#include "src/tint/lang/core/intrinsic/table.h"
+#include "src/tint/lang/core/type/array.h"
+#include "src/tint/lang/core/type/struct.h"
+
+namespace tint::spirv::intrinsic::data {
+
+inline bool MatchStructWithRuntimeArray(core::intrinsic::MatchState&, const core::type::Type* ty) {
+    if (auto* str = ty->As<core::type::Struct>()) {
+        if (str->Members().IsEmpty()) {
+            return false;
+        }
+        if (auto* ary = str->Members().Back()->Type()->As<core::type::Array>()) {
+            if (ary->Count()->Is<core::type::RuntimeArrayCount>()) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+inline const core::type::Type* BuildStructWithRuntimeArray(core::intrinsic::MatchState&,
+                                                           const core::type::Type* ty) {
+    return ty;
+}
+
+}  // namespace tint::spirv::intrinsic::data
+
+#endif  // SRC_TINT_LANG_SPIRV_INTRINSIC_DATA_TYPE_MATCHERS_H_
diff --git a/src/tint/lang/spirv/ir/BUILD.bazel b/src/tint/lang/spirv/ir/BUILD.bazel
new file mode 100644
index 0000000..6af9b2c
--- /dev/null
+++ b/src/tint/lang/spirv/ir/BUILD.bazel
@@ -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.
+
+################################################################################
+# 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 = "ir",
+  srcs = [
+    "builtin_call.cc",
+    "function.cc",
+    "intrinsic.cc",
+    "intrinsic_call.cc",
+  ],
+  hdrs = [
+    "builtin_call.h",
+    "function.h",
+    "intrinsic.h",
+    "intrinsic_call.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/intrinsic/data",
+    "//src/tint/utils/containers",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/spirv/ir/BUILD.cmake b/src/tint/lang/spirv/ir/BUILD.cmake
new file mode 100644
index 0000000..87563f85
--- /dev/null
+++ b/src/tint/lang/spirv/ir/BUILD.cmake
@@ -0,0 +1,55 @@
+# 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
+################################################################################
+
+################################################################################
+# Target:    tint_lang_spirv_ir
+# Kind:      lib
+################################################################################
+tint_add_target(tint_lang_spirv_ir lib
+  lang/spirv/ir/builtin_call.cc
+  lang/spirv/ir/builtin_call.h
+  lang/spirv/ir/function.cc
+  lang/spirv/ir/function.h
+  lang/spirv/ir/intrinsic.cc
+  lang/spirv/ir/intrinsic.h
+  lang/spirv/ir/intrinsic_call.cc
+  lang/spirv/ir/intrinsic_call.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_ir lib
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_lang_spirv_intrinsic_data
+  tint_utils_containers
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
diff --git a/src/tint/lang/spirv/ir/BUILD.gn b/src/tint/lang/spirv/ir/BUILD.gn
new file mode 100644
index 0000000..97f694d
--- /dev/null
+++ b/src/tint/lang/spirv/ir/BUILD.gn
@@ -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.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")
+
+libtint_source_set("ir") {
+  sources = [
+    "builtin_call.cc",
+    "builtin_call.h",
+    "function.cc",
+    "function.h",
+    "intrinsic.cc",
+    "intrinsic.h",
+    "intrinsic_call.cc",
+    "intrinsic_call.h",
+  ]
+  deps = [
+    "${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/spirv/intrinsic/data",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
diff --git a/src/tint/lang/spirv/ir/builtin_call.cc b/src/tint/lang/spirv/ir/builtin_call.cc
new file mode 100644
index 0000000..9cf3f08
--- /dev/null
+++ b/src/tint/lang/spirv/ir/builtin_call.cc
@@ -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.
+
+#include "src/tint/lang/spirv/ir/builtin_call.h"
+
+#include <utility>
+
+#include "src/tint/utils/ice/ice.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::ir::BuiltinCall);
+
+namespace tint::spirv::ir {
+
+BuiltinCall::BuiltinCall(core::ir::InstructionResult* result,
+                         Function func,
+                         VectorRef<core::ir::Value*> arguments)
+    : Base(result, arguments), func_(func) {
+    TINT_ASSERT(func != Function::kNone);
+}
+
+BuiltinCall::~BuiltinCall() = default;
+
+}  // namespace tint::spirv::ir
diff --git a/src/tint/lang/spirv/ir/builtin_call.h b/src/tint/lang/spirv/ir/builtin_call.h
new file mode 100644
index 0000000..5380dbe
--- /dev/null
+++ b/src/tint/lang/spirv/ir/builtin_call.h
@@ -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.
+
+#ifndef SRC_TINT_LANG_SPIRV_IR_BUILTIN_CALL_H_
+#define SRC_TINT_LANG_SPIRV_IR_BUILTIN_CALL_H_
+
+#include <string>
+
+#include "src/tint/lang/core/intrinsic/table_data.h"
+#include "src/tint/lang/core/ir/builtin_call.h"
+#include "src/tint/lang/spirv/intrinsic/data/data.h"
+#include "src/tint/lang/spirv/ir/function.h"
+#include "src/tint/utils/rtti/castable.h"
+
+namespace tint::spirv::ir {
+
+/// A spirv builtin call instruction in the IR.
+class BuiltinCall : public Castable<BuiltinCall, core::ir::BuiltinCall> {
+  public:
+    /// Constructor
+    /// @param result the result value
+    /// @param func the builtin function
+    /// @param args the conversion arguments
+    BuiltinCall(core::ir::InstructionResult* result,
+                Function func,
+                VectorRef<core::ir::Value*> args = tint::Empty);
+    ~BuiltinCall() override;
+
+    /// @returns the builtin function
+    Function Func() { return func_; }
+
+    /// @returns the identifier for the function
+    size_t FuncId() override { return static_cast<size_t>(func_); }
+
+    /// @returns the friendly name for the instruction
+    std::string FriendlyName() override { return str(func_); }
+
+    /// @returns the intrinsic name
+    const char* IntrinsicName() override { return str(func_); }
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() override { return spirv::intrinsic::data::kData; }
+
+  private:
+    Function func_;
+};
+
+}  // namespace tint::spirv::ir
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_BUILTIN_CALL_H_
diff --git a/src/tint/lang/spirv/ir/function.cc b/src/tint/lang/spirv/ir/function.cc
new file mode 100644
index 0000000..1c30eed
--- /dev/null
+++ b/src/tint/lang/spirv/ir/function.cc
@@ -0,0 +1,78 @@
+// 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:
+//   src/tint/lang/spirv/ir/function.cc.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/lang/spirv/ir/function.h"
+
+namespace tint::spirv::ir {
+
+const char* str(Function i) {
+    switch (i) {
+        case Function::kNone:
+            return "<none>";
+        case Function::kArrayLength:
+            return "spirv.array_length";
+        case Function::kAtomicAnd:
+            return "spirv.atomic_and";
+        case Function::kAtomicCompareExchange:
+            return "spirv.atomic_compare_exchange";
+        case Function::kAtomicExchange:
+            return "spirv.atomic_exchange";
+        case Function::kAtomicIadd:
+            return "spirv.atomic_iadd";
+        case Function::kAtomicIsub:
+            return "spirv.atomic_isub";
+        case Function::kAtomicLoad:
+            return "spirv.atomic_load";
+        case Function::kAtomicOr:
+            return "spirv.atomic_or";
+        case Function::kAtomicSmax:
+            return "spirv.atomic_smax";
+        case Function::kAtomicSmin:
+            return "spirv.atomic_smin";
+        case Function::kAtomicStore:
+            return "spirv.atomic_store";
+        case Function::kAtomicUmax:
+            return "spirv.atomic_umax";
+        case Function::kAtomicUmin:
+            return "spirv.atomic_umin";
+        case Function::kAtomicXor:
+            return "spirv.atomic_xor";
+        case Function::kDot:
+            return "spirv.dot";
+        case Function::kMatrixTimesMatrix:
+            return "spirv.matrix_times_matrix";
+        case Function::kMatrixTimesScalar:
+            return "spirv.matrix_times_scalar";
+        case Function::kMatrixTimesVector:
+            return "spirv.matrix_times_vector";
+        case Function::kVectorTimesMatrix:
+            return "spirv.vector_times_matrix";
+        case Function::kSelect:
+            return "spirv.select";
+        case Function::kVectorTimesScalar:
+            return "spirv.vector_times_scalar";
+    }
+    return "<unknown>";
+}
+
+}  // namespace tint::spirv::ir
diff --git a/src/tint/lang/spirv/ir/function.cc.tmpl b/src/tint/lang/spirv/ir/function.cc.tmpl
new file mode 100644
index 0000000..f745c2a
--- /dev/null
+++ b/src/tint/lang/spirv/ir/function.cc.tmpl
@@ -0,0 +1,31 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate function.cc
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/spirv/spirv.def" -}}
+#include "src/tint/lang/spirv/ir/function.h"
+
+namespace tint::spirv::ir {
+
+const char* str(Function i) {
+    switch (i) {
+        case Function::kNone:
+            return "<none>";
+{{- range $I.Sem.Builtins  }}
+        case Function::k{{PascalCase .Name}}:
+            return "spirv.{{.Name}}";
+{{- end  }}
+    }
+    return "<unknown>";
+}
+
+}  // namespace tint::spirv::ir
diff --git a/src/tint/lang/spirv/ir/function.h b/src/tint/lang/spirv/ir/function.h
new file mode 100644
index 0000000..1ac09f8
--- /dev/null
+++ b/src/tint/lang/spirv/ir/function.h
@@ -0,0 +1,75 @@
+// 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:
+//   src/tint/lang/spirv/ir/function.h.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef SRC_TINT_LANG_SPIRV_IR_FUNCTION_H_
+#define SRC_TINT_LANG_SPIRV_IR_FUNCTION_H_
+
+#include <cstdint>
+#include <string>
+
+#include "src/tint/utils/traits/traits.h"
+
+// \cond DO_NOT_DOCUMENT
+namespace tint::spirv::ir {
+
+/// Enumerator of all builtin functions
+enum class Function : uint8_t {
+    kArrayLength,
+    kAtomicAnd,
+    kAtomicCompareExchange,
+    kAtomicExchange,
+    kAtomicIadd,
+    kAtomicIsub,
+    kAtomicLoad,
+    kAtomicOr,
+    kAtomicSmax,
+    kAtomicSmin,
+    kAtomicStore,
+    kAtomicUmax,
+    kAtomicUmin,
+    kAtomicXor,
+    kDot,
+    kMatrixTimesMatrix,
+    kMatrixTimesScalar,
+    kMatrixTimesVector,
+    kVectorTimesMatrix,
+    kSelect,
+    kVectorTimesScalar,
+    kNone,
+};
+
+/// @returns the name of the builtin function type. The spelling, including
+/// case, matches the name in the WGSL spec.
+const char* str(Function i);
+
+/// Emits the name of the builtin function type. The spelling, including case,
+/// matches the name in the WGSL spec.
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& o, Function i) {
+    return o << str(i);
+}
+
+}  // namespace tint::spirv::ir
+// \endcond
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_FUNCTION_H_
diff --git a/src/tint/lang/spirv/ir/function.h.tmpl b/src/tint/lang/spirv/ir/function.h.tmpl
new file mode 100644
index 0000000..987568d
--- /dev/null
+++ b/src/tint/lang/spirv/ir/function.h.tmpl
@@ -0,0 +1,49 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate function.h
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/spirv/spirv.def" -}}
+
+#ifndef SRC_TINT_LANG_SPIRV_IR_FUNCTION_H_
+#define SRC_TINT_LANG_SPIRV_IR_FUNCTION_H_
+
+#include <cstdint>
+#include <string>
+
+#include "src/tint/utils/traits/traits.h"
+
+// \cond DO_NOT_DOCUMENT
+namespace tint::spirv::ir {
+
+/// Enumerator of all builtin functions
+enum class Function : uint8_t {
+{{- range $I.Sem.Builtins }}
+    k{{PascalCase .Name}},
+{{- end }}
+    kNone,
+};
+
+/// @returns the name of the builtin function type. The spelling, including
+/// case, matches the name in the WGSL spec.
+const char* str(Function i);
+
+/// Emits the name of the builtin function type. The spelling, including case,
+/// matches the name in the WGSL spec.
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& o, Function i) {
+  return o << str(i);
+}
+
+}  // namespace tint::spirv::ir
+// \endcond
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_FUNCTION_H_
diff --git a/src/tint/lang/spirv/ir/intrinsic.cc b/src/tint/lang/spirv/ir/intrinsic.cc
new file mode 100644
index 0000000..ed401f7
--- /dev/null
+++ b/src/tint/lang/spirv/ir/intrinsic.cc
@@ -0,0 +1,103 @@
+// 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:
+//   src/tint/lang/spirv/ir/intrinsic.cc.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#include "src/tint/lang/spirv/ir/intrinsic.h"
+
+namespace tint::spirv::ir {
+
+/// ParseIntrinsic parses a Intrinsic from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or Intrinsic::kUndefined if the string could not be parsed.
+Intrinsic ParseIntrinsic(std::string_view str) {
+    if (str == "image_dref_gather") {
+        return Intrinsic::kImageDrefGather;
+    }
+    if (str == "image_fetch") {
+        return Intrinsic::kImageFetch;
+    }
+    if (str == "image_gather") {
+        return Intrinsic::kImageGather;
+    }
+    if (str == "image_query_size") {
+        return Intrinsic::kImageQuerySize;
+    }
+    if (str == "image_query_size_lod") {
+        return Intrinsic::kImageQuerySizeLod;
+    }
+    if (str == "image_read") {
+        return Intrinsic::kImageRead;
+    }
+    if (str == "image_sample_dref_explicit_lod") {
+        return Intrinsic::kImageSampleDrefExplicitLod;
+    }
+    if (str == "image_sample_dref_implicit_lod") {
+        return Intrinsic::kImageSampleDrefImplicitLod;
+    }
+    if (str == "image_sample_explicit_lod") {
+        return Intrinsic::kImageSampleExplicitLod;
+    }
+    if (str == "image_sample_implicit_lod") {
+        return Intrinsic::kImageSampleImplicitLod;
+    }
+    if (str == "image_write") {
+        return Intrinsic::kImageWrite;
+    }
+    if (str == "sampled_image") {
+        return Intrinsic::kSampledImage;
+    }
+    return Intrinsic::kUndefined;
+}
+
+std::string_view ToString(Intrinsic value) {
+    switch (value) {
+        case Intrinsic::kUndefined:
+            return "undefined";
+        case Intrinsic::kImageDrefGather:
+            return "image_dref_gather";
+        case Intrinsic::kImageFetch:
+            return "image_fetch";
+        case Intrinsic::kImageGather:
+            return "image_gather";
+        case Intrinsic::kImageQuerySize:
+            return "image_query_size";
+        case Intrinsic::kImageQuerySizeLod:
+            return "image_query_size_lod";
+        case Intrinsic::kImageRead:
+            return "image_read";
+        case Intrinsic::kImageSampleDrefExplicitLod:
+            return "image_sample_dref_explicit_lod";
+        case Intrinsic::kImageSampleDrefImplicitLod:
+            return "image_sample_dref_implicit_lod";
+        case Intrinsic::kImageSampleExplicitLod:
+            return "image_sample_explicit_lod";
+        case Intrinsic::kImageSampleImplicitLod:
+            return "image_sample_implicit_lod";
+        case Intrinsic::kImageWrite:
+            return "image_write";
+        case Intrinsic::kSampledImage:
+            return "sampled_image";
+    }
+    return "<unknown>";
+}
+
+}  // namespace tint::spirv::ir
diff --git a/src/tint/lang/spirv/ir/intrinsic.cc.tmpl b/src/tint/lang/spirv/ir/intrinsic.cc.tmpl
new file mode 100644
index 0000000..4928c77
--- /dev/null
+++ b/src/tint/lang/spirv/ir/intrinsic.cc.tmpl
@@ -0,0 +1,27 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate intrinsic.cc
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/spirv/spirv.def" -}}
+{{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
+{{- $enum := ($I.Sem.Enum "intrinsic") -}}
+
+#include "src/tint/lang/spirv/ir/intrinsic.h"
+
+namespace tint::spirv::ir {
+
+{{ Eval "ParseEnum" $enum}}
+
+{{ Eval "EnumOStream" $enum}}
+
+}  // namespace tint::spirv::ir
+
diff --git a/src/tint/lang/spirv/ir/intrinsic.h b/src/tint/lang/spirv/ir/intrinsic.h
new file mode 100644
index 0000000..0f49bf9
--- /dev/null
+++ b/src/tint/lang/spirv/ir/intrinsic.h
@@ -0,0 +1,85 @@
+// 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:
+//   src/tint/lang/spirv/ir/intrinsic.h.tmpl
+//
+// To regenerate run: './tools/run gen'
+//
+//                       Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef SRC_TINT_LANG_SPIRV_IR_INTRINSIC_H_
+#define SRC_TINT_LANG_SPIRV_IR_INTRINSIC_H_
+
+#include <cstdint>
+#include <string>
+
+#include "src/tint/utils/traits/traits.h"
+
+namespace tint::spirv::ir {
+
+/// Intrinsic
+enum class Intrinsic : uint8_t {
+    kUndefined,
+    kImageDrefGather,
+    kImageFetch,
+    kImageGather,
+    kImageQuerySize,
+    kImageQuerySizeLod,
+    kImageRead,
+    kImageSampleDrefExplicitLod,
+    kImageSampleDrefImplicitLod,
+    kImageSampleExplicitLod,
+    kImageSampleImplicitLod,
+    kImageWrite,
+    kSampledImage,
+};
+
+/// @param value the enum value
+/// @returns the string for the given enum value
+std::string_view ToString(Intrinsic value);
+
+/// @param out the stream to write to
+/// @param value the Intrinsic
+/// @returns @p out so calls can be chained
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& out, Intrinsic value) {
+    return out << ToString(value);
+}
+
+/// ParseIntrinsic parses a Intrinsic from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or Intrinsic::kUndefined if the string could not be parsed.
+Intrinsic ParseIntrinsic(std::string_view str);
+
+constexpr const char* kIntrinsicStrings[] = {
+    "image_dref_gather",
+    "image_fetch",
+    "image_gather",
+    "image_query_size",
+    "image_query_size_lod",
+    "image_read",
+    "image_sample_dref_explicit_lod",
+    "image_sample_dref_implicit_lod",
+    "image_sample_explicit_lod",
+    "image_sample_implicit_lod",
+    "image_write",
+    "sampled_image",
+};
+
+}  // namespace tint::spirv::ir
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_INTRINSIC_H_
diff --git a/src/tint/lang/spirv/ir/intrinsic.h.tmpl b/src/tint/lang/spirv/ir/intrinsic.h.tmpl
new file mode 100644
index 0000000..6bd3e67
--- /dev/null
+++ b/src/tint/lang/spirv/ir/intrinsic.h.tmpl
@@ -0,0 +1,34 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate intrinsics.h
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- $I := LoadIntrinsics "src/tint/lang/spirv/spirv.def" -}}
+{{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
+{{- $enum := ($I.Sem.Enum "intrinsic") -}}
+
+#ifndef SRC_TINT_LANG_SPIRV_IR_INTRINSIC_H_
+#define SRC_TINT_LANG_SPIRV_IR_INTRINSIC_H_
+
+#include <cstdint>
+#include <string>
+
+#include "src/tint/utils/traits/traits.h"
+
+namespace tint::spirv::ir {
+
+/// Intrinsic
+{{ Eval "DeclareEnum" $enum }}
+
+}  // namespace tint::spirv::ir
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_INTRINSIC_H_
+
diff --git a/src/tint/lang/spirv/ir/intrinsic_call.cc b/src/tint/lang/spirv/ir/intrinsic_call.cc
new file mode 100644
index 0000000..5f2f1ba
--- /dev/null
+++ b/src/tint/lang/spirv/ir/intrinsic_call.cc
@@ -0,0 +1,30 @@
+// 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/ir/intrinsic_call.h"
+
+#include <utility>
+
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::ir::IntrinsicCall);
+
+namespace tint::spirv::ir {
+
+IntrinsicCall::IntrinsicCall(core::ir::InstructionResult* result,
+                             Intrinsic intrinsic,
+                             VectorRef<core::ir::Value*> arguments)
+    : Base(result, arguments), intrinsic_(intrinsic) {}
+
+IntrinsicCall::~IntrinsicCall() = default;
+
+}  // namespace tint::spirv::ir
diff --git a/src/tint/lang/spirv/ir/intrinsic_call.h b/src/tint/lang/spirv/ir/intrinsic_call.h
new file mode 100644
index 0000000..d7c2d43
--- /dev/null
+++ b/src/tint/lang/spirv/ir/intrinsic_call.h
@@ -0,0 +1,50 @@
+// 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_INTRINSIC_CALL_H_
+#define SRC_TINT_LANG_SPIRV_IR_INTRINSIC_CALL_H_
+
+#include <string>
+
+#include "src/tint/lang/core/ir/intrinsic_call.h"
+#include "src/tint/lang/spirv/ir/intrinsic.h"
+#include "src/tint/utils/rtti/castable.h"
+
+namespace tint::spirv::ir {
+
+/// A spir-v intrinsic call instruction in the IR.
+class IntrinsicCall : public Castable<IntrinsicCall, core::ir::IntrinsicCall> {
+  public:
+    /// Constructor
+    /// @param result the result value
+    /// @param intrinsic the kind of intrinsic
+    /// @param args the intrinsic call arguments
+    IntrinsicCall(core::ir::InstructionResult* result,
+                  Intrinsic intrinsic,
+                  VectorRef<core::ir::Value*> args = tint::Empty);
+    ~IntrinsicCall() override;
+
+    /// @returns the kind of the intrinsic
+    Intrinsic Kind() const { return intrinsic_; }
+
+    /// @returns the friendly name for the instruction
+    std::string FriendlyName() override { return "spirv." + std::string(ToString(intrinsic_)); }
+
+  private:
+    Intrinsic intrinsic_ = Intrinsic::kUndefined;
+};
+
+}  // namespace tint::spirv::ir
+
+#endif  // SRC_TINT_LANG_SPIRV_IR_INTRINSIC_CALL_H_
diff --git a/src/tint/lang/spirv/reader/BUILD.bazel b/src/tint/lang/spirv/reader/BUILD.bazel
new file mode 100644
index 0000000..0c153ea
--- /dev/null
+++ b/src/tint/lang/spirv/reader/BUILD.bazel
@@ -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.
+
+################################################################################
+# 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 = "reader",
+  srcs = [
+    "reader.cc",
+  ],
+  hdrs = [
+    "reader.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/reader/common",
+    "//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",
+  ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/ast_parser",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
diff --git a/src/tint/lang/spirv/reader/BUILD.cmake b/src/tint/lang/spirv/reader/BUILD.cmake
index 0ac253d..282235d 100644
--- a/src/tint/lang/spirv/reader/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/BUILD.cmake
@@ -21,6 +21,7 @@
 #                       Do not modify this file directly
 ################################################################################
 
+include(lang/spirv/reader/ast_lower/BUILD.cmake)
 include(lang/spirv/reader/ast_parser/BUILD.cmake)
 include(lang/spirv/reader/common/BUILD.cmake)
 
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
new file mode 100644
index 0000000..5f0918e
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
@@ -0,0 +1,118 @@
+# 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 = "ast_lower",
+  srcs = [
+    "atomics.cc",
+    "decompose_strided_array.cc",
+    "decompose_strided_matrix.cc",
+    "fold_trivial_lets.cc",
+  ],
+  hdrs = [
+    "atomics.h",
+    "decompose_strided_array.h",
+    "decompose_strided_matrix.h",
+    "fold_trivial_lets.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "atomics_test.cc",
+    "decompose_strided_array_test.cc",
+    "decompose_strided_matrix_test.cc",
+    "fold_trivial_lets_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/ast_lower",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg b/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg
new file mode 100644
index 0000000..a460fd5
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_spv_reader"
+}
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
new file mode 100644
index 0000000..29a4202
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
@@ -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.
+
+################################################################################
+# 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_READER)
+################################################################################
+# Target:    tint_lang_spirv_reader_ast_lower
+# Kind:      lib
+# Condition: TINT_BUILD_SPV_READER
+################################################################################
+tint_add_target(tint_lang_spirv_reader_ast_lower lib
+  lang/spirv/reader/ast_lower/atomics.cc
+  lang/spirv/reader/ast_lower/atomics.h
+  lang/spirv/reader/ast_lower/decompose_strided_array.cc
+  lang/spirv/reader/ast_lower/decompose_strided_array.h
+  lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
+  lang/spirv/reader/ast_lower/decompose_strided_matrix.h
+  lang/spirv/reader/ast_lower/fold_trivial_lets.cc
+  lang/spirv/reader/ast_lower/fold_trivial_lets.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_program
+  tint_lang_wgsl_resolver
+  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
+)
+
+endif(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_SPV_READER)
+################################################################################
+# Target:    tint_lang_spirv_reader_ast_lower_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_READER
+################################################################################
+tint_add_target(tint_lang_spirv_reader_ast_lower_test test
+  lang/spirv/reader/ast_lower/atomics_test.cc
+  lang/spirv/reader/ast_lower/decompose_strided_array_test.cc
+  lang/spirv/reader/ast_lower/decompose_strided_matrix_test.cc
+  lang/spirv/reader/ast_lower/fold_trivial_lets_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  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
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_reader_ast_lower_test test
+  "gtest"
+)
+
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower_test test
+    tint_lang_spirv_reader_ast_lower
+  )
+endif(TINT_BUILD_SPV_READER)
+
+endif(TINT_BUILD_SPV_READER)
\ 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
new file mode 100644
index 0000000..730ff27
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
@@ -0,0 +1,114 @@
+# 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_unittests) {
+  import("//testing/test.gni")
+}
+if (tint_build_spv_reader) {
+  libtint_source_set("ast_lower") {
+    sources = [
+      "atomics.cc",
+      "atomics.h",
+      "decompose_strided_array.cc",
+      "decompose_strided_array.h",
+      "decompose_strided_matrix.cc",
+      "decompose_strided_matrix.h",
+      "fold_trivial_lets.cc",
+      "fold_trivial_lets.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/ast",
+      "${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_unittests) {
+  if (tint_build_spv_reader) {
+    tint_unittests_source_set("unittests") {
+      testonly = true
+      sources = [
+        "atomics_test.cc",
+        "decompose_strided_array_test.cc",
+        "decompose_strided_matrix_test.cc",
+        "fold_trivial_lets_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/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",
+        "${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/ast_lower" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc b/src/tint/lang/spirv/reader/ast_lower/atomics.cc
similarity index 88%
rename from src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
rename to src/tint/lang/spirv/reader/ast_lower/atomics.cc
index 4074bac..0d3b3fc 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/atomics.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/spirv_atomic.h"
+#include "src/tint/lang/spirv/reader/ast_lower/atomics.h"
 
 #include <string>
 #include <unordered_map>
@@ -37,13 +37,13 @@
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::SpirvAtomic);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::SpirvAtomic::Stub);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::Atomics);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::Atomics::Stub);
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 
 /// PIMPL state for the transform
-struct SpirvAtomic::State {
+struct Atomics::State {
   private:
     /// A struct that has been forked because a subset of members were made atomic.
     struct ForkedStruct {
@@ -74,7 +74,7 @@
         // Look for stub functions generated by the SPIR-V reader, which are used as placeholders
         // for atomic builtin calls.
         for (auto* fn : ctx.src->AST().Functions()) {
-            if (auto* stub = GetAttribute<Stub>(fn->attributes)) {
+            if (auto* stub = ast::GetAttribute<Stub>(fn->attributes)) {
                 auto* sem = ctx.src->Sem().Get(fn);
 
                 for (auto* call : sem->CallSites()) {
@@ -125,14 +125,14 @@
 
         // If we need to change structure members, then fork them.
         if (!forked_structs.empty()) {
-            ctx.ReplaceAll([&](const Struct* str) {
+            ctx.ReplaceAll([&](const ast::Struct* str) {
                 // Is `str` a structure we need to fork?
                 auto* str_ty = ctx.src->Sem().Get(str);
                 if (auto it = forked_structs.find(str_ty); it != forked_structs.end()) {
                     const auto& forked = it->second;
 
                     // Re-create the structure swapping in the atomic-flavoured members
-                    tint::Vector<const StructMember*, 8> members;
+                    Vector<const ast::StructMember*, 8> members;
                     members.Reserve(str->members.Length());
                     for (size_t i = 0; i < str->members.Length(); i++) {
                         auto* member = str->members[i];
@@ -191,14 +191,14 @@
                     atomic_expressions.Add(index->Object());
                 },
                 [&](const sem::ValueExpression* e) {
-                    if (auto* unary = e->Declaration()->As<UnaryOpExpression>()) {
+                    if (auto* unary = e->Declaration()->As<ast::UnaryOpExpression>()) {
                         atomic_expressions.Add(ctx.src->Sem().GetVal(unary->expr));
                     }
                 });
         }
     }
 
-    Type AtomicTypeFor(const core::type::Type* ty) {
+    ast::Type AtomicTypeFor(const core::type::Type* ty) {
         return Switch(
             ty,  //
             [&](const core::type::I32*) { return b.ty.atomic(CreateASTTypeFor(ctx, ty)); },
@@ -212,7 +212,7 @@
                 if (!count) {
                     ctx.dst->Diagnostics().add_error(
                         diag::System::Transform,
-                        "the SpirvAtomic transform does not currently support array counts that "
+                        "the Atomics transform does not currently support array counts that "
                         "use override values");
                     count = 1;
                 }
@@ -225,7 +225,7 @@
             [&](const core::type::Reference* ref) { return AtomicTypeFor(ref->StoreType()); },
             [&](Default) {
                 TINT_ICE() << "unhandled type: " << ty->FriendlyName();
-                return Type{};
+                return ast::Type{};
             });
     }
 
@@ -253,7 +253,7 @@
             for (auto* vu : atomic_var->Users()) {
                 Switch(
                     vu->Stmt()->Declaration(),
-                    [&](const AssignmentStatement* assign) {
+                    [&](const ast::AssignmentStatement* assign) {
                         auto* sem_lhs = ctx.src->Sem().GetVal(assign->lhs);
                         if (is_ref_to_atomic_var(sem_lhs)) {
                             ctx.Replace(assign, [=] {
@@ -276,7 +276,7 @@
                             return;
                         }
                     },
-                    [&](const VariableDeclStatement* decl) {
+                    [&](const ast::VariableDeclStatement* decl) {
                         auto* var = decl->variable;
                         if (auto* sem_init = ctx.src->Sem().GetVal(var->initializer)) {
                             if (is_ref_to_atomic_var(sem_init->UnwrapLoad())) {
@@ -294,23 +294,25 @@
     }
 };
 
-SpirvAtomic::SpirvAtomic() = default;
-SpirvAtomic::~SpirvAtomic() = default;
+Atomics::Atomics() = default;
+Atomics::~Atomics() = default;
 
-SpirvAtomic::Stub::Stub(GenerationID pid, NodeID nid, core::Function b)
+Atomics::Stub::Stub(GenerationID pid, ast::NodeID nid, core::Function b)
     : Base(pid, nid, tint::Empty), builtin(b) {}
-SpirvAtomic::Stub::~Stub() = default;
-std::string SpirvAtomic::Stub::InternalName() const {
+Atomics::Stub::~Stub() = default;
+std::string Atomics::Stub::InternalName() const {
     return "@internal(spirv-atomic " + std::string(core::str(builtin)) + ")";
 }
 
-const SpirvAtomic::Stub* SpirvAtomic::Stub::Clone(ast::CloneContext& ctx) const {
-    return ctx.dst->ASTNodes().Create<SpirvAtomic::Stub>(ctx.dst->ID(), ctx.dst->AllocateNodeID(),
-                                                         builtin);
+const Atomics::Stub* Atomics::Stub::Clone(ast::CloneContext& ctx) const {
+    return ctx.dst->ASTNodes().Create<Atomics::Stub>(ctx.dst->ID(), ctx.dst->AllocateNodeID(),
+                                                     builtin);
 }
 
-Transform::ApplyResult SpirvAtomic::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult Atomics::Apply(const Program* src,
+                                                      const ast::transform::DataMap&,
+                                                      ast::transform::DataMap&) const {
     return State{src}.Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic.h b/src/tint/lang/spirv/reader/ast_lower/atomics.h
similarity index 72%
rename from src/tint/lang/wgsl/ast/transform/spirv_atomic.h
rename to src/tint/lang/spirv/reader/ast_lower/atomics.h
index da9dcca..e349095 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic.h
+++ b/src/tint/lang/spirv/reader/ast_lower/atomics.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_SPIRV_ATOMIC_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_SPIRV_ATOMIC_H_
+#ifndef SRC_TINT_LANG_SPIRV_READER_AST_LOWER_ATOMICS_H_
+#define SRC_TINT_LANG_SPIRV_READER_AST_LOWER_ATOMICS_H_
 
 #include <string>
 
@@ -21,27 +21,27 @@
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 
-/// SpirvAtomic is a transform that replaces calls to stub functions created by the SPIR-V reader
+/// Atomics is a transform that replaces calls to stub functions created by the SPIR-V reader
 /// with calls to the WGSL atomic builtin. It also makes sure to replace variable declarations that
 /// are the target of the atomic operations with an atomic declaration of the same type. For
 /// structs, it creates a copy of the original struct with atomic members.
-class SpirvAtomic final : public Castable<SpirvAtomic, Transform> {
+class Atomics final : public Castable<Atomics, ast::transform::Transform> {
   public:
     /// Constructor
-    SpirvAtomic();
+    Atomics();
     /// Destructor
-    ~SpirvAtomic() override;
+    ~Atomics() override;
 
     /// Stub is an attribute applied to stub SPIR-V reader generated functions that need to be
     /// translated to an atomic builtin.
-    class Stub final : public Castable<Stub, InternalAttribute> {
+    class Stub final : public Castable<Stub, ast::InternalAttribute> {
       public:
         /// @param pid the identifier of the program that owns this node
         /// @param nid the unique node identifier
         /// @param builtin the atomic builtin this stub represents
-        Stub(GenerationID pid, NodeID nid, core::Function builtin);
+        Stub(GenerationID pid, ast::NodeID nid, core::Function builtin);
         /// Destructor
         ~Stub() override;
 
@@ -58,15 +58,15 @@
         const core::Function builtin;
     };
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_SPIRV_ATOMIC_H_
+#endif  // SRC_TINT_LANG_SPIRV_READER_AST_LOWER_ATOMICS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc b/src/tint/lang/spirv/reader/ast_lower/atomics_test.cc
similarity index 88%
rename from src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc
rename to src/tint/lang/spirv/reader/ast_lower/atomics_test.cc
index 626a93e..9bef265 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/atomics_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/spirv_atomic.h"
+#include "src/tint/lang/spirv/reader/ast_lower/atomics.h"
 
 #include <memory>
 #include <string>
@@ -25,12 +25,12 @@
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 namespace {
 
-class SpirvAtomicTest : public TransformTest {
+class AtomicsTest : public ast::transform::TransformTest {
   public:
-    Output Run(std::string in) {
+    ast::transform::Output Run(std::string in) {
         auto file = std::make_unique<Source::File>("test", std::move(in));
         auto parser = wgsl::reader::Parser(file.get());
         parser.Parse();
@@ -53,7 +53,7 @@
                        b.Return(0_u),
                    },
                    tint::Vector{
-                       b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(), a),
+                       b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(), a),
                    });
             b.Func(std::string{"stub_"} + core::str(a) + "_i32",
                    tint::Vector{
@@ -65,7 +65,7 @@
                        b.Return(0_i),
                    },
                    tint::Vector{
-                       b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(), a),
+                       b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(), a),
                    });
         }
 
@@ -78,8 +78,8 @@
                    b.Return(0_u),
                },
                tint::Vector{
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
-                                                          core::Function::kAtomicLoad),
+                   b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(),
+                                                      core::Function::kAtomicLoad),
                });
         b.Func("stub_atomicLoad_i32",
                tint::Vector{
@@ -90,8 +90,8 @@
                    b.Return(0_i),
                },
                tint::Vector{
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
-                                                          core::Function::kAtomicLoad),
+                   b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(),
+                                                      core::Function::kAtomicLoad),
                });
 
         b.Func("stub_atomicStore_u32",
@@ -101,8 +101,8 @@
                },
                b.ty.void_(), tint::Empty,
                tint::Vector{
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
-                                                          core::Function::kAtomicStore),
+                   b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(),
+                                                      core::Function::kAtomicStore),
                });
         b.Func("stub_atomicStore_i32",
                tint::Vector{
@@ -111,8 +111,8 @@
                },
                b.ty.void_(), tint::Empty,
                tint::Vector{
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(b.ID(), b.AllocateNodeID(),
-                                                          core::Function::kAtomicStore),
+                   b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(),
+                                                      core::Function::kAtomicStore),
                });
 
         b.Func("stub_atomic_compare_exchange_weak_u32",
@@ -126,8 +126,8 @@
                    b.Return(0_u),
                },
                tint::Vector{
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(
-                       b.ID(), b.AllocateNodeID(), core::Function::kAtomicCompareExchangeWeak),
+                   b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(),
+                                                      core::Function::kAtomicCompareExchangeWeak),
                });
         b.Func("stub_atomic_compare_exchange_weak_i32",
                tint::Vector{b.Param("p0", b.ty.i32()), b.Param("p1", b.ty.i32()),
@@ -137,21 +137,21 @@
                    b.Return(0_i),
                },
                tint::Vector{
-                   b.ASTNodes().Create<SpirvAtomic::Stub>(
-                       b.ID(), b.AllocateNodeID(), core::Function::kAtomicCompareExchangeWeak),
+                   b.ASTNodes().Create<Atomics::Stub>(b.ID(), b.AllocateNodeID(),
+                                                      core::Function::kAtomicCompareExchangeWeak),
                });
 
         // Keep this pointer alive after Transform() returns
         files_.emplace_back(std::move(file));
 
-        return TransformTest::Run<SpirvAtomic>(resolver::Resolve(b));
+        return ast::transform::TransformTest::Run<Atomics>(resolver::Resolve(b));
     }
 
   private:
     std::vector<std::unique_ptr<Source::File>> files_;
 };
 
-TEST_F(SpirvAtomicTest, StripUnusedBuiltins) {
+TEST_F(AtomicsTest, StripUnusedBuiltins) {
     auto* src = R"(
 fn f() {
 }
@@ -164,7 +164,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ArrayOfU32) {
+TEST_F(AtomicsTest, ArrayOfU32) {
     auto* src = R"(
 var<workgroup> wg : array<u32, 4>;
 
@@ -186,7 +186,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ArraysOfU32) {
+TEST_F(AtomicsTest, ArraysOfU32) {
     auto* src = R"(
 var<workgroup> wg : array<array<array<u32, 1>, 2>, 3>;
 
@@ -208,7 +208,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AliasedArraysOfU32) {
+TEST_F(AtomicsTest, AliasedArraysOfU32) {
     auto* src = R"(
 alias A0 = u32;
 
@@ -246,7 +246,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, FlatStructSingleAtomic) {
+TEST_F(AtomicsTest, FlatStructSingleAtomic) {
     auto* src = R"(
 struct S {
   a : u32,
@@ -280,7 +280,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, FlatStructMultipleAtomic) {
+TEST_F(AtomicsTest, FlatStructMultipleAtomic) {
     auto* src = R"(
 struct S {
   a : u32,
@@ -325,7 +325,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, NestedStruct) {
+TEST_F(AtomicsTest, NestedStruct) {
     auto* src = R"(
 struct S0 {
   a : u32,
@@ -401,7 +401,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ArrayOfStruct) {
+TEST_F(AtomicsTest, ArrayOfStruct) {
     auto* src = R"(
 struct S {
   a : u32,
@@ -441,7 +441,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, StructOfArray) {
+TEST_F(AtomicsTest, StructOfArray) {
     auto* src = R"(
 struct S {
   a : array<i32>,
@@ -475,7 +475,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ViaPtrLet) {
+TEST_F(AtomicsTest, ViaPtrLet) {
     auto* src = R"(
 struct S {
   i : i32,
@@ -514,7 +514,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, StructIsolatedMixedUsage) {
+TEST_F(AtomicsTest, StructIsolatedMixedUsage) {
     auto* src = R"(
 struct S {
   i : i32,
@@ -561,7 +561,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicLoad) {
+TEST_F(AtomicsTest, AtomicLoad) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -606,7 +606,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicExchange) {
+TEST_F(AtomicsTest, AtomicExchange) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -651,7 +651,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicAdd) {
+TEST_F(AtomicsTest, AtomicAdd) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -696,7 +696,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicSub) {
+TEST_F(AtomicsTest, AtomicSub) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -741,7 +741,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicMin) {
+TEST_F(AtomicsTest, AtomicMin) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -786,7 +786,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicMax) {
+TEST_F(AtomicsTest, AtomicMax) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -831,7 +831,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicAnd) {
+TEST_F(AtomicsTest, AtomicAnd) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -876,7 +876,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicOr) {
+TEST_F(AtomicsTest, AtomicOr) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -921,7 +921,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicXor) {
+TEST_F(AtomicsTest, AtomicXor) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -966,7 +966,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, AtomicCompareExchangeWeak) {
+TEST_F(AtomicsTest, AtomicCompareExchangeWeak) {
     auto* src = R"(
 var<workgroup> wg_u32 : u32;
 var<workgroup> wg_i32 : i32;
@@ -1015,7 +1015,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_Scaler) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_Scaler) {
     auto* src = R"(
 var<workgroup> wg : u32;
 
@@ -1046,7 +1046,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_Struct) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_Struct) {
     auto* src = R"(
 struct S {
   a : u32,
@@ -1089,7 +1089,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_NestedStruct) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_NestedStruct) {
     auto* src = R"(
 struct S0 {
   a : u32,
@@ -1144,7 +1144,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_StructMultipleAtomics) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_StructMultipleAtomics) {
     auto* src = R"(
 struct S {
   a : u32,
@@ -1213,7 +1213,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_ArrayOfScalar) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_ArrayOfScalar) {
     auto* src = R"(
 var<workgroup> wg : array<u32, 4>;
 
@@ -1244,7 +1244,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_ArrayOfStruct) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_ArrayOfStruct) {
     auto* src = R"(
 struct S {
   a : u32,
@@ -1287,7 +1287,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_StructOfArray) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_StructOfArray) {
     auto* src = R"(
 struct S {
   a : array<u32>,
@@ -1330,7 +1330,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SpirvAtomicTest, ReplaceAssignsAndDecls_ViaPtrLet) {
+TEST_F(AtomicsTest, ReplaceAssignsAndDecls_ViaPtrLet) {
     auto* src = R"(
 struct S {
   i : u32,
@@ -1377,4 +1377,4 @@
     EXPECT_EQ(expect, str(got));
 }
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc
similarity index 80%
rename from src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
rename to src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc
index ef1dc3c..1a2ac5e 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/decompose_strided_array.h"
+#include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h"
 
 #include <unordered_map>
 #include <utility>
@@ -33,17 +33,17 @@
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::DecomposeStridedArray);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::DecomposeStridedArray);
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 namespace {
 
 using DecomposedArrays = std::unordered_map<const core::type::Array*, Symbol>;
 
 bool ShouldRun(const Program* program) {
     for (auto* node : program->ASTNodes().Objects()) {
-        if (auto* ident = node->As<TemplatedIdentifier>()) {
-            if (GetAttribute<StrideAttribute>(ident->attributes)) {
+        if (auto* ident = node->As<ast::TemplatedIdentifier>()) {
+            if (ast::GetAttribute<ast::StrideAttribute>(ident->attributes)) {
                 return true;
             }
         }
@@ -57,9 +57,10 @@
 
 DecomposeStridedArray::~DecomposeStridedArray() = default;
 
-Transform::ApplyResult DecomposeStridedArray::Apply(const Program* src,
-                                                    const DataMap&,
-                                                    DataMap&) const {
+ast::transform::Transform::ApplyResult DecomposeStridedArray::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -79,8 +80,8 @@
     // stride for the array element type, then replace the array element type with
     // a structure, holding a single field with a @size attribute equal to the
     // array stride.
-    ctx.ReplaceAll([&](const IdentifierExpression* expr) -> const IdentifierExpression* {
-        auto* ident = expr->identifier->As<TemplatedIdentifier>();
+    ctx.ReplaceAll([&](const ast::IdentifierExpression* expr) -> const ast::IdentifierExpression* {
+        auto* ident = expr->identifier->As<ast::TemplatedIdentifier>();
         if (!ident) {
             return nullptr;
         }
@@ -95,12 +96,12 @@
         if (!arr->IsStrideImplicit()) {
             auto el_ty = tint::GetOrCreate(decomposed, arr, [&] {
                 auto name = b.Symbols().New("strided_arr");
-                auto* member_ty = ctx.Clone(ident->arguments[0]->As<IdentifierExpression>());
-                auto* member = b.Member(kMemberName, Type{member_ty},
-                                        tint::Vector{
+                auto* member_ty = ctx.Clone(ident->arguments[0]->As<ast::IdentifierExpression>());
+                auto* member = b.Member(kMemberName, ast::Type{member_ty},
+                                        Vector{
                                             b.MemberSize(AInt(arr->Stride())),
                                         });
-                b.Structure(name, tint::Vector{member});
+                b.Structure(name, Vector{member});
                 return name;
             });
             if (ident->arguments.Length() > 1) {
@@ -110,14 +111,14 @@
                 return b.Expr(b.ty.array(b.ty(el_ty)));
             }
         }
-        if (GetAttribute<StrideAttribute>(ident->attributes)) {
+        if (ast::GetAttribute<ast::StrideAttribute>(ident->attributes)) {
             // Strip the @stride attribute
-            auto* ty = ctx.Clone(ident->arguments[0]->As<IdentifierExpression>());
+            auto* ty = ctx.Clone(ident->arguments[0]->As<ast::IdentifierExpression>());
             if (ident->arguments.Length() > 1) {
                 auto* count = ctx.Clone(ident->arguments[1]);
-                return b.Expr(b.ty.array(Type{ty}, count));
+                return b.Expr(b.ty.array(ast::Type{ty}, count));
             } else {
-                return b.Expr(b.ty.array(Type{ty}));
+                return b.Expr(b.ty.array(ast::Type{ty}));
             }
         }
         return nullptr;
@@ -127,7 +128,7 @@
     // element changed to a single field structure. These expressions are adjusted
     // to insert an additional member accessor for the single structure field.
     // Example: `arr[i]` -> `arr[i].el`
-    ctx.ReplaceAll([&](const IndexAccessorExpression* idx) -> const Expression* {
+    ctx.ReplaceAll([&](const ast::IndexAccessorExpression* idx) -> const ast::Expression* {
         if (auto* ty = src->TypeOf(idx->object)) {
             if (auto* arr = ty->UnwrapRef()->As<core::type::Array>()) {
                 if (!arr->IsStrideImplicit()) {
@@ -145,7 +146,7 @@
     //   `@stride(32) array<i32, 3>(1, 2, 3)`
     // ->
     //   `array<strided_arr, 3>(strided_arr(1), strided_arr(2), strided_arr(3))`
-    ctx.ReplaceAll([&](const CallExpression* expr) -> const Expression* {
+    ctx.ReplaceAll([&](const ast::CallExpression* expr) -> const ast::Expression* {
         if (!expr->args.IsEmpty()) {
             if (auto* call = sem.Get(expr)->UnwrapMaterialize()->As<sem::Call>()) {
                 if (auto* ctor = call->Target()->As<sem::ValueConstructor>()) {
@@ -158,7 +159,7 @@
 
                         auto* target = ctx.Clone(expr->target);
 
-                        tint::Vector<const Expression*, 8> args;
+                        Vector<const ast::Expression*, 8> args;
                         if (auto it = decomposed.find(arr); it != decomposed.end()) {
                             args.Reserve(expr->args.Length());
                             for (auto* arg : expr->args) {
@@ -180,4 +181,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.h b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h
similarity index 67%
rename from src/tint/lang/wgsl/ast/transform/decompose_strided_array.h
rename to src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h
index 29c7d37..1ab8894 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.h
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_STRIDED_ARRAY_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_STRIDED_ARRAY_H_
+#ifndef SRC_TINT_LANG_SPIRV_READER_AST_LOWER_DECOMPOSE_STRIDED_ARRAY_H_
+#define SRC_TINT_LANG_SPIRV_READER_AST_LOWER_DECOMPOSE_STRIDED_ARRAY_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 
 /// DecomposeStridedArray transforms replaces arrays with a non-default
 /// `@stride` attribute with an array of structure elements, where the
@@ -27,7 +27,8 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class DecomposeStridedArray final : public Castable<DecomposeStridedArray, Transform> {
+class DecomposeStridedArray final
+    : public Castable<DecomposeStridedArray, ast::transform::Transform> {
   public:
     /// Constructor
     DecomposeStridedArray();
@@ -35,12 +36,12 @@
     /// Destructor
     ~DecomposeStridedArray() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_STRIDED_ARRAY_H_
+#endif  // SRC_TINT_LANG_SPIRV_READER_AST_LOWER_DECOMPOSE_STRIDED_ARRAY_H_
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array_test.cc
similarity index 83%
rename from src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
rename to src/tint/lang/spirv/reader/ast_lower/decompose_strided_array_test.cc
index bcfc473..f2984e6 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/decompose_strided_array.h"
+#include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h"
 
 #include <memory>
 #include <utility>
@@ -29,10 +29,12 @@
 using namespace tint::core::number_suffixes;  // NOLINT
 using namespace tint::core::fluent_types;     // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 namespace {
 
-using DecomposeStridedArrayTest = TransformTest;
+using DecomposeStridedArrayTest = ast::transform::TransformTest;
+using Unshadow = ast::transform::Unshadow;
+using SimplifyPointers = ast::transform::SimplifyPointers;
 
 TEST_F(DecomposeStridedArrayTest, ShouldRunEmptyModule) {
     ProgramBuilder b;
@@ -52,7 +54,7 @@
 
     ProgramBuilder b;
     b.GlobalVar("arr",
-                b.ty.array<f32, 4u>(tint::Vector{
+                b.ty.array<f32, 4u>(Vector{
                     b.Stride(4),
                 }),
                 core::AddressSpace::kPrivate);
@@ -64,7 +66,7 @@
 
     ProgramBuilder b;
     b.GlobalVar("arr",
-                b.ty.array<f32, 4u>(tint::Vector{
+                b.ty.array<f32, 4u>(Vector{
                     b.Stride(16),
                 }),
                 core::AddressSpace::kPrivate);
@@ -91,21 +93,21 @@
 
     ProgramBuilder b;
     b.GlobalVar("arr",
-                b.ty.array<f32, 4u>(tint::Vector{
+                b.ty.array<f32, 4u>(Vector{
                     b.Stride(4),
                 }),
                 core::AddressSpace::kPrivate);
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a",
-                            b.ty.array<f32, 4u>(tint::Vector{
+                            b.ty.array<f32, 4u>(Vector{
                                 b.Stride(4),
                             }),
                             b.Expr("arr"))),
                b.Decl(b.Let("b", b.ty.f32(), b.IndexAccessor("arr", 1_i))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -135,21 +137,21 @@
 
     ProgramBuilder b;
     b.GlobalVar("arr",
-                b.ty.array<f32, 4u>(tint::Vector{
+                b.ty.array<f32, 4u>(Vector{
                     b.Stride(32),
                 }),
                 core::AddressSpace::kPrivate);
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a",
-                            b.ty.array<f32, 4u>(tint::Vector{
+                            b.ty.array<f32, 4u>(Vector{
                                 b.Stride(32),
                             }),
                             b.Expr("arr"))),
                b.Decl(b.Let("b", b.ty.f32(), b.IndexAccessor("arr", 1_i))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -185,21 +187,21 @@
     //   let b : f32 = s.a[1];
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty.array<f32, 4u>(tint::Vector{
-                                                              b.Stride(32),
-                                                          }))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty.array<f32, 4u>(Vector{
+                                                        b.Stride(32),
+                                                    }))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kUniform, b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a",
-                            b.ty.array<f32, 4u>(tint::Vector{
+                            b.ty.array<f32, 4u>(Vector{
                                 b.Stride(32),
                             }),
                             b.MemberAccessor("s", "a"))),
                b.Decl(b.Let("b", b.ty.f32(), b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -239,25 +241,25 @@
     //   let b : f32 = s.a[1][2];
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty.array(b.ty.vec4<f32>(), 4_u,
-                                                                     tint::Vector{
-                                                                         b.Stride(16),
-                                                                     }))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty.array(b.ty.vec4<f32>(), 4_u,
+                                                               Vector{
+                                                                   b.Stride(16),
+                                                               }))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kUniform, b.Group(0_a), b.Binding(0_a));
     b.Func(
         "f", tint::Empty, b.ty.void_(),
-        tint::Vector{
+        Vector{
             b.Decl(b.Let("a",
                          b.ty.array(b.ty.vec4<f32>(), 4_u,
-                                    tint::Vector{
+                                    Vector{
                                         b.Stride(16),
                                     }),
                          b.MemberAccessor("s", "a"))),
             b.Decl(b.Let("b", b.ty.f32(),
                          b.IndexAccessor(b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i), 2_i))),
         },
-        tint::Vector{
-            b.Stage(PipelineStage::kCompute),
+        Vector{
+            b.Stage(ast::PipelineStage::kCompute),
             b.WorkgroupSize(1_i),
         });
 
@@ -293,21 +295,21 @@
     //   let b : f32 = s.a[1];
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty.array<f32, 4u>(tint::Vector{
-                                                              b.Stride(32),
-                                                          }))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty.array<f32, 4u>(Vector{
+                                                        b.Stride(32),
+                                                    }))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a",
-                            b.ty.array<f32, 4u>(tint::Vector{
+                            b.ty.array<f32, 4u>(Vector{
                                 b.Stride(32),
                             }),
                             b.MemberAccessor("s", "a"))),
                b.Decl(b.Let("b", b.ty.f32(), b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -347,21 +349,21 @@
     //   let b : f32 = s.a[1];
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty.array<f32, 4u>(tint::Vector{
-                                                              b.Stride(4),
-                                                          }))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty.array<f32, 4u>(Vector{
+                                                        b.Stride(4),
+                                                    }))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a",
-                            b.ty.array<f32, 4u>(tint::Vector{
+                            b.ty.array<f32, 4u>(Vector{
                                 b.Stride(4),
                             }),
                             b.MemberAccessor("s", "a"))),
                b.Decl(b.Let("b", b.ty.f32(), b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -397,24 +399,24 @@
     //   s.a[1i] = 5.0;
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty.array<f32, 4u>(tint::Vector{
-                                                              b.Stride(32),
-                                                          }))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty.array<f32, 4u>(Vector{
+                                                        b.Stride(32),
+                                                    }))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
-               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(tint::Vector{
+           Vector{
+               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(Vector{
                                                         b.Stride(32),
                                                     }))),
-               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(tint::Vector{
+               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(Vector{
                                                                b.Stride(32),
                                                            }),
                                                            1_f, 2_f, 3_f, 4_f)),
                b.Assign(b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i), 5_f),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -457,26 +459,26 @@
     //   s.a[1] = 5.0;
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{
-                                   b.Member("a", b.ty.array<f32, 4u>(tint::Vector{
+    auto* S = b.Structure("S", Vector{
+                                   b.Member("a", b.ty.array<f32, 4u>(Vector{
                                                      b.Stride(4),
                                                  })),
                                });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
-               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(tint::Vector{
+           Vector{
+               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(Vector{
                                                         b.Stride(4),
                                                     }))),
-               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(tint::Vector{
+               b.Assign(b.MemberAccessor("s", "a"), b.Call(b.ty.array<f32, 4u>(Vector{
                                                                b.Stride(4),
                                                            }),
                                                            1_f, 2_f, 3_f, 4_f)),
                b.Assign(b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i), 5_f),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -517,25 +519,25 @@
     //   (*b)[1] = 5.0;
     // }
     ProgramBuilder b;
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty.array<f32, 4u>(tint::Vector{
-                                                              b.Stride(32),
-                                                          }))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty.array<f32, 4u>(Vector{
+                                                        b.Stride(32),
+                                                    }))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a", b.AddressOf(b.MemberAccessor("s", "a")))),
                b.Decl(b.Let("b", b.AddressOf(b.Deref(b.AddressOf(b.Deref("a")))))),
                b.Decl(b.Let("c", b.Deref("b"))),
                b.Decl(b.Let("d", b.IndexAccessor(b.Deref("b"), 1_i))),
-               b.Assign(b.Deref("b"), b.Call(b.ty.array<f32, 4u>(tint::Vector{
+               b.Assign(b.Deref("b"), b.Call(b.ty.array<f32, 4u>(Vector{
                                                  b.Stride(32),
                                              }),
                                              1_f, 2_f, 3_f, 4_f)),
                b.Assign(b.IndexAccessor(b.Deref("b"), 1_i), 5_f),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -582,22 +584,22 @@
     //   s.a[1] = 5.0;
     // }
     ProgramBuilder b;
-    b.Alias("ARR", b.ty.array<f32, 4u>(tint::Vector{
+    b.Alias("ARR", b.ty.array<f32, 4u>(Vector{
                        b.Stride(32),
                    }));
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty("ARR"))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty("ARR"))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a", b.ty("ARR"), b.MemberAccessor("s", "a"))),
                b.Decl(b.Let("b", b.ty.f32(), b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i))),
                b.Assign(b.MemberAccessor("s", "a"), b.Call("ARR")),
                b.Assign(b.MemberAccessor("s", "a"), b.Call("ARR", 1_f, 2_f, 3_f, 4_f)),
                b.Assign(b.IndexAccessor(b.MemberAccessor("s", "a"), 1_i), 5_f),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -649,27 +651,27 @@
     // }
 
     ProgramBuilder b;
-    b.Alias("ARR_A", b.ty.array<f32, 2>(tint::Vector{
+    b.Alias("ARR_A", b.ty.array<f32, 2>(Vector{
                          b.Stride(8),
                      }));
     b.Alias("ARR_B", b.ty.array(  //
                          b.ty.array(b.ty("ARR_A"), 3_u,
-                                    tint::Vector{
+                                    Vector{
                                         b.Stride(16),
                                     }),
                          4_u,
-                         tint::Vector{
+                         Vector{
                              b.Stride(128),
                          }));
-    auto* S = b.Structure("S", tint::Vector{b.Member("a", b.ty("ARR_B"))});
+    auto* S = b.Structure("S", Vector{b.Member("a", b.ty("ARR_B"))});
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a", b.ty("ARR_B"), b.MemberAccessor("s", "a"))),
                b.Decl(b.Let("b",
                             b.ty.array(b.ty("ARR_A"), 3_u,
-                                       tint::Vector{
+                                       Vector{
                                            b.Stride(16),
                                        }),
                             b.IndexAccessor(                 //
@@ -699,8 +701,8 @@
                             1_i),
                         5_f),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -742,4 +744,4 @@
     EXPECT_EQ(expect, str(got));
 }
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
similarity index 82%
rename from src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
rename to src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
index 79bdce9..4ab6afc 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.h"
+#include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h"
 
 #include <unordered_map>
 #include <utility>
@@ -30,9 +30,9 @@
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::DecomposeStridedMatrix);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::DecomposeStridedMatrix);
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 namespace {
 
 /// MatrixInfo describes a matrix member with a custom stride
@@ -43,9 +43,9 @@
     const core::type::Matrix* matrix = nullptr;
 
     /// @returns the identifier of an array that holds an vector column for each row of the matrix.
-    Type array(ast::Builder* b) const {
+    ast::Type array(ast::Builder* b) const {
         return b->ty.array(b->ty.vec<f32>(matrix->rows()), u32(matrix->columns()),
-                           tint::Vector{
+                           Vector{
                                b->Stride(stride),
                            });
     }
@@ -66,9 +66,10 @@
 
 DecomposeStridedMatrix::~DecomposeStridedMatrix() = default;
 
-Transform::ApplyResult DecomposeStridedMatrix::Apply(const Program* src,
-                                                     const DataMap&,
-                                                     DataMap&) const {
+ast::transform::Transform::ApplyResult DecomposeStridedMatrix::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
@@ -77,7 +78,7 @@
     // and populate the `decomposed` map with the members that have been replaced.
     Hashmap<const core::type::StructMember*, MatrixInfo, 8> decomposed;
     for (auto* node : src->ASTNodes().Objects()) {
-        if (auto* str = node->As<Struct>()) {
+        if (auto* str = node->As<ast::Struct>()) {
             auto* str_ty = src->Sem().Get(str);
             if (!str_ty->UsedAs(core::AddressSpace::kUniform) &&
                 !str_ty->UsedAs(core::AddressSpace::kStorage)) {
@@ -88,7 +89,8 @@
                 if (!matrix) {
                     continue;
                 }
-                auto* attr = GetAttribute<StrideAttribute>(member->Declaration()->attributes);
+                auto* attr =
+                    ast::GetAttribute<ast::StrideAttribute>(member->Declaration()->attributes);
                 if (!attr) {
                     continue;
                 }
@@ -115,16 +117,17 @@
     // preserve these without calling conversion functions.
     // Example:
     //   ssbo.mat[2] -> ssbo.mat[2]
-    ctx.ReplaceAll([&](const IndexAccessorExpression* expr) -> const IndexAccessorExpression* {
-        if (auto* access = src->Sem().Get<sem::StructMemberAccess>(expr->object)) {
-            if (decomposed.Contains(access->Member())) {
-                auto* obj = ctx.CloneWithoutTransform(expr->object);
-                auto* idx = ctx.Clone(expr->index);
-                return b.IndexAccessor(obj, idx);
+    ctx.ReplaceAll(
+        [&](const ast::IndexAccessorExpression* expr) -> const ast::IndexAccessorExpression* {
+            if (auto* access = src->Sem().Get<sem::StructMemberAccess>(expr->object)) {
+                if (decomposed.Contains(access->Member())) {
+                    auto* obj = ctx.CloneWithoutTransform(expr->object);
+                    auto* idx = ctx.Clone(expr->index);
+                    return b.IndexAccessor(obj, idx);
+                }
             }
-        }
-        return nullptr;
-    });
+            return nullptr;
+        });
 
     // For all struct member accesses to the matrix on the LHS of an assignment,
     // we need to convert the matrix to the array before assigning to the
@@ -132,7 +135,7 @@
     // Example:
     //   ssbo.mat = mat_to_arr(m)
     std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> mat_to_arr;
-    ctx.ReplaceAll([&](const AssignmentStatement* stmt) -> const Statement* {
+    ctx.ReplaceAll([&](const ast::AssignmentStatement* stmt) -> const ast::Statement* {
         if (auto* access = src->Sem().Get<sem::StructMemberAccess>(stmt->lhs)) {
             if (auto info = decomposed.Find(access->Member())) {
                 auto fn = tint::GetOrCreate(mat_to_arr, *info, [&] {
@@ -145,16 +148,16 @@
                     auto array = [&] { return info->array(ctx.dst); };
 
                     auto mat = b.Sym("m");
-                    tint::Vector<const Expression*, 4> columns;
+                    Vector<const ast::Expression*, 4> columns;
                     for (uint32_t i = 0; i < static_cast<uint32_t>(info->matrix->columns()); i++) {
                         columns.Push(b.IndexAccessor(mat, u32(i)));
                     }
                     b.Func(name,
-                           tint::Vector{
+                           Vector{
                                b.Param(mat, matrix()),
                            },
                            array(),
-                           tint::Vector{
+                           Vector{
                                b.Return(b.Call(array(), columns)),
                            });
                     return name;
@@ -171,7 +174,7 @@
     // matrix type. Example:
     //   m = arr_to_mat(ssbo.mat)
     std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> arr_to_mat;
-    ctx.ReplaceAll([&](const MemberAccessorExpression* expr) -> const Expression* {
+    ctx.ReplaceAll([&](const ast::MemberAccessorExpression* expr) -> const ast::Expression* {
         if (auto* access = src->Sem().Get(expr)->UnwrapLoad()->As<sem::StructMemberAccess>()) {
             if (auto info = decomposed.Find(access->Member())) {
                 auto fn = tint::GetOrCreate(arr_to_mat, *info, [&] {
@@ -184,16 +187,16 @@
                     auto array = [&] { return info->array(ctx.dst); };
 
                     auto arr = b.Sym("arr");
-                    tint::Vector<const Expression*, 4> columns;
+                    Vector<const ast::Expression*, 4> columns;
                     for (uint32_t i = 0; i < static_cast<uint32_t>(info->matrix->columns()); i++) {
                         columns.Push(b.IndexAccessor(arr, u32(i)));
                     }
                     b.Func(name,
-                           tint::Vector{
+                           Vector{
                                b.Param(arr, array()),
                            },
                            matrix(),
-                           tint::Vector{
+                           Vector{
                                b.Return(b.Call(matrix(), columns)),
                            });
                     return name;
@@ -208,4 +211,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.h b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h
similarity index 67%
rename from src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.h
rename to src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h
index 5da4fc0..58abe70 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.h
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_STRIDED_MATRIX_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_STRIDED_MATRIX_H_
+#ifndef SRC_TINT_LANG_SPIRV_READER_AST_LOWER_DECOMPOSE_STRIDED_MATRIX_H_
+#define SRC_TINT_LANG_SPIRV_READER_AST_LOWER_DECOMPOSE_STRIDED_MATRIX_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 
 /// DecomposeStridedMatrix transforms replaces matrix members of storage or
 /// uniform buffer structures, that have a stride attribute, into an array
@@ -27,7 +27,8 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class DecomposeStridedMatrix final : public Castable<DecomposeStridedMatrix, Transform> {
+class DecomposeStridedMatrix final
+    : public Castable<DecomposeStridedMatrix, ast::transform::Transform> {
   public:
     /// Constructor
     DecomposeStridedMatrix();
@@ -35,12 +36,12 @@
     /// Destructor
     ~DecomposeStridedMatrix() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_DECOMPOSE_STRIDED_MATRIX_H_
+#endif  // SRC_TINT_LANG_SPIRV_READER_AST_LOWER_DECOMPOSE_STRIDED_MATRIX_H_
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix_test.cc
similarity index 70%
rename from src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
rename to src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix_test.cc
index b0f5217..fe47741 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.h"
+#include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h"
 
 #include <memory>
 #include <utility>
@@ -26,13 +26,15 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 namespace {
 
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
 
-using DecomposeStridedMatrixTest = TransformTest;
+using DecomposeStridedMatrixTest = ast::transform::TransformTest;
+using Unshadow = ast::transform::Unshadow;
+using SimplifyPointers = ast::transform::SimplifyPointers;
 
 TEST_F(DecomposeStridedMatrixTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -70,22 +72,22 @@
     //   let x : mat2x2<f32> = s.m;
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(16_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(16_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kUniform, b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("x", b.ty.mat2x2<f32>(), b.MemberAccessor("s", "m"))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -127,23 +129,23 @@
     //   let x : vec2<f32> = s.m[1];
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(16_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(16_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kUniform, b.Group(0_a), b.Binding(0_a));
     b.Func(
         "f", tint::Empty, b.ty.void_(),
-        tint::Vector{
+        Vector{
             b.Decl(b.Let("x", b.ty.vec2<f32>(), b.IndexAccessor(b.MemberAccessor("s", "m"), 1_i))),
         },
-        tint::Vector{
-            b.Stage(PipelineStage::kCompute),
+        Vector{
+            b.Stage(ast::PipelineStage::kCompute),
             b.WorkgroupSize(1_i),
         });
 
@@ -181,22 +183,22 @@
     //   let x : mat2x2<f32> = s.m;
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(16_u),
-                                          b.create<StrideAttribute>(8u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(16_u),
+                              b.create<ast::StrideAttribute>(8u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kUniform, b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("x", b.ty.mat2x2<f32>(), b.MemberAccessor("s", "m"))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -235,23 +237,23 @@
     //   let x : mat2x2<f32> = s.m;
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(8_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(8_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("x", b.ty.mat2x2<f32>(), b.MemberAccessor("s", "m"))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -293,24 +295,24 @@
     //   let x : vec2<f32> = s.m[1];
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(16_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(16_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func(
         "f", tint::Empty, b.ty.void_(),
-        tint::Vector{
+        Vector{
             b.Decl(b.Let("x", b.ty.vec2<f32>(), b.IndexAccessor(b.MemberAccessor("s", "m"), 1_i))),
         },
-        tint::Vector{
-            b.Stage(PipelineStage::kCompute),
+        Vector{
+            b.Stage(ast::PipelineStage::kCompute),
             b.WorkgroupSize(1_i),
         });
 
@@ -348,25 +350,25 @@
     //   s.m = mat2x2<f32>(vec2<f32>(1.0, 2.0), vec2<f32>(3.0, 4.0));
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(8_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(8_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func(
         "f", tint::Empty, b.ty.void_(),
-        tint::Vector{
+        Vector{
             b.Assign(b.MemberAccessor("s", "m"),
                      b.Call<mat2x2<f32>>(b.Call<vec2<f32>>(1_f, 2_f), b.Call<vec2<f32>>(3_f, 4_f))),
         },
-        tint::Vector{
-            b.Stage(PipelineStage::kCompute),
+        Vector{
+            b.Stage(ast::PipelineStage::kCompute),
             b.WorkgroupSize(1_i),
         });
 
@@ -408,24 +410,24 @@
     //   s.m[1] = vec2<f32>(1.0, 2.0);
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(8_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(8_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func(
         "f", tint::Empty, b.ty.void_(),
-        tint::Vector{
+        Vector{
             b.Assign(b.IndexAccessor(b.MemberAccessor("s", "m"), 1_i), b.Call<vec2<f32>>(1_f, 2_f)),
         },
-        tint::Vector{
-            b.Stage(PipelineStage::kCompute),
+        Vector{
+            b.Stage(ast::PipelineStage::kCompute),
             b.WorkgroupSize(1_i),
         });
 
@@ -469,19 +471,19 @@
     //   (*b)[1] = vec2<f32>(5.0, 6.0);
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(8_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(8_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kStorage, core::Access::kReadWrite,
                 b.Group(0_a), b.Binding(0_a));
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("a", b.AddressOf(b.MemberAccessor("s", "m")))),
                b.Decl(b.Let("b", b.AddressOf(b.Deref(b.AddressOf(b.Deref("a")))))),
                b.Decl(b.Let("x", b.Deref("b"))),
@@ -491,8 +493,8 @@
                                                           b.Call<vec2<f32>>(3_f, 4_f))),
                b.Assign(b.IndexAccessor(b.Deref("b"), 1_i), b.Call<vec2<f32>>(5_f, 6_f)),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -542,22 +544,22 @@
     //   let x : mat2x2<f32> = s.m;
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(8_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(8_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kPrivate);
     b.Func("f", tint::Empty, b.ty.void_(),
-           tint::Vector{
+           Vector{
                b.Decl(b.Let("x", b.ty.mat2x2<f32>(), b.MemberAccessor("s", "m"))),
            },
-           tint::Vector{
-               b.Stage(PipelineStage::kCompute),
+           Vector{
+               b.Stage(ast::PipelineStage::kCompute),
                b.WorkgroupSize(1_i),
            });
 
@@ -596,24 +598,24 @@
     //   s.m = mat2x2<f32>(vec2<f32>(1.0, 2.0), vec2<f32>(3.0, 4.0));
     // }
     ProgramBuilder b;
-    auto* S =
-        b.Structure("S", tint::Vector{
-                             b.Member("m", b.ty.mat2x2<f32>(),
-                                      tint::Vector{
-                                          b.MemberOffset(8_u),
-                                          b.create<StrideAttribute>(32u),
-                                          b.Disable(DisabledValidation::kIgnoreStrideAttribute),
-                                      }),
-                         });
+    auto* S = b.Structure(
+        "S", Vector{
+                 b.Member("m", b.ty.mat2x2<f32>(),
+                          Vector{
+                              b.MemberOffset(8_u),
+                              b.create<ast::StrideAttribute>(32u),
+                              b.Disable(ast::DisabledValidation::kIgnoreStrideAttribute),
+                          }),
+             });
     b.GlobalVar("s", b.ty.Of(S), core::AddressSpace::kPrivate);
     b.Func(
         "f", tint::Empty, b.ty.void_(),
-        tint::Vector{
+        Vector{
             b.Assign(b.MemberAccessor("s", "m"),
                      b.Call<mat2x2<f32>>(b.Call<vec2<f32>>(1_f, 2_f), b.Call<vec2<f32>>(3_f, 4_f))),
         },
-        tint::Vector{
-            b.Stage(PipelineStage::kCompute),
+        Vector{
+            b.Stage(ast::PipelineStage::kCompute),
             b.WorkgroupSize(1_i),
         });
 
@@ -640,4 +642,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc
similarity index 82%
rename from src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
rename to src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc
index e4a8e16..4e65452 100644
--- a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/fold_trivial_lets.h"
+#include "src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h"
 
 #include <utility>
 
@@ -23,9 +23,9 @@
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/utils/containers/hashmap.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::FoldTrivialLets);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::FoldTrivialLets);
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 
 /// PIMPL state for the transform.
 struct FoldTrivialLets::State {
@@ -44,11 +44,11 @@
 
     /// Process a block.
     /// @param block the block
-    void ProcessBlock(const BlockStatement* block) {
+    void ProcessBlock(const ast::BlockStatement* block) {
         // PendingLet describes a let declaration that might be inlined.
         struct PendingLet {
             // The let declaration.
-            const VariableDeclStatement* decl = nullptr;
+            const ast::VariableDeclStatement* decl = nullptr;
             // The number of uses that have not yet been inlined.
             size_t remaining_uses = 0;
         };
@@ -57,8 +57,8 @@
         Hashmap<const sem::Variable*, PendingLet, 16> pending_lets;
 
         // Helper that folds pending let declarations into `expr` if possible.
-        auto fold_lets = [&](const Expression* expr) {
-            TraverseExpressions(expr, [&](const IdentifierExpression* ident) {
+        auto fold_lets = [&](const ast::Expression* expr) {
+            ast::TraverseExpressions(expr, [&](const ast::IdentifierExpression* ident) {
                 if (auto* user = sem.Get<sem::VariableUser>(ident)) {
                     auto itr = pending_lets.Find(user->Variable());
                     if (itr) {
@@ -75,19 +75,19 @@
                         }
                     }
                 }
-                return TraverseAction::Descend;
+                return ast::TraverseAction::Descend;
             });
         };
 
         // Loop over all statements in the block.
         for (auto* stmt : block->statements) {
             // Check for a let declarations.
-            if (auto* decl = stmt->As<VariableDeclStatement>()) {
-                if (auto* let = decl->variable->As<Let>()) {
+            if (auto* decl = stmt->As<ast::VariableDeclStatement>()) {
+                if (auto* let = decl->variable->As<ast::Let>()) {
                     // If the initializer doesn't have side effects, we might be able to inline it.
                     if (!sem.GetVal(let->initializer)->HasSideEffects()) {  //
                         auto num_users = sem.Get(let)->Users().Length();
-                        if (let->initializer->Is<IdentifierExpression>()) {
+                        if (let->initializer->Is<ast::IdentifierExpression>()) {
                             // The initializer is a single identifier expression.
                             // We can fold it into multiple uses in the next non-let statement.
                             // We also fold previous pending lets into this one, but only if
@@ -113,14 +113,14 @@
 
             // Fold pending let declarations into a select few places that are frequently generated
             // by the SPIR_V reader.
-            if (auto* assign = stmt->As<AssignmentStatement>()) {
+            if (auto* assign = stmt->As<ast::AssignmentStatement>()) {
                 // We can fold into the RHS of an assignment statement if the RHS and LHS
                 // expressions have no side effects.
                 if (!sem.GetVal(assign->lhs)->HasSideEffects() &&
                     !sem.GetVal(assign->rhs)->HasSideEffects()) {
                     fold_lets(assign->rhs);
                 }
-            } else if (auto* ifelse = stmt->As<IfStatement>()) {
+            } else if (auto* ifelse = stmt->As<ast::IfStatement>()) {
                 // We can fold into the condition of an if statement if the condition expression has
                 // no side effects.
                 if (!sem.GetVal(ifelse->condition)->HasSideEffects()) {
@@ -139,7 +139,7 @@
     ApplyResult Run() {
         // Process all blocks in the module.
         for (auto* node : src->ASTNodes().Objects()) {
-            if (auto* block = node->As<BlockStatement>()) {
+            if (auto* block = node->As<ast::BlockStatement>()) {
                 ProcessBlock(block);
             }
         }
@@ -152,8 +152,10 @@
 
 FoldTrivialLets::~FoldTrivialLets() = default;
 
-Transform::ApplyResult FoldTrivialLets::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult FoldTrivialLets::Apply(const Program* src,
+                                                              const ast::transform::DataMap&,
+                                                              ast::transform::DataMap&) const {
     return State(src).Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.h b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h
similarity index 66%
rename from src/tint/lang/wgsl/ast/transform/fold_trivial_lets.h
rename to src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h
index 913922d..e11151e 100644
--- a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.h
+++ b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOLD_TRIVIAL_LETS_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOLD_TRIVIAL_LETS_H_
+#ifndef SRC_TINT_LANG_SPIRV_READER_AST_LOWER_FOLD_TRIVIAL_LETS_H_
+#define SRC_TINT_LANG_SPIRV_READER_AST_LOWER_FOLD_TRIVIAL_LETS_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 
 /// FoldTrivialLets is a transform that inlines the initializers of let declarations whose
 /// initializers are just identifier expressions, or lets that are only used once. This is used to
 /// clean up unnecessary let declarations created by the SPIR-V reader.
-class FoldTrivialLets final : public Castable<FoldTrivialLets, Transform> {
+class FoldTrivialLets final : public Castable<FoldTrivialLets, ast::transform::Transform> {
   public:
     /// Constructor
     FoldTrivialLets();
@@ -30,15 +30,15 @@
     /// Destructor
     ~FoldTrivialLets() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOLD_TRIVIAL_LETS_H_
+#endif  // SRC_TINT_LANG_SPIRV_READER_AST_LOWER_FOLD_TRIVIAL_LETS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets_test.cc b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets_test.cc
similarity index 95%
rename from src/tint/lang/wgsl/ast/transform/fold_trivial_lets_test.cc
rename to src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets_test.cc
index d5b266f..0bdbf3a 100644
--- a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets_test.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets_test.cc
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/fold_trivial_lets.h"
+#include "src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::reader {
 namespace {
 
-using FoldTrivialLetsTest = TransformTest;
+using FoldTrivialLetsTest = ast::transform::TransformTest;
 
 TEST_F(FoldTrivialLetsTest, Fold_IdentInitializer_AssignRHS) {
     auto* src = R"(
@@ -283,4 +283,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
new file mode 100644
index 0000000..ff65b46
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
@@ -0,0 +1,189 @@
+# 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 = "ast_parser",
+  srcs = [
+    "ast_parser.cc",
+    "construct.cc",
+    "entry_point_info.cc",
+    "enum_converter.cc",
+    "function.cc",
+    "namer.cc",
+    "parse.cc",
+    "type.cc",
+    "usage.cc",
+  ],
+  hdrs = [
+    "ast_parser.h",
+    "attributes.h",
+    "construct.h",
+    "entry_point_info.h",
+    "enum_converter.h",
+    "fail_stream.h",
+    "function.h",
+    "namer.h",
+    "parse.h",
+    "type.h",
+    "usage.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/reader/common",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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_reader": [
+      "//src/tint/lang/spirv/reader/ast_lower",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+      "@spirv_tools//:spirv_tools_opt",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "ast_parser_test.cc",
+    "barrier_test.cc",
+    "constant_test.cc",
+    "convert_member_decoration_test.cc",
+    "convert_type_test.cc",
+    "enum_converter_test.cc",
+    "fail_stream_test.cc",
+    "function_arithmetic_test.cc",
+    "function_bit_test.cc",
+    "function_call_test.cc",
+    "function_cfg_test.cc",
+    "function_composite_test.cc",
+    "function_conversion_test.cc",
+    "function_decl_test.cc",
+    "function_glsl_std_450_test.cc",
+    "function_logical_test.cc",
+    "function_memory_test.cc",
+    "function_misc_test.cc",
+    "function_var_test.cc",
+    "get_decorations_test.cc",
+    "handle_test.cc",
+    "helper_test.cc",
+    "helper_test.h",
+    "import_test.cc",
+    "module_function_decl_test.cc",
+    "module_var_test.cc",
+    "named_types_test.cc",
+    "namer_test.cc",
+    "parser_test.cc",
+    "spirv_tools_helpers_test.cc",
+    "spirv_tools_helpers_test.h",
+    "type_test.cc",
+    "usage_test.cc",
+    "user_name_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/reader/common",
+    "//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",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/ast_parser",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+      "@spirv_tools//:spirv_tools_opt",
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
index ae9cb5e..2748739 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
@@ -76,6 +76,12 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_ast_parser lib
+    tint_lang_spirv_reader_ast_lower
+  )
+endif(TINT_BUILD_SPV_READER)
+
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_lang_spirv_reader_ast_parser lib
     "spirv-headers"
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
index 54163d6..5c3d82b 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
@@ -78,6 +78,10 @@
       "${tint_src_dir}/utils/traits",
     ]
 
+    if (tint_build_spv_reader) {
+      deps += [ "${tint_src_dir}/lang/spirv/reader/ast_lower" ]
+    }
+
     if (tint_build_spv_reader || tint_build_spv_writer) {
       deps += [
         "${tint_spirv_headers_dir}:spv_headers",
diff --git a/src/tint/lang/spirv/reader/ast_parser/function.cc b/src/tint/lang/spirv/reader/ast_parser/function.cc
index 8097840..e83e959 100644
--- a/src/tint/lang/spirv/reader/ast_parser/function.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/function.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/type/depth_texture.h"
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/spirv/reader/ast_lower/atomics.h"
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
@@ -35,7 +36,6 @@
 #include "src/tint/lang/wgsl/ast/return_statement.h"
 #include "src/tint/lang/wgsl/ast/stage_attribute.h"
 #include "src/tint/lang/wgsl/ast/switch_statement.h"
-#include "src/tint/lang/wgsl/ast/transform/spirv_atomic.h"
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/utils/containers/hashmap.h"
@@ -5878,8 +5878,8 @@
             std::move(params), ret_type,
             /* body */ nullptr,
             tint::Vector{
-                builder_.ASTNodes().Create<ast::transform::SpirvAtomic::Stub>(
-                    builder_.ID(), builder_.AllocateNodeID(), builtin),
+                builder_.ASTNodes().Create<Atomics::Stub>(builder_.ID(), builder_.AllocateNodeID(),
+                                                          builtin),
                 builder_.Disable(ast::DisabledValidation::kFunctionHasNoBody),
             });
 
diff --git a/src/tint/lang/spirv/reader/ast_parser/parse.cc b/src/tint/lang/spirv/reader/ast_parser/parse.cc
index 7b60434..a0bee33 100644
--- a/src/tint/lang/spirv/reader/ast_parser/parse.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/parse.cc
@@ -16,14 +16,14 @@
 
 #include <utility>
 
+#include "src/tint/lang/spirv/reader/ast_lower/atomics.h"
+#include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h"
+#include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h"
+#include "src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h"
 #include "src/tint/lang/spirv/reader/ast_parser/ast_parser.h"
-#include "src/tint/lang/wgsl/ast/transform/decompose_strided_array.h"
-#include "src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.h"
-#include "src/tint/lang/wgsl/ast/transform/fold_trivial_lets.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
-#include "src/tint/lang/wgsl/ast/transform/spirv_atomic.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
@@ -56,6 +56,7 @@
                     case core::Extension::kChromiumDisableUniformityAnalysis:
                     case core::Extension::kChromiumExperimentalDp4A:
                     case core::Extension::kChromiumExperimentalFullPtrParameters:
+                    case core::Extension::kChromiumExperimentalPixelLocal:
                     case core::Extension::kChromiumExperimentalPushConstant:
                     case core::Extension::kChromiumExperimentalReadWriteStorageTexture:
                     case core::Extension::kChromiumExperimentalSubgroups:
@@ -89,11 +90,11 @@
     ast::transform::DataMap outputs;
     manager.Add<ast::transform::Unshadow>();
     manager.Add<ast::transform::SimplifyPointers>();
-    manager.Add<ast::transform::FoldTrivialLets>();
-    manager.Add<ast::transform::DecomposeStridedMatrix>();
-    manager.Add<ast::transform::DecomposeStridedArray>();
+    manager.Add<FoldTrivialLets>();
+    manager.Add<DecomposeStridedMatrix>();
+    manager.Add<DecomposeStridedArray>();
     manager.Add<ast::transform::RemoveUnreachableStatements>();
-    manager.Add<ast::transform::SpirvAtomic>();
+    manager.Add<Atomics>();
     return manager.Run(&program, {}, outputs);
 }
 
diff --git a/src/tint/lang/spirv/reader/common/BUILD.bazel b/src/tint/lang/spirv/reader/common/BUILD.bazel
new file mode 100644
index 0000000..8e7a112
--- /dev/null
+++ b/src/tint/lang/spirv/reader/common/BUILD.bazel
@@ -0,0 +1,39 @@
+# 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 = "common",
+  srcs = [
+    "common.cc",
+  ],
+  hdrs = [
+    "options.h",
+  ],
+  deps = [
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/spirv/spirv.def b/src/tint/lang/spirv/spirv.def
new file mode 100644
index 0000000..3949ffe
--- /dev/null
+++ b/src/tint/lang/spirv/spirv.def
@@ -0,0 +1,117 @@
+// 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.
+
+////////////////////////////////////////////////////////////////////////////////
+// Spirv builtin definition file                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// TODO(crbug.com/2036): add an include facility and move these duplicate match and type lines
+// into a common file.
+
+type bool
+type f32
+type f16
+type i32
+type u32
+type vec2<T>
+type vec3<T>
+type vec4<T>
+type mat2x2<T>
+type mat2x3<T>
+type mat2x4<T>
+type mat3x2<T>
+type mat3x3<T>
+type mat3x4<T>
+type mat4x2<T>
+type mat4x3<T>
+type mat4x4<T>
+@display("vec{N}<{T}>")     type vec<N: num, T>
+@display("mat{N}x{M}<{T}>") type mat<N: num, M: num, T>
+type atomic<T>
+type ptr<S: address_space, T, A: access>
+
+type struct_with_runtime_array
+
+enum address_space {
+  function
+  private
+  workgroup
+  uniform
+  storage
+  push_constant
+  pixel_local
+}
+
+enum access {
+  read
+  write
+  read_write
+}
+
+match f32_f16: f32 | f16
+match iu32: i32 | u32
+match scalar: f32 | f16 | i32 | u32 | bool
+
+match read_write: access.read_write
+match storage
+  : address_space.storage
+match workgroup_or_storage
+  : address_space.workgroup
+  | address_space.storage
+
+////////////////////////////////////////////////////////////////////////////////
+// Enumerators                                                                //
+////////////////////////////////////////////////////////////////////////////////
+
+enum intrinsic {
+  image_fetch
+  image_gather
+  image_dref_gather
+  image_query_size
+  image_query_size_lod
+  image_read
+  image_sample_implicit_lod
+  image_sample_explicit_lod
+  image_sample_dref_implicit_lod
+  image_sample_dref_explicit_lod
+  image_write
+  sampled_image
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Builtin Functions                                                          //
+////////////////////////////////////////////////////////////////////////////////
+fn array_length<I: u32, A: access>(ptr<storage, struct_with_runtime_array, A>, I) -> u32
+@stage("fragment", "compute") fn atomic_and<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_compare_exchange<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, U, T, T) -> T
+@stage("fragment", "compute") fn atomic_exchange<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_iadd<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_isub<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_load<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U) -> T
+@stage("fragment", "compute") fn atomic_or<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_smax<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_smin<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_store<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T)
+@stage("fragment", "compute") fn atomic_umax<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_umin<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+@stage("fragment", "compute") fn atomic_xor<T: iu32, U: u32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, U, U, T) -> T
+fn dot<N: num, T: f32_f16>(vec<N, T>, vec<N, T>) -> T
+fn matrix_times_matrix<T: f32_f16, K: num, C: num, R: num>(mat<K, R, T>, mat<C, K, T>) -> mat<C, R, T>
+fn matrix_times_scalar<T: f32_f16, N: num, M: num>(mat<N, M, T>, T) -> mat<N, M, T>
+fn matrix_times_vector<T: f32_f16, N: num, M: num>(mat<N, M, T>, vec<N, T>) -> vec<M, T>
+fn vector_times_matrix<T: f32_f16, N: num, M: num>(vec<N, T>, mat<M, N, T>) -> vec<M, T>
+fn select<T: scalar>(bool, T, T) -> T
+fn select<N: num, T: scalar>(vec<N, bool>, vec<N, T>, vec<N, T>) -> vec<N, T>
+fn vector_times_scalar<T: f32_f16, N: num>(vec<N, T>, T) -> vec<N, T>
+
diff --git a/src/tint/lang/spirv/writer/BUILD.bazel b/src/tint/lang/spirv/writer/BUILD.bazel
new file mode 100644
index 0000000..f4c8091
--- /dev/null
+++ b/src/tint/lang/spirv/writer/BUILD.bazel
@@ -0,0 +1,205 @@
+# 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 = "writer",
+  srcs = [
+    "writer.cc",
+  ],
+  hdrs = [
+    "output.h",
+    "writer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir/transform",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/intrinsic/data",
+    "//src/tint/lang/spirv/ir",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/reader/program_to_ir",
+    "//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_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/ast_printer",
+      "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/printer",
+      "//src/tint/lang/spirv/writer/raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "access_test.cc",
+    "atomic_builtin_test.cc",
+    "binary_test.cc",
+    "bitcast_test.cc",
+    "builtin_test.cc",
+    "constant_test.cc",
+    "construct_test.cc",
+    "convert_test.cc",
+    "discard_test.cc",
+    "function_test.cc",
+    "if_test.cc",
+    "let_test.cc",
+    "loop_test.cc",
+    "switch_test.cc",
+    "swizzle_test.cc",
+    "texture_builtin_test.cc",
+    "type_test.cc",
+    "unary_test.cc",
+    "var_test.cc",
+    "writer_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/intrinsic/data",
+    "//src/tint/lang/spirv/ir",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/common:test",
+      "//src/tint/lang/spirv/writer/printer",
+      "//src/tint/lang/spirv/writer/raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "writer_bench.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/writer/BUILD.cmake b/src/tint/lang/spirv/writer/BUILD.cmake
index 26d0fec..239c5b5 100644
--- a/src/tint/lang/spirv/writer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/BUILD.cmake
@@ -22,6 +22,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/printer/BUILD.cmake)
 include(lang/spirv/writer/raise/BUILD.cmake)
@@ -43,9 +44,16 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
+  tint_lang_core_ir_transform
   tint_lang_core_type
+  tint_lang_spirv_intrinsic_data
+  tint_lang_spirv_ir
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
+  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -62,14 +70,6 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer lib
-    tint_lang_core_ir
-    tint_lang_core_ir_transform
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_lang_spirv_writer lib
     "spirv-headers"
@@ -80,15 +80,10 @@
   tint_target_add_dependencies(tint_lang_spirv_writer lib
     tint_lang_spirv_writer_ast_printer
     tint_lang_spirv_writer_common
-  )
-endif(TINT_BUILD_SPV_WRITER)
-
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer lib
     tint_lang_spirv_writer_printer
     tint_lang_spirv_writer_raise
   )
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_SPV_WRITER)
 
 endif(TINT_BUILD_SPV_WRITER)
 if(TINT_BUILD_SPV_WRITER)
@@ -98,6 +93,26 @@
 # Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_test test
+  lang/spirv/writer/access_test.cc
+  lang/spirv/writer/atomic_builtin_test.cc
+  lang/spirv/writer/binary_test.cc
+  lang/spirv/writer/bitcast_test.cc
+  lang/spirv/writer/builtin_test.cc
+  lang/spirv/writer/constant_test.cc
+  lang/spirv/writer/construct_test.cc
+  lang/spirv/writer/convert_test.cc
+  lang/spirv/writer/discard_test.cc
+  lang/spirv/writer/function_test.cc
+  lang/spirv/writer/if_test.cc
+  lang/spirv/writer/let_test.cc
+  lang/spirv/writer/loop_test.cc
+  lang/spirv/writer/switch_test.cc
+  lang/spirv/writer/swizzle_test.cc
+  lang/spirv/writer/texture_builtin_test.cc
+  lang/spirv/writer/type_test.cc
+  lang/spirv/writer/unary_test.cc
+  lang/spirv/writer/var_test.cc
+  lang/spirv/writer/writer_test.cc
 )
 
 tint_target_add_dependencies(tint_lang_spirv_writer_test test
@@ -105,7 +120,12 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
+  tint_lang_spirv_intrinsic_data
+  tint_lang_spirv_ir
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -125,34 +145,6 @@
   "gtest"
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_sources(tint_lang_spirv_writer_test test
-    "lang/spirv/writer/access_test.cc"
-    "lang/spirv/writer/atomic_builtin_test.cc"
-    "lang/spirv/writer/binary_test.cc"
-    "lang/spirv/writer/bitcast_test.cc"
-    "lang/spirv/writer/builtin_test.cc"
-    "lang/spirv/writer/constant_test.cc"
-    "lang/spirv/writer/construct_test.cc"
-    "lang/spirv/writer/convert_test.cc"
-    "lang/spirv/writer/discard_test.cc"
-    "lang/spirv/writer/function_test.cc"
-    "lang/spirv/writer/if_test.cc"
-    "lang/spirv/writer/let_test.cc"
-    "lang/spirv/writer/loop_test.cc"
-    "lang/spirv/writer/switch_test.cc"
-    "lang/spirv/writer/swizzle_test.cc"
-    "lang/spirv/writer/texture_builtin_test.cc"
-    "lang/spirv/writer/type_test.cc"
-    "lang/spirv/writer/unary_test.cc"
-    "lang/spirv/writer/var_test.cc"
-    "lang/spirv/writer/writer_test.cc"
-  )
-  tint_target_add_dependencies(tint_lang_spirv_writer_test test
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_lang_spirv_writer_test test
     "spirv-headers"
@@ -164,15 +156,10 @@
   tint_target_add_dependencies(tint_lang_spirv_writer_test test
     tint_lang_spirv_writer_common
     tint_lang_spirv_writer_common_test
-  )
-endif(TINT_BUILD_SPV_WRITER)
-
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer_test test
     tint_lang_spirv_writer_printer
     tint_lang_spirv_writer_raise
   )
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_SPV_WRITER)
 
 endif(TINT_BUILD_SPV_WRITER)
 if(TINT_BUILD_SPV_WRITER)
@@ -182,6 +169,7 @@
 # Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_bench bench
+  lang/spirv/writer/writer_bench.cc
 )
 
 tint_target_add_dependencies(tint_lang_spirv_writer_bench bench
@@ -209,12 +197,6 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_sources(tint_lang_spirv_writer_bench bench
-    "lang/spirv/writer/writer_bench.cc"
-  )
-endif(TINT_BUILD_IR)
-
 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 b280bc0..0d7c728 100644
--- a/src/tint/lang/spirv/writer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/BUILD.gn
@@ -40,9 +40,16 @@
       "${tint_src_dir}/api/options",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/intrinsic/data",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/ir/transform",
       "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/spirv/intrinsic/data",
+      "${tint_src_dir}/lang/spirv/ir",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
+      "${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",
@@ -59,14 +66,6 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (tint_build_ir) {
-      deps += [
-        "${tint_src_dir}/lang/core/ir",
-        "${tint_src_dir}/lang/core/ir/transform",
-        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-      ]
-    }
-
     if (tint_build_spv_reader || tint_build_spv_writer) {
       deps += [ "${tint_spirv_headers_dir}:spv_headers" ]
     }
@@ -75,11 +74,6 @@
       deps += [
         "${tint_src_dir}/lang/spirv/writer/ast_printer",
         "${tint_src_dir}/lang/spirv/writer/common",
-      ]
-    }
-
-    if (tint_build_spv_writer && tint_build_ir) {
-      deps += [
         "${tint_src_dir}/lang/spirv/writer/printer",
         "${tint_src_dir}/lang/spirv/writer/raise",
       ]
@@ -90,14 +84,40 @@
   if (tint_build_spv_writer) {
     tint_unittests_source_set("unittests") {
       testonly = true
-      sources = []
+      sources = [
+        "access_test.cc",
+        "atomic_builtin_test.cc",
+        "binary_test.cc",
+        "bitcast_test.cc",
+        "builtin_test.cc",
+        "constant_test.cc",
+        "construct_test.cc",
+        "convert_test.cc",
+        "discard_test.cc",
+        "function_test.cc",
+        "if_test.cc",
+        "let_test.cc",
+        "loop_test.cc",
+        "switch_test.cc",
+        "swizzle_test.cc",
+        "texture_builtin_test.cc",
+        "type_test.cc",
+        "unary_test.cc",
+        "var_test.cc",
+        "writer_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/intrinsic",
+        "${tint_src_dir}/lang/core/intrinsic/data",
+        "${tint_src_dir}/lang/core/ir",
         "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/spirv/intrinsic/data",
+        "${tint_src_dir}/lang/spirv/ir",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -113,32 +133,6 @@
         "${tint_src_dir}/utils/traits",
       ]
 
-      if (tint_build_ir) {
-        sources += [
-          "access_test.cc",
-          "atomic_builtin_test.cc",
-          "binary_test.cc",
-          "bitcast_test.cc",
-          "builtin_test.cc",
-          "constant_test.cc",
-          "construct_test.cc",
-          "convert_test.cc",
-          "discard_test.cc",
-          "function_test.cc",
-          "if_test.cc",
-          "let_test.cc",
-          "loop_test.cc",
-          "switch_test.cc",
-          "swizzle_test.cc",
-          "texture_builtin_test.cc",
-          "type_test.cc",
-          "unary_test.cc",
-          "var_test.cc",
-          "writer_test.cc",
-        ]
-        deps += [ "${tint_src_dir}/lang/core/ir" ]
-      }
-
       if (tint_build_spv_reader || tint_build_spv_writer) {
         deps += [
           "${tint_spirv_headers_dir}:spv_headers",
@@ -151,11 +145,6 @@
         deps += [
           "${tint_src_dir}/lang/spirv/writer/common",
           "${tint_src_dir}/lang/spirv/writer/common:unittests",
-        ]
-      }
-
-      if (tint_build_spv_writer && tint_build_ir) {
-        deps += [
           "${tint_src_dir}/lang/spirv/writer/printer",
           "${tint_src_dir}/lang/spirv/writer/raise",
         ]
diff --git a/src/tint/lang/spirv/writer/access_test.cc b/src/tint/lang/spirv/writer/access_test.cc
index d31d884..ffdd185 100644
--- a/src/tint/lang/spirv/writer/access_test.cc
+++ b/src/tint/lang/spirv/writer/access_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
@@ -61,7 +59,11 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result = OpAccessChain %_ptr_Function_int %arr %idx");
+    EXPECT_INST(R"(
+         %13 = OpBitcast %uint %idx
+         %14 = OpExtInst %uint %15 UMin %13 %uint_3
+     %result = OpAccessChain %_ptr_Function_int %arr %14
+)");
 }
 
 TEST_F(SpirvWriterTest, Access_Matrix_Value_ConstantIndex) {
@@ -112,9 +114,15 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result_vector = OpAccessChain %_ptr_Function_v2float %mat %idx");
-    EXPECT_INST("%15 = OpAccessChain %_ptr_Function_float %result_vector %idx");
-    EXPECT_INST("%result_scalar = OpLoad %float %15");
+    EXPECT_INST(R"(
+         %14 = OpBitcast %uint %idx
+         %15 = OpExtInst %uint %16 UMin %14 %uint_1
+%result_vector = OpAccessChain %_ptr_Function_v2float %mat %15
+         %20 = OpBitcast %uint %idx
+         %21 = OpExtInst %uint %16 UMin %20 %uint_1
+         %22 = OpAccessChain %_ptr_Function_float %result_vector %21
+%result_scalar = OpLoad %float %22
+)");
 }
 
 TEST_F(SpirvWriterTest, Access_Vector_Value_ConstantIndex) {
@@ -143,7 +151,11 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%result = OpVectorExtractDynamic %int %vec %idx");
+    EXPECT_INST(R"(
+         %10 = OpBitcast %uint %idx
+         %11 = OpExtInst %uint %12 UMin %10 %uint_3
+     %result = OpVectorExtractDynamic %int %vec %11
+)");
 }
 
 TEST_F(SpirvWriterTest, Access_NestedVector_Value_DynamicIndex) {
@@ -158,8 +170,12 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%14 = OpCompositeExtract %v4int %arr 1 2");
-    EXPECT_INST("%result = OpVectorExtractDynamic %int %14 %idx");
+    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
+)");
 }
 
 TEST_F(SpirvWriterTest, Access_Struct_Value_ConstantIndex) {
@@ -231,8 +247,12 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%11 = OpAccessChain %_ptr_Function_int %vec %idx");
-    EXPECT_INST("%result = OpLoad %int %11");
+    EXPECT_INST(R"(
+         %12 = OpBitcast %uint %idx
+         %13 = OpExtInst %uint %14 UMin %12 %uint_3
+         %16 = OpAccessChain %_ptr_Function_int %vec %13
+     %result = OpLoad %int %16
+)");
 }
 
 TEST_F(SpirvWriterTest, StoreVectorElement_ConstantIndex) {
@@ -259,8 +279,12 @@
     });
 
     ASSERT_TRUE(Generate()) << Error() << output_;
-    EXPECT_INST("%11 = OpAccessChain %_ptr_Function_int %vec %idx");
-    EXPECT_INST("OpStore %11 %int_42");
+    EXPECT_INST(R"(
+         %12 = OpBitcast %uint %idx
+         %13 = OpExtInst %uint %14 UMin %12 %uint_3
+         %16 = OpAccessChain %_ptr_Function_int %vec %13
+               OpStore %16 %int_42
+)");
 }
 
 }  // namespace
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
new file mode 100644
index 0000000..b695ddb
--- /dev/null
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
@@ -0,0 +1,170 @@
+# 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 = "ast_printer",
+  srcs = [
+    "ast_printer.cc",
+    "builder.cc",
+  ],
+  hdrs = [
+    "ast_printer.h",
+    "builder.h",
+    "scalar_constant.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/helpers",
+    "//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_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/ast_raise",
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "accessor_expression_test.cc",
+    "assign_test.cc",
+    "ast_builtin_test.cc",
+    "ast_discard_test.cc",
+    "ast_function_test.cc",
+    "ast_if_test.cc",
+    "ast_loop_test.cc",
+    "ast_printer_test.cc",
+    "ast_switch_test.cc",
+    "ast_type_test.cc",
+    "binary_expression_test.cc",
+    "bitcast_expression_test.cc",
+    "block_test.cc",
+    "builtin_texture_test.cc",
+    "call_test.cc",
+    "const_assert_test.cc",
+    "constructor_expression_test.cc",
+    "entry_point_test.cc",
+    "format_conversion_test.cc",
+    "function_attribute_test.cc",
+    "function_variable_test.cc",
+    "global_variable_test.cc",
+    "helper_test.h",
+    "ident_expression_test.cc",
+    "literal_test.cc",
+    "return_test.cc",
+    "scalar_constant_test.cc",
+    "unary_op_expression_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer",
+      "//src/tint/lang/spirv/writer/ast_printer",
+      "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/common:test",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
index acaaf78..0a81588 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
@@ -69,6 +69,7 @@
 
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_lang_spirv_writer_ast_printer lib
+    tint_lang_spirv_writer_ast_raise
     tint_lang_spirv_writer_common
   )
 endif(TINT_BUILD_SPV_WRITER)
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
index 1243e2b..94377d4 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
@@ -68,7 +68,10 @@
     }
 
     if (tint_build_spv_writer) {
-      deps += [ "${tint_src_dir}/lang/spirv/writer/common" ]
+      deps += [
+        "${tint_src_dir}/lang/spirv/writer/ast_raise",
+        "${tint_src_dir}/lang/spirv/writer/common",
+      ]
     }
   }
 }
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 b386d02..c36f0bc 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -17,19 +17,22 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
+#include "src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h"
+#include "src/tint/lang/spirv/writer/ast_raise/merge_return.h"
+#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/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"
 #include "src/tint/lang/wgsl/ast/transform/builtin_polyfill.h"
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
-#include "src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h"
 #include "src/tint/lang/wgsl/ast/transform/demote_to_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
-#include "src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
-#include "src/tint/lang/wgsl/ast/transform/merge_return.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
@@ -39,10 +42,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/std140.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
-#include "src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.h"
-#include "src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.h"
 #include "src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h"
-#include "src/tint/lang/wgsl/ast/transform/while_to_loop.h"
 #include "src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h"
 
 namespace tint::spirv::writer {
@@ -52,7 +52,7 @@
     ast::transform::DataMap data;
 
     if (options.clamp_frag_depth) {
-        manager.Add<tint::ast::transform::ClampFragDepth>();
+        manager.Add<ClampFragDepth>();
     }
 
     manager.Add<ast::transform::DisableUniformityAnalysis>();
@@ -74,9 +74,9 @@
 
     manager.Add<ast::transform::RemovePhonies>();
     manager.Add<ast::transform::VectorizeScalarMatrixInitializers>();
-    manager.Add<ast::transform::VectorizeMatrixConversions>();
-    manager.Add<ast::transform::WhileToLoop>();  // ZeroInitWorkgroupMemory
-    manager.Add<ast::transform::MergeReturn>();
+    manager.Add<VectorizeMatrixConversions>();
+    manager.Add<WhileToLoop>();  // ZeroInitWorkgroupMemory
+    manager.Add<MergeReturn>();
 
     if (!options.disable_robustness) {
         // Robustness must come after PromoteSideEffectsToDecl
@@ -163,10 +163,10 @@
     manager.Add<ast::transform::Std140>();
 
     // VarForDynamicIndex must come after Std140
-    manager.Add<ast::transform::VarForDynamicIndex>();
+    manager.Add<VarForDynamicIndex>();
 
     // ForLoopToLoop must come after Std140, ZeroInitWorkgroupMemory
-    manager.Add<ast::transform::ForLoopToLoop>();
+    manager.Add<ForLoopToLoop>();
 
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
         ast::transform::CanonicalizeEntryPointIO::Config(
@@ -179,8 +179,12 @@
     return result;
 }
 
-ASTPrinter::ASTPrinter(const Program* program, bool zero_initialize_workgroup_memory)
-    : builder_(program, zero_initialize_workgroup_memory) {}
+ASTPrinter::ASTPrinter(const Program* program,
+                       bool zero_initialize_workgroup_memory,
+                       bool experimental_require_subgroup_uniform_control_flow)
+    : builder_(program,
+               zero_initialize_workgroup_memory,
+               experimental_require_subgroup_uniform_control_flow) {}
 
 bool ASTPrinter::Generate() {
     if (builder_.Build()) {
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.h b/src/tint/lang/spirv/writer/ast_printer/ast_printer.h
index a5687db..7c69ef6 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.h
@@ -43,7 +43,12 @@
     /// @param program the program to generate
     /// @param zero_initialize_workgroup_memory `true` to initialize all the
     /// variables in the Workgroup address space with OpConstantNull
-    ASTPrinter(const Program* program, bool zero_initialize_workgroup_memory);
+    /// @param experimental_require_subgroup_uniform_control_flow `true` to require
+    /// `SPV_KHR_subgroup_uniform_control_flow` extension and `SubgroupUniformControlFlowKHR`
+    /// execution mode for compute stage entry points.
+    ASTPrinter(const Program* program,
+               bool zero_initialize_workgroup_memory,
+               bool experimental_require_subgroup_uniform_control_flow);
 
     /// @returns true on successful generation; false otherwise
     bool Generate();
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_type_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_type_test.cc
index 2aec6e5..a1b3ce6 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_type_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_type_test.cc
@@ -628,8 +628,7 @@
 INSTANTIATE_TEST_SUITE_P(
     SpirvASTPrinterTest_Type,
     PtrDataTest,
-    testing::Values(PtrData{core::AddressSpace::kUndefined, SpvStorageClassMax},
-                    PtrData{core::AddressSpace::kIn, SpvStorageClassInput},
+    testing::Values(PtrData{core::AddressSpace::kIn, SpvStorageClassInput},
                     PtrData{core::AddressSpace::kOut, SpvStorageClassOutput},
                     PtrData{core::AddressSpace::kUniform, SpvStorageClassUniform},
                     PtrData{core::AddressSpace::kWorkgroup, SpvStorageClassWorkgroup},
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index 7b0afae..65a13af 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Tint Authors.  //
+// Copyright 2020 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
@@ -248,10 +248,14 @@
 
 Builder::AccessorInfo::~AccessorInfo() {}
 
-Builder::Builder(const Program* program, bool zero_initialize_workgroup_memory)
+Builder::Builder(const Program* program,
+                 bool zero_initialize_workgroup_memory,
+                 bool experimental_require_subgroup_uniform_control_flow)
     : builder_(ProgramBuilder::Wrap(program)),
       scope_stack_{Scope{}},
-      zero_initialize_workgroup_memory_(zero_initialize_workgroup_memory) {}
+      zero_initialize_workgroup_memory_(zero_initialize_workgroup_memory),
+      experimental_require_subgroup_uniform_control_flow_(
+          experimental_require_subgroup_uniform_control_flow) {}
 
 Builder::~Builder() = default;
 
@@ -280,6 +284,11 @@
         GenerateExtension(ext);
     }
 
+    // Emit SPV_KHR_subgroup_uniform_control_flow extension if required.
+    if (experimental_require_subgroup_uniform_control_flow_) {
+        module_.PushExtension("SPV_KHR_subgroup_uniform_control_flow");
+    }
+
     for (auto* var : builder_.AST().GlobalVariables()) {
         if (!GenerateGlobalVariable(var)) {
             return false;
@@ -483,9 +492,18 @@
         if (builtin == core::BuiltinValue::kFragDepth) {
             module_.PushExecutionMode(spv::Op::OpExecutionMode,
                                       {Operand(id), U32Operand(SpvExecutionModeDepthReplacing)});
+            break;
         }
     }
 
+    // Use SubgroupUniformControlFlow execution mode for compute stage if required.
+    if (experimental_require_subgroup_uniform_control_flow_ &&
+        func->PipelineStage() == ast::PipelineStage::kCompute) {
+        module_.PushExecutionMode(
+            spv::Op::OpExecutionMode,
+            {Operand(id), U32Operand(SpvExecutionModeSubgroupUniformControlFlowKHR)});
+    }
+
     return true;
 }
 
@@ -1931,6 +1949,8 @@
     }
 
     // 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)));
@@ -1938,6 +1958,8 @@
         return 0;
     }
 
+    TINT_END_DISABLE_WARNING(MAYBE_UNINITIALIZED);  // GCC false-positive
+
     return std::get<uint32_t>(result_mat_id);
 }
 
@@ -3964,8 +3986,8 @@
     return true;
 }
 
-SpvStorageClass Builder::ConvertAddressSpace(core::AddressSpace klass) const {
-    switch (klass) {
+SpvStorageClass Builder::ConvertAddressSpace(core::AddressSpace address_space) const {
+    switch (address_space) {
         case core::AddressSpace::kIn:
             return SpvStorageClassInput;
         case core::AddressSpace::kOut:
@@ -3984,9 +4006,12 @@
             return SpvStorageClassPrivate;
         case core::AddressSpace::kFunction:
             return SpvStorageClassFunction;
+
+        case core::AddressSpace::kPixelLocal:
         case core::AddressSpace::kUndefined:
             break;
     }
+    TINT_UNREACHABLE() << "unhandled address space '" << address_space << "'";
     return SpvStorageClassMax;
 }
 
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.h b/src/tint/lang/spirv/writer/ast_printer/builder.h
index adca66c..0f0c84b 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.h
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.h
@@ -82,7 +82,12 @@
     /// @param program the program
     /// @param zero_initialize_workgroup_memory `true` to initialize all the
     /// variables in the Workgroup address space with OpConstantNull
-    explicit Builder(const Program* program, bool zero_initialize_workgroup_memory = false);
+    /// @param experimental_require_subgroup_uniform_control_flow `true` to require
+    /// `SPV_KHR_subgroup_uniform_control_flow` extension and `SubgroupUniformControlFlowKHR`
+    /// execution mode for compute stage entry points.
+    explicit Builder(const Program* program,
+                     bool zero_initialize_workgroup_memory = false,
+                     bool experimental_require_subgroup_uniform_control_flow = false);
     ~Builder();
 
     /// Generates the SPIR-V instructions for the given program
@@ -537,6 +542,7 @@
     std::vector<uint32_t> merge_stack_;
     std::vector<uint32_t> continue_stack_;
     bool zero_initialize_workgroup_memory_ = false;
+    bool experimental_require_subgroup_uniform_control_flow_ = false;
 
     struct ContinuingInfo {
         ContinuingInfo(const ast::Statement* last_statement,
diff --git a/src/tint/lang/spirv/writer/ast_printer/entry_point_test.cc b/src/tint/lang/spirv/writer/ast_printer/entry_point_test.cc
index 95c910f..fdced66 100644
--- a/src/tint/lang/spirv/writer/ast_printer/entry_point_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/entry_point_test.cc
@@ -208,6 +208,11 @@
     // fn frag_main(inputs : Interface) -> @builtin(frag_depth) f32 {
     //   return inputs.value;
     // }
+    //
+    // @compute @workgroup_size(1)
+    // fn compute_main() {
+    //   return;
+    // }
 
     auto* interface =
         Structure("Interface",
@@ -232,6 +237,9 @@
              Builtin(core::BuiltinValue::kFragDepth),
          });
 
+    Func("compute_main", tint::Empty, ty.void_(), Vector{Return()},
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_u)});
+
     Builder& b = SanitizeAndBuild();
 
     ASSERT_TRUE(b.Build()) << b.Diagnostics();
@@ -240,8 +248,10 @@
 OpMemoryModel Logical GLSL450
 OpEntryPoint Vertex %23 "vert_main" %1 %5 %9
 OpEntryPoint Fragment %34 "frag_main" %10 %12 %14
+OpEntryPoint GLCompute %40 "compute_main"
 OpExecutionMode %34 OriginUpperLeft
 OpExecutionMode %34 DepthReplacing
+OpExecutionMode %40 LocalSize 1 1 1
 OpName %1 "value_1"
 OpName %5 "pos_1"
 OpName %9 "vertex_point_size"
@@ -256,6 +266,7 @@
 OpName %30 "frag_main_inner"
 OpName %31 "inputs"
 OpName %34 "frag_main"
+OpName %40 "compute_main"
 OpDecorate %1 Location 1
 OpDecorate %5 BuiltIn Position
 OpDecorate %9 BuiltIn PointSize
@@ -315,6 +326,160 @@
 OpStore %14 %36
 OpReturn
 OpFunctionEnd
+%40 = OpFunction %22 None %21
+%41 = OpLabel
+OpReturn
+OpFunctionEnd
+)");
+
+    Validate(b);
+}
+
+// Tests SPIRV generation with experimental_require_subgroup_uniform_control_flow in
+// spirv::writer::Options set to true, should require "SPV_KHR_subgroup_uniform_control_flow"
+// extension and use SubgroupUniformControlFlowKHR execution mode on compute stage entry points.
+TEST_F(SpirvASTPrinterTest, EntryPoint_ExperimentalSubgroupUniformControlFlow) {
+    // struct Interface {
+    //   @location(1) value : f32;
+    //   @builtin(position) pos : vec4<f32>;
+    // };
+    //
+    // @vertex
+    // fn vert_main() -> Interface {
+    //   return Interface(42.0, vec4<f32>());
+    // }
+    //
+    // @fragment
+    // fn frag_main(inputs : Interface) -> @builtin(frag_depth) f32 {
+    //   return inputs.value;
+    // }
+    //
+    // @compute @workgroup_size(1)
+    // fn compute_main() {
+    //   return;
+    // }
+
+    auto* interface =
+        Structure("Interface",
+                  Vector{
+                      Member("value", ty.f32(), Vector{Location(1_u)}),
+                      Member("pos", ty.vec4<f32>(), Vector{Builtin(core::BuiltinValue::kPosition)}),
+                  });
+
+    auto* vert_retval = Call(ty.Of(interface), 42_f, Call<vec4<f32>>());
+    Func("vert_main", tint::Empty, ty.Of(interface), Vector{Return(vert_retval)},
+         Vector{
+             Stage(ast::PipelineStage::kVertex),
+         });
+
+    auto* frag_inputs = Param("inputs", ty.Of(interface));
+    Func("frag_main", Vector{frag_inputs}, ty.f32(),
+         Vector{
+             Return(MemberAccessor(Expr("inputs"), "value")),
+         },
+         Vector{Stage(ast::PipelineStage::kFragment)},
+         Vector{
+             Builtin(core::BuiltinValue::kFragDepth),
+         });
+
+    Func("compute_main", tint::Empty, ty.void_(), Vector{Return()},
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_u)});
+
+    Options options = DefaultOptions();
+    options.experimental_require_subgroup_uniform_control_flow = true;
+
+    Builder& b = SanitizeAndBuild(options);
+
+    ASSERT_TRUE(b.Build()) << b.Diagnostics();
+
+    EXPECT_EQ(DumpModule(b.Module()), R"(OpCapability Shader
+OpExtension "SPV_KHR_subgroup_uniform_control_flow"
+OpMemoryModel Logical GLSL450
+OpEntryPoint Vertex %23 "vert_main" %1 %5 %9
+OpEntryPoint Fragment %34 "frag_main" %10 %12 %14
+OpEntryPoint GLCompute %40 "compute_main"
+OpExecutionMode %34 OriginUpperLeft
+OpExecutionMode %34 DepthReplacing
+OpExecutionMode %40 LocalSize 1 1 1
+OpExecutionMode %40 SubgroupUniformControlFlowKHR
+OpName %1 "value_1"
+OpName %5 "pos_1"
+OpName %9 "vertex_point_size"
+OpName %10 "value_2"
+OpName %12 "pos_2"
+OpName %14 "value_3"
+OpName %16 "Interface"
+OpMemberName %16 0 "value"
+OpMemberName %16 1 "pos"
+OpName %17 "vert_main_inner"
+OpName %23 "vert_main"
+OpName %30 "frag_main_inner"
+OpName %31 "inputs"
+OpName %34 "frag_main"
+OpName %40 "compute_main"
+OpDecorate %1 Location 1
+OpDecorate %5 BuiltIn Position
+OpDecorate %9 BuiltIn PointSize
+OpDecorate %10 Location 1
+OpDecorate %12 BuiltIn FragCoord
+OpDecorate %14 BuiltIn FragDepth
+OpMemberDecorate %16 0 Offset 0
+OpMemberDecorate %16 1 Offset 16
+%3 = OpTypeFloat 32
+%2 = OpTypePointer Output %3
+%4 = OpConstantNull %3
+%1 = OpVariable %2 Output %4
+%7 = OpTypeVector %3 4
+%6 = OpTypePointer Output %7
+%8 = OpConstantNull %7
+%5 = OpVariable %6 Output %8
+%9 = OpVariable %2 Output %4
+%11 = OpTypePointer Input %3
+%10 = OpVariable %11 Input
+%13 = OpTypePointer Input %7
+%12 = OpVariable %13 Input
+%14 = OpVariable %2 Output %4
+%16 = OpTypeStruct %3 %7
+%15 = OpTypeFunction %16
+%19 = OpConstant %3 42
+%20 = OpConstantComposite %16 %19 %8
+%22 = OpTypeVoid
+%21 = OpTypeFunction %22
+%28 = OpConstant %3 1
+%29 = OpTypeFunction %3 %16
+%17 = OpFunction %16 None %15
+%18 = OpLabel
+OpReturnValue %20
+OpFunctionEnd
+%23 = OpFunction %22 None %21
+%24 = OpLabel
+%25 = OpFunctionCall %16 %17
+%26 = OpCompositeExtract %3 %25 0
+OpStore %1 %26
+%27 = OpCompositeExtract %7 %25 1
+OpStore %5 %27
+OpStore %9 %28
+OpReturn
+OpFunctionEnd
+%30 = OpFunction %3 None %29
+%31 = OpFunctionParameter %16
+%32 = OpLabel
+%33 = OpCompositeExtract %3 %31 0
+OpReturnValue %33
+OpFunctionEnd
+%34 = OpFunction %22 None %21
+%35 = OpLabel
+%37 = OpLoad %3 %10
+%38 = OpLoad %7 %12
+%39 = OpCompositeConstruct %16 %37 %38
+%36 = OpFunctionCall %3 %30 %39
+OpStore %14 %36
+OpReturn
+OpFunctionEnd
+%40 = OpFunction %22 None %21
+%41 = OpLabel
+OpReturn
+OpFunctionEnd
 )");
 
     Validate(b);
diff --git a/src/tint/lang/spirv/writer/ast_printer/helper_test.h b/src/tint/lang/spirv/writer/ast_printer/helper_test.h
index cee975d..e97bb77 100644
--- a/src/tint/lang/spirv/writer/ast_printer/helper_test.h
+++ b/src/tint/lang/spirv/writer/ast_printer/helper_test.h
@@ -80,7 +80,12 @@
         auto result = Sanitize(program.get(), options);
         [&] { ASSERT_TRUE(result.program.IsValid()) << result.program.Diagnostics().str(); }();
         *program = std::move(result.program);
-        spirv_builder = std::make_unique<Builder>(program.get());
+        bool zero_initialize_workgroup_memory =
+            !options.disable_workgroup_init &&
+            options.use_zero_initialize_workgroup_memory_extension;
+        spirv_builder =
+            std::make_unique<Builder>(program.get(), zero_initialize_workgroup_memory,
+                                      options.experimental_require_subgroup_uniform_control_flow);
         return *spirv_builder;
     }
 
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
new file mode 100644
index 0000000..52b5e8b
--- /dev/null
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
@@ -0,0 +1,121 @@
+# 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 = "ast_raise",
+  srcs = [
+    "clamp_frag_depth.cc",
+    "for_loop_to_loop.cc",
+    "merge_return.cc",
+    "var_for_dynamic_index.cc",
+    "vectorize_matrix_conversions.cc",
+    "while_to_loop.cc",
+  ],
+  hdrs = [
+    "clamp_frag_depth.h",
+    "for_loop_to_loop.h",
+    "merge_return.h",
+    "var_for_dynamic_index.h",
+    "vectorize_matrix_conversions.h",
+    "while_to_loop.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "clamp_frag_depth_test.cc",
+    "for_loop_to_loop_test.cc",
+    "merge_return_test.cc",
+    "var_for_dynamic_index_test.cc",
+    "vectorize_matrix_conversions_test.cc",
+    "while_to_loop_test.cc",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/ast_raise",
+    ],
+    "//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/ast_raise/BUILD.cfg b/src/tint/lang/spirv/writer/ast_raise/BUILD.cfg
new file mode 100644
index 0000000..0a24987
--- /dev/null
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_spv_writer"
+}
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
new file mode 100644
index 0000000..3ae2021
--- /dev/null
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
@@ -0,0 +1,122 @@
+# 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_ast_raise
+# Kind:      lib
+# Condition: TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_writer_ast_raise lib
+  lang/spirv/writer/ast_raise/clamp_frag_depth.cc
+  lang/spirv/writer/ast_raise/clamp_frag_depth.h
+  lang/spirv/writer/ast_raise/for_loop_to_loop.cc
+  lang/spirv/writer/ast_raise/for_loop_to_loop.h
+  lang/spirv/writer/ast_raise/merge_return.cc
+  lang/spirv/writer/ast_raise/merge_return.h
+  lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
+  lang/spirv/writer/ast_raise/var_for_dynamic_index.h
+  lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
+  lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
+  lang/spirv/writer/ast_raise/while_to_loop.cc
+  lang/spirv/writer/ast_raise/while_to_loop.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_writer_ast_raise lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_program
+  tint_lang_wgsl_resolver
+  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
+)
+
+endif(TINT_BUILD_SPV_WRITER)
+if(TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_writer_ast_raise_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_writer_ast_raise_test test
+  lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
+  lang/spirv/writer/ast_raise/for_loop_to_loop_test.cc
+  lang/spirv/writer/ast_raise/merge_return_test.cc
+  lang/spirv/writer/ast_raise/var_for_dynamic_index_test.cc
+  lang/spirv/writer/ast_raise/vectorize_matrix_conversions_test.cc
+  lang/spirv/writer/ast_raise/while_to_loop_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_writer_ast_raise_test test
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  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
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_writer_ast_raise_test test
+  "gtest"
+)
+
+if(TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_ast_raise_test test
+    tint_lang_spirv_writer_ast_raise
+  )
+endif(TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_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
new file mode 100644
index 0000000..fbcc1cc
--- /dev/null
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
@@ -0,0 +1,117 @@
+# 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_unittests) {
+  import("//testing/test.gni")
+}
+if (tint_build_spv_writer) {
+  libtint_source_set("ast_raise") {
+    sources = [
+      "clamp_frag_depth.cc",
+      "clamp_frag_depth.h",
+      "for_loop_to_loop.cc",
+      "for_loop_to_loop.h",
+      "merge_return.cc",
+      "merge_return.h",
+      "var_for_dynamic_index.cc",
+      "var_for_dynamic_index.h",
+      "vectorize_matrix_conversions.cc",
+      "vectorize_matrix_conversions.h",
+      "while_to_loop.cc",
+      "while_to_loop.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/ast",
+      "${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_unittests) {
+  if (tint_build_spv_writer) {
+    tint_unittests_source_set("unittests") {
+      testonly = true
+      sources = [
+        "clamp_frag_depth_test.cc",
+        "for_loop_to_loop_test.cc",
+        "merge_return_test.cc",
+        "var_for_dynamic_index_test.cc",
+        "vectorize_matrix_conversions_test.cc",
+        "while_to_loop_test.cc",
+      ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/type",
+        "${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",
+        "${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/ast_raise" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
similarity index 82%
rename from src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
rename to src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
index e12b5b9..af4a0c5 100644
--- a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h"
+#include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
 
 #include <utility>
 
@@ -31,9 +31,9 @@
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ClampFragDepth);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::ClampFragDepth);
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 /// PIMPL state for the transform
 struct ClampFragDepth::State {
@@ -53,7 +53,7 @@
     Transform::ApplyResult Run() {
         // Abort on any use of push constants in the module.
         for (auto* global : src->AST().GlobalVariables()) {
-            if (auto* var = global->As<Var>()) {
+            if (auto* var = global->As<ast::Var>()) {
                 auto* v = src->Sem().Get(var);
                 if (TINT_UNLIKELY(v->AddressSpace() == core::AddressSpace::kPushConstant)) {
                     TINT_ICE()
@@ -84,15 +84,15 @@
         b.Enable(core::Extension::kChromiumExperimentalPushConstant);
 
         b.Structure(b.Symbols().New("FragDepthClampArgs"),
-                    tint::Vector{b.Member("min", b.ty.f32()), b.Member("max", b.ty.f32())});
+                    Vector{b.Member("min", b.ty.f32()), b.Member("max", b.ty.f32())});
 
         auto args_sym = b.Symbols().New("frag_depth_clamp_args");
         b.GlobalVar(args_sym, b.ty("FragDepthClampArgs"), core::AddressSpace::kPushConstant);
 
         auto base_fn_sym = b.Symbols().New("clamp_frag_depth");
-        b.Func(base_fn_sym, tint::Vector{b.Param("v", b.ty.f32())}, b.ty.f32(),
-               tint::Vector{b.Return(b.Call("clamp", "v", b.MemberAccessor(args_sym, "min"),
-                                            b.MemberAccessor(args_sym, "max")))});
+        b.Func(base_fn_sym, Vector{b.Param("v", b.ty.f32())}, b.ty.f32(),
+               Vector{b.Return(b.Call("clamp", "v", b.MemberAccessor(args_sym, "min"),
+                                      b.MemberAccessor(args_sym, "max")))});
 
         // If true, the currently cloned function returns frag depth directly as a scalar
         bool returns_frag_depth_as_value = false;
@@ -103,14 +103,14 @@
 
         // Map of io struct to helper function to return the structure with the depth clamping
         // applied.
-        Hashmap<const Struct*, Symbol, 4u> io_structs_clamp_helpers;
+        Hashmap<const ast::Struct*, Symbol, 4u> io_structs_clamp_helpers;
 
         // Register a callback that will be called for each visted AST function.
         // This call wraps the cloning of the function's statements, and will assign to
         // `returns_frag_depth_as_value` or `returns_frag_depth_as_struct_helper` if the function's
         // return value requires depth clamping.
-        ctx.ReplaceAll([&](const Function* fn) {
-            if (fn->PipelineStage() != PipelineStage::kFragment) {
+        ctx.ReplaceAll([&](const ast::Function* fn) {
+            if (fn->PipelineStage() != ast::PipelineStage::kFragment) {
                 return ctx.CloneWithoutTransform(fn);
             }
 
@@ -131,17 +131,17 @@
                     auto fn_sym =
                         b.Symbols().New("clamp_frag_depth_" + struct_ty->name->symbol.Name());
 
-                    tint::Vector<const Expression*, 8u> initializer_args;
+                    Vector<const ast::Expression*, 8u> initializer_args;
                     for (auto* member : struct_ty->members) {
-                        const Expression* arg =
+                        const ast::Expression* arg =
                             b.MemberAccessor("s", ctx.Clone(member->name->symbol));
                         if (ContainsFragDepth(member->attributes)) {
                             arg = b.Call(base_fn_sym, arg);
                         }
                         initializer_args.Push(arg);
                     }
-                    tint::Vector params{b.Param("s", ctx.Clone(return_ty))};
-                    tint::Vector body{
+                    Vector params{b.Param("s", ctx.Clone(return_ty))};
+                    Vector body{
                         b.Return(b.Call(ctx.Clone(return_ty), std::move(initializer_args))),
                     };
                     b.Func(fn_sym, params, ctx.Clone(return_ty), body);
@@ -156,7 +156,7 @@
         });
 
         // Replace the return statements `return expr` with `return clamp_frag_depth(expr)`.
-        ctx.ReplaceAll([&](const ReturnStatement* stmt) -> const ReturnStatement* {
+        ctx.ReplaceAll([&](const ast::ReturnStatement* stmt) -> const ast::ReturnStatement* {
             if (returns_frag_depth_as_value) {
                 return b.Return(stmt->source, b.Call(base_fn_sym, ctx.Clone(stmt->value)));
             }
@@ -175,7 +175,7 @@
     /// @returns true if the transform should run
     bool ShouldRun() {
         for (auto* fn : src->AST().Functions()) {
-            if (fn->PipelineStage() == PipelineStage::kFragment &&
+            if (fn->PipelineStage() == ast::PipelineStage::kFragment &&
                 (ReturnsFragDepthAsValue(fn) || ReturnsFragDepthInStruct(fn))) {
                 return true;
             }
@@ -185,9 +185,9 @@
     }
     /// @param attrs the attributes to examine
     /// @returns true if @p attrs contains a `@builtin(frag_depth)` attribute
-    bool ContainsFragDepth(VectorRef<const Attribute*> attrs) {
+    bool ContainsFragDepth(VectorRef<const ast::Attribute*> attrs) {
         for (auto* attribute : attrs) {
-            if (auto* builtin_attr = attribute->As<BuiltinAttribute>()) {
+            if (auto* builtin_attr = attribute->As<ast::BuiltinAttribute>()) {
                 auto builtin = sem.Get(builtin_attr)->Value();
                 if (builtin == core::BuiltinValue::kFragDepth) {
                     return true;
@@ -200,14 +200,14 @@
 
     /// @param fn the function to examine
     /// @returns true if @p fn has a return type with a `@builtin(frag_depth)` attribute
-    bool ReturnsFragDepthAsValue(const Function* fn) {
+    bool ReturnsFragDepthAsValue(const ast::Function* fn) {
         return ContainsFragDepth(fn->return_type_attributes);
     }
 
     /// @param fn the function to examine
     /// @returns true if @p fn has a return structure with a `@builtin(frag_depth)` attribute on one
     /// of the members
-    bool ReturnsFragDepthInStruct(const Function* fn) {
+    bool ReturnsFragDepthInStruct(const ast::Function* fn) {
         if (auto* struct_ty = sem.Get(fn)->ReturnType()->As<sem::Struct>()) {
             for (auto* member : struct_ty->Members()) {
                 if (ContainsFragDepth(member->Declaration()->attributes)) {
@@ -223,8 +223,10 @@
 ClampFragDepth::ClampFragDepth() = default;
 ClampFragDepth::~ClampFragDepth() = default;
 
-Transform::ApplyResult ClampFragDepth::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult ClampFragDepth::Apply(const Program* src,
+                                                             const ast::transform::DataMap&,
+                                                             ast::transform::DataMap&) const {
     return State{src}.Run();
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
similarity index 74%
rename from src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
rename to src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
index 464a608..cb3ce85 100644
--- a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
+++ b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_CLAMP_FRAG_DEPTH_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_CLAMP_FRAG_DEPTH_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_CLAMP_FRAG_DEPTH_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_CLAMP_FRAG_DEPTH_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 /// Add clamping of the `@builtin(frag_depth)` output of fragment shaders using two push constants
 /// provided by the outside environment. For example the following code:
@@ -49,22 +49,22 @@
 ///     return clamp_frag_depth(0.0);
 ///   }
 /// ```
-class ClampFragDepth final : public Castable<ClampFragDepth, Transform> {
+class ClampFragDepth final : public Castable<ClampFragDepth, ast::transform::Transform> {
   public:
     /// Constructor
     ClampFragDepth();
     /// Destructor
     ~ClampFragDepth() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 
   private:
     struct State;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_CLAMP_FRAG_DEPTH_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_CLAMP_FRAG_DEPTH_H_
diff --git a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth_test.cc b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
similarity index 97%
rename from src/tint/lang/wgsl/ast/transform/clamp_frag_depth_test.cc
rename to src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
index 103f482..2bfba5f 100644
--- a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth_test.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h"
+#include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
-using ClampFragDepthTest = TransformTest;
+using ClampFragDepthTest = ast::transform::TransformTest;
 
 TEST_F(ClampFragDepthTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -378,4 +378,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc
similarity index 74%
rename from src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
rename to src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc
index 873849f..36fc251 100644
--- a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h"
+#include "src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h"
 
 #include <utility>
 
@@ -21,14 +21,14 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ForLoopToLoop);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::ForLoopToLoop);
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
 bool ShouldRun(const Program* program) {
     for (auto* node : program->ASTNodes().Objects()) {
-        if (node->Is<ForLoopStatement>()) {
+        if (node->Is<ast::ForLoopStatement>()) {
             return true;
         }
     }
@@ -41,7 +41,9 @@
 
 ForLoopToLoop::~ForLoopToLoop() = default;
 
-Transform::ApplyResult ForLoopToLoop::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult ForLoopToLoop::Apply(const Program* src,
+                                                            const ast::transform::DataMap&,
+                                                            ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -49,8 +51,8 @@
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
-    ctx.ReplaceAll([&](const ForLoopStatement* for_loop) -> const Statement* {
-        tint::Vector<const Statement*, 8> stmts;
+    ctx.ReplaceAll([&](const ast::ForLoopStatement* for_loop) -> const ast::Statement* {
+        tint::Vector<const ast::Statement*, 8> stmts;
         if (auto* cond = for_loop->condition) {
             // !condition
             auto* not_cond = b.Not(ctx.Clone(cond));
@@ -65,7 +67,7 @@
             stmts.Push(ctx.Clone(stmt));
         }
 
-        const BlockStatement* continuing = nullptr;
+        const ast::BlockStatement* continuing = nullptr;
         if (auto* cont = for_loop->continuing) {
             continuing = b.Block(ctx.Clone(cont));
         }
@@ -84,4 +86,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h
similarity index 60%
rename from src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h
rename to src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h
index 2b5c5fa..776e1f3 100644
--- a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h
+++ b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h
@@ -12,16 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOR_LOOP_TO_LOOP_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOR_LOOP_TO_LOOP_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_FOR_LOOP_TO_LOOP_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_FOR_LOOP_TO_LOOP_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
-/// ForLoopToLoop is a Transform that converts a for-loop statement into a loop
-/// statement. This is required by the SPIR-V writer.
-class ForLoopToLoop final : public Castable<ForLoopToLoop, Transform> {
+/// ForLoopToLoop is a Transform that converts a for-loop statement into a loop statement.
+class ForLoopToLoop final : public Castable<ForLoopToLoop, ast::transform::Transform> {
   public:
     /// Constructor
     ForLoopToLoop();
@@ -29,12 +28,12 @@
     /// Destructor
     ~ForLoopToLoop() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_FOR_LOOP_TO_LOOP_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_FOR_LOOP_TO_LOOP_H_
diff --git a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop_test.cc b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop_test.cc
similarity index 96%
rename from src/tint/lang/wgsl/ast/transform/for_loop_to_loop_test.cc
rename to src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop_test.cc
index 711089f..7becae1 100644
--- a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop_test.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop_test.cc
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h"
+#include "src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
-using ForLoopToLoopTest = TransformTest;
+using ForLoopToLoopTest = ast::transform::TransformTest;
 
 TEST_F(ForLoopToLoopTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -369,4 +369,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/merge_return.cc b/src/tint/lang/spirv/writer/ast_raise/merge_return.cc
similarity index 80%
rename from src/tint/lang/wgsl/ast/transform/merge_return.cc
rename to src/tint/lang/spirv/writer/ast_raise/merge_return.cc
index 6f7905a..2d30235 100644
--- a/src/tint/lang/wgsl/ast/transform/merge_return.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/merge_return.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/merge_return.h"
+#include "src/tint/lang/spirv/writer/ast_raise/merge_return.h"
 
 #include <utility>
 
@@ -23,21 +23,21 @@
 #include "src/tint/utils/macros/scoped_assignment.h"
 #include "src/tint/utils/rtti/switch.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::MergeReturn);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::MergeReturn);
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 namespace {
 
 /// Returns `true` if `stmt` has the behavior `behavior`.
-bool HasBehavior(const Program* program, const Statement* stmt, sem::Behavior behavior) {
+bool HasBehavior(const Program* program, const ast::Statement* stmt, sem::Behavior behavior) {
     return program->Sem().Get(stmt)->Behaviors().Contains(behavior);
 }
 
 /// Returns `true` if `func` needs to be transformed.
-bool NeedsTransform(const Program* program, const Function* func) {
+bool NeedsTransform(const Program* program, const ast::Function* func) {
     // Entry points and intrinsic declarations never need transforming.
     if (func->IsEntryPoint() || func->body == nullptr) {
         return false;
@@ -51,7 +51,7 @@
         if (HasBehavior(program, s, sem::Behavior::kReturn)) {
             // If this statement is itself a return, it will be the only exit point,
             // so no need to apply the transform to the function.
-            if (s->Is<ReturnStatement>()) {
+            if (s->Is<ast::ReturnStatement>()) {
                 return false;
             } else {
                 // Apply the transform in all other cases.
@@ -80,7 +80,7 @@
     ast::Builder& b;
 
     /// The function.
-    const Function* function;
+    const ast::Function* function;
 
     /// The symbol for the return flag variable.
     Symbol flag;
@@ -94,32 +94,32 @@
   public:
     /// Constructor
     /// @param context the clone context
-    State(program::CloneContext& context, const Function* func)
+    State(program::CloneContext& context, const ast::Function* func)
         : ctx(context), b(*ctx.dst), function(func) {}
 
     /// Process a statement (recursively).
-    void ProcessStatement(const Statement* stmt) {
+    void ProcessStatement(const ast::Statement* stmt) {
         if (stmt == nullptr || !HasBehavior(ctx.src, stmt, sem::Behavior::kReturn)) {
             return;
         }
 
         Switch(
-            stmt, [&](const BlockStatement* block) { ProcessBlock(block); },
-            [&](const CaseStatement* c) { ProcessStatement(c->body); },
-            [&](const ForLoopStatement* f) {
+            stmt, [&](const ast::BlockStatement* block) { ProcessBlock(block); },
+            [&](const ast::CaseStatement* c) { ProcessStatement(c->body); },
+            [&](const ast::ForLoopStatement* f) {
                 TINT_SCOPED_ASSIGNMENT(is_in_loop_or_switch, true);
                 ProcessStatement(f->body);
             },
-            [&](const IfStatement* i) {
+            [&](const ast::IfStatement* i) {
                 ProcessStatement(i->body);
                 ProcessStatement(i->else_statement);
             },
-            [&](const LoopStatement* l) {
+            [&](const ast::LoopStatement* l) {
                 TINT_SCOPED_ASSIGNMENT(is_in_loop_or_switch, true);
                 ProcessStatement(l->body);
             },
-            [&](const ReturnStatement* r) {
-                tint::Vector<const Statement*, 3> stmts;
+            [&](const ast::ReturnStatement* r) {
+                Vector<const ast::Statement*, 3> stmts;
                 // Set the return flag to signal that we have hit a return.
                 stmts.Push(b.Assign(b.Expr(flag), true));
                 if (r->value) {
@@ -132,25 +132,25 @@
                 }
                 ctx.Replace(r, b.Block(std::move(stmts)));
             },
-            [&](const SwitchStatement* s) {
+            [&](const ast::SwitchStatement* s) {
                 TINT_SCOPED_ASSIGNMENT(is_in_loop_or_switch, true);
                 for (auto* c : s->body) {
                     ProcessStatement(c);
                 }
             },
-            [&](const WhileStatement* w) {
+            [&](const ast::WhileStatement* w) {
                 TINT_SCOPED_ASSIGNMENT(is_in_loop_or_switch, true);
                 ProcessStatement(w->body);
             },
             [&](Default) { TINT_ICE() << "unhandled statement type"; });
     }
 
-    void ProcessBlock(const BlockStatement* block) {
+    void ProcessBlock(const ast::BlockStatement* block) {
         // We will rebuild the contents of the block statement.
         // We may introduce conditionals around statements that follow a statement with the
         // `Return` behavior, so build a stack of statement lists that represent the new
         // (potentially nested) conditional blocks.
-        tint::Vector<tint::Vector<const Statement*, 8>, 8> new_stmts({{}});
+        Vector<Vector<const ast::Statement*, 8>, 8> new_stmts({{}});
 
         // Insert variables for the return flag and return value at the top of the function.
         if (block == function->body) {
@@ -175,12 +175,12 @@
                 if (is_in_loop_or_switch) {
                     // We're in a loop/switch, and so we would have inserted a `break`.
                     // If we've just come out of a loop/switch statement, we need to `break` again.
-                    if (s->IsAnyOf<LoopStatement, ForLoopStatement, SwitchStatement>()) {
+                    if (s->IsAnyOf<ast::LoopStatement, ast::ForLoopStatement,
+                                   ast::SwitchStatement>()) {
                         // If the loop only has the 'Return' behavior, we can just unconditionally
                         // break. Otherwise check the return flag.
                         if (HasBehavior(ctx.src, s, sem::Behavior::kNext)) {
-                            new_stmts.Back().Push(
-                                b.If(b.Expr(flag), b.Block(tint::Vector{b.Break()})));
+                            new_stmts.Back().Push(b.If(b.Expr(flag), b.Block(Vector{b.Break()})));
                         } else {
                             new_stmts.Back().Push(b.Break());
                         }
@@ -195,7 +195,7 @@
 
         // Descend the stack of new block statements, wrapping them in conditionals.
         while (new_stmts.Length() > 1) {
-            const IfStatement* i = nullptr;
+            const ast::IfStatement* i = nullptr;
             if (new_stmts.Back().Length() > 0) {
                 i = b.If(b.Not(b.Expr(flag)), b.Block(new_stmts.Back()));
             }
@@ -216,7 +216,9 @@
 
 }  // namespace
 
-Transform::ApplyResult MergeReturn::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult MergeReturn::Apply(const Program* src,
+                                                          const ast::transform::DataMap&,
+                                                          ast::transform::DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
@@ -240,4 +242,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/merge_return.h b/src/tint/lang/spirv/writer/ast_raise/merge_return.h
similarity index 62%
rename from src/tint/lang/wgsl/ast/transform/merge_return.h
rename to src/tint/lang/spirv/writer/ast_raise/merge_return.h
index 07e085d..4a5d324 100644
--- a/src/tint/lang/wgsl/ast/transform/merge_return.h
+++ b/src/tint/lang/spirv/writer/ast_raise/merge_return.h
@@ -12,27 +12,27 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_MERGE_RETURN_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_MERGE_RETURN_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_MERGE_RETURN_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_MERGE_RETURN_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 /// Merge return statements into a single return at the end of the function.
-class MergeReturn final : public Castable<MergeReturn, Transform> {
+class MergeReturn final : public Castable<MergeReturn, ast::transform::Transform> {
   public:
     /// Constructor
     MergeReturn();
     /// Destructor
     ~MergeReturn() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_MERGE_RETURN_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_MERGE_RETURN_H_
diff --git a/src/tint/lang/wgsl/ast/transform/merge_return_test.cc b/src/tint/lang/spirv/writer/ast_raise/merge_return_test.cc
similarity index 98%
rename from src/tint/lang/wgsl/ast/transform/merge_return_test.cc
rename to src/tint/lang/spirv/writer/ast_raise/merge_return_test.cc
index 4d486bc..1777234 100644
--- a/src/tint/lang/wgsl/ast/transform/merge_return_test.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/merge_return_test.cc
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/merge_return.h"
+#include "src/tint/lang/spirv/writer/ast_raise/merge_return.h"
 
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
-using MergeReturnTest = TransformTest;
+using MergeReturnTest = ast::transform::TransformTest;
 
 TEST_F(MergeReturnTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -857,4 +857,4 @@
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
similarity index 67%
rename from src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
rename to src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
index 70df29f..e16172f 100644
--- a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.h"
+#include "src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h"
 
 #include <utility>
 
@@ -21,31 +21,33 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::VarForDynamicIndex);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::VarForDynamicIndex);
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 VarForDynamicIndex::VarForDynamicIndex() = default;
 
 VarForDynamicIndex::~VarForDynamicIndex() = default;
 
-Transform::ApplyResult VarForDynamicIndex::Apply(const Program* src,
-                                                 const DataMap&,
-                                                 DataMap&) const {
+ast::transform::Transform::ApplyResult VarForDynamicIndex::Apply(const Program* src,
+                                                                 const ast::transform::DataMap&,
+                                                                 ast::transform::DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
-    HoistToDeclBefore hoist_to_decl_before(ctx);
+    ast::transform::HoistToDeclBefore hoist_to_decl_before(ctx);
 
     // Extracts array and matrix values that are dynamically indexed to a
     // temporary `var` local that is then indexed.
-    auto dynamic_index_to_var = [&](const IndexAccessorExpression* access_expr) {
+    auto dynamic_index_to_var = [&](const ast::IndexAccessorExpression* access_expr) {
         auto* index_expr = access_expr->index;
         auto* object_expr = access_expr->object;
         auto& sem = src->Sem();
 
-        if (sem.GetVal(index_expr)->ConstantValue()) {
-            // Index expression resolves to a compile time value.
+        auto stage = sem.GetVal(index_expr)->Stage();
+        if (stage == core::EvaluationStage::kConstant ||
+            stage == core::EvaluationStage::kNotEvaluated) {
+            // Index expression resolves to a compile time value or will not be evaluated.
             // As this isn't a dynamic index, we can ignore this.
             return true;
         }
@@ -58,13 +60,14 @@
 
         // TODO(bclayton): group multiple accesses in the same object.
         // e.g. arr[i] + arr[i+1] // Don't create two vars for this
-        return hoist_to_decl_before.Add(indexed, object_expr, HoistToDeclBefore::VariableKind::kVar,
+        return hoist_to_decl_before.Add(indexed, object_expr,
+                                        ast::transform::HoistToDeclBefore::VariableKind::kVar,
                                         "var_for_index");
     };
 
     bool index_accessor_found = false;
     for (auto* node : src->ASTNodes().Objects()) {
-        if (auto* access_expr = node->As<IndexAccessorExpression>()) {
+        if (auto* access_expr = node->As<ast::IndexAccessorExpression>()) {
             if (!dynamic_index_to_var(access_expr)) {
                 return resolver::Resolve(b);
             }
@@ -79,4 +82,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.h b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h
similarity index 69%
rename from src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.h
rename to src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h
index fa1d612..44b1409 100644
--- a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.h
+++ b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h
@@ -12,18 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_VAR_FOR_DYNAMIC_INDEX_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_VAR_FOR_DYNAMIC_INDEX_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_VAR_FOR_DYNAMIC_INDEX_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_VAR_FOR_DYNAMIC_INDEX_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 /// A transform that extracts array and matrix values that are dynamically
 /// indexed to a temporary `var` local before performing the index. This
 /// transform is used by the SPIR-V writer as there is no SPIR-V instruction
 /// that can dynamically index a non-pointer composite.
-class VarForDynamicIndex final : public Castable<VarForDynamicIndex, Transform> {
+class VarForDynamicIndex final : public Castable<VarForDynamicIndex, ast::transform::Transform> {
   public:
     /// Constructor
     VarForDynamicIndex();
@@ -31,12 +31,12 @@
     /// Destructor
     ~VarForDynamicIndex() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_VAR_FOR_DYNAMIC_INDEX_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_VAR_FOR_DYNAMIC_INDEX_H_
diff --git a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index_test.cc b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index_test.cc
similarity index 87%
rename from src/tint/lang/wgsl/ast/transform/var_for_dynamic_index_test.cc
rename to src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index_test.cc
index 84a5d9b..dc27d18 100644
--- a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index_test.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index_test.cc
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.h"
-#include "src/tint/lang/wgsl/ast/transform/for_loop_to_loop.h"
+#include "src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h"
+#include "src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
-using VarForDynamicIndexTest = TransformTest;
+using VarForDynamicIndexTest = ast::transform::TransformTest;
 
 TEST_F(VarForDynamicIndexTest, EmptyModule) {
     auto* src = "";
@@ -49,7 +49,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -73,7 +73,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -105,7 +105,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -138,7 +138,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -171,7 +171,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -204,7 +204,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -237,7 +237,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -277,7 +277,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -311,7 +311,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -364,7 +364,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -398,7 +398,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -451,7 +451,7 @@
 }
 )";
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -467,7 +467,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -483,7 +483,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -501,7 +501,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -519,7 +519,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -537,7 +537,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -553,11 +553,37 @@
 
     auto* expect = src;
 
-    DataMap data;
+    ast::transform::DataMap data;
+    auto got = Run<VarForDynamicIndex>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(VarForDynamicIndexTest, ShortCircuitedArrayAccess) {
+    auto* src = R"(
+const foo = (false && (array<f32, 4>()[0] == 0));
+)";
+
+    auto* expect = src;
+
+    ast::transform::DataMap data;
+    auto got = Run<VarForDynamicIndex>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(VarForDynamicIndexTest, ShortCircuitedMatrixAccess) {
+    auto* src = R"(
+const foo = (false && (mat4x4<f32>()[0][0] == 0));
+)";
+
+    auto* expect = src;
+
+    ast::transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
 }
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
similarity index 88%
rename from src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
rename to src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
index bb00bc0..13ab74b 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.h"
+#include "src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h"
 
 #include <tuple>
 #include <unordered_map>
@@ -31,9 +31,9 @@
 
 using namespace tint::core::fluent_types;  // NOLINT
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::VectorizeMatrixConversions);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::VectorizeMatrixConversions);
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
 bool ShouldRun(const Program* program) {
@@ -59,9 +59,10 @@
 
 VectorizeMatrixConversions::~VectorizeMatrixConversions() = default;
 
-Transform::ApplyResult VectorizeMatrixConversions::Apply(const Program* src,
-                                                         const DataMap&,
-                                                         DataMap&) const {
+ast::transform::Transform::ApplyResult VectorizeMatrixConversions::Apply(
+    const Program* src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -74,7 +75,7 @@
 
     std::unordered_map<HelperFunctionKey, Symbol> matrix_convs;
 
-    ctx.ReplaceAll([&](const CallExpression* expr) -> const CallExpression* {
+    ctx.ReplaceAll([&](const ast::CallExpression* expr) -> const ast::CallExpression* {
         auto* call = src->Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
         auto* ty_conv = call->Target()->As<sem::ValueConversion>();
         if (!ty_conv) {
@@ -105,7 +106,7 @@
         }
 
         auto build_vectorized_conversion_expression = [&](auto&& src_expression_builder) {
-            tint::Vector<const Expression*, 4> columns;
+            Vector<const ast::Expression*, 4> columns;
             for (uint32_t c = 0; c < dst_type->columns(); c++) {
                 auto* src_matrix_expr = src_expression_builder();
                 auto* src_column_expr = b.IndexAccessor(src_matrix_expr, b.Expr(AInt(c)));
@@ -129,11 +130,11 @@
                                             src_type->type()->FriendlyName() + "_" +
                                             dst_type->type()->FriendlyName());
                 b.Func(name,
-                       tint::Vector{
+                       Vector{
                            b.Param("value", CreateASTTypeFor(ctx, src_type)),
                        },
                        CreateASTTypeFor(ctx, dst_type),
-                       tint::Vector{
+                       Vector{
                            b.Return(build_vectorized_conversion_expression([&] {  //
                                return b.Expr("value");
                            })),
@@ -148,4 +149,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.h b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
similarity index 60%
rename from src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.h
rename to src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
index d08c0cc..8e4e795 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.h
+++ b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
@@ -12,15 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_VECTORIZE_MATRIX_CONVERSIONS_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_VECTORIZE_MATRIX_CONVERSIONS_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_VECTORIZE_MATRIX_CONVERSIONS_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_VECTORIZE_MATRIX_CONVERSIONS_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 /// A transform that converts matrix conversions (between f32 and f16 matrices) to the vector form.
-class VectorizeMatrixConversions final : public Castable<VectorizeMatrixConversions, Transform> {
+class VectorizeMatrixConversions final
+    : public Castable<VectorizeMatrixConversions, ast::transform::Transform> {
   public:
     /// Constructor
     VectorizeMatrixConversions();
@@ -28,12 +29,12 @@
     /// Destructor
     ~VectorizeMatrixConversions() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_VECTORIZE_MATRIX_CONVERSIONS_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_VECTORIZE_MATRIX_CONVERSIONS_H_
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions_test.cc b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions_test.cc
similarity index 97%
rename from src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions_test.cc
rename to src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions_test.cc
index 12a414e..fc1bcbc 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions_test.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.h"
+#include "src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h"
 
 #include <string>
 #include <utility>
@@ -20,10 +20,11 @@
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 #include "src/tint/utils/text/string.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
-using VectorizeMatrixConversionsTest = TransformTestWithParam<std::pair<uint32_t, uint32_t>>;
+using VectorizeMatrixConversionsTest =
+    ast::transform::TransformTestWithParam<std::pair<uint32_t, uint32_t>>;
 
 TEST_F(VectorizeMatrixConversionsTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -408,4 +409,4 @@
                                          std::make_pair(4, 4)));
 
 }  // namespace
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/while_to_loop.cc b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc
similarity index 71%
rename from src/tint/lang/wgsl/ast/transform/while_to_loop.cc
rename to src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc
index 93b1ee7..942214f 100644
--- a/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/while_to_loop.h"
+#include "src/tint/lang/spirv/writer/ast_raise/while_to_loop.h"
 
 #include <utility>
 
@@ -21,14 +21,14 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::WhileToLoop);
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::WhileToLoop);
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
 bool ShouldRun(const Program* program) {
     for (auto* node : program->ASTNodes().Objects()) {
-        if (node->Is<WhileStatement>()) {
+        if (node->Is<ast::WhileStatement>()) {
             return true;
         }
     }
@@ -41,7 +41,9 @@
 
 WhileToLoop::~WhileToLoop() = default;
 
-Transform::ApplyResult WhileToLoop::Apply(const Program* src, const DataMap&, DataMap&) const {
+ast::transform::Transform::ApplyResult WhileToLoop::Apply(const Program* src,
+                                                          const ast::transform::DataMap&,
+                                                          ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
@@ -49,8 +51,8 @@
     ProgramBuilder b;
     program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
-    ctx.ReplaceAll([&](const WhileStatement* w) -> const Statement* {
-        tint::Vector<const Statement*, 16> stmts;
+    ctx.ReplaceAll([&](const ast::WhileStatement* w) -> const ast::Statement* {
+        tint::Vector<const ast::Statement*, 16> stmts;
         auto* cond = w->condition;
 
         // !condition
@@ -66,7 +68,7 @@
             stmts.Push(ctx.Clone(stmt));
         }
 
-        const BlockStatement* continuing = nullptr;
+        const ast::BlockStatement* continuing = nullptr;
 
         auto* body = b.Block(stmts);
         auto* loop = b.Loop(body, continuing);
@@ -78,4 +80,4 @@
     return resolver::Resolve(b);
 }
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/ast/transform/while_to_loop.h b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.h
similarity index 63%
rename from src/tint/lang/wgsl/ast/transform/while_to_loop.h
rename to src/tint/lang/spirv/writer/ast_raise/while_to_loop.h
index 2a60203..e86fa6e 100644
--- a/src/tint/lang/wgsl/ast/transform/while_to_loop.h
+++ b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.h
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_WHILE_TO_LOOP_H_
-#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_WHILE_TO_LOOP_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_WHILE_TO_LOOP_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_WHILE_TO_LOOP_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 
 /// WhileToLoop is a Transform that converts a while statement into a loop
 /// statement. This is required by the SPIR-V writer.
-class WhileToLoop final : public Castable<WhileToLoop, Transform> {
+class WhileToLoop final : public Castable<WhileToLoop, ast::transform::Transform> {
   public:
     /// Constructor
     WhileToLoop();
@@ -29,12 +29,12 @@
     /// Destructor
     ~WhileToLoop() override;
 
-    /// @copydoc Transform::Apply
+    /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const DataMap& inputs,
-                      DataMap& outputs) const override;
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
 };
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_WHILE_TO_LOOP_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_WHILE_TO_LOOP_H_
diff --git a/src/tint/lang/wgsl/ast/transform/while_to_loop_test.cc b/src/tint/lang/spirv/writer/ast_raise/while_to_loop_test.cc
similarity index 91%
rename from src/tint/lang/wgsl/ast/transform/while_to_loop_test.cc
rename to src/tint/lang/spirv/writer/ast_raise/while_to_loop_test.cc
index fc84975..0731680 100644
--- a/src/tint/lang/wgsl/ast/transform/while_to_loop_test.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/while_to_loop_test.cc
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/wgsl/ast/transform/while_to_loop.h"
+#include "src/tint/lang/spirv/writer/ast_raise/while_to_loop.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::ast::transform {
+namespace tint::spirv::writer {
 namespace {
 
-using WhileToLoopTest = TransformTest;
+using WhileToLoopTest = ast::transform::TransformTest;
 
 TEST_F(WhileToLoopTest, ShouldRunEmptyModule) {
     auto* src = R"()";
@@ -126,4 +126,4 @@
 
 }  // namespace
 
-}  // namespace tint::ast::transform
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/atomic_builtin_test.cc b/src/tint/lang/spirv/writer/atomic_builtin_test.cc
index 349143d..5352fd1 100644
--- a/src/tint/lang/spirv/writer/atomic_builtin_test.cc
+++ b/src/tint/lang/spirv/writer/atomic_builtin_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/core/type/builtin_structs.h"
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
diff --git a/src/tint/lang/spirv/writer/binary_test.cc b/src/tint/lang/spirv/writer/binary_test.cc
index 4e11b69..8e3fe2e 100644
--- a/src/tint/lang/spirv/writer/binary_test.cc
+++ b/src/tint/lang/spirv/writer/binary_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 #include "src/tint/lang/core/fluent_types.h"
diff --git a/src/tint/lang/spirv/writer/bitcast_test.cc b/src/tint/lang/spirv/writer/bitcast_test.cc
index 3506bab..c30a2d1 100644
--- a/src/tint/lang/spirv/writer/bitcast_test.cc
+++ b/src/tint/lang/spirv/writer/bitcast_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
diff --git a/src/tint/lang/spirv/writer/builtin_test.cc b/src/tint/lang/spirv/writer/builtin_test.cc
index b28400e..a725e1a 100644
--- a/src/tint/lang/spirv/writer/builtin_test.cc
+++ b/src/tint/lang/spirv/writer/builtin_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 #include "src/tint/lang/core/function.h"
diff --git a/src/tint/lang/spirv/writer/common/BUILD.bazel b/src/tint/lang/spirv/writer/common/BUILD.bazel
new file mode 100644
index 0000000..db94b5b
--- /dev/null
+++ b/src/tint/lang/spirv/writer/common/BUILD.bazel
@@ -0,0 +1,133 @@
+# 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 = "common",
+  srcs = [
+    "binary_writer.cc",
+    "function.cc",
+    "instruction.cc",
+    "module.cc",
+    "operand.cc",
+  ],
+  hdrs = [
+    "binary_writer.h",
+    "function.h",
+    "instruction.h",
+    "module.h",
+    "operand.h",
+    "options.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "binary_writer_test.cc",
+    "helper_test.h",
+    "instruction_test.cc",
+    "module_test.cc",
+    "operand_test.cc",
+    "spv_dump_test.cc",
+    "spv_dump_test.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/intrinsic/data",
+    "//src/tint/lang/spirv/ir",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/printer",
+      "//src/tint/lang/spirv/writer/raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/writer/common/BUILD.cmake b/src/tint/lang/spirv/writer/common/BUILD.cmake
index de89ec2..9a74b57 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/common/BUILD.cmake
@@ -67,6 +67,7 @@
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_common_test test
   lang/spirv/writer/common/binary_writer_test.cc
+  lang/spirv/writer/common/helper_test.h
   lang/spirv/writer/common/instruction_test.cc
   lang/spirv/writer/common/module_test.cc
   lang/spirv/writer/common/operand_test.cc
@@ -79,7 +80,12 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
+  tint_lang_spirv_intrinsic_data
+  tint_lang_spirv_ir
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -99,15 +105,6 @@
   "gtest"
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_sources(tint_lang_spirv_writer_common_test test
-    "lang/spirv/writer/common/helper_test.h"
-  )
-  tint_target_add_dependencies(tint_lang_spirv_writer_common_test test
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_lang_spirv_writer_common_test test
     "spirv-headers"
@@ -118,14 +115,9 @@
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_lang_spirv_writer_common_test test
     tint_lang_spirv_writer_common
-  )
-endif(TINT_BUILD_SPV_WRITER)
-
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer_common_test test
     tint_lang_spirv_writer_printer
     tint_lang_spirv_writer_raise
   )
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_SPV_WRITER)
 
 endif(TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/writer/common/BUILD.gn b/src/tint/lang/spirv/writer/common/BUILD.gn
index 0d00bd5..c15d605 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.gn
+++ b/src/tint/lang/spirv/writer/common/BUILD.gn
@@ -65,6 +65,7 @@
       testonly = true
       sources = [
         "binary_writer_test.cc",
+        "helper_test.h",
         "instruction_test.cc",
         "module_test.cc",
         "operand_test.cc",
@@ -77,7 +78,12 @@
         "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/intrinsic",
+        "${tint_src_dir}/lang/core/intrinsic/data",
+        "${tint_src_dir}/lang/core/ir",
         "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/lang/spirv/intrinsic/data",
+        "${tint_src_dir}/lang/spirv/ir",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -93,11 +99,6 @@
         "${tint_src_dir}/utils/traits",
       ]
 
-      if (tint_build_ir) {
-        sources += [ "helper_test.h" ]
-        deps += [ "${tint_src_dir}/lang/core/ir" ]
-      }
-
       if (tint_build_spv_reader || tint_build_spv_writer) {
         deps += [
           "${tint_spirv_headers_dir}:spv_headers",
@@ -107,11 +108,8 @@
       }
 
       if (tint_build_spv_writer) {
-        deps += [ "${tint_src_dir}/lang/spirv/writer/common" ]
-      }
-
-      if (tint_build_spv_writer && tint_build_ir) {
         deps += [
+          "${tint_src_dir}/lang/spirv/writer/common",
           "${tint_src_dir}/lang/spirv/writer/printer",
           "${tint_src_dir}/lang/spirv/writer/raise",
         ]
diff --git a/src/tint/lang/spirv/writer/common/helper_test.h b/src/tint/lang/spirv/writer/common/helper_test.h
index e534c6a..5e727a9 100644
--- a/src/tint/lang/spirv/writer/common/helper_test.h
+++ b/src/tint/lang/spirv/writer/common/helper_test.h
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #ifndef SRC_TINT_LANG_SPIRV_WRITER_COMMON_HELPER_TEST_H_
 #define SRC_TINT_LANG_SPIRV_WRITER_COMMON_HELPER_TEST_H_
 
@@ -102,9 +100,10 @@
 
     /// Run the specified writer on the IR module and validate the result.
     /// @param writer the writer to use for SPIR-V generation
+    /// @param options the optional writer options to use when raising the IR
     /// @returns true if generation and validation succeeded
-    bool Generate(Printer& writer) {
-        auto raised = raise::Raise(&mod, {});
+    bool Generate(Printer& writer, Options options = {}) {
+        auto raised = raise::Raise(&mod, options);
         if (!raised) {
             err_ = raised.Failure();
             return false;
@@ -128,8 +127,9 @@
     }
 
     /// Run the writer on the IR module and validate the result.
+    /// @param options the optional writer options to use when raising the IR
     /// @returns true if generation and validation succeeded
-    bool Generate() { return Generate(writer_); }
+    bool Generate(Options options = {}) { return Generate(writer_, options); }
 
     /// Validate the generated SPIR-V using the SPIR-V Tools Validator.
     /// @param binary the SPIR-V binary module to validate
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index 8741fa7..fce8659 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -52,19 +52,26 @@
     /// Set to `true` to disable index clamping on the runtime-sized arrays in robustness transform.
     bool disable_runtime_sized_array_index_clamping = false;
 
-#if TINT_BUILD_IR
     /// Set to `true` to generate SPIR-V via the Tint IR instead of from the AST.
     bool use_tint_ir = false;
-#endif
+
+    /// 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;
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
                  emit_vertex_point_size,
                  disable_workgroup_init,
+                 clamp_frag_depth,
                  external_texture_options,
+                 binding_remapper_options,
                  use_zero_initialize_workgroup_memory_extension,
                  disable_image_robustness,
-                 disable_runtime_sized_array_index_clamping);
+                 disable_runtime_sized_array_index_clamping,
+                 use_tint_ir,
+                 experimental_require_subgroup_uniform_control_flow);
 };
 
 }  // 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 9b26464..e54f8ef 100644
--- a/src/tint/lang/spirv/writer/constant_test.cc
+++ b/src/tint/lang/spirv/writer/constant_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/core/fluent_types.h"
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
diff --git a/src/tint/lang/spirv/writer/construct_test.cc b/src/tint/lang/spirv/writer/construct_test.cc
index b8035b9..6d81b72 100644
--- a/src/tint/lang/spirv/writer/construct_test.cc
+++ b/src/tint/lang/spirv/writer/construct_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
diff --git a/src/tint/lang/spirv/writer/convert_test.cc b/src/tint/lang/spirv/writer/convert_test.cc
index c1c3344..10fb71a 100644
--- a/src/tint/lang/spirv/writer/convert_test.cc
+++ b/src/tint/lang/spirv/writer/convert_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
diff --git a/src/tint/lang/spirv/writer/discard_test.cc b/src/tint/lang/spirv/writer/discard_test.cc
index 74d383d..5fe3f7c 100644
--- a/src/tint/lang/spirv/writer/discard_test.cc
+++ b/src/tint/lang/spirv/writer/discard_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 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 6ab2c34..0f41ed6 100644
--- a/src/tint/lang/spirv/writer/function_test.cc
+++ b/src/tint/lang/spirv/writer/function_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
diff --git a/src/tint/lang/spirv/writer/if_test.cc b/src/tint/lang/spirv/writer/if_test.cc
index 2dae65a..152e583 100644
--- a/src/tint/lang/spirv/writer/if_test.cc
+++ b/src/tint/lang/spirv/writer/if_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
diff --git a/src/tint/lang/spirv/writer/let_test.cc b/src/tint/lang/spirv/writer/let_test.cc
index 01863ff..2794ef9 100644
--- a/src/tint/lang/spirv/writer/let_test.cc
+++ b/src/tint/lang/spirv/writer/let_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
diff --git a/src/tint/lang/spirv/writer/loop_test.cc b/src/tint/lang/spirv/writer/loop_test.cc
index b7e8d8f..1a44fdd 100644
--- a/src/tint/lang/spirv/writer/loop_test.cc
+++ b/src/tint/lang/spirv/writer/loop_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.bazel b/src/tint/lang/spirv/writer/printer/BUILD.bazel
new file mode 100644
index 0000000..338ee9f
--- /dev/null
+++ b/src/tint/lang/spirv/writer/printer/BUILD.bazel
@@ -0,0 +1,95 @@
+# 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 = "printer",
+  srcs = [
+    "printer.cc",
+  ],
+  hdrs = [
+    "printer.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/intrinsic/data",
+    "//src/tint/lang/spirv/ir",
+    "//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_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/ast_printer",
+      "//src/tint/lang/spirv/writer/common",
+      "//src/tint/lang/spirv/writer/raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.cfg b/src/tint/lang/spirv/writer/printer/BUILD.cfg
index bc2bb91..0a24987 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.cfg
+++ b/src/tint/lang/spirv/writer/printer/BUILD.cfg
@@ -1,3 +1,3 @@
 {
-    "condition": "tint_build_spv_writer && tint_build_ir"
+    "condition": "tint_build_spv_writer"
 }
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.cmake b/src/tint/lang/spirv/writer/printer/BUILD.cmake
index fc7ac77..cecda96 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/printer/BUILD.cmake
@@ -21,11 +21,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+if(TINT_BUILD_SPV_WRITER)
 ################################################################################
 # Target:    tint_lang_spirv_writer_printer
 # Kind:      lib
-# Condition: TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR
+# Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_printer lib
   lang/spirv/writer/printer/printer.cc
@@ -37,7 +37,12 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
+  tint_lang_spirv_intrinsic_data
+  tint_lang_spirv_ir
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -56,12 +61,6 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer_printer lib
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_lang_spirv_writer_printer lib
     "spirv-headers"
@@ -72,13 +71,8 @@
   tint_target_add_dependencies(tint_lang_spirv_writer_printer lib
     tint_lang_spirv_writer_ast_printer
     tint_lang_spirv_writer_common
+    tint_lang_spirv_writer_raise
   )
 endif(TINT_BUILD_SPV_WRITER)
 
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer_printer lib
-    tint_lang_spirv_writer_raise
-  )
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
\ No newline at end of file
+endif(TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.gn b/src/tint/lang/spirv/writer/printer/BUILD.gn
index d7f4df9..9cc2ff0 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/printer/BUILD.gn
@@ -24,7 +24,7 @@
 import("../../../../../../scripts/tint_overrides_with_defaults.gni")
 
 import("${tint_src_dir}/tint.gni")
-if (tint_build_spv_writer && tint_build_ir) {
+if (tint_build_spv_writer) {
   libtint_source_set("printer") {
     sources = [
       "printer.cc",
@@ -35,7 +35,12 @@
       "${tint_src_dir}/api/options",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/intrinsic/data",
+      "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/spirv/intrinsic/data",
+      "${tint_src_dir}/lang/spirv/ir",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -54,10 +59,6 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/core/ir" ]
-    }
-
     if (tint_build_spv_reader || tint_build_spv_writer) {
       deps += [ "${tint_spirv_headers_dir}:spv_headers" ]
     }
@@ -66,11 +67,8 @@
       deps += [
         "${tint_src_dir}/lang/spirv/writer/ast_printer",
         "${tint_src_dir}/lang/spirv/writer/common",
+        "${tint_src_dir}/lang/spirv/writer/raise",
       ]
     }
-
-    if (tint_build_spv_writer && tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/spirv/writer/raise" ]
-    }
   }
 }
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index c5c275b..eba5239 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -74,6 +74,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/intrinsic_call.h"
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
 #include "src/tint/lang/spirv/writer/common/module.h"
 #include "src/tint/lang/spirv/writer/raise/builtin_polyfill.h"
@@ -762,9 +763,10 @@
             [&](core::ir::Binary* b) { EmitBinary(b); },                          //
             [&](core::ir::Bitcast* b) { EmitBitcast(b); },                        //
             [&](core::ir::CoreBuiltinCall* b) { EmitCoreBuiltinCall(b); },        //
+            [&](spirv::ir::BuiltinCall* b) { EmitSpirvBuiltinCall(b); },          //
             [&](core::ir::Construct* c) { EmitConstruct(c); },                    //
             [&](core::ir::Convert* c) { EmitConvert(c); },                        //
-            [&](core::ir::IntrinsicCall* i) { EmitIntrinsicCall(i); },            //
+            [&](spirv::ir::IntrinsicCall* i) { EmitIntrinsicCall(i); },           //
             [&](core::ir::Load* l) { EmitLoad(l); },                              //
             [&](core::ir::LoadVectorElement* l) { EmitLoadVectorElement(l); },    //
             [&](core::ir::Loop* l) { EmitLoop(l); },                              //
@@ -1085,6 +1087,89 @@
                                 {Type(ty), Value(bitcast), Value(bitcast->Val())});
 }
 
+void Printer::EmitSpirvBuiltinCall(spirv::ir::BuiltinCall* builtin) {
+    auto id = Value(builtin);
+
+    spv::Op op = spv::Op::Max;
+    switch (builtin->Func()) {
+        case spirv::ir::Function::kArrayLength:
+            op = spv::Op::OpArrayLength;
+            break;
+        case spirv::ir::Function::kAtomicIadd:
+            op = spv::Op::OpAtomicIAdd;
+            break;
+        case spirv::ir::Function::kAtomicIsub:
+            op = spv::Op::OpAtomicISub;
+            break;
+        case spirv::ir::Function::kAtomicAnd:
+            op = spv::Op::OpAtomicAnd;
+            break;
+        case spirv::ir::Function::kAtomicCompareExchange:
+            op = spv::Op::OpAtomicCompareExchange;
+            break;
+        case spirv::ir::Function::kAtomicExchange:
+            op = spv::Op::OpAtomicExchange;
+            break;
+        case spirv::ir::Function::kAtomicLoad:
+            op = spv::Op::OpAtomicLoad;
+            break;
+        case spirv::ir::Function::kAtomicOr:
+            op = spv::Op::OpAtomicOr;
+            break;
+        case spirv::ir::Function::kAtomicSmax:
+            op = spv::Op::OpAtomicSMax;
+            break;
+        case spirv::ir::Function::kAtomicSmin:
+            op = spv::Op::OpAtomicSMin;
+            break;
+        case spirv::ir::Function::kAtomicStore:
+            op = spv::Op::OpAtomicStore;
+            break;
+        case spirv::ir::Function::kAtomicUmax:
+            op = spv::Op::OpAtomicUMax;
+            break;
+        case spirv::ir::Function::kAtomicUmin:
+            op = spv::Op::OpAtomicUMin;
+            break;
+        case spirv::ir::Function::kAtomicXor:
+            op = spv::Op::OpAtomicXor;
+            break;
+        case spirv::ir::Function::kDot:
+            op = spv::Op::OpDot;
+            break;
+        case spirv::ir::Function::kMatrixTimesMatrix:
+            op = spv::Op::OpMatrixTimesMatrix;
+            break;
+        case spirv::ir::Function::kMatrixTimesScalar:
+            op = spv::Op::OpMatrixTimesScalar;
+            break;
+        case spirv::ir::Function::kMatrixTimesVector:
+            op = spv::Op::OpMatrixTimesVector;
+            break;
+        case spirv::ir::Function::kSelect:
+            op = spv::Op::OpSelect;
+            break;
+        case spirv::ir::Function::kVectorTimesMatrix:
+            op = spv::Op::OpVectorTimesMatrix;
+            break;
+        case spirv::ir::Function::kVectorTimesScalar:
+            op = spv::Op::OpVectorTimesScalar;
+            break;
+        case spirv::ir::Function::kNone:
+            TINT_ICE() << "undefined spirv ir function";
+            return;
+    }
+
+    OperandList operands;
+    if (!builtin->Result()->Type()->Is<core::type::Void>()) {
+        operands = {Type(builtin->Result()->Type()), id};
+    }
+    for (auto* arg : builtin->Args()) {
+        operands.push_back(Value(arg));
+    }
+    current_function_.push_inst(op, operands);
+}
+
 void Printer::EmitCoreBuiltinCall(core::ir::CoreBuiltinCall* builtin) {
     auto* result_ty = builtin->Result()->Type();
 
@@ -1529,109 +1614,52 @@
     current_function_.push_inst(op, std::move(operands));
 }
 
-void Printer::EmitIntrinsicCall(core::ir::IntrinsicCall* call) {
+void Printer::EmitIntrinsicCall(spirv::ir::IntrinsicCall* call) {
     auto id = Value(call);
 
     spv::Op op = spv::Op::Max;
     switch (call->Kind()) {
-        case core::ir::IntrinsicCall::Kind::kSpirvArrayLength:
-            op = spv::Op::OpArrayLength;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicIAdd:
-            op = spv::Op::OpAtomicIAdd;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicISub:
-            op = spv::Op::OpAtomicISub;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicAnd:
-            op = spv::Op::OpAtomicAnd;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicCompareExchange:
-            op = spv::Op::OpAtomicCompareExchange;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicExchange:
-            op = spv::Op::OpAtomicExchange;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicLoad:
-            op = spv::Op::OpAtomicLoad;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicOr:
-            op = spv::Op::OpAtomicOr;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicSMax:
-            op = spv::Op::OpAtomicSMax;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicSMin:
-            op = spv::Op::OpAtomicSMin;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicStore:
-            op = spv::Op::OpAtomicStore;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicUMax:
-            op = spv::Op::OpAtomicUMax;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicUMin:
-            op = spv::Op::OpAtomicUMin;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvAtomicXor:
-            op = spv::Op::OpAtomicXor;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvDot:
-            op = spv::Op::OpDot;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageFetch:
+        case spirv::ir::Intrinsic::kImageFetch:
             op = spv::Op::OpImageFetch;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageGather:
+        case spirv::ir::Intrinsic::kImageGather:
             op = spv::Op::OpImageGather;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageDrefGather:
+        case spirv::ir::Intrinsic::kImageDrefGather:
             op = spv::Op::OpImageDrefGather;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageQuerySize:
+        case spirv::ir::Intrinsic::kImageQuerySize:
             module_.PushCapability(SpvCapabilityImageQuery);
             op = spv::Op::OpImageQuerySize;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageQuerySizeLod:
+        case spirv::ir::Intrinsic::kImageQuerySizeLod:
             module_.PushCapability(SpvCapabilityImageQuery);
             op = spv::Op::OpImageQuerySizeLod;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageSampleImplicitLod:
+        case spirv::ir::Intrinsic::kImageRead:
+            op = spv::Op::OpImageRead;
+            break;
+        case spirv::ir::Intrinsic::kImageSampleImplicitLod:
             op = spv::Op::OpImageSampleImplicitLod;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageSampleExplicitLod:
+        case spirv::ir::Intrinsic::kImageSampleExplicitLod:
             op = spv::Op::OpImageSampleExplicitLod;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageSampleDrefImplicitLod:
+        case spirv::ir::Intrinsic::kImageSampleDrefImplicitLod:
             op = spv::Op::OpImageSampleDrefImplicitLod;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageSampleDrefExplicitLod:
+        case spirv::ir::Intrinsic::kImageSampleDrefExplicitLod:
             op = spv::Op::OpImageSampleDrefExplicitLod;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvImageWrite:
+        case spirv::ir::Intrinsic::kImageWrite:
             op = spv::Op::OpImageWrite;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesMatrix:
-            op = spv::Op::OpMatrixTimesMatrix;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesScalar:
-            op = spv::Op::OpMatrixTimesScalar;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesVector:
-            op = spv::Op::OpMatrixTimesVector;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvSampledImage:
+        case spirv::ir::Intrinsic::kSampledImage:
             op = spv::Op::OpSampledImage;
             break;
-        case core::ir::IntrinsicCall::Kind::kSpirvSelect:
-            op = spv::Op::OpSelect;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvVectorTimesMatrix:
-            op = spv::Op::OpVectorTimesMatrix;
-            break;
-        case core::ir::IntrinsicCall::Kind::kSpirvVectorTimesScalar:
-            op = spv::Op::OpVectorTimesScalar;
-            break;
+        case spirv::ir::Intrinsic::kUndefined:
+            TINT_ICE() << "undefined spirv intrinsic";
+            return;
     }
 
     OperandList operands;
diff --git a/src/tint/lang/spirv/writer/printer/printer.h b/src/tint/lang/spirv/writer/printer/printer.h
index 4218ade..f991bfe 100644
--- a/src/tint/lang/spirv/writer/printer/printer.h
+++ b/src/tint/lang/spirv/writer/printer/printer.h
@@ -24,6 +24,8 @@
 #include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/constant.h"
 #include "src/tint/lang/core/texel_format.h"
+#include "src/tint/lang/spirv/ir/builtin_call.h"
+#include "src/tint/lang/spirv/ir/intrinsic_call.h"
 #include "src/tint/lang/spirv/writer/common/binary_writer.h"
 #include "src/tint/lang/spirv/writer/common/function.h"
 #include "src/tint/lang/spirv/writer/common/module.h"
@@ -200,6 +202,10 @@
 
     /// Emit a builtin function call instruction.
     /// @param call the builtin call instruction to emit
+    void EmitSpirvBuiltinCall(spirv::ir::BuiltinCall* call);
+
+    /// Emit a builtin function call instruction.
+    /// @param call the builtin call instruction to emit
     void EmitCoreBuiltinCall(core::ir::CoreBuiltinCall* call);
 
     /// Emit a construct instruction.
@@ -212,7 +218,7 @@
 
     /// Emit an intrinsic call instruction.
     /// @param call the intrinsic call instruction to emit
-    void EmitIntrinsicCall(core::ir::IntrinsicCall* call);
+    void EmitIntrinsicCall(spirv::ir::IntrinsicCall* call);
 
     /// Emit a load instruction.
     /// @param load the load instruction to emit
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.bazel b/src/tint/lang/spirv/writer/raise/BUILD.bazel
new file mode 100644
index 0000000..8e185e7
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/BUILD.bazel
@@ -0,0 +1,146 @@
+# 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 = "raise",
+  srcs = [
+    "builtin_polyfill.cc",
+    "expand_implicit_splats.cc",
+    "handle_matrix_arithmetic.cc",
+    "merge_return.cc",
+    "raise.cc",
+    "shader_io.cc",
+    "var_for_dynamic_index.cc",
+  ],
+  hdrs = [
+    "builtin_polyfill.h",
+    "expand_implicit_splats.h",
+    "handle_matrix_arithmetic.h",
+    "merge_return.h",
+    "raise.h",
+    "shader_io.h",
+    "var_for_dynamic_index.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir/transform",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/spirv/intrinsic/data",
+    "//src/tint/lang/spirv/ir",
+    "//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_reader_or_tint_build_spv_writer": [
+      "@spirv_headers//:spirv_cpp11_headers", "@spirv_headers//:spirv_c_headers",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "builtin_polyfill_test.cc",
+    "expand_implicit_splats_test.cc",
+    "handle_matrix_arithmetic_test.cc",
+    "merge_return_test.cc",
+    "shader_io_test.cc",
+    "var_for_dynamic_index_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir/transform:test",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer/raise",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.cfg b/src/tint/lang/spirv/writer/raise/BUILD.cfg
index bc2bb91..0a24987 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.cfg
+++ b/src/tint/lang/spirv/writer/raise/BUILD.cfg
@@ -1,3 +1,3 @@
 {
-    "condition": "tint_build_spv_writer && tint_build_ir"
+    "condition": "tint_build_spv_writer"
 }
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.cmake b/src/tint/lang/spirv/writer/raise/BUILD.cmake
index f5d7856..65a438c 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/raise/BUILD.cmake
@@ -21,11 +21,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+if(TINT_BUILD_SPV_WRITER)
 ################################################################################
 # Target:    tint_lang_spirv_writer_raise
 # Kind:      lib
-# Condition: TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR
+# Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_raise lib
   lang/spirv/writer/raise/builtin_polyfill.cc
@@ -49,7 +49,13 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
+  tint_lang_core_ir_transform
   tint_lang_core_type
+  tint_lang_spirv_intrinsic_data
+  tint_lang_spirv_ir
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -65,13 +71,6 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer_raise lib
-    tint_lang_core_ir
-    tint_lang_core_ir_transform
-  )
-endif(TINT_BUILD_IR)
-
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_lang_spirv_writer_raise lib
     "spirv-headers"
@@ -84,12 +83,12 @@
   )
 endif(TINT_BUILD_SPV_WRITER)
 
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_SPV_WRITER)
+if(TINT_BUILD_SPV_WRITER)
 ################################################################################
 # Target:    tint_lang_spirv_writer_raise_test
 # Kind:      test
-# Condition: TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR
+# Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_raise_test test
   lang/spirv/writer/raise/builtin_polyfill_test.cc
@@ -104,6 +103,10 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
+  tint_lang_core_ir_transform_test
   tint_lang_core_type
   tint_utils_containers
   tint_utils_diagnostic
@@ -124,17 +127,10 @@
   "gtest"
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_spirv_writer_raise_test test
-    tint_lang_core_ir
-    tint_lang_core_ir_transform_test
-  )
-endif(TINT_BUILD_IR)
-
-if(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_lang_spirv_writer_raise_test test
     tint_lang_spirv_writer_raise
   )
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
+endif(TINT_BUILD_SPV_WRITER)
 
-endif(TINT_BUILD_SPV_WRITER AND TINT_BUILD_IR)
\ No newline at end of file
+endif(TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.gn b/src/tint/lang/spirv/writer/raise/BUILD.gn
index 1e3296d..de5a1d6 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.gn
+++ b/src/tint/lang/spirv/writer/raise/BUILD.gn
@@ -28,7 +28,7 @@
 if (tint_build_unittests) {
   import("//testing/test.gni")
 }
-if (tint_build_spv_writer && tint_build_ir) {
+if (tint_build_spv_writer) {
   libtint_source_set("raise") {
     sources = [
       "builtin_polyfill.cc",
@@ -51,7 +51,13 @@
       "${tint_src_dir}/api/options",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/intrinsic/data",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/ir/transform",
       "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/spirv/intrinsic/data",
+      "${tint_src_dir}/lang/spirv/ir",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
@@ -67,13 +73,6 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (tint_build_ir) {
-      deps += [
-        "${tint_src_dir}/lang/core/ir",
-        "${tint_src_dir}/lang/core/ir/transform",
-      ]
-    }
-
     if (tint_build_spv_reader || tint_build_spv_writer) {
       deps += [ "${tint_spirv_headers_dir}:spv_headers" ]
     }
@@ -84,7 +83,7 @@
   }
 }
 if (tint_build_unittests) {
-  if (tint_build_spv_writer && tint_build_ir) {
+  if (tint_build_spv_writer) {
     tint_unittests_source_set("unittests") {
       testonly = true
       sources = [
@@ -100,6 +99,10 @@
         "${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/intrinsic/data",
+        "${tint_src_dir}/lang/core/ir",
+        "${tint_src_dir}/lang/core/ir/transform:unittests",
         "${tint_src_dir}/lang/core/type",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
@@ -116,14 +119,7 @@
         "${tint_src_dir}/utils/traits",
       ]
 
-      if (tint_build_ir) {
-        deps += [
-          "${tint_src_dir}/lang/core/ir",
-          "${tint_src_dir}/lang/core/ir/transform:unittests",
-        ]
-      }
-
-      if (tint_build_spv_writer && tint_build_ir) {
+      if (tint_build_spv_writer) {
         deps += [ "${tint_src_dir}/lang/spirv/writer/raise" ]
       }
     }
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
index bd73280..7a3bd38 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
@@ -28,6 +28,8 @@
 #include "src/tint/lang/core/type/sampled_texture.h"
 #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/intrinsic_call.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::raise::LiteralOperand);
@@ -185,9 +187,9 @@
         auto* const_idx = access->Indices()[0]->As<core::ir::Constant>();
 
         // Replace the builtin call with a call to the spirv.array_length intrinsic.
-        auto* call =
-            b.Call(builtin->Result()->Type(), core::ir::IntrinsicCall::Kind::kSpirvArrayLength,
-                   Vector{access->Object(), Literal(u32(const_idx->Value()->ValueAs<uint32_t>()))});
+        auto* call = b.Call<spirv::ir::BuiltinCall>(
+            builtin->Result()->Type(), spirv::ir::Function::kArrayLength,
+            Vector{access->Object(), Literal(u32(const_idx->Value()->ValueAs<uint32_t>()))});
         call->InsertBefore(builtin);
         return call->Result();
     }
@@ -212,28 +214,28 @@
         }();
         auto* memory_semantics = b.Constant(u32(SpvMemorySemanticsMaskNone));
 
-        // Helper to build the intrinsic call with the common operands.
-        auto build = [&](const core::type::Type* type,
-                         enum core::ir::IntrinsicCall::Kind intrinsic) {
-            return b.Call(type, intrinsic, pointer, memory, memory_semantics);
+        // Helper to build the builtin call with the common operands.
+        auto build = [&](const core::type::Type* type, enum spirv::ir::Function intrinsic) {
+            return b.Call<spirv::ir::BuiltinCall>(type, intrinsic, pointer, memory,
+                                                  memory_semantics);
         };
 
         // Create the replacement call instruction.
         core::ir::Call* call = nullptr;
         switch (builtin->Func()) {
             case core::Function::kAtomicAdd:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicIAdd);
+                call = build(result_ty, spirv::ir::Function::kAtomicIadd);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicAnd:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicAnd);
+                call = build(result_ty, spirv::ir::Function::kAtomicAnd);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicCompareExchangeWeak: {
                 auto* cmp = builtin->Args()[1];
                 auto* value = builtin->Args()[2];
                 auto* int_ty = value->Type();
-                call = build(int_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicCompareExchange);
+                call = build(int_ty, spirv::ir::Function::kAtomicCompareExchange);
                 call->AppendArg(memory_semantics);
                 call->AppendArg(value);
                 call->AppendArg(cmp);
@@ -251,42 +253,42 @@
                 break;
             }
             case core::Function::kAtomicExchange:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicExchange);
+                call = build(result_ty, spirv::ir::Function::kAtomicExchange);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicLoad:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicLoad);
+                call = build(result_ty, spirv::ir::Function::kAtomicLoad);
                 break;
             case core::Function::kAtomicOr:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicOr);
+                call = build(result_ty, spirv::ir::Function::kAtomicOr);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicMax:
                 if (result_ty->is_signed_integer_scalar()) {
-                    call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicSMax);
+                    call = build(result_ty, spirv::ir::Function::kAtomicSmax);
                 } else {
-                    call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicUMax);
+                    call = build(result_ty, spirv::ir::Function::kAtomicUmax);
                 }
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicMin:
                 if (result_ty->is_signed_integer_scalar()) {
-                    call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicSMin);
+                    call = build(result_ty, spirv::ir::Function::kAtomicSmin);
                 } else {
-                    call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicUMin);
+                    call = build(result_ty, spirv::ir::Function::kAtomicUmin);
                 }
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicStore:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicStore);
+                call = build(result_ty, spirv::ir::Function::kAtomicStore);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicSub:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicISub);
+                call = build(result_ty, spirv::ir::Function::kAtomicIsub);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             case core::Function::kAtomicXor:
-                call = build(result_ty, core::ir::IntrinsicCall::Kind::kSpirvAtomicXor);
+                call = build(result_ty, spirv::ir::Function::kAtomicXor);
                 call->AppendArg(builtin->Args()[1]);
                 break;
             default:
@@ -327,8 +329,8 @@
 
         // Replace the builtin call with a call to the spirv.dot intrinsic.
         auto args = Vector<core::ir::Value*, 4>(builtin->Args());
-        auto* call = b.Call(builtin->Result()->Type(), core::ir::IntrinsicCall::Kind::kSpirvDot,
-                            std::move(args));
+        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result()->Type(),
+                                                    spirv::ir::Function::kDot, std::move(args));
         call->InsertBefore(builtin);
         return call->Result();
     }
@@ -358,8 +360,8 @@
         }
 
         // Replace the builtin call with a call to the spirv.select intrinsic.
-        auto* call = b.Call(builtin->Result()->Type(), core::ir::IntrinsicCall::Kind::kSpirvSelect,
-                            std::move(args));
+        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result()->Type(),
+                                                    spirv::ir::Function::kSelect, std::move(args));
         call->InsertBefore(builtin);
         return call->Result();
     }
@@ -469,9 +471,9 @@
         auto* texture_ty = texture->Type()->As<core::type::Texture>();
 
         // Use OpSampledImage to create an OpTypeSampledImage object.
-        auto* sampled_image =
-            b.Call(ty.Get<SampledImage>(texture_ty),
-                   core::ir::IntrinsicCall::Kind::kSpirvSampledImage, Vector{texture, sampler});
+        auto* sampled_image = b.Call<spirv::ir::IntrinsicCall>(ty.Get<SampledImage>(texture_ty),
+                                                               spirv::ir::Intrinsic::kSampledImage,
+                                                               Vector{texture, sampler});
         sampled_image->InsertBefore(builtin);
 
         // Append the array index to the coordinates if provided.
@@ -481,38 +483,38 @@
         }
 
         // Determine which SPIR-V intrinsic to use and which optional image operands are needed.
-        enum core::ir::IntrinsicCall::Kind intrinsic;
+        enum spirv::ir::Intrinsic intrinsic;
         core::ir::Value* depth = nullptr;
         ImageOperands operands;
         switch (builtin->Func()) {
             case core::Function::kTextureSample:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageSampleImplicitLod;
+                intrinsic = spirv::ir::Intrinsic::kImageSampleImplicitLod;
                 operands.offset = next_arg();
                 break;
             case core::Function::kTextureSampleBias:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageSampleImplicitLod;
+                intrinsic = spirv::ir::Intrinsic::kImageSampleImplicitLod;
                 operands.bias = next_arg();
                 operands.offset = next_arg();
                 break;
             case core::Function::kTextureSampleCompare:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageSampleDrefImplicitLod;
+                intrinsic = spirv::ir::Intrinsic::kImageSampleDrefImplicitLod;
                 depth = next_arg();
                 operands.offset = next_arg();
                 break;
             case core::Function::kTextureSampleCompareLevel:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageSampleDrefExplicitLod;
+                intrinsic = spirv::ir::Intrinsic::kImageSampleDrefExplicitLod;
                 depth = next_arg();
                 operands.lod = b.Constant(0_f);
                 operands.offset = next_arg();
                 break;
             case core::Function::kTextureSampleGrad:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageSampleExplicitLod;
+                intrinsic = spirv::ir::Intrinsic::kImageSampleExplicitLod;
                 operands.ddx = next_arg();
                 operands.ddy = next_arg();
                 operands.offset = next_arg();
                 break;
             case core::Function::kTextureSampleLevel:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageSampleExplicitLod;
+                intrinsic = spirv::ir::Intrinsic::kImageSampleExplicitLod;
                 operands.lod = next_arg();
                 operands.offset = next_arg();
                 break;
@@ -536,7 +538,8 @@
         // Call the intrinsic.
         // If this is a depth comparison, the result is always f32, otherwise vec4f.
         auto* result_ty = depth ? static_cast<const core::type::Type*>(ty.f32()) : ty.vec4<f32>();
-        auto* texture_call = b.Call(result_ty, intrinsic, std::move(intrinsic_args));
+        auto* texture_call =
+            b.Call<spirv::ir::IntrinsicCall>(result_ty, intrinsic, std::move(intrinsic_args));
         texture_call->InsertBefore(builtin);
 
         auto* result = texture_call->Result();
@@ -576,9 +579,9 @@
         auto* texture_ty = texture->Type()->As<core::type::Texture>();
 
         // Use OpSampledImage to create an OpTypeSampledImage object.
-        auto* sampled_image =
-            b.Call(ty.Get<SampledImage>(texture_ty),
-                   core::ir::IntrinsicCall::Kind::kSpirvSampledImage, Vector{texture, sampler});
+        auto* sampled_image = b.Call<spirv::ir::IntrinsicCall>(ty.Get<SampledImage>(texture_ty),
+                                                               spirv::ir::Intrinsic::kSampledImage,
+                                                               Vector{texture, sampler});
         sampled_image->InsertBefore(builtin);
 
         // Append the array index to the coordinates if provided.
@@ -588,16 +591,16 @@
         }
 
         // Determine which SPIR-V intrinsic to use and which optional image operands are needed.
-        enum core::ir::IntrinsicCall::Kind intrinsic;
+        enum spirv::ir::Intrinsic intrinsic;
         core::ir::Value* depth = nullptr;
         ImageOperands operands;
         switch (builtin->Func()) {
             case core::Function::kTextureGather:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageGather;
+                intrinsic = spirv::ir::Intrinsic::kImageGather;
                 operands.offset = next_arg();
                 break;
             case core::Function::kTextureGatherCompare:
-                intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageDrefGather;
+                intrinsic = spirv::ir::Intrinsic::kImageDrefGather;
                 depth = next_arg();
                 operands.offset = next_arg();
                 break;
@@ -622,7 +625,8 @@
 
         // Call the intrinsic.
         auto* result_ty = builtin->Result()->Type();
-        auto* texture_call = b.Call(result_ty, intrinsic, std::move(intrinsic_args));
+        auto* texture_call =
+            b.Call<spirv::ir::IntrinsicCall>(result_ty, intrinsic, std::move(intrinsic_args));
         texture_call->InsertBefore(builtin);
         return texture_call->Result();
     }
@@ -670,8 +674,11 @@
         if (expects_scalar_result) {
             result_ty = ty.vec4(result_ty);
         }
-        auto* texture_call = b.Call(result_ty, core::ir::IntrinsicCall::Kind::kSpirvImageFetch,
-                                    std::move(intrinsic_args));
+        auto intrinsic = texture_ty->Is<core::type::StorageTexture>()
+                             ? spirv::ir::Intrinsic::kImageRead
+                             : spirv::ir::Intrinsic::kImageFetch;
+        auto* texture_call =
+            b.Call<spirv::ir::IntrinsicCall>(result_ty, intrinsic, std::move(intrinsic_args));
         texture_call->InsertBefore(builtin);
         auto* result = texture_call->Result();
 
@@ -718,8 +725,8 @@
         AppendImageOperands(operands, intrinsic_args, builtin, /* requires_float_lod */ false);
 
         // Call the intrinsic.
-        auto* texture_call = b.Call(ty.void_(), core::ir::IntrinsicCall::Kind::kSpirvImageWrite,
-                                    std::move(intrinsic_args));
+        auto* texture_call = b.Call<spirv::ir::IntrinsicCall>(
+            ty.void_(), spirv::ir::Intrinsic::kImageWrite, std::move(intrinsic_args));
         texture_call->InsertBefore(builtin);
         return texture_call->Result();
     }
@@ -741,13 +748,13 @@
         intrinsic_args.Push(texture);
 
         // Determine which SPIR-V intrinsic to use, and add the Lod argument if needed.
-        enum core::ir::IntrinsicCall::Kind intrinsic;
+        enum spirv::ir::Intrinsic intrinsic;
         if (texture_ty
                 ->IsAnyOf<core::type::MultisampledTexture, core::type::DepthMultisampledTexture,
                           core::type::StorageTexture>()) {
-            intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageQuerySize;
+            intrinsic = spirv::ir::Intrinsic::kImageQuerySize;
         } else {
-            intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageQuerySizeLod;
+            intrinsic = spirv::ir::Intrinsic::kImageQuerySizeLod;
             if (auto* lod = next_arg()) {
                 intrinsic_args.Push(lod);
             } else {
@@ -764,7 +771,8 @@
         }
 
         // Call the intrinsic.
-        auto* texture_call = b.Call(result_ty, intrinsic, std::move(intrinsic_args));
+        auto* texture_call =
+            b.Call<spirv::ir::IntrinsicCall>(result_ty, intrinsic, std::move(intrinsic_args));
         texture_call->InsertBefore(builtin);
 
         auto* result = texture_call->Result();
@@ -790,18 +798,19 @@
         intrinsic_args.Push(texture);
 
         // Determine which SPIR-V intrinsic to use, and add the Lod argument if needed.
-        enum core::ir::IntrinsicCall::Kind intrinsic;
+        enum spirv::ir::Intrinsic intrinsic;
         if (texture_ty
                 ->IsAnyOf<core::type::MultisampledTexture, core::type::DepthMultisampledTexture,
                           core::type::StorageTexture>()) {
-            intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageQuerySize;
+            intrinsic = spirv::ir::Intrinsic::kImageQuerySize;
         } else {
-            intrinsic = core::ir::IntrinsicCall::Kind::kSpirvImageQuerySizeLod;
+            intrinsic = spirv::ir::Intrinsic::kImageQuerySizeLod;
             intrinsic_args.Push(b.Constant(0_u));
         }
 
         // Call the intrinsic.
-        auto* texture_call = b.Call(ty.vec3<u32>(), intrinsic, std::move(intrinsic_args));
+        auto* texture_call =
+            b.Call<spirv::ir::IntrinsicCall>(ty.vec3<u32>(), intrinsic, std::move(intrinsic_args));
         texture_call->InsertBefore(builtin);
 
         // Extract the third component to get the number of array layers.
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc
index 586b90d..0fa6eaf 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill_test.cc
@@ -1579,7 +1579,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureSampleCompare_2D) {
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2d));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* dref = b.FunctionParam("dref", ty.f32());
     auto* func = b.Function("foo", ty.f32());
@@ -1591,7 +1591,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:f32 = textureSampleCompare %t, %s, %coords, %dref
     ret %6
@@ -1601,7 +1601,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:spirv.sampled_image = spirv.sampled_image %t, %s
     %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 0u
@@ -1618,7 +1618,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureSampleCompare_2D_Offset) {
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2d));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* dref = b.FunctionParam("dref", ty.f32());
     auto* func = b.Function("foo", ty.f32());
@@ -1631,7 +1631,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:f32 = textureSampleCompare %t, %s, %coords, %dref, vec2<i32>(1i)
     ret %6
@@ -1641,7 +1641,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:spirv.sampled_image = spirv.sampled_image %t, %s
     %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 8u, vec2<i32>(1i)
@@ -1658,7 +1658,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureSampleCompare_2DArray_Offset) {
     auto* t = b.FunctionParam(
         "t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2dArray));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* array_idx = b.FunctionParam("array_idx", ty.i32());
     auto* bias = b.FunctionParam("bias", ty.f32());
@@ -1672,7 +1672,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d_array, %s:sampler_comparison, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
   %b1 = block {
     %7:f32 = textureSampleCompare %t, %s, %coords, %array_idx, %bias, vec2<i32>(1i)
     ret %7
@@ -1682,7 +1682,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d_array, %s:sampler_comparison, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
   %b1 = block {
     %7:spirv.sampled_image = spirv.sampled_image %t, %s
     %8:f32 = convert %array_idx
@@ -1701,7 +1701,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureSampleCompareLevel_2D) {
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2d));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* dref = b.FunctionParam("dref", ty.f32());
     auto* func = b.Function("foo", ty.f32());
@@ -1714,7 +1714,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:f32 = textureSampleCompareLevel %t, %s, %coords, %dref
     ret %6
@@ -1724,10 +1724,10 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:spirv.sampled_image = spirv.sampled_image %t, %s
-    %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 2u, 0.0f
+    %7:f32 = spirv.image_sample_dref_explicit_lod %6, %coords, %dref, 2u, 0.0f
     ret %7
   }
 }
@@ -1741,7 +1741,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureSampleCompareLevel_2D_Offset) {
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2d));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* dref = b.FunctionParam("dref", ty.f32());
     auto* func = b.Function("foo", ty.f32());
@@ -1754,7 +1754,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:f32 = textureSampleCompareLevel %t, %s, %coords, %dref, vec2<i32>(1i)
     ret %6
@@ -1764,10 +1764,10 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %dref:f32):f32 -> %b1 {
   %b1 = block {
     %6:spirv.sampled_image = spirv.sampled_image %t, %s
-    %7:f32 = spirv.image_sample_dref_implicit_lod %6, %coords, %dref, 10u, 0.0f, vec2<i32>(1i)
+    %7:f32 = spirv.image_sample_dref_explicit_lod %6, %coords, %dref, 10u, 0.0f, vec2<i32>(1i)
     ret %7
   }
 }
@@ -1781,7 +1781,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureSampleCompareLevel_2DArray_Offset) {
     auto* t = b.FunctionParam(
         "t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2dArray));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* array_idx = b.FunctionParam("array_idx", ty.i32());
     auto* bias = b.FunctionParam("bias", ty.f32());
@@ -1795,7 +1795,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d_array, %s:sampler_comparison, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
   %b1 = block {
     %7:f32 = textureSampleCompareLevel %t, %s, %coords, %array_idx, %bias, vec2<i32>(1i)
     ret %7
@@ -1805,12 +1805,12 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
+%foo = func(%t:texture_depth_2d_array, %s:sampler_comparison, %coords:vec2<f32>, %array_idx:i32, %bias:f32):f32 -> %b1 {
   %b1 = block {
     %7:spirv.sampled_image = spirv.sampled_image %t, %s
     %8:f32 = convert %array_idx
     %9:vec3<f32> = construct %coords, %8
-    %10:f32 = spirv.image_sample_dref_implicit_lod %7, %9, %bias, 10u, 0.0f, vec2<i32>(1i)
+    %10:f32 = spirv.image_sample_dref_explicit_lod %7, %9, %bias, 10u, 0.0f, vec2<i32>(1i)
     ret %10
   }
 }
@@ -1833,14 +1833,14 @@
 
     b.Append(func->Block(), [&] {
         auto* result =
-            b.Call(ty.vec4<f32>(), core::Function::kTextureSampleBias, t, s, coords, ddx, ddy);
+            b.Call(ty.vec4<f32>(), core::Function::kTextureSampleGrad, t, s, coords, ddx, ddy);
         b.Return(func, result);
     });
 
     auto* src = R"(
 %foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
   %b1 = block {
-    %7:vec4<f32> = textureSampleBias %t, %s, %coords, %ddx, %ddy
+    %7:vec4<f32> = textureSampleGrad %t, %s, %coords, %ddx, %ddy
     ret %7
   }
 }
@@ -1851,7 +1851,7 @@
 %foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
   %b1 = block {
     %7:spirv.sampled_image = spirv.sampled_image %t, %s
-    %8:vec4<f32> = spirv.image_sample_implicit_lod %7, %coords, 9u, %ddx, %ddy
+    %8:vec4<f32> = spirv.image_sample_explicit_lod %7, %coords, 4u, %ddx, %ddy
     ret %8
   }
 }
@@ -1873,7 +1873,7 @@
     func->SetParams({t, s, coords, ddx, ddy});
 
     b.Append(func->Block(), [&] {
-        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureSampleBias, t, s, coords, ddx,
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureSampleGrad, t, s, coords, ddx,
                               ddy, b.Splat(ty.vec2<i32>(), 1_i, 2));
         b.Return(func, result);
     });
@@ -1881,7 +1881,7 @@
     auto* src = R"(
 %foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
   %b1 = block {
-    %7:vec4<f32> = textureSampleBias %t, %s, %coords, %ddx, %ddy, vec2<i32>(1i)
+    %7:vec4<f32> = textureSampleGrad %t, %s, %coords, %ddx, %ddy, vec2<i32>(1i)
     ret %7
   }
 }
@@ -1892,7 +1892,7 @@
 %foo = func(%t:texture_2d<f32>, %s:sampler, %coords:vec2<f32>, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
   %b1 = block {
     %7:spirv.sampled_image = spirv.sampled_image %t, %s
-    %8:vec4<f32> = spirv.image_sample_implicit_lod %7, %coords, 9u, %ddx, %ddy
+    %8:vec4<f32> = spirv.image_sample_explicit_lod %7, %coords, 12u, %ddx, %ddy, vec2<i32>(1i)
     ret %8
   }
 }
@@ -1915,7 +1915,7 @@
     func->SetParams({t, s, coords, array_idx, ddx, ddy});
 
     b.Append(func->Block(), [&] {
-        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureSampleBias, t, s, coords,
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureSampleGrad, t, s, coords,
                               array_idx, ddx, ddy, b.Splat(ty.vec2<i32>(), 1_i, 2));
         b.Return(func, result);
     });
@@ -1923,7 +1923,7 @@
     auto* src = R"(
 %foo = func(%t:texture_2d_array<f32>, %s:sampler, %coords:vec2<f32>, %array_idx:i32, %ddx:vec2<f32>, %ddy:vec2<f32>):vec4<f32> -> %b1 {
   %b1 = block {
-    %8:vec4<f32> = textureSampleBias %t, %s, %coords, %array_idx, %ddx, %ddy, vec2<i32>(1i)
+    %8:vec4<f32> = textureSampleGrad %t, %s, %coords, %array_idx, %ddx, %ddy, vec2<i32>(1i)
     ret %8
   }
 }
@@ -1936,7 +1936,7 @@
     %8:spirv.sampled_image = spirv.sampled_image %t, %s
     %9:f32 = convert %array_idx
     %10:vec3<f32> = construct %coords, %9
-    %11:vec4<f32> = spirv.image_sample_implicit_lod %8, %10, 9u, %ddx, %ddy
+    %11:vec4<f32> = spirv.image_sample_explicit_lod %8, %10, 12u, %ddx, %ddy, vec2<i32>(1i)
     ret %11
   }
 }
@@ -2234,7 +2234,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureGatherCompare_Depth2D) {
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2d));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* depth = b.FunctionParam("depth", ty.f32());
     auto* func = b.Function("foo", ty.vec4<f32>());
@@ -2247,7 +2247,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
   %b1 = block {
     %6:vec4<f32> = textureGatherCompare %t, %s, %coords, %depth
     ret %6
@@ -2257,7 +2257,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
   %b1 = block {
     %6:spirv.sampled_image = spirv.sampled_image %t, %s
     %7:vec4<f32> = spirv.image_dref_gather %6, %coords, %depth, 0u
@@ -2274,7 +2274,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureGatherCompare_Depth2D_Offset) {
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2d));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* depth = b.FunctionParam("depth", ty.f32());
     auto* func = b.Function("foo", ty.vec4<f32>());
@@ -2287,7 +2287,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
   %b1 = block {
     %6:vec4<f32> = textureGatherCompare %t, %s, %coords, %depth, vec2<i32>(1i)
     ret %6
@@ -2297,7 +2297,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d, %s:sampler, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
+%foo = func(%t:texture_depth_2d, %s:sampler_comparison, %coords:vec2<f32>, %depth:f32):vec4<f32> -> %b1 {
   %b1 = block {
     %6:spirv.sampled_image = spirv.sampled_image %t, %s
     %7:vec4<f32> = spirv.image_dref_gather %6, %coords, %depth, 8u, vec2<i32>(1i)
@@ -2314,7 +2314,7 @@
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureGatherCompare_Depth2DArray) {
     auto* t = b.FunctionParam(
         "t", ty.Get<core::type::DepthTexture>(core::type::TextureDimension::k2dArray));
-    auto* s = b.FunctionParam("s", ty.sampler());
+    auto* s = b.FunctionParam("s", ty.comparison_sampler());
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     auto* array_idx = b.FunctionParam("array_idx", ty.u32());
     auto* depth = b.FunctionParam("depth", ty.f32());
@@ -2328,7 +2328,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:u32, %depth:f32):vec4<f32> -> %b1 {
+%foo = func(%t:texture_depth_2d_array, %s:sampler_comparison, %coords:vec2<f32>, %array_idx:u32, %depth:f32):vec4<f32> -> %b1 {
   %b1 = block {
     %7:vec4<f32> = textureGatherCompare %t, %s, %coords, %array_idx, %depth
     ret %7
@@ -2338,7 +2338,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_depth_2d_array, %s:sampler, %coords:vec2<f32>, %array_idx:u32, %depth:f32):vec4<f32> -> %b1 {
+%foo = func(%t:texture_depth_2d_array, %s:sampler_comparison, %coords:vec2<f32>, %array_idx:u32, %depth:f32):vec4<f32> -> %b1 {
   %b1 = block {
     %7:spirv.sampled_image = spirv.sampled_image %t, %s
     %8:f32 = convert %array_idx
@@ -2355,13 +2355,13 @@
 }
 
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureStore_2D) {
-    auto format = core::TexelFormat::kR32Float;
+    auto format = core::TexelFormat::kR32Uint;
     auto* t =
         b.FunctionParam("t", ty.Get<core::type::StorageTexture>(
                                  core::type::TextureDimension::k2d, format, core::Access::kWrite,
                                  core::type::StorageTexture::SubtypeFor(format, ty)));
     auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
-    auto* texel = b.FunctionParam("texel", ty.i32());
+    auto* texel = b.FunctionParam("texel", ty.vec4<u32>());
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({t, coords, texel});
 
@@ -2371,7 +2371,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_storage_2d<r32float, write>, %coords:vec2<i32>, %texel:i32):void -> %b1 {
+%foo = func(%t:texture_storage_2d<r32uint, write>, %coords:vec2<i32>, %texel:vec4<u32>):void -> %b1 {
   %b1 = block {
     %5:void = textureStore %t, %coords, %texel
     ret
@@ -2381,7 +2381,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_storage_2d<r32float, write>, %coords:vec2<i32>, %texel:i32):void -> %b1 {
+%foo = func(%t:texture_storage_2d<r32uint, write>, %coords:vec2<i32>, %texel:vec4<u32>):void -> %b1 {
   %b1 = block {
     %5:void = spirv.image_write %t, %coords, %texel, 0u
     ret
@@ -2395,14 +2395,14 @@
 }
 
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureStore_2DArray) {
-    auto format = core::TexelFormat::kR32Float;
+    auto format = core::TexelFormat::kRgba8Sint;
     auto* t = b.FunctionParam(
         "t", ty.Get<core::type::StorageTexture>(
                  core::type::TextureDimension::k2dArray, format, core::Access::kWrite,
                  core::type::StorageTexture::SubtypeFor(format, ty)));
     auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
     auto* array_idx = b.FunctionParam("array_idx", ty.i32());
-    auto* texel = b.FunctionParam("texel", ty.i32());
+    auto* texel = b.FunctionParam("texel", ty.vec4<i32>());
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({t, coords, array_idx, texel});
 
@@ -2412,7 +2412,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_storage_2d_array<r32float, write>, %coords:vec2<i32>, %array_idx:i32, %texel:i32):void -> %b1 {
+%foo = func(%t:texture_storage_2d_array<rgba8sint, write>, %coords:vec2<i32>, %array_idx:i32, %texel:vec4<i32>):void -> %b1 {
   %b1 = block {
     %6:void = textureStore %t, %coords, %array_idx, %texel
     ret
@@ -2422,7 +2422,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_storage_2d_array<r32float, write>, %coords:vec2<i32>, %array_idx:i32, %texel:i32):void -> %b1 {
+%foo = func(%t:texture_storage_2d_array<rgba8sint, write>, %coords:vec2<i32>, %array_idx:i32, %texel:vec4<i32>):void -> %b1 {
   %b1 = block {
     %6:vec3<i32> = construct %coords, %array_idx
     %7:void = spirv.image_write %t, %6, %texel, 0u
@@ -2437,14 +2437,14 @@
 }
 
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureStore_2DArray_IndexDifferentType) {
-    auto format = core::TexelFormat::kR32Float;
+    auto format = core::TexelFormat::kRgba32Uint;
     auto* t = b.FunctionParam(
         "t", ty.Get<core::type::StorageTexture>(
                  core::type::TextureDimension::k2dArray, format, core::Access::kWrite,
                  core::type::StorageTexture::SubtypeFor(format, ty)));
     auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
     auto* array_idx = b.FunctionParam("array_idx", ty.u32());
-    auto* texel = b.FunctionParam("texel", ty.i32());
+    auto* texel = b.FunctionParam("texel", ty.vec4<u32>());
     auto* func = b.Function("foo", ty.void_());
     func->SetParams({t, coords, array_idx, texel});
 
@@ -2454,7 +2454,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_storage_2d_array<r32float, write>, %coords:vec2<i32>, %array_idx:u32, %texel:i32):void -> %b1 {
+%foo = func(%t:texture_storage_2d_array<rgba32uint, write>, %coords:vec2<i32>, %array_idx:u32, %texel:vec4<u32>):void -> %b1 {
   %b1 = block {
     %6:void = textureStore %t, %coords, %array_idx, %texel
     ret
@@ -2464,7 +2464,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_storage_2d_array<r32float, write>, %coords:vec2<i32>, %array_idx:u32, %texel:i32):void -> %b1 {
+%foo = func(%t:texture_storage_2d_array<rgba32uint, write>, %coords:vec2<i32>, %array_idx:u32, %texel:vec4<u32>):void -> %b1 {
   %b1 = block {
     %6:i32 = convert %array_idx
     %7:vec3<i32> = construct %coords, %6
@@ -2767,10 +2767,10 @@
 
 TEST_F(SpirvWriter_BuiltinPolyfillTest, TextureNumLayers_Storage2DArray) {
     auto format = core::TexelFormat::kR32Float;
-    auto* t =
-        b.FunctionParam("t", ty.Get<core::type::StorageTexture>(
-                                 core::type::TextureDimension::k2d, format, core::Access::kWrite,
-                                 core::type::StorageTexture::SubtypeFor(format, ty)));
+    auto* t = b.FunctionParam(
+        "t", ty.Get<core::type::StorageTexture>(
+                 core::type::TextureDimension::k2dArray, format, core::Access::kWrite,
+                 core::type::StorageTexture::SubtypeFor(format, ty)));
     auto* func = b.Function("foo", ty.u32());
     func->SetParams({t});
 
@@ -2780,7 +2780,7 @@
     });
 
     auto* src = R"(
-%foo = func(%t:texture_storage_2d<r32float, write>):u32 -> %b1 {
+%foo = func(%t:texture_storage_2d_array<r32float, write>):u32 -> %b1 {
   %b1 = block {
     %3:u32 = textureNumLayers %t
     ret %3
@@ -2790,7 +2790,7 @@
     EXPECT_EQ(src, str());
 
     auto* expect = R"(
-%foo = func(%t:texture_storage_2d<r32float, write>):u32 -> %b1 {
+%foo = func(%t:texture_storage_2d_array<r32float, write>):u32 -> %b1 {
   %b1 = block {
     %3:vec3<u32> = spirv.image_query_size %t
     %4:u32 = access %3, 2u
diff --git a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
index fbf1ee1..c222b71 100644
--- a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
+++ b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
@@ -19,6 +19,8 @@
 #include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/spirv/ir/builtin_call.h"
+#include "src/tint/lang/spirv/ir/function.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
@@ -88,7 +90,8 @@
         auto* result_ty = binary->Result()->Type();
         if (result_ty->is_float_vector() && binary->Kind() == core::ir::Binary::Kind::kMultiply) {
             // Use OpVectorTimesScalar for floating point multiply.
-            auto* vts = b.Call(result_ty, core::ir::IntrinsicCall::Kind::kSpirvVectorTimesScalar);
+            auto* vts =
+                b.Call<spirv::ir::BuiltinCall>(result_ty, spirv::ir::Function::kVectorTimesScalar);
             if (binary->LHS()->Type()->Is<core::type::Scalar>()) {
                 vts->AppendArg(binary->RHS());
                 vts->AppendArg(binary->LHS());
diff --git a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
index 6824dda..0a5dd30 100644
--- a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
+++ b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/spirv/ir/builtin_call.h"
 #include "src/tint/utils/ice/ice.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
@@ -97,22 +98,22 @@
                 // Select the SPIR-V intrinsic that corresponds to the operation being performed.
                 if (lhs_ty->Is<core::type::Matrix>()) {
                     if (rhs_ty->Is<core::type::Scalar>()) {
-                        replace(b.Call(ty, core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesScalar,
-                                       lhs, rhs));
+                        replace(b.Call<spirv::ir::BuiltinCall>(
+                            ty, spirv::ir::Function::kMatrixTimesScalar, lhs, rhs));
                     } else if (rhs_ty->Is<core::type::Vector>()) {
-                        replace(b.Call(ty, core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesVector,
-                                       lhs, rhs));
+                        replace(b.Call<spirv::ir::BuiltinCall>(
+                            ty, spirv::ir::Function::kMatrixTimesVector, lhs, rhs));
                     } else if (rhs_ty->Is<core::type::Matrix>()) {
-                        replace(b.Call(ty, core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesMatrix,
-                                       lhs, rhs));
+                        replace(b.Call<spirv::ir::BuiltinCall>(
+                            ty, spirv::ir::Function::kMatrixTimesMatrix, lhs, rhs));
                     }
                 } else {
                     if (lhs_ty->Is<core::type::Scalar>()) {
-                        replace(b.Call(ty, core::ir::IntrinsicCall::Kind::kSpirvMatrixTimesScalar,
-                                       rhs, lhs));
+                        replace(b.Call<spirv::ir::BuiltinCall>(
+                            ty, spirv::ir::Function::kMatrixTimesScalar, rhs, lhs));
                     } else if (lhs_ty->Is<core::type::Vector>()) {
-                        replace(b.Call(ty, core::ir::IntrinsicCall::Kind::kSpirvVectorTimesMatrix,
-                                       lhs, rhs));
+                        replace(b.Call<spirv::ir::BuiltinCall>(
+                            ty, spirv::ir::Function::kVectorTimesMatrix, lhs, rhs));
                     }
                 }
                 break;
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index f429783..71edb73 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/ir/transform/builtin_polyfill.h"
 #include "src/tint/lang/core/ir/transform/demote_to_helper.h"
 #include "src/tint/lang/core/ir/transform/multiplanar_external_texture.h"
+#include "src/tint/lang/core/ir/transform/robustness.h"
 #include "src/tint/lang/core/ir/transform/std140.h"
 #include "src/tint/lang/spirv/writer/raise/builtin_polyfill.h"
 #include "src/tint/lang/spirv/writer/raise/expand_implicit_splats.h"
@@ -56,6 +57,16 @@
     core_polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
     RUN_TRANSFORM(core::ir::transform::BuiltinPolyfill, module, core_polyfills);
 
+    if (!options.disable_robustness) {
+        core::ir::transform::RobustnessConfig config;
+        if (options.disable_image_robustness) {
+            config.clamp_texture = false;
+        }
+        config.disable_runtime_sized_array_index_clamping =
+            options.disable_runtime_sized_array_index_clamping;
+        RUN_TRANSFORM(core::ir::transform::Robustness, module, config);
+    }
+
     RUN_TRANSFORM(core::ir::transform::MultiplanarExternalTexture, module,
                   options.external_texture_options);
 
diff --git a/src/tint/lang/spirv/writer/switch_test.cc b/src/tint/lang/spirv/writer/switch_test.cc
index cd52ea6..bfcefca 100644
--- a/src/tint/lang/spirv/writer/switch_test.cc
+++ b/src/tint/lang/spirv/writer/switch_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 using namespace tint::core::number_suffixes;  // NOLINT
diff --git a/src/tint/lang/spirv/writer/swizzle_test.cc b/src/tint/lang/spirv/writer/swizzle_test.cc
index 1eb32b8..28a4e35 100644
--- a/src/tint/lang/spirv/writer/swizzle_test.cc
+++ b/src/tint/lang/spirv/writer/swizzle_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 namespace tint::spirv::writer {
diff --git a/src/tint/lang/spirv/writer/texture_builtin_test.cc b/src/tint/lang/spirv/writer/texture_builtin_test.cc
index baa3e04..c77274b 100644
--- a/src/tint/lang/spirv/writer/texture_builtin_test.cc
+++ b/src/tint/lang/spirv/writer/texture_builtin_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/core/fluent_types.h"
 #include "src/tint/lang/core/function.h"
 #include "src/tint/lang/core/type/depth_multisampled_texture.h"
@@ -193,7 +191,9 @@
             }
         });
 
-        ASSERT_TRUE(Generate()) << Error() << output_;
+        Options options;
+        options.disable_image_robustness = true;
+        ASSERT_TRUE(Generate(options)) << Error() << output_;
         for (auto& inst : params.instructions) {
             EXPECT_INST(inst);
         }
@@ -1332,7 +1332,7 @@
                                  kDepthMultisampledTexture,
                                  core::type::TextureDimension::k2d,
                                  /* texel type */ kF32,
-                                 {{"coords", 3, kI32}, {"sample_idx", 1, kI32}},
+                                 {{"coords", 2, kI32}, {"sample_idx", 1, kI32}},
                                  {"result", 1, kF32},
                                  {
                                      "OpImageFetch %v4float %t %coords Sample %sample_idx",
@@ -1770,80 +1770,80 @@
                                  core::type::TextureDimension::k1d,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kSampledTexture,
                                  core::type::TextureDimension::k2d,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kSampledTexture,
                                  core::type::TextureDimension::k2dArray,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kSampledTexture,
                                  core::type::TextureDimension::k3d,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kSampledTexture,
                                  core::type::TextureDimension::kCube,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kSampledTexture,
                                  core::type::TextureDimension::kCubeArray,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kDepthTexture,
                                  core::type::TextureDimension::k2d,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kDepthTexture,
                                  core::type::TextureDimension::k2dArray,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kDepthTexture,
                                  core::type::TextureDimension::kCube,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              },
                              TextureBuiltinTestCase{
                                  kDepthTexture,
                                  core::type::TextureDimension::kCubeArray,
                                  /* texel type */ kF32,
                                  {},
-                                 {"result", 1, kI32},
-                                 {"%result = OpImageQueryLevels %int %t"},
+                                 {"result", 1, kU32},
+                                 {"%result = OpImageQueryLevels %uint %t"},
                              }),
                          PrintCase);
 
@@ -1930,12 +1930,103 @@
         b.Return(func);
     });
 
-    ASSERT_TRUE(Generate()) << Error() << output_;
+    Options options;
+    options.disable_image_robustness = true;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
     EXPECT_INST(R"(
          %13 = OpVectorShuffle %v4float %value %value 2 1 0 3
                OpImageWrite %texture %coords %13 None
 )");
 }
 
+////////////////////////////////////////////////////////////////
+//// Texture robustness enabled.
+////////////////////////////////////////////////////////////////
+
+TEST_F(SpirvWriterTest, TextureDimensions_WithRobustness) {
+    auto* texture_ty =
+        ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+
+    auto* texture = b.FunctionParam("texture", texture_ty);
+    auto* level = b.FunctionParam("level", ty.i32());
+    auto* func = b.Function("foo", ty.vec2<u32>());
+    func->SetParams({texture, level});
+    b.Append(func->Block(), [&] {
+        auto* dims = b.Call(ty.vec2<u32>(), core::Function::kTextureDimensions, texture, level);
+        b.Return(func, dims);
+        mod.SetName(dims, "dims");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+         %11 = OpImageQueryLevels %uint %texture
+         %12 = OpISub %uint %11 %uint_1
+         %14 = OpBitcast %uint %level
+         %15 = OpExtInst %uint %16 UMin %14 %12
+       %dims = OpImageQuerySizeLod %v2uint %texture %15
+)");
+}
+
+TEST_F(SpirvWriterTest, TextureLoad_WithRobustness) {
+    auto* texture_ty =
+        ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+
+    auto* texture = b.FunctionParam("texture", texture_ty);
+    auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+    auto* level = b.FunctionParam("level", ty.i32());
+    auto* func = b.Function("foo", ty.vec4<f32>());
+    func->SetParams({texture, coords, level});
+    b.Append(func->Block(), [&] {
+        auto* result = b.Call(ty.vec4<f32>(), core::Function::kTextureLoad, texture, coords, level);
+        b.Return(func, result);
+        mod.SetName(result, "result");
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+         %13 = OpImageQuerySizeLod %v2uint %texture %uint_0
+         %15 = OpISub %v2uint %13 %16
+         %18 = OpExtInst %v2uint %19 UMin %coords %15
+         %20 = OpImageQueryLevels %uint %texture
+         %21 = OpISub %uint %20 %uint_1
+         %22 = OpBitcast %uint %level
+         %23 = OpExtInst %uint %19 UMin %22 %21
+     %result = OpImageFetch %v4float %texture %18 Lod %23
+)");
+}
+
+TEST_F(SpirvWriterTest, TextureStore_WithRobustness) {
+    auto format = core::TexelFormat::kRgba8Unorm;
+    auto* texture_ty = ty.Get<core::type::StorageTexture>(
+        core::type::TextureDimension::k2dArray, format, core::Access::kWrite,
+        core::type::StorageTexture::SubtypeFor(format, ty));
+
+    auto* texture = b.FunctionParam("texture", texture_ty);
+    auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
+    auto* layer = b.FunctionParam("layer", ty.i32());
+    auto* value = b.FunctionParam("value", ty.vec4<f32>());
+    auto* func = b.Function("foo", ty.void_());
+    func->SetParams({texture, coords, layer, value});
+    b.Append(func->Block(), [&] {
+        b.Call(ty.void_(), core::Function::kTextureStore, texture, coords, layer, value);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+         %15 = OpImageQuerySize %v3uint %texture
+         %17 = OpVectorShuffle %v2uint %15 %15 0 1
+         %18 = OpISub %v2uint %17 %19
+         %21 = OpExtInst %v2uint %22 UMin %coords %18
+         %23 = OpImageQuerySize %v3uint %texture
+         %24 = OpCompositeExtract %uint %23 2
+         %25 = OpISub %uint %24 %uint_1
+         %26 = OpBitcast %uint %layer
+         %27 = OpExtInst %uint %22 UMin %26 %25
+         %28 = OpCompositeConstruct %v3uint %21 %27
+               OpImageWrite %texture %28 %value None
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/type_test.cc b/src/tint/lang/spirv/writer/type_test.cc
index ec395bb..cb21390 100644
--- a/src/tint/lang/spirv/writer/type_test.cc
+++ b/src/tint/lang/spirv/writer/type_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/core/type/type.h"
 #include "src/tint/lang/core/fluent_types.h"
 #include "src/tint/lang/core/type/bool.h"
diff --git a/src/tint/lang/spirv/writer/unary_test.cc b/src/tint/lang/spirv/writer/unary_test.cc
index b991a61..1d36ac8 100644
--- a/src/tint/lang/spirv/writer/unary_test.cc
+++ b/src/tint/lang/spirv/writer/unary_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 #include "src/tint/lang/core/ir/unary.h"
diff --git a/src/tint/lang/spirv/writer/var_test.cc b/src/tint/lang/spirv/writer/var_test.cc
index 6581e3d..c73fa13 100644
--- a/src/tint/lang/spirv/writer/var_test.cc
+++ b/src/tint/lang/spirv/writer/var_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index 42fe4c2..81acf02 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -17,18 +17,15 @@
 #include <memory>
 #include <utility>
 
+#include "src/tint/lang/core/ir/transform/binding_remapper.h"
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.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/program_to_ir/program_to_ir.h"
 
 // Included by 'ast_printer.h', included again here for './tools/run gen' track the dependency.
 #include "spirv/unified1/spirv.h"
 
-#if TINT_BUILD_IR
-#include "src/tint/lang/core/ir/transform/binding_remapper.h"
-#include "src/tint/lang/spirv/writer/printer/printer.h"             // nogncheck
-#include "src/tint/lang/spirv/writer/raise/raise.h"                 // nogncheck
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"  // nogncheck
-#endif                                                              // TINT_BUILD_IR
-
 namespace tint::spirv::writer {
 
 Output::Output() = default;
@@ -44,7 +41,7 @@
         !options.disable_workgroup_init && options.use_zero_initialize_workgroup_memory_extension;
 
     Output output;
-#if TINT_BUILD_IR
+
     if (options.use_tint_ir) {
         // Convert the AST program to an IR module.
         auto converted = wgsl::reader::ProgramToIR(program);
@@ -73,9 +70,7 @@
             return std::move(spirv.Failure());
         }
         output.spirv = std::move(spirv.Get());
-    } else  // NOLINT(readability/braces)
-#endif
-    {
+    } else {
         // Sanitize the program.
         auto sanitized_result = Sanitize(program, options);
         if (!sanitized_result.program.IsValid()) {
@@ -83,8 +78,9 @@
         }
 
         // Generate the SPIR-V code.
-        auto impl = std::make_unique<ASTPrinter>(&sanitized_result.program,
-                                                 zero_initialize_workgroup_memory);
+        auto impl = std::make_unique<ASTPrinter>(
+            &sanitized_result.program, zero_initialize_workgroup_memory,
+            options.experimental_require_subgroup_uniform_control_flow);
         if (!impl->Generate()) {
             return impl->Diagnostics().str();
         }
diff --git a/src/tint/lang/spirv/writer/writer_bench.cc b/src/tint/lang/spirv/writer/writer_bench.cc
index cb52e9e..3d74eb8 100644
--- a/src/tint/lang/spirv/writer/writer_bench.cc
+++ b/src/tint/lang/spirv/writer/writer_bench.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include <string>
 
 #include "src/tint/cmd/bench/bench.h"
diff --git a/src/tint/lang/spirv/writer/writer_test.cc b/src/tint/lang/spirv/writer/writer_test.cc
index 1d99514..d0ca418 100644
--- a/src/tint/lang/spirv/writer/writer_test.cc
+++ b/src/tint/lang/spirv/writer/writer_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// GEN_BUILD:CONDITION(tint_build_ir)
-
 #include "src/tint/lang/spirv/writer/common/helper_test.h"
 
 #include "gmock/gmock.h"
diff --git a/src/tint/lang/wgsl/BUILD.bazel b/src/tint/lang/wgsl/BUILD.bazel
new file mode 100644
index 0000000..44d29ff
--- /dev/null
+++ b/src/tint/lang/wgsl/BUILD.bazel
@@ -0,0 +1,88 @@
+# 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 = "test",
+  alwayslink = True,
+  srcs = [
+    "wgsl_test.cc",
+  ] + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "ir_roundtrip_test.cc",
+    ],
+    "//conditions:default": [],
+  }),
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/helpers:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/reader",
+    "//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",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+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/BUILD.cmake b/src/tint/lang/wgsl/BUILD.cmake
index 90ce7f7..64bdc34 100644
--- a/src/tint/lang/wgsl/BUILD.cmake
+++ b/src/tint/lang/wgsl/BUILD.cmake
@@ -42,14 +42,17 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl_ast
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program
   tint_lang_wgsl_reader
+  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
   tint_utils_ice
@@ -69,16 +72,8 @@
   "gtest"
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_wgsl_test test
-    tint_lang_core_ir
-    tint_lang_wgsl_reader_program_to_ir
-    tint_lang_wgsl_writer_ir_to_program
-  )
-endif(TINT_BUILD_IR)
-
-if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_IR)
+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 AND TINT_BUILD_IR)
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/wgsl/BUILD.gn b/src/tint/lang/wgsl/BUILD.gn
index 922f8e8..67a6ed6 100644
--- a/src/tint/lang/wgsl/BUILD.gn
+++ b/src/tint/lang/wgsl/BUILD.gn
@@ -37,14 +37,17 @@
       "${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/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/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",
       "${tint_src_dir}/utils/ice",
@@ -60,15 +63,7 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (tint_build_ir) {
-      deps += [
-        "${tint_src_dir}/lang/core/ir",
-        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-        "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
-      ]
-    }
-
-    if (tint_build_wgsl_reader && tint_build_wgsl_writer && tint_build_ir) {
+    if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
       sources += [ "ir_roundtrip_test.cc" ]
     }
   }
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
new file mode 100644
index 0000000..ebc4a92
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -0,0 +1,308 @@
+# 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 = "ast",
+  srcs = [
+    "accessor_expression.cc",
+    "alias.cc",
+    "assignment_statement.cc",
+    "attribute.cc",
+    "binary_expression.cc",
+    "binding_attribute.cc",
+    "bitcast_expression.cc",
+    "block_statement.cc",
+    "bool_literal_expression.cc",
+    "break_if_statement.cc",
+    "break_statement.cc",
+    "builder.cc",
+    "builtin_attribute.cc",
+    "call_expression.cc",
+    "call_statement.cc",
+    "case_selector.cc",
+    "case_statement.cc",
+    "clone_context.cc",
+    "compound_assignment_statement.cc",
+    "const.cc",
+    "const_assert.cc",
+    "continue_statement.cc",
+    "diagnostic_attribute.cc",
+    "diagnostic_control.cc",
+    "diagnostic_directive.cc",
+    "diagnostic_rule_name.cc",
+    "disable_validation_attribute.cc",
+    "discard_statement.cc",
+    "enable.cc",
+    "expression.cc",
+    "extension.cc",
+    "float_literal_expression.cc",
+    "for_loop_statement.cc",
+    "function.cc",
+    "group_attribute.cc",
+    "id_attribute.cc",
+    "identifier.cc",
+    "identifier_expression.cc",
+    "if_statement.cc",
+    "increment_decrement_statement.cc",
+    "index_accessor_expression.cc",
+    "index_attribute.cc",
+    "int_literal_expression.cc",
+    "internal_attribute.cc",
+    "interpolate_attribute.cc",
+    "invariant_attribute.cc",
+    "let.cc",
+    "literal_expression.cc",
+    "location_attribute.cc",
+    "loop_statement.cc",
+    "member_accessor_expression.cc",
+    "module.cc",
+    "must_use_attribute.cc",
+    "node.cc",
+    "override.cc",
+    "parameter.cc",
+    "phony_expression.cc",
+    "pipeline_stage.cc",
+    "return_statement.cc",
+    "stage_attribute.cc",
+    "statement.cc",
+    "stride_attribute.cc",
+    "struct.cc",
+    "struct_member.cc",
+    "struct_member_align_attribute.cc",
+    "struct_member_offset_attribute.cc",
+    "struct_member_size_attribute.cc",
+    "switch_statement.cc",
+    "templated_identifier.cc",
+    "type.cc",
+    "type_decl.cc",
+    "unary_op_expression.cc",
+    "var.cc",
+    "variable.cc",
+    "variable_decl_statement.cc",
+    "while_statement.cc",
+    "workgroup_attribute.cc",
+  ],
+  hdrs = [
+    "accessor_expression.h",
+    "alias.h",
+    "assignment_statement.h",
+    "attribute.h",
+    "binary_expression.h",
+    "binding_attribute.h",
+    "bitcast_expression.h",
+    "block_statement.h",
+    "bool_literal_expression.h",
+    "break_if_statement.h",
+    "break_statement.h",
+    "builder.h",
+    "builtin_attribute.h",
+    "call_expression.h",
+    "call_statement.h",
+    "case_selector.h",
+    "case_statement.h",
+    "clone_context.h",
+    "compound_assignment_statement.h",
+    "const.h",
+    "const_assert.h",
+    "continue_statement.h",
+    "diagnostic_attribute.h",
+    "diagnostic_control.h",
+    "diagnostic_directive.h",
+    "diagnostic_rule_name.h",
+    "disable_validation_attribute.h",
+    "discard_statement.h",
+    "enable.h",
+    "expression.h",
+    "extension.h",
+    "float_literal_expression.h",
+    "for_loop_statement.h",
+    "function.h",
+    "group_attribute.h",
+    "id_attribute.h",
+    "identifier.h",
+    "identifier_expression.h",
+    "if_statement.h",
+    "increment_decrement_statement.h",
+    "index_accessor_expression.h",
+    "index_attribute.h",
+    "int_literal_expression.h",
+    "internal_attribute.h",
+    "interpolate_attribute.h",
+    "invariant_attribute.h",
+    "let.h",
+    "literal_expression.h",
+    "location_attribute.h",
+    "loop_statement.h",
+    "member_accessor_expression.h",
+    "module.h",
+    "must_use_attribute.h",
+    "node.h",
+    "node_id.h",
+    "override.h",
+    "parameter.h",
+    "phony_expression.h",
+    "pipeline_stage.h",
+    "return_statement.h",
+    "stage_attribute.h",
+    "statement.h",
+    "stride_attribute.h",
+    "struct.h",
+    "struct_member.h",
+    "struct_member_align_attribute.h",
+    "struct_member_offset_attribute.h",
+    "struct_member_size_attribute.h",
+    "switch_statement.h",
+    "templated_identifier.h",
+    "traverse_expressions.h",
+    "type.h",
+    "type_decl.h",
+    "unary_op_expression.h",
+    "var.h",
+    "variable.h",
+    "variable_decl_statement.h",
+    "while_statement.h",
+    "workgroup_attribute.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "alias_test.cc",
+    "assignment_statement_test.cc",
+    "binary_expression_test.cc",
+    "binding_attribute_test.cc",
+    "bitcast_expression_test.cc",
+    "block_statement_test.cc",
+    "bool_literal_expression_test.cc",
+    "break_if_statement_test.cc",
+    "break_statement_test.cc",
+    "builtin_attribute_test.cc",
+    "builtin_texture_helper_test.cc",
+    "builtin_texture_helper_test.h",
+    "call_expression_test.cc",
+    "call_statement_test.cc",
+    "case_selector_test.cc",
+    "case_statement_test.cc",
+    "clone_context_test.cc",
+    "compound_assignment_statement_test.cc",
+    "const_assert_test.cc",
+    "continue_statement_test.cc",
+    "diagnostic_attribute_test.cc",
+    "diagnostic_control_test.cc",
+    "diagnostic_directive_test.cc",
+    "diagnostic_rule_name_test.cc",
+    "discard_statement_test.cc",
+    "enable_test.cc",
+    "float_literal_expression_test.cc",
+    "for_loop_statement_test.cc",
+    "function_test.cc",
+    "group_attribute_test.cc",
+    "helper_test.cc",
+    "helper_test.h",
+    "id_attribute_test.cc",
+    "identifier_expression_test.cc",
+    "identifier_test.cc",
+    "if_statement_test.cc",
+    "increment_decrement_statement_test.cc",
+    "index_accessor_expression_test.cc",
+    "index_attribute_test.cc",
+    "int_literal_expression_test.cc",
+    "interpolate_attribute_test.cc",
+    "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",
+    "stage_attribute_test.cc",
+    "stride_attribute_test.cc",
+    "struct_member_align_attribute_test.cc",
+    "struct_member_offset_attribute_test.cc",
+    "struct_member_size_attribute_test.cc",
+    "struct_member_test.cc",
+    "struct_test.cc",
+    "switch_statement_test.cc",
+    "templated_identifier_test.cc",
+    "traverse_expressions_test.cc",
+    "unary_op_expression_test.cc",
+    "variable_decl_statement_test.cc",
+    "variable_test.cc",
+    "while_statement_test.cc",
+    "workgroup_attribute_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index 872e20c..e575633 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -1727,8 +1727,8 @@
     /// @param options the extra options passed to the ast::Var initializer
     /// Can be any of the following, in any order:
     ///   * ast::Type           - specifies the variable's type
-    ///   * core::AddressSpace   - specifies the variable address space
-    ///   * core::Access         - specifies the variable's access control
+    ///   * core::AddressSpace  - specifies the variable address space
+    ///   * core::Access        - specifies the variable's access control
     ///   * ast::Expression*    - specifies the variable's initializer expression
     ///   * ast::Attribute*     - specifies the variable's attributes (repeatable, or vector)
     /// Note that non-repeatable arguments of the same type will use the last argument's value.
@@ -1744,10 +1744,10 @@
     /// @param options the extra options passed to the ast::Var initializer
     /// Can be any of the following, in any order:
     ///   * ast::Type           - specifies the variable's type
-    ///   * core::AddressSpace   - specifies the variable address space
-    ///   * core::Access         - specifies the variable's access control
+    ///   * core::AddressSpace  - specifies the variable address space
+    ///   * core::Access        - specifies the variable's access control
     ///   * ast::Expression*    - specifies the variable's initializer expression
-    ///   * ast::Attribute*    - specifies the variable's attributes (repeatable, or vector)
+    ///   * ast::Attribute*     - specifies the variable's attributes (repeatable, or vector)
     /// Note that non-repeatable arguments of the same type will use the last argument's value.
     /// @returns a new `ast::Var`, which is automatically registered as a global variable with the
     /// ast::Module.
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.bazel b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
new file mode 100644
index 0000000..d035628
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
@@ -0,0 +1,195 @@
+# 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 = "transform",
+  srcs = [
+    "add_block_attribute.cc",
+    "add_empty_entry_point.cc",
+    "array_length_from_uniform.cc",
+    "binding_remapper.cc",
+    "builtin_polyfill.cc",
+    "canonicalize_entry_point_io.cc",
+    "data.cc",
+    "demote_to_helper.cc",
+    "direct_variable_access.cc",
+    "disable_uniformity_analysis.cc",
+    "expand_compound_assignment.cc",
+    "first_index_offset.cc",
+    "get_insertion_point.cc",
+    "hoist_to_decl_before.cc",
+    "manager.cc",
+    "multiplanar_external_texture.cc",
+    "preserve_padding.cc",
+    "promote_initializers_to_let.cc",
+    "promote_side_effects_to_decl.cc",
+    "remove_phonies.cc",
+    "remove_unreachable_statements.cc",
+    "renamer.cc",
+    "robustness.cc",
+    "simplify_pointers.cc",
+    "single_entry_point.cc",
+    "std140.cc",
+    "substitute_override.cc",
+    "transform.cc",
+    "unshadow.cc",
+    "vectorize_scalar_matrix_initializers.cc",
+    "vertex_pulling.cc",
+    "zero_init_workgroup_memory.cc",
+  ],
+  hdrs = [
+    "add_block_attribute.h",
+    "add_empty_entry_point.h",
+    "array_length_from_uniform.h",
+    "binding_remapper.h",
+    "builtin_polyfill.h",
+    "canonicalize_entry_point_io.h",
+    "data.h",
+    "demote_to_helper.h",
+    "direct_variable_access.h",
+    "disable_uniformity_analysis.h",
+    "expand_compound_assignment.h",
+    "first_index_offset.h",
+    "get_insertion_point.h",
+    "hoist_to_decl_before.h",
+    "manager.h",
+    "multiplanar_external_texture.h",
+    "preserve_padding.h",
+    "promote_initializers_to_let.h",
+    "promote_side_effects_to_decl.h",
+    "remove_phonies.h",
+    "remove_unreachable_statements.h",
+    "renamer.h",
+    "robustness.h",
+    "simplify_pointers.h",
+    "single_entry_point.h",
+    "std140.h",
+    "substitute_override.h",
+    "transform.h",
+    "unshadow.h",
+    "vectorize_scalar_matrix_initializers.h",
+    "vertex_pulling.h",
+    "zero_init_workgroup_memory.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "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 = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index 21e7fa31..c5ed810 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -36,22 +36,10 @@
   lang/wgsl/ast/transform/binding_remapper.h
   lang/wgsl/ast/transform/builtin_polyfill.cc
   lang/wgsl/ast/transform/builtin_polyfill.h
-  lang/wgsl/ast/transform/calculate_array_length.cc
-  lang/wgsl/ast/transform/calculate_array_length.h
   lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
   lang/wgsl/ast/transform/canonicalize_entry_point_io.h
-  lang/wgsl/ast/transform/clamp_frag_depth.cc
-  lang/wgsl/ast/transform/clamp_frag_depth.h
-  lang/wgsl/ast/transform/combine_samplers.cc
-  lang/wgsl/ast/transform/combine_samplers.h
   lang/wgsl/ast/transform/data.cc
   lang/wgsl/ast/transform/data.h
-  lang/wgsl/ast/transform/decompose_memory_access.cc
-  lang/wgsl/ast/transform/decompose_memory_access.h
-  lang/wgsl/ast/transform/decompose_strided_array.cc
-  lang/wgsl/ast/transform/decompose_strided_array.h
-  lang/wgsl/ast/transform/decompose_strided_matrix.cc
-  lang/wgsl/ast/transform/decompose_strided_matrix.h
   lang/wgsl/ast/transform/demote_to_helper.cc
   lang/wgsl/ast/transform/demote_to_helper.h
   lang/wgsl/ast/transform/direct_variable_access.cc
@@ -62,40 +50,20 @@
   lang/wgsl/ast/transform/expand_compound_assignment.h
   lang/wgsl/ast/transform/first_index_offset.cc
   lang/wgsl/ast/transform/first_index_offset.h
-  lang/wgsl/ast/transform/fold_trivial_lets.cc
-  lang/wgsl/ast/transform/fold_trivial_lets.h
-  lang/wgsl/ast/transform/for_loop_to_loop.cc
-  lang/wgsl/ast/transform/for_loop_to_loop.h
   lang/wgsl/ast/transform/get_insertion_point.cc
   lang/wgsl/ast/transform/get_insertion_point.h
   lang/wgsl/ast/transform/hoist_to_decl_before.cc
   lang/wgsl/ast/transform/hoist_to_decl_before.h
-  lang/wgsl/ast/transform/localize_struct_array_assignment.cc
-  lang/wgsl/ast/transform/localize_struct_array_assignment.h
   lang/wgsl/ast/transform/manager.cc
   lang/wgsl/ast/transform/manager.h
-  lang/wgsl/ast/transform/merge_return.cc
-  lang/wgsl/ast/transform/merge_return.h
-  lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
-  lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.h
-  lang/wgsl/ast/transform/msl_subgroup_ballot.cc
-  lang/wgsl/ast/transform/msl_subgroup_ballot.h
   lang/wgsl/ast/transform/multiplanar_external_texture.cc
   lang/wgsl/ast/transform/multiplanar_external_texture.h
-  lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
-  lang/wgsl/ast/transform/num_workgroups_from_uniform.h
-  lang/wgsl/ast/transform/packed_vec3.cc
-  lang/wgsl/ast/transform/packed_vec3.h
-  lang/wgsl/ast/transform/pad_structs.cc
-  lang/wgsl/ast/transform/pad_structs.h
   lang/wgsl/ast/transform/preserve_padding.cc
   lang/wgsl/ast/transform/preserve_padding.h
   lang/wgsl/ast/transform/promote_initializers_to_let.cc
   lang/wgsl/ast/transform/promote_initializers_to_let.h
   lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
   lang/wgsl/ast/transform/promote_side_effects_to_decl.h
-  lang/wgsl/ast/transform/remove_continue_in_switch.cc
-  lang/wgsl/ast/transform/remove_continue_in_switch.h
   lang/wgsl/ast/transform/remove_phonies.cc
   lang/wgsl/ast/transform/remove_phonies.h
   lang/wgsl/ast/transform/remove_unreachable_statements.cc
@@ -108,32 +76,18 @@
   lang/wgsl/ast/transform/simplify_pointers.h
   lang/wgsl/ast/transform/single_entry_point.cc
   lang/wgsl/ast/transform/single_entry_point.h
-  lang/wgsl/ast/transform/spirv_atomic.cc
-  lang/wgsl/ast/transform/spirv_atomic.h
   lang/wgsl/ast/transform/std140.cc
   lang/wgsl/ast/transform/std140.h
   lang/wgsl/ast/transform/substitute_override.cc
   lang/wgsl/ast/transform/substitute_override.h
-  lang/wgsl/ast/transform/texture_1d_to_2d.cc
-  lang/wgsl/ast/transform/texture_1d_to_2d.h
-  lang/wgsl/ast/transform/texture_builtins_from_uniform.cc
-  lang/wgsl/ast/transform/texture_builtins_from_uniform.h
   lang/wgsl/ast/transform/transform.cc
   lang/wgsl/ast/transform/transform.h
-  lang/wgsl/ast/transform/truncate_interstage_variables.cc
-  lang/wgsl/ast/transform/truncate_interstage_variables.h
   lang/wgsl/ast/transform/unshadow.cc
   lang/wgsl/ast/transform/unshadow.h
-  lang/wgsl/ast/transform/var_for_dynamic_index.cc
-  lang/wgsl/ast/transform/var_for_dynamic_index.h
-  lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
-  lang/wgsl/ast/transform/vectorize_matrix_conversions.h
   lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
   lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h
   lang/wgsl/ast/transform/vertex_pulling.cc
   lang/wgsl/ast/transform/vertex_pulling.h
-  lang/wgsl/ast/transform/while_to_loop.cc
-  lang/wgsl/ast/transform/while_to_loop.h
   lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
   lang/wgsl/ast/transform/zero_init_workgroup_memory.h
 )
@@ -173,58 +127,35 @@
   lang/wgsl/ast/transform/array_length_from_uniform_test.cc
   lang/wgsl/ast/transform/binding_remapper_test.cc
   lang/wgsl/ast/transform/builtin_polyfill_test.cc
-  lang/wgsl/ast/transform/calculate_array_length_test.cc
   lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
-  lang/wgsl/ast/transform/clamp_frag_depth_test.cc
-  lang/wgsl/ast/transform/combine_samplers_test.cc
-  lang/wgsl/ast/transform/decompose_memory_access_test.cc
-  lang/wgsl/ast/transform/decompose_strided_array_test.cc
-  lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
   lang/wgsl/ast/transform/demote_to_helper_test.cc
   lang/wgsl/ast/transform/direct_variable_access_test.cc
   lang/wgsl/ast/transform/disable_uniformity_analysis_test.cc
   lang/wgsl/ast/transform/expand_compound_assignment_test.cc
   lang/wgsl/ast/transform/first_index_offset_test.cc
-  lang/wgsl/ast/transform/fold_trivial_lets_test.cc
-  lang/wgsl/ast/transform/for_loop_to_loop_test.cc
   lang/wgsl/ast/transform/get_insertion_point_test.cc
   lang/wgsl/ast/transform/helper_test.h
   lang/wgsl/ast/transform/hoist_to_decl_before_test.cc
-  lang/wgsl/ast/transform/localize_struct_array_assignment_test.cc
   lang/wgsl/ast/transform/manager_test.cc
-  lang/wgsl/ast/transform/merge_return_test.cc
-  lang/wgsl/ast/transform/module_scope_var_to_entry_point_param_test.cc
-  lang/wgsl/ast/transform/msl_subgroup_ballot_test.cc
   lang/wgsl/ast/transform/multiplanar_external_texture_test.cc
-  lang/wgsl/ast/transform/num_workgroups_from_uniform_test.cc
-  lang/wgsl/ast/transform/packed_vec3_test.cc
-  lang/wgsl/ast/transform/pad_structs_test.cc
   lang/wgsl/ast/transform/preserve_padding_test.cc
   lang/wgsl/ast/transform/promote_initializers_to_let_test.cc
   lang/wgsl/ast/transform/promote_side_effects_to_decl_test.cc
-  lang/wgsl/ast/transform/remove_continue_in_switch_test.cc
   lang/wgsl/ast/transform/remove_phonies_test.cc
   lang/wgsl/ast/transform/remove_unreachable_statements_test.cc
   lang/wgsl/ast/transform/renamer_test.cc
   lang/wgsl/ast/transform/robustness_test.cc
   lang/wgsl/ast/transform/simplify_pointers_test.cc
   lang/wgsl/ast/transform/single_entry_point_test.cc
-  lang/wgsl/ast/transform/spirv_atomic_test.cc
   lang/wgsl/ast/transform/std140_exhaustive_test.cc
   lang/wgsl/ast/transform/std140_f16_test.cc
   lang/wgsl/ast/transform/std140_f32_test.cc
   lang/wgsl/ast/transform/std140_test.cc
   lang/wgsl/ast/transform/substitute_override_test.cc
-  lang/wgsl/ast/transform/texture_1d_to_2d_test.cc
-  lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc
   lang/wgsl/ast/transform/transform_test.cc
-  lang/wgsl/ast/transform/truncate_interstage_variables_test.cc
   lang/wgsl/ast/transform/unshadow_test.cc
-  lang/wgsl/ast/transform/var_for_dynamic_index_test.cc
-  lang/wgsl/ast/transform/vectorize_matrix_conversions_test.cc
   lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers_test.cc
   lang/wgsl/ast/transform/vertex_pulling_test.cc
-  lang/wgsl/ast/transform/while_to_loop_test.cc
   lang/wgsl/ast/transform/zero_init_workgroup_memory_test.cc
 )
 
@@ -239,7 +170,6 @@
   tint_lang_wgsl_ast_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
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 3f325ce..3c49f99 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -41,22 +41,10 @@
     "binding_remapper.h",
     "builtin_polyfill.cc",
     "builtin_polyfill.h",
-    "calculate_array_length.cc",
-    "calculate_array_length.h",
     "canonicalize_entry_point_io.cc",
     "canonicalize_entry_point_io.h",
-    "clamp_frag_depth.cc",
-    "clamp_frag_depth.h",
-    "combine_samplers.cc",
-    "combine_samplers.h",
     "data.cc",
     "data.h",
-    "decompose_memory_access.cc",
-    "decompose_memory_access.h",
-    "decompose_strided_array.cc",
-    "decompose_strided_array.h",
-    "decompose_strided_matrix.cc",
-    "decompose_strided_matrix.h",
     "demote_to_helper.cc",
     "demote_to_helper.h",
     "direct_variable_access.cc",
@@ -67,40 +55,20 @@
     "expand_compound_assignment.h",
     "first_index_offset.cc",
     "first_index_offset.h",
-    "fold_trivial_lets.cc",
-    "fold_trivial_lets.h",
-    "for_loop_to_loop.cc",
-    "for_loop_to_loop.h",
     "get_insertion_point.cc",
     "get_insertion_point.h",
     "hoist_to_decl_before.cc",
     "hoist_to_decl_before.h",
-    "localize_struct_array_assignment.cc",
-    "localize_struct_array_assignment.h",
     "manager.cc",
     "manager.h",
-    "merge_return.cc",
-    "merge_return.h",
-    "module_scope_var_to_entry_point_param.cc",
-    "module_scope_var_to_entry_point_param.h",
-    "msl_subgroup_ballot.cc",
-    "msl_subgroup_ballot.h",
     "multiplanar_external_texture.cc",
     "multiplanar_external_texture.h",
-    "num_workgroups_from_uniform.cc",
-    "num_workgroups_from_uniform.h",
-    "packed_vec3.cc",
-    "packed_vec3.h",
-    "pad_structs.cc",
-    "pad_structs.h",
     "preserve_padding.cc",
     "preserve_padding.h",
     "promote_initializers_to_let.cc",
     "promote_initializers_to_let.h",
     "promote_side_effects_to_decl.cc",
     "promote_side_effects_to_decl.h",
-    "remove_continue_in_switch.cc",
-    "remove_continue_in_switch.h",
     "remove_phonies.cc",
     "remove_phonies.h",
     "remove_unreachable_statements.cc",
@@ -113,32 +81,18 @@
     "simplify_pointers.h",
     "single_entry_point.cc",
     "single_entry_point.h",
-    "spirv_atomic.cc",
-    "spirv_atomic.h",
     "std140.cc",
     "std140.h",
     "substitute_override.cc",
     "substitute_override.h",
-    "texture_1d_to_2d.cc",
-    "texture_1d_to_2d.h",
-    "texture_builtins_from_uniform.cc",
-    "texture_builtins_from_uniform.h",
     "transform.cc",
     "transform.h",
-    "truncate_interstage_variables.cc",
-    "truncate_interstage_variables.h",
     "unshadow.cc",
     "unshadow.h",
-    "var_for_dynamic_index.cc",
-    "var_for_dynamic_index.h",
-    "vectorize_matrix_conversions.cc",
-    "vectorize_matrix_conversions.h",
     "vectorize_scalar_matrix_initializers.cc",
     "vectorize_scalar_matrix_initializers.h",
     "vertex_pulling.cc",
     "vertex_pulling.h",
-    "while_to_loop.cc",
-    "while_to_loop.h",
     "zero_init_workgroup_memory.cc",
     "zero_init_workgroup_memory.h",
   ]
@@ -176,58 +130,35 @@
       "array_length_from_uniform_test.cc",
       "binding_remapper_test.cc",
       "builtin_polyfill_test.cc",
-      "calculate_array_length_test.cc",
       "canonicalize_entry_point_io_test.cc",
-      "clamp_frag_depth_test.cc",
-      "combine_samplers_test.cc",
-      "decompose_memory_access_test.cc",
-      "decompose_strided_array_test.cc",
-      "decompose_strided_matrix_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",
-      "fold_trivial_lets_test.cc",
-      "for_loop_to_loop_test.cc",
       "get_insertion_point_test.cc",
       "helper_test.h",
       "hoist_to_decl_before_test.cc",
-      "localize_struct_array_assignment_test.cc",
       "manager_test.cc",
-      "merge_return_test.cc",
-      "module_scope_var_to_entry_point_param_test.cc",
-      "msl_subgroup_ballot_test.cc",
       "multiplanar_external_texture_test.cc",
-      "num_workgroups_from_uniform_test.cc",
-      "packed_vec3_test.cc",
-      "pad_structs_test.cc",
       "preserve_padding_test.cc",
       "promote_initializers_to_let_test.cc",
       "promote_side_effects_to_decl_test.cc",
-      "remove_continue_in_switch_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",
-      "spirv_atomic_test.cc",
       "std140_exhaustive_test.cc",
       "std140_f16_test.cc",
       "std140_f32_test.cc",
       "std140_test.cc",
       "substitute_override_test.cc",
-      "texture_1d_to_2d_test.cc",
-      "texture_builtins_from_uniform_test.cc",
       "transform_test.cc",
-      "truncate_interstage_variables_test.cc",
       "unshadow_test.cc",
-      "var_for_dynamic_index_test.cc",
-      "vectorize_matrix_conversions_test.cc",
       "vectorize_scalar_matrix_initializers_test.cc",
       "vertex_pulling_test.cc",
-      "while_to_loop_test.cc",
       "zero_init_workgroup_memory_test.cc",
     ]
     deps = [
@@ -242,7 +173,6 @@
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${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",
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
index 4e77074..876f7fb 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
@@ -1183,6 +1183,22 @@
                         }
                         return Symbol{};
 
+                    case core::Function::kTextureLoad:
+                        if (cfg.builtins.bgra8unorm) {
+                            auto& sig = builtin->Signature();
+                            auto* tex = sig.Parameter(core::ParameterUsage::kTexture);
+                            if (auto* stex = tex->Type()->As<core::type::StorageTexture>()) {
+                                if (stex->texel_format() == core::TexelFormat::kBgra8Unorm) {
+                                    ctx.Replace(expr, [this, expr] {
+                                        return ctx.dst->MemberAccessor(
+                                            ctx.CloneWithoutTransform(expr), "bgra");
+                                    });
+                                    made_changes = true;
+                                }
+                            }
+                        }
+                        return Symbol{};
+
                     case core::Function::kTextureSampleBaseClampToEdge:
                         if (cfg.builtins.texture_sample_base_clamp_to_edge_2d_f32) {
                             auto& sig = builtin->Signature();
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc b/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
index 683a4df..e5fad8b 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
@@ -441,6 +441,32 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(BuiltinPolyfillTest, Bgra8unorm_TextureLoad) {
+    auto* src = R"(
+enable chromium_experimental_read_write_storage_texture;
+
+@group(0) @binding(0) var tex : texture_storage_2d<bgra8unorm, read>;
+
+fn f(coords : vec2<i32>) -> vec4<f32> {
+  return textureLoad(tex, coords);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_read_write_storage_texture;
+
+@group(0) @binding(0) var tex : texture_storage_2d<rgba8unorm, read>;
+
+fn f(coords : vec2<i32>) -> vec4<f32> {
+  return textureLoad(tex, coords).bgra;
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillBgra8unorm());
+
+    EXPECT_EQ(expect, str(got));
+}
+
 TEST_F(BuiltinPolyfillTest, Bgra8unorm_TextureStore) {
     auto* src = R"(
 @group(0) @binding(0) var tex : texture_storage_2d<bgra8unorm, write>;
@@ -513,6 +539,32 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(BuiltinPolyfillTest, Bgra8unorm_TextureLoadAndStore) {
+    auto* src = R"(
+enable chromium_experimental_read_write_storage_texture;
+
+@group(0) @binding(0) var tex : texture_storage_2d<bgra8unorm, read_write>;
+
+fn f(coords : vec2<i32>) {
+  textureStore(tex, coords, textureLoad(tex, coords));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_read_write_storage_texture;
+
+@group(0) @binding(0) var tex : texture_storage_2d<rgba8unorm, read_write>;
+
+fn f(coords : vec2<i32>) {
+  textureStore(tex, coords, textureLoad(tex, coords).bgra.bgra);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillBgra8unorm());
+
+    EXPECT_EQ(expect, str(got));
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // bitshiftModulo
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index be33712..e9d244e 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -176,16 +176,16 @@
         }
     }
 
-    /// Clones the shader IO attributes from @p in.
+    /// Clones the shader IO and internal attributes from @p in.
     /// @param in the attributes to clone
     /// @param do_interpolate whether to clone InterpolateAttribute
     /// @return the cloned attributes
-    template <size_t N>
-    auto CloneShaderIOAttributes(const tint::Vector<const Attribute*, N> in, bool do_interpolate) {
-        tint::Vector<const Attribute*, N> out;
+    auto CloneShaderIOAttributes(tint::VectorRef<const Attribute*> in, bool do_interpolate) {
+        tint::Vector<const Attribute*, 8> out;
         for (auto* attr : in) {
-            if (IsShaderIOAttribute(attr) &&
-                (do_interpolate || !attr->template Is<InterpolateAttribute>())) {
+            if ((IsShaderIOAttribute(attr) &&
+                 (do_interpolate || !attr->template Is<InterpolateAttribute>())) ||
+                attr->Is<ast::InternalAttribute>()) {
                 CloneAttribute(attr, out);
             }
         }
diff --git a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc b/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
deleted file mode 100644
index c55eb98..0000000
--- a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright 2022 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/ast/transform/remove_continue_in_switch.h"
-
-#include <string>
-#include <unordered_map>
-#include <utility>
-
-#include "src/tint/lang/wgsl/ast/continue_statement.h"
-#include "src/tint/lang/wgsl/ast/switch_statement.h"
-#include "src/tint/lang/wgsl/ast/transform/get_insertion_point.h"
-#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/block_statement.h"
-#include "src/tint/lang/wgsl/sem/for_loop_statement.h"
-#include "src/tint/lang/wgsl/sem/loop_statement.h"
-#include "src/tint/lang/wgsl/sem/switch_statement.h"
-#include "src/tint/lang/wgsl/sem/while_statement.h"
-#include "src/tint/utils/containers/map.h"
-
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::RemoveContinueInSwitch);
-
-namespace tint::ast::transform {
-
-/// PIMPL state for the transform
-struct RemoveContinueInSwitch::State {
-    /// Constructor
-    /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
-
-    /// Runs the transform
-    /// @returns the new program or SkipTransform if the transform is not required
-    ApplyResult Run() {
-        bool made_changes = false;
-
-        for (auto* node : src->ASTNodes().Objects()) {
-            auto* cont = node->As<ContinueStatement>();
-            if (!cont) {
-                continue;
-            }
-
-            // If first parent is not a switch within a loop, skip
-            auto* switch_stmt = GetParentSwitchInLoop(sem, cont);
-            if (!switch_stmt) {
-                continue;
-            }
-
-            made_changes = true;
-
-            auto cont_var_name = tint::GetOrCreate(switch_to_cont_var_name, switch_stmt, [&] {
-                // Create and insert 'var tint_continue : bool = false;' before the
-                // switch.
-                auto var_name = b.Symbols().New("tint_continue");
-                auto* decl = b.Decl(b.Var(var_name, b.ty.bool_(), b.Expr(false)));
-                auto ip = utils::GetInsertionPoint(ctx, switch_stmt);
-                ctx.InsertBefore(ip.first->Declaration()->statements, ip.second, decl);
-
-                // Create and insert 'if (tint_continue) { continue; }' after
-                // switch.
-                auto* if_stmt = b.If(b.Expr(var_name), b.Block(b.Continue()));
-                ctx.InsertAfter(ip.first->Declaration()->statements, ip.second, if_stmt);
-
-                // Return the new var name
-                return var_name;
-            });
-
-            // Replace 'continue;' with '{ tint_continue = true; break; }'
-            auto* new_stmt = b.Block(                   //
-                b.Assign(b.Expr(cont_var_name), true),  //
-                b.Break());
-
-            ctx.Replace(cont, new_stmt);
-        }
-
-        if (!made_changes) {
-            return SkipTransform;
-        }
-
-        ctx.Clone();
-        return resolver::Resolve(b);
-    }
-
-  private:
-    /// The source program
-    const Program* const src;
-    /// The target program builder
-    ProgramBuilder b;
-    /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
-    /// Alias to src->sem
-    const sem::Info& sem = src->Sem();
-
-    // Map of switch statement to 'tint_continue' variable.
-    std::unordered_map<const SwitchStatement*, Symbol> switch_to_cont_var_name;
-
-    // If `cont` is within a switch statement within a loop, returns a pointer to
-    // that switch statement.
-    static const SwitchStatement* GetParentSwitchInLoop(const sem::Info& sem,
-                                                        const ContinueStatement* cont) {
-        // Find whether first parent is a switch or a loop
-        auto* sem_stmt = sem.Get(cont);
-        auto* sem_parent = sem_stmt->FindFirstParent<sem::SwitchStatement, sem::LoopBlockStatement,
-                                                     sem::ForLoopStatement, sem::WhileStatement>();
-        if (!sem_parent) {
-            return nullptr;
-        }
-        return sem_parent->Declaration()->As<SwitchStatement>();
-    }
-};
-
-RemoveContinueInSwitch::RemoveContinueInSwitch() = default;
-RemoveContinueInSwitch::~RemoveContinueInSwitch() = default;
-
-Transform::ApplyResult RemoveContinueInSwitch::Apply(const Program* src,
-                                                     const DataMap&,
-                                                     DataMap&) const {
-    State state(src);
-    return state.Run();
-}
-
-}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/robustness_test.cc b/src/tint/lang/wgsl/ast/transform/robustness_test.cc
index c2d3fce..e2074ef 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness_test.cc
@@ -1788,7 +1788,7 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-// Texture
+// Texture builtin calls.
 ////////////////////////////////////////////////////////////////////////////////
 
 TEST_P(RobustnessTest, TextureDimensions) {
diff --git a/src/tint/lang/wgsl/helpers/BUILD.bazel b/src/tint/lang/wgsl/helpers/BUILD.bazel
new file mode 100644
index 0000000..3dada81
--- /dev/null
+++ b/src/tint/lang/wgsl/helpers/BUILD.bazel
@@ -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.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 = [
+    "append_vector.cc",
+    "check_supported_extensions.cc",
+    "flatten_bindings.cc",
+  ],
+  hdrs = [
+    "append_vector.h",
+    "check_supported_extensions.h",
+    "flatten_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/ast",
+    "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/inspector",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "append_vector_test.cc",
+    "check_supported_extensions_test.cc",
+    "flatten_bindings_test.cc",
+    "ir_program_test.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/helpers",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/reader",
+    "//src/tint/lang/wgsl/reader/program_to_ir",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/helpers/BUILD.cmake b/src/tint/lang/wgsl/helpers/BUILD.cmake
index 0c6b82f..085c869 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.cmake
+++ b/src/tint/lang/wgsl/helpers/BUILD.cmake
@@ -75,12 +75,14 @@
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_helpers
   tint_lang_wgsl_program
   tint_lang_wgsl_reader
+  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -101,10 +103,3 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_helpers_test test
   "gtest"
 )
-
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_wgsl_helpers_test test
-    tint_lang_core_ir
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_IR)
diff --git a/src/tint/lang/wgsl/helpers/BUILD.gn b/src/tint/lang/wgsl/helpers/BUILD.gn
index 0ebfb1f..b676822 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.gn
+++ b/src/tint/lang/wgsl/helpers/BUILD.gn
@@ -78,12 +78,14 @@
       "${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/ast",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
       "${tint_src_dir}/lang/wgsl/helpers",
       "${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/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
@@ -100,12 +102,5 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
-
-    if (tint_build_ir) {
-      deps += [
-        "${tint_src_dir}/lang/core/ir",
-        "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-      ]
-    }
   }
 }
diff --git a/src/tint/lang/wgsl/inspector/BUILD.bazel b/src/tint/lang/wgsl/inspector/BUILD.bazel
new file mode 100644
index 0000000..79a0b76
--- /dev/null
+++ b/src/tint/lang/wgsl/inspector/BUILD.bazel
@@ -0,0 +1,104 @@
+# 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 = "inspector",
+  srcs = [
+    "entry_point.cc",
+    "inspector.cc",
+    "resource_binding.cc",
+    "scalar.cc",
+  ],
+  hdrs = [
+    "entry_point.h",
+    "inspector.h",
+    "resource_binding.h",
+    "scalar.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "inspector_builder_test.cc",
+    "inspector_builder_test.h",
+    "inspector_runner_test.cc",
+    "inspector_runner_test.h",
+    "inspector_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/inspector/entry_point.h b/src/tint/lang/wgsl/inspector/entry_point.h
index 1c7525f..7d8e242 100644
--- a/src/tint/lang/wgsl/inspector/entry_point.h
+++ b/src/tint/lang/wgsl/inspector/entry_point.h
@@ -45,6 +45,14 @@
     kUnknown,
 };
 
+/// Types of `pixel_local` variable members.
+enum class PixelLocalMemberType : uint8_t {
+    kF32,
+    kU32,
+    kI32,
+    kUnknown,
+};
+
 /// Type of interpolation of a stage variable.
 enum class InterpolationType : uint8_t { kPerspective, kLinear, kFlat, kUnknown };
 
@@ -138,12 +146,17 @@
     /// size is derived from an override-expression. In this situation you first need to run the
     /// tint::ast::transform::SubstituteOverride transform before using the inspector.
     std::optional<WorkgroupSize> workgroup_size;
+    /// The total size in bytes of all Workgroup storage-class storage accessed via the entry point.
+    uint32_t workgroup_storage_size = 0;
     /// List of the input variable accessed via this entry point.
     std::vector<StageVariable> input_variables;
     /// List of the output variable accessed via this entry point.
     std::vector<StageVariable> output_variables;
     /// List of the pipeline overridable constants accessed via this entry point.
     std::vector<Override> overrides;
+    /// List of the variable types used in the `pixel_local` block accessed by this entry point (if
+    /// any).
+    std::vector<PixelLocalMemberType> pixel_local_members;
     /// Does the entry point use the sample_mask builtin as an input builtin
     /// variable.
     bool input_sample_mask_used = false;
@@ -161,6 +174,10 @@
     bool num_workgroups_used = false;
     /// Does the entry point use the frag_depth builtin
     bool frag_depth_used = false;
+    /// Does the entry point use the vertex_index builtin
+    bool vertex_index_used = false;
+    /// Does the entry point use the instance_index builtin
+    bool instance_index_used = false;
 };
 
 }  // namespace tint::inspector
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index ee1b751..7688ece 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -139,6 +139,7 @@
     switch (func->PipelineStage()) {
         case ast::PipelineStage::kCompute: {
             entry_point.stage = PipelineStage::kCompute;
+            entry_point.workgroup_storage_size = ComputeWorkgroupStorageSize(func);
 
             auto wgsize = sem->WorkgroupSize();
             if (wgsize[0].has_value() && wgsize[1].has_value() && wgsize[2].has_value()) {
@@ -149,6 +150,7 @@
         }
         case ast::PipelineStage::kFragment: {
             entry_point.stage = PipelineStage::kFragment;
+            entry_point.pixel_local_members = ComputePixelLocalMemberTypes(func);
             break;
         }
         case ast::PipelineStage::kVertex: {
@@ -177,6 +179,10 @@
             core::BuiltinValue::kSampleMask, param->Type(), param->Declaration()->attributes);
         entry_point.num_workgroups_used |= ContainsBuiltin(
             core::BuiltinValue::kNumWorkgroups, param->Type(), param->Declaration()->attributes);
+        entry_point.vertex_index_used |= ContainsBuiltin(
+            core::BuiltinValue::kVertexIndex, param->Type(), param->Declaration()->attributes);
+        entry_point.instance_index_used |= ContainsBuiltin(
+            core::BuiltinValue::kInstanceIndex, param->Type(), param->Declaration()->attributes);
     }
 
     if (!sem->ReturnType()->Is<core::type::Void>()) {
@@ -302,27 +308,6 @@
     return result;
 }
 
-uint32_t Inspector::GetStorageSize(const std::string& entry_point) {
-    auto* func = FindEntryPointByName(entry_point);
-    if (!func) {
-        return 0;
-    }
-
-    size_t size = 0;
-    auto* func_sem = program_->Sem().Get(func);
-    for (auto& ruv : func_sem->TransitivelyReferencedUniformVariables()) {
-        size += ruv.first->Type()->UnwrapRef()->Size();
-    }
-    for (auto& rsv : func_sem->TransitivelyReferencedStorageBufferVariables()) {
-        size += rsv.first->Type()->UnwrapRef()->Size();
-    }
-
-    if (static_cast<uint64_t>(size) > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
-        return std::numeric_limits<uint32_t>::max();
-    }
-    return static_cast<uint32_t>(size);
-}
-
 std::vector<ResourceBinding> Inspector::GetResourceBindings(const std::string& entry_point) {
     auto* func = FindEntryPointByName(entry_point);
     if (!func) {
@@ -538,31 +523,6 @@
     return new_pairs;
 }
 
-uint32_t Inspector::GetWorkgroupStorageSize(const std::string& entry_point) {
-    auto* func = FindEntryPointByName(entry_point);
-    if (!func) {
-        return 0;
-    }
-
-    uint32_t total_size = 0;
-    auto* func_sem = program_->Sem().Get(func);
-    for (const sem::Variable* var : func_sem->TransitivelyReferencedGlobals()) {
-        if (var->AddressSpace() == core::AddressSpace::kWorkgroup) {
-            auto* ty = var->Type()->UnwrapRef();
-            uint32_t align = ty->Align();
-            uint32_t size = ty->Size();
-
-            // This essentially matches std430 layout rules from GLSL, which are in
-            // turn specified as an upper bound for Vulkan layout sizing. Since D3D
-            // and Metal are even less specific, we assume Vulkan behavior as a
-            // good-enough approximation everywhere.
-            total_size += tint::RoundUp(align, size);
-        }
-    }
-
-    return total_size;
-}
-
 std::vector<std::string> Inspector::GetUsedExtensionNames() {
     auto& extensions = program_->Sem().Module()->Extensions();
     std::vector<std::string> out;
@@ -924,6 +884,57 @@
     return {interpolation_type, sampling_type};
 }
 
+uint32_t Inspector::ComputeWorkgroupStorageSize(const ast::Function* func) const {
+    uint32_t total_size = 0;
+    auto* func_sem = program_->Sem().Get(func);
+    for (const sem::Variable* var : func_sem->TransitivelyReferencedGlobals()) {
+        if (var->AddressSpace() == core::AddressSpace::kWorkgroup) {
+            auto* ty = var->Type()->UnwrapRef();
+            uint32_t align = ty->Align();
+            uint32_t size = ty->Size();
+
+            // This essentially matches std430 layout rules from GLSL, which are in
+            // turn specified as an upper bound for Vulkan layout sizing. Since D3D
+            // and Metal are even less specific, we assume Vulkan behavior as a
+            // good-enough approximation everywhere.
+            total_size += tint::RoundUp(align, size);
+        }
+    }
+
+    return total_size;
+}
+
+std::vector<PixelLocalMemberType> Inspector::ComputePixelLocalMemberTypes(
+    const ast::Function* func) const {
+    auto* func_sem = program_->Sem().Get(func);
+    for (const sem::Variable* var : func_sem->TransitivelyReferencedGlobals()) {
+        if (var->AddressSpace() != core::AddressSpace::kPixelLocal) {
+            continue;
+        }
+
+        auto* str = var->Type()->UnwrapRef()->As<sem::Struct>();
+
+        std::vector<PixelLocalMemberType> types;
+        types.reserve(str->Members().Length());
+        for (auto* member : str->Members()) {
+            PixelLocalMemberType type = Switch(
+                member->Type(),  //
+                [&](const core::type::F32*) { return PixelLocalMemberType::kF32; },
+                [&](const core::type::I32*) { return PixelLocalMemberType::kI32; },
+                [&](const core::type::U32*) { return PixelLocalMemberType::kU32; },
+                [&](Default) {
+                    TINT_UNREACHABLE() << "unhandled component type";
+                    return PixelLocalMemberType::kUnknown;
+                });
+            types.push_back(type);
+        }
+
+        return types;
+    }
+
+    return {};
+}
+
 template <size_t N, typename F>
 void Inspector::GetOriginatingResources(std::array<const ast::Expression*, N> exprs, F&& callback) {
     if (TINT_UNLIKELY(!program_->IsValid())) {
diff --git a/src/tint/lang/wgsl/inspector/inspector.h b/src/tint/lang/wgsl/inspector/inspector.h
index a615b40..e1f6d6e 100644
--- a/src/tint/lang/wgsl/inspector/inspector.h
+++ b/src/tint/lang/wgsl/inspector/inspector.h
@@ -67,11 +67,6 @@
     std::map<std::string, OverrideId> GetNamedOverrideIds();
 
     /// @param entry_point name of the entry point to get information about.
-    /// @returns the total size of shared storage required by an entry point,
-    ///          including all uniform storage buffers.
-    uint32_t GetStorageSize(const std::string& entry_point);
-
-    /// @param entry_point name of the entry point to get information about.
     /// @returns vector of all of the resource bindings.
     std::vector<ResourceBinding> GetResourceBindings(const std::string& entry_point);
 
@@ -136,11 +131,6 @@
     std::vector<SamplerTexturePair> GetSamplerTextureUses(const std::string& entry_point,
                                                           const BindingPoint& placeholder);
 
-    /// @param entry_point name of the entry point to get information about.
-    /// @returns the total size in bytes of all Workgroup storage-class storage
-    /// referenced transitively by the entry point.
-    uint32_t GetWorkgroupStorageSize(const std::string& entry_point);
-
     /// @returns vector of all valid extension names used by the program. There
     /// will be no duplicated names in the returned vector even if an extension
     /// is enabled multiple times.
@@ -228,6 +218,14 @@
         const core::type::Type* type,
         VectorRef<const ast::Attribute*> attributes) const;
 
+    /// @param func the root function of the callgraph to consider for the computation.
+    /// @returns the total size in bytes of all Workgroup storage-class storage accessed via func.
+    uint32_t ComputeWorkgroupStorageSize(const ast::Function* func) const;
+
+    /// @param func the root function of the callgraph to consider for the computation
+    /// @returns the list of member types for the `pixel_local` variable accessed via func, if any.
+    std::vector<PixelLocalMemberType> ComputePixelLocalMemberTypes(const ast::Function* func) const;
+
     /// For a N-uple of expressions, resolve to the appropriate global resources
     /// and call 'cb'.
     /// 'cb' may be called multiple times.
diff --git a/src/tint/lang/wgsl/inspector/inspector_test.cc b/src/tint/lang/wgsl/inspector/inspector_test.cc
index 2721c9f..ad171bd 100644
--- a/src/tint/lang/wgsl/inspector/inspector_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_test.cc
@@ -64,7 +64,6 @@
       public testing::TestWithParam<InspectorGetEntryPointInterpolateTestParams> {};
 class InspectorGetOverrideDefaultValuesTest : public InspectorBuilder, public testing::Test {};
 class InspectorGetConstantNameToIdMapTest : public InspectorBuilder, public testing::Test {};
-class InspectorGetStorageSizeTest : public InspectorBuilder, public testing::Test {};
 class InspectorGetResourceBindingsTest : public InspectorBuilder, public testing::Test {};
 class InspectorGetUniformBufferResourceBindingsTest : public InspectorBuilder,
                                                       public testing::Test {};
@@ -127,8 +126,6 @@
 
 class InspectorGetSamplerTextureUsesTest : public InspectorRunner, public testing::Test {};
 
-class InspectorGetWorkgroupStorageSizeTest : public InspectorBuilder, public testing::Test {};
-
 class InspectorGetUsedExtensionNamesTest : public InspectorRunner, public testing::Test {};
 
 class InspectorGetEnableDirectivesTest : public InspectorRunner, public testing::Test {};
@@ -267,6 +264,119 @@
     EXPECT_EQ(1u, workgroup_size->z);
 }
 
+TEST_F(InspectorGetEntryPointTest, WorkgroupStorageSizeEmpty) {
+    MakeEmptyBodyFunction("ep_func", Vector{
+                                         Stage(ast::PipelineStage::kCompute),
+                                         WorkgroupSize(1_i),
+                                     });
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_EQ(0u, result[0].workgroup_storage_size);
+}
+
+TEST_F(InspectorGetEntryPointTest, WorkgroupStorageSizeSimple) {
+    AddWorkgroupStorage("wg_f32", ty.f32());
+    MakePlainGlobalReferenceBodyFunction("f32_func", "wg_f32", ty.f32(), tint::Empty);
+
+    MakeCallerBodyFunction("ep_func", Vector{std::string("f32_func")},
+                           Vector{
+                               Stage(ast::PipelineStage::kCompute),
+                               WorkgroupSize(1_i),
+                           });
+
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_EQ(4u, result[0].workgroup_storage_size);
+}
+
+TEST_F(InspectorGetEntryPointTest, WorkgroupStorageSizeCompoundTypes) {
+    // This struct should occupy 68 bytes. 4 from the i32 field, and another 64
+    // from the 4-element array with 16-byte stride.
+    auto* wg_struct_type = MakeStructType("WgStruct", Vector{
+                                                          ty.i32(),
+                                                          ty.array<i32, 4>(Vector{
+                                                              Stride(16),
+                                                          }),
+                                                      });
+    AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
+    MakeStructVariableReferenceBodyFunction("wg_struct_func", "wg_struct_var",
+                                            Vector{
+                                                MemberInfo{0, ty.i32()},
+                                            });
+
+    // Plus another 4 bytes from this other workgroup-class f32.
+    AddWorkgroupStorage("wg_f32", ty.f32());
+    MakePlainGlobalReferenceBodyFunction("f32_func", "wg_f32", ty.f32(), tint::Empty);
+
+    MakeCallerBodyFunction("ep_func", Vector{std::string("wg_struct_func"), "f32_func"},
+                           Vector{
+                               Stage(ast::PipelineStage::kCompute),
+                               WorkgroupSize(1_i),
+                           });
+
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_EQ(72u, result[0].workgroup_storage_size);
+}
+
+TEST_F(InspectorGetEntryPointTest, WorkgroupStorageSizeAlignmentPadding) {
+    // vec3<f32> has an alignment of 16 but a size of 12. We leverage this to test
+    // that our padded size calculation for workgroup storage is accurate.
+    AddWorkgroupStorage("wg_vec3", ty.vec3<f32>());
+    MakePlainGlobalReferenceBodyFunction("wg_func", "wg_vec3", ty.vec3<f32>(), tint::Empty);
+
+    MakeCallerBodyFunction("ep_func", Vector{std::string("wg_func")},
+                           Vector{
+                               Stage(ast::PipelineStage::kCompute),
+                               WorkgroupSize(1_i),
+                           });
+
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_EQ(16u, result[0].workgroup_storage_size);
+}
+
+TEST_F(InspectorGetEntryPointTest, WorkgroupStorageSizeStructAlignment) {
+    // Per WGSL spec, a struct's size is the offset its last member plus the size
+    // of its last member, rounded up to the alignment of its largest member. So
+    // here the struct is expected to occupy 1024 bytes of workgroup storage.
+    const auto* wg_struct_type = MakeStructTypeFromMembers(
+        "WgStruct", Vector{
+                        MakeStructMember(0, ty.f32(), Vector{MemberAlign(1024_i)}),
+                    });
+
+    AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
+    MakeStructVariableReferenceBodyFunction("wg_struct_func", "wg_struct_var",
+                                            Vector{
+                                                MemberInfo{0, ty.f32()},
+                                            });
+
+    MakeCallerBodyFunction("ep_func", Vector{std::string("wg_struct_func")},
+                           Vector{
+                               Stage(ast::PipelineStage::kCompute),
+                               WorkgroupSize(1_i),
+                           });
+
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_EQ(1024u, result[0].workgroup_storage_size);
+}
+
 TEST_F(InspectorGetEntryPointTest, NoInOutVariables) {
     MakeEmptyBodyFunction("func", tint::Empty);
 
@@ -1405,6 +1515,59 @@
     EXPECT_EQ(InterpolationSampling::kCenter, result[0].input_variables[0].interpolation_sampling);
 }
 
+TEST_F(InspectorGetEntryPointTest, PixelLocalMemberDefault) {
+    // @fragment fn foo() {}
+    MakeEmptyBodyFunction("foo", Vector{
+                                     Stage(ast::PipelineStage::kFragment),
+                                 });
+
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_EQ(0u, result[0].pixel_local_members.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, PixelLocalMemberTypes) {
+    // enable chromium_experimental_pixel_local;
+    // struct Ure {
+    //   toto : u32;
+    //   titi : f32;
+    //   tata: i32;
+    //   tonton : u32; // Check having the same type multiple times
+    // }
+    // var<pixel_local> pls : Ure;
+    // @fragment fn foo() {  _ = pls; }
+
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("Ure", Vector{
+                         Member("toto", ty.u32()),
+                         Member("titi", ty.f32()),
+                         Member("tata", ty.i32()),
+                         Member("tonton", ty.u32()),
+                     });
+    GlobalVar("pls", core::AddressSpace::kPixelLocal, ty("Ure"));
+    Func("foo", tint::Empty, ty.void_(),
+         Vector{
+             Assign(Phony(), "pls"),
+         },
+         Vector{
+             Stage(ast::PipelineStage::kFragment),
+         });
+
+    Inspector& inspector = Build();
+    auto result = inspector.GetEntryPoints();
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+    ASSERT_EQ(4u, result[0].pixel_local_members.size());
+    ASSERT_EQ(PixelLocalMemberType::kU32, result[0].pixel_local_members[0]);
+    ASSERT_EQ(PixelLocalMemberType::kF32, result[0].pixel_local_members[1]);
+    ASSERT_EQ(PixelLocalMemberType::kI32, result[0].pixel_local_members[2]);
+    ASSERT_EQ(PixelLocalMemberType::kU32, result[0].pixel_local_members[3]);
+}
+
 TEST_P(InspectorGetEntryPointInterpolateTest, Test) {
     auto& params = GetParam();
     Structure("in_struct",
@@ -1688,110 +1851,6 @@
     EXPECT_EQ(result["c"], program_->Sem().Get(c)->OverrideId());
 }
 
-TEST_F(InspectorGetStorageSizeTest, Empty) {
-    MakeEmptyBodyFunction("ep_func", Vector{
-                                         Stage(ast::PipelineStage::kCompute),
-                                         WorkgroupSize(1_i),
-                                     });
-    Inspector& inspector = Build();
-    EXPECT_EQ(0u, inspector.GetStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetStorageSizeTest, Simple_NonStruct) {
-    AddUniformBuffer("ub_var", ty.i32(), 0, 0);
-    AddStorageBuffer("sb_var", ty.i32(), core::Access::kReadWrite, 1, 0);
-    AddStorageBuffer("rosb_var", ty.i32(), core::Access::kRead, 1, 1);
-    Func("ep_func", tint::Empty, ty.void_(),
-         Vector{
-             Decl(Let("ub", Expr("ub_var"))),
-             Decl(Let("sb", Expr("sb_var"))),
-             Decl(Let("rosb", Expr("rosb_var"))),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kCompute),
-             WorkgroupSize(1_i),
-         });
-
-    Inspector& inspector = Build();
-
-    EXPECT_EQ(12u, inspector.GetStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetStorageSizeTest, Simple_Struct) {
-    auto* ub_struct_type = MakeUniformBufferType("ub_type", Vector{
-                                                                ty.i32(),
-                                                                ty.i32(),
-                                                            });
-    AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
-    MakeStructVariableReferenceBodyFunction("ub_func", "ub_var",
-                                            Vector{
-                                                MemberInfo{0, ty.i32()},
-                                            });
-
-    auto sb = MakeStorageBufferTypes("sb_type", Vector{
-                                                    ty.i32(),
-                                                });
-    AddStorageBuffer("sb_var", sb(), core::Access::kReadWrite, 1, 0);
-    MakeStructVariableReferenceBodyFunction("sb_func", "sb_var",
-                                            Vector{
-                                                MemberInfo{0, ty.i32()},
-                                            });
-
-    auto ro_sb = MakeStorageBufferTypes("rosb_type", Vector{
-                                                         ty.i32(),
-                                                     });
-    AddStorageBuffer("rosb_var", ro_sb(), core::Access::kRead, 1, 1);
-    MakeStructVariableReferenceBodyFunction("rosb_func", "rosb_var",
-                                            Vector{
-                                                MemberInfo{0, ty.i32()},
-                                            });
-
-    MakeCallerBodyFunction("ep_func", Vector{std::string("ub_func"), "sb_func", "rosb_func"},
-                           Vector{
-                               Stage(ast::PipelineStage::kCompute),
-                               WorkgroupSize(1_i),
-                           });
-
-    Inspector& inspector = Build();
-
-    EXPECT_EQ(16u, inspector.GetStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetStorageSizeTest, NonStructVec3) {
-    AddUniformBuffer("ub_var", ty.vec3<f32>(), 0, 0);
-    Func("ep_func", tint::Empty, ty.void_(),
-         Vector{
-             Decl(Let("ub", Expr("ub_var"))),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kCompute),
-             WorkgroupSize(1_i),
-         });
-
-    Inspector& inspector = Build();
-
-    EXPECT_EQ(12u, inspector.GetStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetStorageSizeTest, StructVec3) {
-    auto* ub_struct_type = MakeUniformBufferType("ub_type", Vector{
-                                                                ty.vec3<f32>(),
-                                                            });
-    AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
-    Func("ep_func", tint::Empty, ty.void_(),
-         Vector{
-             Decl(Let("ub", Expr("ub_var"))),
-         },
-         Vector{
-             Stage(ast::PipelineStage::kCompute),
-             WorkgroupSize(1_i),
-         });
-
-    Inspector& inspector = Build();
-
-    EXPECT_EQ(16u, inspector.GetStorageSize("ep_func"));
-}
-
 TEST_F(InspectorGetResourceBindingsTest, Empty) {
     MakeCallerBodyFunction("ep_func", tint::Empty,
                            Vector{
@@ -3487,99 +3546,6 @@
     }
 }
 
-TEST_F(InspectorGetWorkgroupStorageSizeTest, Empty) {
-    MakeEmptyBodyFunction("ep_func", Vector{
-                                         Stage(ast::PipelineStage::kCompute),
-                                         WorkgroupSize(1_i),
-                                     });
-    Inspector& inspector = Build();
-    EXPECT_EQ(0u, inspector.GetWorkgroupStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetWorkgroupStorageSizeTest, Simple) {
-    AddWorkgroupStorage("wg_f32", ty.f32());
-    MakePlainGlobalReferenceBodyFunction("f32_func", "wg_f32", ty.f32(), tint::Empty);
-
-    MakeCallerBodyFunction("ep_func", Vector{std::string("f32_func")},
-                           Vector{
-                               Stage(ast::PipelineStage::kCompute),
-                               WorkgroupSize(1_i),
-                           });
-
-    Inspector& inspector = Build();
-    EXPECT_EQ(4u, inspector.GetWorkgroupStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetWorkgroupStorageSizeTest, CompoundTypes) {
-    // This struct should occupy 68 bytes. 4 from the i32 field, and another 64
-    // from the 4-element array with 16-byte stride.
-    auto* wg_struct_type = MakeStructType("WgStruct", Vector{
-                                                          ty.i32(),
-                                                          ty.array<i32, 4>(Vector{
-                                                              Stride(16),
-                                                          }),
-                                                      });
-    AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
-    MakeStructVariableReferenceBodyFunction("wg_struct_func", "wg_struct_var",
-                                            Vector{
-                                                MemberInfo{0, ty.i32()},
-                                            });
-
-    // Plus another 4 bytes from this other workgroup-class f32.
-    AddWorkgroupStorage("wg_f32", ty.f32());
-    MakePlainGlobalReferenceBodyFunction("f32_func", "wg_f32", ty.f32(), tint::Empty);
-
-    MakeCallerBodyFunction("ep_func", Vector{std::string("wg_struct_func"), "f32_func"},
-                           Vector{
-                               Stage(ast::PipelineStage::kCompute),
-                               WorkgroupSize(1_i),
-                           });
-
-    Inspector& inspector = Build();
-    EXPECT_EQ(72u, inspector.GetWorkgroupStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetWorkgroupStorageSizeTest, AlignmentPadding) {
-    // vec3<f32> has an alignment of 16 but a size of 12. We leverage this to test
-    // that our padded size calculation for workgroup storage is accurate.
-    AddWorkgroupStorage("wg_vec3", ty.vec3<f32>());
-    MakePlainGlobalReferenceBodyFunction("wg_func", "wg_vec3", ty.vec3<f32>(), tint::Empty);
-
-    MakeCallerBodyFunction("ep_func", Vector{std::string("wg_func")},
-                           Vector{
-                               Stage(ast::PipelineStage::kCompute),
-                               WorkgroupSize(1_i),
-                           });
-
-    Inspector& inspector = Build();
-    EXPECT_EQ(16u, inspector.GetWorkgroupStorageSize("ep_func"));
-}
-
-TEST_F(InspectorGetWorkgroupStorageSizeTest, StructAlignment) {
-    // Per WGSL spec, a struct's size is the offset its last member plus the size
-    // of its last member, rounded up to the alignment of its largest member. So
-    // here the struct is expected to occupy 1024 bytes of workgroup storage.
-    const auto* wg_struct_type = MakeStructTypeFromMembers(
-        "WgStruct", Vector{
-                        MakeStructMember(0, ty.f32(), Vector{MemberAlign(1024_i)}),
-                    });
-
-    AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
-    MakeStructVariableReferenceBodyFunction("wg_struct_func", "wg_struct_var",
-                                            Vector{
-                                                MemberInfo{0, ty.f32()},
-                                            });
-
-    MakeCallerBodyFunction("ep_func", Vector{std::string("wg_struct_func")},
-                           Vector{
-                               Stage(ast::PipelineStage::kCompute),
-                               WorkgroupSize(1_i),
-                           });
-
-    Inspector& inspector = Build();
-    EXPECT_EQ(1024u, inspector.GetWorkgroupStorageSize("ep_func"));
-}
-
 // Test calling GetUsedExtensionNames on a empty shader.
 TEST_F(InspectorGetUsedExtensionNamesTest, Empty) {
     std::string shader = "";
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index c4bda0a..8f08426 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -12,7 +12,7 @@
 // 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 && tint_build_ir)
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader && tint_build_wgsl_writer)
 
 #include "src/tint/lang/wgsl/helpers/ir_program_test.h"
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
diff --git a/src/tint/lang/wgsl/program/BUILD.bazel b/src/tint/lang/wgsl/program/BUILD.bazel
new file mode 100644
index 0000000..aefd945
--- /dev/null
+++ b/src/tint/lang/wgsl/program/BUILD.bazel
@@ -0,0 +1,98 @@
+# 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 = "program",
+  srcs = [
+    "clone_context.cc",
+    "program.cc",
+    "program_builder.cc",
+  ],
+  hdrs = [
+    "clone_context.h",
+    "program.h",
+    "program_builder.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "clone_context_test.cc",
+    "program_builder_test.cc",
+    "program_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/reader/BUILD.bazel b/src/tint/lang/wgsl/reader/BUILD.bazel
new file mode 100644
index 0000000..4e8ffe5
--- /dev/null
+++ b/src/tint/lang/wgsl/reader/BUILD.bazel
@@ -0,0 +1,91 @@
+# 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 = "reader",
+  srcs = [
+    "reader.cc",
+  ],
+  hdrs = [
+    "reader.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "reader_bench.cc",
+  ],
+  deps = [
+    "//src/tint/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//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"],
+)
+
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.bazel b/src/tint/lang/wgsl/reader/parser/BUILD.bazel
new file mode 100644
index 0000000..fdb4446
--- /dev/null
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.bazel
@@ -0,0 +1,169 @@
+# 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 = "parser",
+  srcs = [
+    "classify_template_args.cc",
+    "lexer.cc",
+    "parser.cc",
+    "token.cc",
+  ],
+  hdrs = [
+    "classify_template_args.h",
+    "detail.h",
+    "lexer.h",
+    "parser.h",
+    "token.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "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 = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
index e226ead..9f71c25 100644
--- a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
@@ -164,7 +164,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -178,7 +178,7 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -226,7 +226,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -237,7 +237,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -249,7 +249,7 @@
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_read_write_storage_texture', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
new file mode 100644
index 0000000..b78023b
--- /dev/null
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
@@ -0,0 +1,111 @@
+# 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 = "program_to_ir",
+  srcs = [
+    "program_to_ir.cc",
+  ],
+  hdrs = [
+    "program_to_ir.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "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 = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/helpers:test",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/reader",
+    "//src/tint/lang/wgsl/reader/program_to_ir",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cfg b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cfg
deleted file mode 100644
index 0ca3ed3..0000000
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "condition": "tint_build_ir"
-}
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 28e5889..ee5707d 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
@@ -21,11 +21,9 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_program_to_ir
 # Kind:      lib
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_program_to_ir lib
   lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -36,6 +34,9 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
@@ -55,18 +56,9 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_wgsl_reader_program_to_ir lib
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_wgsl_reader_program_to_ir_test
 # Kind:      test
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_wgsl_reader_program_to_ir_test test
   lang/wgsl/reader/program_to_ir/accessor_test.cc
@@ -88,11 +80,13 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl_ast
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program
   tint_lang_wgsl_reader
+  tint_lang_wgsl_reader_program_to_ir
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -113,12 +107,3 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_reader_program_to_ir_test test
   "gtest"
 )
-
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_wgsl_reader_program_to_ir_test test
-    tint_lang_core_ir
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
\ 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 f4eb7f4..6b6d958 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
@@ -28,19 +28,69 @@
 if (tint_build_unittests) {
   import("//testing/test.gni")
 }
-if (tint_build_ir) {
-  libtint_source_set("program_to_ir") {
+
+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/intrinsic/data",
+    "${tint_src_dir}/lang/core/ir",
+    "${tint_src_dir}/lang/core/type",
+    "${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_unittests) {
+  tint_unittests_source_set("unittests") {
+    testonly = true
     sources = [
-      "program_to_ir.cc",
-      "program_to_ir.h",
+      "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/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/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",
@@ -56,64 +106,5 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
-
-    if (tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/core/ir" ]
-    }
-  }
-}
-if (tint_build_unittests) {
-  if (tint_build_ir) {
-    tint_unittests_source_set("unittests") {
-      testonly = true
-      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/type",
-        "${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/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_ir) {
-        deps += [
-          "${tint_src_dir}/lang/core/ir",
-          "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
-        ]
-      }
-    }
   }
 }
diff --git a/src/tint/lang/wgsl/resolver/BUILD.bazel b/src/tint/lang/wgsl/resolver/BUILD.bazel
new file mode 100644
index 0000000..d9cf277
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/BUILD.bazel
@@ -0,0 +1,166 @@
+# 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 = "resolver",
+  srcs = [
+    "dependency_graph.cc",
+    "resolve.cc",
+    "resolver.cc",
+    "sem_helper.cc",
+    "uniformity.cc",
+    "validator.cc",
+  ],
+  hdrs = [
+    "dependency_graph.h",
+    "resolve.h",
+    "resolver.h",
+    "sem_helper.h",
+    "uniformity.h",
+    "validator.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/type",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "address_space_layout_validation_test.cc",
+    "address_space_validation_test.cc",
+    "alias_analysis_test.cc",
+    "array_accessor_test.cc",
+    "assignment_validation_test.cc",
+    "atomics_test.cc",
+    "atomics_validation_test.cc",
+    "attribute_validation_test.cc",
+    "bitcast_validation_test.cc",
+    "builtin_enum_test.cc",
+    "builtin_structs_test.cc",
+    "builtin_test.cc",
+    "builtin_validation_test.cc",
+    "builtins_validation_test.cc",
+    "call_test.cc",
+    "call_validation_test.cc",
+    "compound_assignment_validation_test.cc",
+    "compound_statement_test.cc",
+    "const_assert_test.cc",
+    "control_block_validation_test.cc",
+    "dependency_graph_test.cc",
+    "diagnostic_control_test.cc",
+    "dual_source_blending_extension_test.cc",
+    "entry_point_validation_test.cc",
+    "evaluation_stage_test.cc",
+    "expression_kind_test.cc",
+    "f16_extension_test.cc",
+    "function_validation_test.cc",
+    "host_shareable_validation_test.cc",
+    "increment_decrement_validation_test.cc",
+    "inferred_type_test.cc",
+    "is_host_shareable_test.cc",
+    "is_storeable_test.cc",
+    "load_test.cc",
+    "materialize_test.cc",
+    "override_test.cc",
+    "pixel_local_extension_test.cc",
+    "ptr_ref_test.cc",
+    "ptr_ref_validation_test.cc",
+    "resolver_behavior_test.cc",
+    "resolver_helper_test.cc",
+    "resolver_helper_test.h",
+    "resolver_test.cc",
+    "root_identifier_test.cc",
+    "side_effects_test.cc",
+    "struct_address_space_use_test.cc",
+    "struct_layout_test.cc",
+    "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",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/core/type:test",
+    "//src/tint/lang/wgsl/ast",
+    "//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/sem:test",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/resolver/BUILD.cmake b/src/tint/lang/wgsl/resolver/BUILD.cmake
index 6f0a3d3..a2e516c 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.cmake
+++ b/src/tint/lang/wgsl/resolver/BUILD.cmake
@@ -106,6 +106,7 @@
   lang/wgsl/resolver/load_test.cc
   lang/wgsl/resolver/materialize_test.cc
   lang/wgsl/resolver/override_test.cc
+  lang/wgsl/resolver/pixel_local_extension_test.cc
   lang/wgsl/resolver/ptr_ref_test.cc
   lang/wgsl/resolver/ptr_ref_validation_test.cc
   lang/wgsl/resolver/resolver_behavior_test.cc
diff --git a/src/tint/lang/wgsl/resolver/BUILD.gn b/src/tint/lang/wgsl/resolver/BUILD.gn
index 2e40118..319ea05 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.gn
+++ b/src/tint/lang/wgsl/resolver/BUILD.gn
@@ -109,6 +109,7 @@
       "load_test.cc",
       "materialize_test.cc",
       "override_test.cc",
+      "pixel_local_extension_test.cc",
       "ptr_ref_test.cc",
       "ptr_ref_validation_test.cc",
       "resolver_behavior_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc b/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
index 92c13b5..4b09e58 100644
--- a/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
@@ -469,7 +469,7 @@
     GlobalVar("a", ty.u32(), core::AddressSpace::kPushConstant);
 
     Func("main", {}, ty.void_(), Vector{Assign(Phony(), "a")},
-         Vector{Stage(ast::PipelineStage::kCompute), create<ast::WorkgroupAttribute>(Expr(1_i))});
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
 
     EXPECT_TRUE(r()->Resolve());
 }
@@ -487,7 +487,7 @@
     GlobalVar(Source{{3, 4}}, "b", ty.u32(), core::AddressSpace::kPushConstant);
 
     Func(Source{{5, 6}}, "main", {}, ty.void_(), Vector{Assign(Phony(), "a"), Assign(Phony(), "b")},
-         Vector{Stage(ast::PipelineStage::kCompute), create<ast::WorkgroupAttribute>(Expr(1_i))});
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -520,7 +520,7 @@
 
     Func(Source{{9, 10}}, "main", {}, ty.void_(),
          Vector{CallStmt(Call("uses_a")), CallStmt(Call("uses_b"))},
-         Vector{Stage(ast::PipelineStage::kCompute), create<ast::WorkgroupAttribute>(Expr(1_i))});
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
@@ -548,9 +548,9 @@
     GlobalVar("b", ty.u32(), core::AddressSpace::kPushConstant);
 
     Func("uses_a", {}, ty.void_(), Vector{Assign(Phony(), "a")},
-         Vector{Stage(ast::PipelineStage::kCompute), create<ast::WorkgroupAttribute>(Expr(1_i))});
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
     Func("uses_b", {}, ty.void_(), Vector{Assign(Phony(), "b")},
-         Vector{Stage(ast::PipelineStage::kCompute), create<ast::WorkgroupAttribute>(Expr(1_i))});
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
 
     EXPECT_TRUE(r()->Resolve());
 }
diff --git a/src/tint/lang/wgsl/resolver/function_validation_test.cc b/src/tint/lang/wgsl/resolver/function_validation_test.cc
index caf20dc..e1d479a 100644
--- a/src/tint/lang/wgsl/resolver/function_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/function_validation_test.cc
@@ -1048,10 +1048,15 @@
 using ResolverFunctionParameterValidationTest = TestWithParams;
 TEST_P(ResolverFunctionParameterValidationTest, AddressSpaceNoExtension) {
     auto& param = GetParam();
-    auto ptr_type = ty("ptr", Ident(Source{{12, 34}}, param.address_space), ty.i32());
+    Structure("S", Vector{Member("a", ty.i32())});
+    auto ptr_type = ty("ptr", Ident(Source{{12, 34}}, param.address_space), ty("S"));
     auto* arg = Param(Source{{12, 34}}, "p", ptr_type);
     Func("f", Vector{arg}, ty.void_(), tint::Empty);
 
+    if (param.address_space == core::AddressSpace::kPixelLocal) {
+        Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    }
+
     if (param.expectation == Expectation::kAlwaysPass) {
         ASSERT_TRUE(r()->Resolve()) << r()->error();
     } else {
@@ -1060,7 +1065,7 @@
         EXPECT_FALSE(r()->Resolve());
         if (param.expectation == Expectation::kInvalid) {
             std::string err = R"(12:34 error: unresolved address space '${addr_space}'
-12:34 note: Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')";
+12:34 note: Possible values: 'function', 'pixel_local', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')";
             err = tint::ReplaceAll(err, "${addr_space}", tint::ToString(param.address_space));
             EXPECT_EQ(r()->error(), err);
         } else {
@@ -1070,13 +1075,18 @@
         }
     }
 }
-TEST_P(ResolverFunctionParameterValidationTest, AddressSpaceWithExtension) {
+TEST_P(ResolverFunctionParameterValidationTest, AddressSpaceWithFullPtrParameterExtension) {
     auto& param = GetParam();
-    auto ptr_type = ty("ptr", Ident(Source{{12, 34}}, param.address_space), ty.i32());
+    Structure("S", Vector{Member("a", ty.i32())});
+    auto ptr_type = ty("ptr", Ident(Source{{12, 34}}, param.address_space), ty("S"));
     auto* arg = Param(Source{{12, 34}}, "p", ptr_type);
     Enable(core::Extension::kChromiumExperimentalFullPtrParameters);
     Func("f", Vector{arg}, ty.void_(), tint::Empty);
 
+    if (param.address_space == core::AddressSpace::kPixelLocal) {
+        Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    }
+
     if (param.expectation == Expectation::kAlwaysPass ||
         param.expectation == Expectation::kPassWithFullPtrParameterExtension) {
         ASSERT_TRUE(r()->Resolve()) << r()->error();
@@ -1084,7 +1094,7 @@
         EXPECT_FALSE(r()->Resolve());
         if (param.expectation == Expectation::kInvalid) {
             std::string err = R"(12:34 error: unresolved address space '${addr_space}'
-12:34 note: Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')";
+12:34 note: Possible values: 'function', 'pixel_local', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')";
             err = tint::ReplaceAll(err, "${addr_space}", tint::ToString(param.address_space));
             EXPECT_EQ(r()->error(), err);
         } else {
@@ -1105,6 +1115,7 @@
         TestParams{core::AddressSpace::kWorkgroup, Expectation::kPassWithFullPtrParameterExtension},
         TestParams{core::AddressSpace::kHandle, Expectation::kInvalid},
         TestParams{core::AddressSpace::kStorage, Expectation::kPassWithFullPtrParameterExtension},
+        TestParams{core::AddressSpace::kPixelLocal, Expectation::kAlwaysFail},
         TestParams{core::AddressSpace::kPrivate, Expectation::kAlwaysPass},
         TestParams{core::AddressSpace::kFunction, Expectation::kAlwaysPass}));
 
diff --git a/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc b/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
new file mode 100644
index 0000000..2c97b5d
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
@@ -0,0 +1,351 @@
+// 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/resolver.h"
+#include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+
+#include "gmock/gmock.h"
+
+namespace tint::resolver {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using ResolverPixelLocalExtensionTest = ResolverTest;
+
+TEST_F(ResolverPixelLocalExtensionTest, AddressSpaceUsedWithExtension) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { a : i32 }
+    // var<pixel_local> v : S;
+    Enable(Source{{12, 34}}, core::Extension::kChromiumExperimentalPixelLocal);
+
+    Structure("S", Vector{Member("a", ty.i32())});
+
+    GlobalVar("v", ty("S"), core::AddressSpace::kPixelLocal);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, AddressSpaceUsedWithoutExtension) {
+    // struct S { a : i32 }
+    // var<pixel_local> v : S;
+
+    Structure("S", Vector{Member("a", ty.i32())});
+
+    AST().AddGlobalVariable(create<ast::Var>(
+        /* name */ Ident("v"),
+        /* type */ ty("S"),
+        /* declared_address_space */ Expr(Source{{12, 34}}, core::AddressSpace::kPixelLocal),
+        /* declared_access */ nullptr,
+        /* initializer */ nullptr,
+        /* attributes */ Empty));
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: 'pixel_local' address space requires the 'chromium_experimental_pixel_local' extension enabled)");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, PixelLocalTwoVariablesUsedInEntryPoint) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : i32 }
+    // var<pixel_local> a : S;
+    // var<pixel_local> b : S;
+    // @compute @workgroup_size(1) fn main() {
+    //   _ = a.i;
+    //   _ = b.i;
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{1, 2}}, "a", ty("S"), core::AddressSpace::kPixelLocal);
+    GlobalVar(Source{{3, 4}}, "b", ty("S"), core::AddressSpace::kPixelLocal);
+
+    Func(Source{{5, 6}}, "main", {}, ty.void_(),
+         Vector{Assign(Phony(), MemberAccessor("a", "i")),
+                Assign(Phony(), MemberAccessor("b", "i"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(5:6 error: entry point 'main' uses two different 'pixel_local' variables.
+3:4 note: first 'pixel_local' variable declaration is here
+1:2 note: second 'pixel_local' variable declaration is here)");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, PixelLocalTwoVariablesUsedInEntryPointWithFunctionGraph) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : i32 }
+    // var<pixel_local> a : S;
+    // var<pixel_local> b : S;
+    // fn uses_a() {
+    //   _ = a.i;
+    // }
+    // fn uses_b() {
+    //   _ = b.i;
+    // }
+    // @compute @workgroup_size(1) fn main() {
+    //   uses_a();
+    //   uses_b();
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{1, 2}}, "a", ty("S"), core::AddressSpace::kPixelLocal);
+    GlobalVar(Source{{3, 4}}, "b", ty("S"), core::AddressSpace::kPixelLocal);
+
+    Func(Source{{5, 6}}, "uses_a", {}, ty.void_(),
+         Vector{Assign(Phony(), MemberAccessor("a", "i"))});
+    Func(Source{{7, 8}}, "uses_b", {}, ty.void_(),
+         Vector{Assign(Phony(), MemberAccessor("b", "i"))});
+
+    Func(Source{{9, 10}}, "main", {}, ty.void_(),
+         Vector{CallStmt(Call("uses_a")), CallStmt(Call("uses_b"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(9:10 error: entry point 'main' uses two different 'pixel_local' variables.
+3:4 note: first 'pixel_local' variable declaration is here
+7:8 note: called by function 'uses_b'
+9:10 note: called by entry point 'main'
+1:2 note: second 'pixel_local' variable declaration is here
+5:6 note: called by function 'uses_a'
+9:10 note: called by entry point 'main')");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, VertexStageDirect) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : 32; }
+    // var<pixel_local> v : S;
+    // @vertex fn F() -> @position vec4f {
+    //   v.i = 42;
+    //   return vec4f();
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{56, 78}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+    Func("F", Empty, ty.vec4<f32>(),
+         Vector{
+             Assign(MemberAccessor(Expr(Source{{12, 34}}, "v"), "i"), 42_a),
+             Return(Call<vec4<f32>>()),
+         },
+         Vector{Stage(ast::PipelineStage::kVertex)},
+         Vector{Builtin(core::BuiltinValue::kPosition)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: var with 'pixel_local' address space cannot be used by vertex pipeline stage
+56:78 note: variable is declared here)");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, ComputeStageDirect) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : 32; }
+    // var<pixel_local> v : S;
+    // @compute @workgroup_size(1) fn F() {
+    //   v.i = 42;
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{56, 78}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+    Func("F", Empty, ty.void_(),
+         Vector{Assign(MemberAccessor(Expr(Source{{12, 34}}, "v"), "i"), 42_a)},
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: var with 'pixel_local' address space cannot be used by compute pipeline stage
+56:78 note: variable is declared here)");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, FragmentStageDirect) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : 32; }
+    // var<pixel_local> v : S;
+    // @fragment fn F() {
+    //   v.i = 42;
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{56, 78}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+    Func("F", Empty, ty.void_(),
+         Vector{Assign(MemberAccessor(Expr(Source{{12, 34}}, "v"), "i"), 42_a)},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, VertexStageIndirect) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : 32; }
+    // var<pixel_local> v : S;
+    // fn X() { v = 42; }
+    // fn Y() { .iX(); }
+    // @vertex fn F() -> @position vec4f {
+    //   X();
+    //   return vec4f();
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{3, 4}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+    Func(Source{{5, 6}}, "X", Empty, ty.void_(),
+         Vector{Assign(MemberAccessor(Expr(Source{{1, 2}}, "v"), "i"), 42_a)});
+    Func(Source{{7, 8}}, "Y", Empty, ty.void_(), Vector{CallStmt(Call("X"))});
+    Func(Source{{9, 1}}, "F", Empty, ty.vec4<f32>(),
+         Vector{
+             CallStmt(Call("Y")),
+             Return(Call<vec4<f32>>()),
+         },
+         Vector{Stage(ast::PipelineStage::kVertex)},
+         Vector{Builtin(core::BuiltinValue::kPosition)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(1:2 error: var with 'pixel_local' address space cannot be used by vertex pipeline stage
+3:4 note: variable is declared here
+5:6 note: called by function 'X'
+7:8 note: called by function 'Y'
+9:1 note: called by entry point 'F')");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, ComputeStageIndirect) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : 32; }
+    // var<pixel_local> v : S;
+    // fn X() { v = 42; }
+    // fn Y() { .iX(); }
+    // @compute @workgroup_size(1) fn F() {
+    //   Y();
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{3, 4}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+    Func(Source{{5, 6}}, "X", Empty, ty.void_(),
+         Vector{Assign(MemberAccessor(Expr(Source{{1, 2}}, "v"), "i"), 42_a)});
+    Func(Source{{7, 8}}, "Y", Empty, ty.void_(), Vector{CallStmt(Call("X"))});
+    Func(Source{{9, 1}}, "F", Empty, ty.void_(), Vector{CallStmt(Call("Y"))},
+         Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(1_a)});
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(1:2 error: var with 'pixel_local' address space cannot be used by compute pipeline stage
+3:4 note: variable is declared here
+5:6 note: called by function 'X'
+7:8 note: called by function 'Y'
+9:1 note: called by entry point 'F')");
+}
+
+TEST_F(ResolverPixelLocalExtensionTest, FragmentStageIndirect) {
+    // enable chromium_experimental_pixel_local;
+    // struct S { i : 32; }
+    // var<pixel_local> v : S;
+    // fn X() { v = 42; }
+    // fn Y() { .iX(); }
+    // @fragment fn F() {
+    //   Y();
+    // }
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{Member("i", ty.i32())});
+    GlobalVar(Source{{3, 4}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+    Func(Source{{5, 6}}, "X", Empty, ty.void_(),
+         Vector{Assign(MemberAccessor(Expr(Source{{1, 2}}, "v"), "i"), 42_a)});
+    Func(Source{{7, 8}}, "Y", Empty, ty.void_(), Vector{CallStmt(Call("X"))});
+    Func(Source{{9, 1}}, "F", Empty, ty.void_(), Vector{CallStmt(Call("Y"))},
+         Vector{Stage(ast::PipelineStage::kFragment)});
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+namespace type_tests {
+struct Case {
+    builder::ast_type_func_ptr type;
+    std::string name;
+    bool pass;
+};
+
+static std::ostream& operator<<(std::ostream& o, const Case& c) {
+    return o << c.name;
+}
+
+template <typename T>
+Case Pass() {
+    return Case{builder::DataType<T>::AST, builder::DataType<T>::Name(), true};
+}
+
+template <typename T>
+Case Fail() {
+    return Case{builder::DataType<T>::AST, builder::DataType<T>::Name(), false};
+}
+
+using ResolverPixelLocalExtensionTest_Types = ResolverTestWithParam<Case>;
+
+TEST_P(ResolverPixelLocalExtensionTest_Types, Direct) {
+    // var<pixel_local> v : <type>;
+
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    GlobalVar(Source{{12, 34}}, "v", GetParam().type(*this), core::AddressSpace::kPixelLocal);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: 'pixel_local' variable only support struct storage types)");
+}
+
+TEST_P(ResolverPixelLocalExtensionTest_Types, Struct) {
+    // struct S {
+    //   a : i32,
+    //   m : <type>,
+    // }
+    // var<pixel_local> v : S;
+
+    Enable(core::Extension::kChromiumExperimentalPixelLocal);
+    Structure("S", Vector{
+                       Member("a", ty.i32()),
+                       Member(Source{{12, 34}}, "m", GetParam().type(*this)),
+                   });
+    GlobalVar(Source{{56, 78}}, "v", ty("S"), core::AddressSpace::kPixelLocal);
+
+    if (GetParam().pass) {
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+    } else {
+        EXPECT_FALSE(r()->Resolve());
+        EXPECT_EQ(
+            r()->error(),
+            R"(12:34 error: struct members used in the 'pixel_local' address space can only be of the type 'i32', 'u32' or 'f32'
+56:78 note: struct 'S' used in the 'pixel_local' address space here)");
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(Valid,
+                         ResolverPixelLocalExtensionTest_Types,
+                         testing::Values(Pass<i32>(),  //
+                                         Pass<u32>(),  //
+                                         Pass<f32>()));
+
+INSTANTIATE_TEST_SUITE_P(Invalid,
+                         ResolverPixelLocalExtensionTest_Types,
+                         testing::Values(Fail<bool>(),
+                                         Fail<atomic<i32>>(),
+                                         Fail<vec2<f32>>(),
+                                         Fail<vec3<i32>>(),
+                                         Fail<vec4<u32>>(),
+                                         Fail<array<u32, 4>>()));
+
+}  // namespace type_tests
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 1219e92..5327b0d 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -117,10 +117,8 @@
     : builder_(builder),
       diagnostics_(builder->Diagnostics()),
       const_eval_(builder->constants, diagnostics_),
-      intrinsic_table_(core::intrinsic::Table::Create(core::intrinsic::data::kData,
-                                                      builder->Types(),
-                                                      builder->Symbols(),
-                                                      builder->Diagnostics())),
+      intrinsic_context_{core::intrinsic::data::kData, builder->Types(), builder->Symbols(),
+                         builder->Diagnostics()},
       sem_(builder),
       validator_(builder,
                  sem_,
@@ -208,7 +206,7 @@
         return false;
     }
 
-    if (!validator_.PushConstants(entry_points_)) {
+    if (!validator_.ModuleScopeVarUsages(entry_points_)) {
         return false;
     }
 
@@ -529,7 +527,7 @@
     auto address_space = core::AddressSpace::kUndefined;
     if (var->declared_address_space) {
         auto expr = AddressSpaceExpression(var->declared_address_space);
-        if (!expr) {
+        if (TINT_UNLIKELY(!expr)) {
             return nullptr;
         }
         address_space = expr->Value();
@@ -1602,7 +1600,20 @@
 sem::BuiltinEnumExpression<core::AddressSpace>* Resolver::AddressSpaceExpression(
     const ast::Expression* expr) {
     identifier_resolve_hint_ = {expr, "address space", core::kAddressSpaceStrings};
-    return sem_.AsAddressSpace(Expression(expr));
+    auto address_space_expr = sem_.AsAddressSpace(Expression(expr));
+    if (TINT_UNLIKELY(!address_space_expr)) {
+        return nullptr;
+    }
+    if (TINT_UNLIKELY(
+            address_space_expr->Value() == core::AddressSpace::kPixelLocal &&
+            !enabled_extensions_.Contains(core::Extension::kChromiumExperimentalPixelLocal))) {
+        StringStream err;
+        err << "'pixel_local' address space requires the '"
+            << core::Extension::kChromiumExperimentalPixelLocal << "' extension enabled";
+        AddError(err.str(), expr->source);
+        return nullptr;
+    }
+    return address_space_expr;
 }
 
 sem::BuiltinEnumExpression<core::BuiltinValue>* Resolver::BuiltinValueExpression(
@@ -2072,7 +2083,8 @@
     auto ctor_or_conv = [&](CtorConvIntrinsic ty,
                             const core::type::Type* template_arg) -> sem::Call* {
         auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type(); });
-        auto match = intrinsic_table_->Lookup(ty, template_arg, arg_tys, args_stage, expr->source);
+        auto match = core::intrinsic::Lookup(intrinsic_context_, ty, template_arg, arg_tys,
+                                             args_stage, expr->source);
         if (!match) {
             return nullptr;
         }
@@ -2396,7 +2408,8 @@
     }
 
     auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type(); });
-    auto overload = intrinsic_table_->Lookup(fn, arg_tys, arg_stage, expr->source);
+    auto overload =
+        core::intrinsic::Lookup(intrinsic_context_, fn, arg_tys, arg_stage, expr->source);
     if (!overload) {
         return nullptr;
     }
@@ -3548,8 +3561,8 @@
     }
 
     auto stage = core::EarliestStage(lhs->Stage(), rhs->Stage());
-    auto overload =
-        intrinsic_table_->Lookup(expr->op, lhs->Type(), rhs->Type(), stage, expr->source, false);
+    auto overload = core::intrinsic::Lookup(intrinsic_context_, expr->op, lhs->Type(), rhs->Type(),
+                                            stage, expr->source, false);
     if (!overload) {
         return nullptr;
     }
@@ -3670,7 +3683,8 @@
 
         default: {
             stage = expr->Stage();
-            auto overload = intrinsic_table_->Lookup(unary->op, expr_ty, stage, unary->source);
+            auto overload = core::intrinsic::Lookup(intrinsic_context_, unary->op, expr_ty, stage,
+                                                    unary->source);
             if (!overload) {
                 return nullptr;
             }
@@ -4698,8 +4712,8 @@
         auto stage = core::EarliestStage(lhs->Stage(), rhs->Stage());
 
         auto overload =
-            intrinsic_table_->Lookup(stmt->op, lhs->Type()->UnwrapRef(), rhs->Type()->UnwrapRef(),
-                                     stage, stmt->source, true);
+            core::intrinsic::Lookup(intrinsic_context_, stmt->op, lhs->Type()->UnwrapRef(),
+                                    rhs->Type()->UnwrapRef(), stage, stmt->source, true);
         if (!overload) {
             return false;
         }
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index 09305a2..506beae 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -612,7 +612,7 @@
     ProgramBuilder* const builder_;
     diag::List& diagnostics_;
     core::constant::Eval const_eval_;
-    std::unique_ptr<core::intrinsic::Table> const intrinsic_table_;
+    core::intrinsic::Context intrinsic_context_;
     DependencyGraph dependencies_;
     SemHelper sem_;
     Validator validator_;
@@ -635,10 +635,9 @@
     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::Table::Overload, core::Function>, sem::Builtin*, 64>
-        builtins_;
-    Hashmap<core::intrinsic::Table::Overload, sem::ValueConstructor*, 16> constructors_;
-    Hashmap<core::intrinsic::Table::Overload, sem::ValueConversion*, 16> converters_;
+    Hashmap<std::pair<core::intrinsic::Overload, core::Function>, sem::Builtin*, 64> builtins_;
+    Hashmap<core::intrinsic::Overload, sem::ValueConstructor*, 16> constructors_;
+    Hashmap<core::intrinsic::Overload, sem::ValueConversion*, 16> converters_;
 };
 
 }  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/resolver_helper_test.h b/src/tint/lang/wgsl/resolver/resolver_helper_test.h
index a8048d7..7daf352 100644
--- a/src/tint/lang/wgsl/resolver/resolver_helper_test.h
+++ b/src/tint/lang/wgsl/resolver/resolver_helper_test.h
@@ -687,6 +687,29 @@
     }
 };
 
+/// Helper for building atomic types and expressions
+template <typename T>
+struct DataType<core::fluent_types::atomic<T>> {
+    /// The element type
+    using ElementType = typename DataType<T>::ElementType;
+
+    /// true as atomics are a composite type
+    static constexpr bool is_composite = true;
+
+    /// @param b the ProgramBuilder
+    /// @return a new AST atomic type
+    static inline ast::Type AST(ProgramBuilder& b) { return b.ty.atomic(DataType<T>::AST(b)); }
+
+    /// @param b the ProgramBuilder
+    /// @return the semantic atomic type
+    static inline const core::type::Type* Sem(ProgramBuilder& b) {
+        return b.Types().atomic(DataType<T>::Sem(b));
+    }
+
+    /// @returns the WGSL name for the type
+    static inline std::string Name() { return "atomic<" + DataType<T>::Name() + ">"; }
+};
+
 /// Struct of all creation pointer types
 struct CreatePtrs {
     /// ast node type create function
diff --git a/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc b/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc
index 173934f..041a74b 100644
--- a/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc
+++ b/src/tint/lang/wgsl/resolver/unresolved_identifier_test.cc
@@ -38,7 +38,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: unresolved address space 'privte'
 12:34 note: Did you mean 'private'?
-Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
+Possible values: 'function', 'pixel_local', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ResolverUnresolvedIdentifierSuggestions, BuiltinValue) {
diff --git a/src/tint/lang/wgsl/resolver/validation_test.cc b/src/tint/lang/wgsl/resolver/validation_test.cc
index 997002d..0634f94 100644
--- a/src/tint/lang/wgsl/resolver/validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/validation_test.cc
@@ -81,9 +81,10 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "3:4 error: workgroup memory cannot be used by vertex pipeline "
-              "stage\n1:2 note: variable is declared here");
+    EXPECT_EQ(
+        r()->error(),
+        R"(3:4 error: var with 'workgroup' address space cannot be used by vertex pipeline stage
+1:2 note: variable is declared here)");
 }
 
 TEST_F(ResolverValidationTest, WorkgroupMemoryUsedInFragmentStage) {
@@ -111,8 +112,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(3:4 error: workgroup memory cannot be used by fragment pipeline stage
+    EXPECT_EQ(
+        r()->error(),
+        R"(3:4 error: var with 'workgroup' address space cannot be used by fragment pipeline stage
 1:2 note: variable is declared here
 5:6 note: called by function 'f2'
 7:8 note: called by function 'f1'
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 42f7e84..22e769f 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -16,6 +16,7 @@
 
 #include <algorithm>
 #include <limits>
+#include <tuple>
 #include <utility>
 
 #include "src/tint/lang/core/fluent_types.h"
@@ -1946,27 +1947,34 @@
         }
     };
 
-    auto check_workgroup_storage = [&](const sem::Function* func,
-                                       const sem::Function* entry_point) {
-        auto stage = entry_point->Declaration()->PipelineStage();
-        if (stage != ast::PipelineStage::kCompute) {
-            for (auto* var : func->DirectlyReferencedGlobals()) {
-                if (var->AddressSpace() == core::AddressSpace::kWorkgroup) {
-                    StringStream stage_name;
-                    stage_name << stage;
-                    for (auto* user : var->Users()) {
-                        if (func == user->Stmt()->Function()) {
-                            AddError("workgroup memory cannot be used by " + stage_name.str() +
-                                         " pipeline stage",
-                                     user->Declaration()->source);
-                            break;
-                        }
-                    }
-                    AddNote("variable is declared here", var->Declaration()->source);
-                    backtrace(func, entry_point);
-                    return false;
+    auto check_var_uses = [&](const sem::Function* func, const sem::Function* entry_point) {
+        auto err = [&](ast::PipelineStage stage, const sem::GlobalVariable* var) {
+            Source source;
+            for (auto* user : var->Users()) {
+                if (func == user->Stmt()->Function()) {
+                    source = user->Declaration()->source;
+                    break;
                 }
             }
+            StringStream msg;
+            msg << "var with '" << var->AddressSpace() << "' address space cannot be used by "
+                << stage << " pipeline stage";
+            AddError(msg.str(), source);
+            AddNote("variable is declared here", var->Declaration()->source);
+            backtrace(func, entry_point);
+            return false;
+        };
+
+        auto stage = entry_point->Declaration()->PipelineStage();
+        for (auto* var : func->DirectlyReferencedGlobals()) {
+            if (stage != ast::PipelineStage::kCompute &&
+                var->AddressSpace() == core::AddressSpace::kWorkgroup) {
+                return err(stage, var);
+            }
+            if (stage != ast::PipelineStage::kFragment &&
+                var->AddressSpace() == core::AddressSpace::kPixelLocal) {
+                return err(stage, var);
+            }
         }
         return true;
     };
@@ -2000,7 +2008,7 @@
     };
 
     auto check_func = [&](const sem::Function* func, const sem::Function* entry_point) {
-        if (!check_workgroup_storage(func, entry_point)) {
+        if (!check_var_uses(func, entry_point)) {
             return false;
         }
         if (!check_builtin_calls(func, entry_point)) {
@@ -2028,66 +2036,15 @@
     return true;
 }
 
-bool Validator::PushConstants(VectorRef<sem::Function*> entry_points) const {
+bool Validator::ModuleScopeVarUsages(VectorRef<sem::Function*> entry_points) const {
     for (auto* entry_point : entry_points) {
-        // State checked and modified by check_push_constant so that it remembers previously seen
-        // push_constant variables for an entry-point.
-        const sem::Variable* push_constant_var = nullptr;
-        const sem::Function* push_constant_func = nullptr;
-
-        auto check_push_constant = [&](const sem::Function* func, const sem::Function* ep) {
-            for (auto* var : func->DirectlyReferencedGlobals()) {
-                if (var->AddressSpace() != core::AddressSpace::kPushConstant ||
-                    var == push_constant_var) {
-                    continue;
-                }
-
-                if (push_constant_var == nullptr) {
-                    push_constant_var = var;
-                    push_constant_func = func;
-                    continue;
-                }
-
-                AddError("entry point '" + ep->Declaration()->name->symbol.Name() +
-                             "' uses two different 'push_constant' variables.",
-                         ep->Declaration()->source);
-                AddNote("first 'push_constant' variable declaration is here",
-                        var->Declaration()->source);
-                if (func != ep) {
-                    TraverseCallChain(ep, func, [&](const sem::Function* f) {
-                        AddNote(
-                            "called by function '" + f->Declaration()->name->symbol.Name() + "'",
-                            f->Declaration()->source);
-                    });
-                    AddNote(
-                        "called by entry point '" + ep->Declaration()->name->symbol.Name() + "'",
-                        ep->Declaration()->source);
-                }
-                AddNote("second 'push_constant' variable declaration is here",
-                        push_constant_var->Declaration()->source);
-                if (push_constant_func != ep) {
-                    TraverseCallChain(ep, push_constant_func, [&](const sem::Function* f) {
-                        AddNote(
-                            "called by function '" + f->Declaration()->name->symbol.Name() + "'",
-                            f->Declaration()->source);
-                    });
-                    AddNote(
-                        "called by entry point '" + ep->Declaration()->name->symbol.Name() + "'",
-                        ep->Declaration()->source);
-                }
-                return false;
-            }
-
-            return true;
-        };
-
-        if (!check_push_constant(entry_point, entry_point)) {
+        if (!CheckNoMultipleModuleScopeVarsOfAddressSpace(entry_point,
+                                                          core::AddressSpace::kPushConstant)) {
             return false;
         }
-        for (auto* func : entry_point->TransitivelyCalledFunctions()) {
-            if (!check_push_constant(func, entry_point)) {
-                return false;
-            }
+        if (!CheckNoMultipleModuleScopeVarsOfAddressSpace(entry_point,
+                                                          core::AddressSpace::kPixelLocal)) {
+            return false;
         }
     }
 
@@ -2661,20 +2618,49 @@
         return false;
     }
 
-    if (address_space == core::AddressSpace::kPushConstant &&
-        !enabled_extensions_.Contains(core::Extension::kChromiumExperimentalPushConstant) &&
-        IsValidationEnabled(attributes, ast::DisabledValidation::kIgnoreAddressSpace)) {
-        AddError(
-            "use of variable address space 'push_constant' requires enabling extension "
-            "'chromium_experimental_push_constant'",
-            source);
-        return false;
-    }
-
-    if (address_space == core::AddressSpace::kStorage && access == core::Access::kWrite) {
-        // The access mode for the storage address space can only be 'read' or 'read_write'.
-        AddError("access mode 'write' is not valid for the 'storage' address space", source);
-        return false;
+    switch (address_space) {
+        case core::AddressSpace::kPixelLocal:
+            if (auto* str = store_ty->As<sem::Struct>()) {
+                for (auto* member : str->Members()) {
+                    using Allowed = std::tuple<core::type::I32, core::type::U32, core::type::F32>;
+                    if (TINT_UNLIKELY(!member->Type()->TypeInfo().IsAnyOfTuple<Allowed>())) {
+                        AddError(
+                            "struct members used in the 'pixel_local' address space can only be of "
+                            "the type 'i32', 'u32' or 'f32'",
+                            member->Declaration()->source);
+                        AddNote("struct '" + str->Name().Name() +
+                                    "' used in the 'pixel_local' address space here",
+                                source);
+                        return false;
+                    }
+                }
+            } else if (TINT_UNLIKELY(!store_ty->TypeInfo().Is<core::type::Struct>())) {
+                AddError("'pixel_local' variable only support struct storage types", source);
+                return false;
+            }
+            break;
+        case core::AddressSpace::kPushConstant:
+            if (TINT_UNLIKELY(!enabled_extensions_.Contains(
+                                  core::Extension::kChromiumExperimentalPushConstant) &&
+                              IsValidationEnabled(attributes,
+                                                  ast::DisabledValidation::kIgnoreAddressSpace))) {
+                AddError(
+                    "use of variable address space 'push_constant' requires enabling extension "
+                    "'chromium_experimental_push_constant'",
+                    source);
+                return false;
+            }
+            break;
+        case core::AddressSpace::kStorage:
+            if (TINT_UNLIKELY(access == core::Access::kWrite)) {
+                // The access mode for the storage address space can only be 'read' or 'read_write'.
+                AddError("access mode 'write' is not valid for the 'storage' address space",
+                         source);
+                return false;
+            }
+            break;
+        default:
+            break;
     }
 
     auto atomic_error = [&]() -> const char* {
@@ -2715,4 +2701,64 @@
         [&](Default) { return true; });
 }
 
+bool Validator::CheckNoMultipleModuleScopeVarsOfAddressSpace(sem::Function* entry_point,
+                                                             core::AddressSpace space) const {
+    // State checked and modified by check() so that it remembers previously seen push_constant
+    // variables for an entry-point.
+    const sem::Variable* seen_var = nullptr;
+    const sem::Function* seen_func = nullptr;
+
+    auto check = [&](const sem::Function* func, const sem::Function* ep) {
+        for (auto* var : func->DirectlyReferencedGlobals()) {
+            if (var->AddressSpace() != space || var == seen_var) {
+                continue;
+            }
+
+            if (seen_var == nullptr) {
+                seen_var = var;
+                seen_func = func;
+                continue;
+            }
+
+            std::string s{core::ToString(space)};
+
+            AddError("entry point '" + ep->Declaration()->name->symbol.Name() +
+                         "' uses two different '" + s + "' variables.",
+                     ep->Declaration()->source);
+            AddNote("first '" + s + "' variable declaration is here", var->Declaration()->source);
+            if (func != ep) {
+                TraverseCallChain(ep, func, [&](const sem::Function* f) {
+                    AddNote("called by function '" + f->Declaration()->name->symbol.Name() + "'",
+                            f->Declaration()->source);
+                });
+                AddNote("called by entry point '" + ep->Declaration()->name->symbol.Name() + "'",
+                        ep->Declaration()->source);
+            }
+            AddNote("second '" + s + "' variable declaration is here",
+                    seen_var->Declaration()->source);
+            if (seen_func != ep) {
+                TraverseCallChain(ep, seen_func, [&](const sem::Function* f) {
+                    AddNote("called by function '" + f->Declaration()->name->symbol.Name() + "'",
+                            f->Declaration()->source);
+                });
+                AddNote("called by entry point '" + ep->Declaration()->name->symbol.Name() + "'",
+                        ep->Declaration()->source);
+            }
+            return false;
+        }
+
+        return true;
+    };
+
+    if (!check(entry_point, entry_point)) {
+        return false;
+    }
+    for (auto* func : entry_point->TransitivelyCalledFunctions()) {
+        if (!check(func, entry_point)) {
+            return false;
+        }
+    }
+    return true;
+}
+
 }  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index 40c6cfb..27e9082 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -154,10 +154,11 @@
     /// @returns true on success, false otherwise.
     bool PipelineStages(VectorRef<sem::Function*> entry_points) const;
 
-    /// Validates push_constant variables
+    /// Validates usages of module-scope vars.
+    /// @note Must only be called after all functions have been resolved.
     /// @param entry_points the entry points to the module
     /// @returns true on success, false otherwise.
-    bool PushConstants(VectorRef<sem::Function*> entry_points) const;
+    bool ModuleScopeVarUsages(VectorRef<sem::Function*> entry_points) const;
 
     /// Validates aliases
     /// @param alias the alias to validate
@@ -547,6 +548,15 @@
                                      core::AddressSpace address_space,
                                      VectorRef<const tint::ast::Attribute*> attributes,
                                      const Source& source) const;
+
+    /// Raises an error if the entry_point @p entry_point uses two or more module-scope 'var's with
+    /// the address space @p space.
+    /// @param entry_point the entry point
+    /// @param space the address space
+    /// @returns true if no duplicate uses were found or false if an error was raised.
+    bool CheckNoMultipleModuleScopeVarsOfAddressSpace(sem::Function* entry_point,
+                                                      core::AddressSpace space) const;
+
     SymbolTable& symbols_;
     diag::List& diagnostics_;
     SemHelper& sem_;
diff --git a/src/tint/lang/wgsl/sem/BUILD.bazel b/src/tint/lang/wgsl/sem/BUILD.bazel
new file mode 100644
index 0000000..a8d4579
--- /dev/null
+++ b/src/tint/lang/wgsl/sem/BUILD.bazel
@@ -0,0 +1,157 @@
+# 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 = "sem",
+  srcs = [
+    "accessor_expression.cc",
+    "array_count.cc",
+    "behavior.cc",
+    "block_statement.cc",
+    "break_if_statement.cc",
+    "builtin.cc",
+    "builtin_enum_expression.cc",
+    "call.cc",
+    "call_target.cc",
+    "expression.cc",
+    "for_loop_statement.cc",
+    "function.cc",
+    "function_expression.cc",
+    "if_statement.cc",
+    "index_accessor_expression.cc",
+    "info.cc",
+    "load.cc",
+    "loop_statement.cc",
+    "materialize.cc",
+    "member_accessor_expression.cc",
+    "module.cc",
+    "node.cc",
+    "statement.cc",
+    "struct.cc",
+    "switch_statement.cc",
+    "type_expression.cc",
+    "value_constructor.cc",
+    "value_conversion.cc",
+    "value_expression.cc",
+    "variable.cc",
+    "while_statement.cc",
+  ],
+  hdrs = [
+    "accessor_expression.h",
+    "array_count.h",
+    "behavior.h",
+    "block_statement.h",
+    "break_if_statement.h",
+    "builtin.h",
+    "builtin_enum_expression.h",
+    "call.h",
+    "call_target.h",
+    "expression.h",
+    "for_loop_statement.h",
+    "function.h",
+    "function_expression.h",
+    "if_statement.h",
+    "index_accessor_expression.h",
+    "info.h",
+    "load.h",
+    "loop_statement.h",
+    "materialize.h",
+    "member_accessor_expression.h",
+    "module.h",
+    "node.h",
+    "pipeline_stage_set.h",
+    "sampler_texture_pair.h",
+    "statement.h",
+    "struct.h",
+    "switch_statement.h",
+    "type_expression.h",
+    "type_mappings.h",
+    "value_constructor.h",
+    "value_conversion.h",
+    "value_expression.h",
+    "variable.h",
+    "while_statement.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "builtin_test.cc",
+    "diagnostic_severity_test.cc",
+    "helper_test.h",
+    "struct_test.cc",
+    "value_expression_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/writer/BUILD.bazel b/src/tint/lang/wgsl/writer/BUILD.bazel
new file mode 100644
index 0000000..850e260
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/BUILD.bazel
@@ -0,0 +1,96 @@
+# 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 = "writer",
+  srcs = [
+    "options.cc",
+    "output.cc",
+    "writer.cc",
+  ],
+  hdrs = [
+    "options.h",
+    "output.h",
+    "writer.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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/syntax_tree_printer",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/generator",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "writer_bench.cc",
+  ],
+  deps = [
+    "//src/tint/cmd/bench",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
new file mode 100644
index 0000000..64c5a7b3
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
@@ -0,0 +1,124 @@
+# 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 = "ast_printer",
+  srcs = [
+    "ast_printer.cc",
+  ],
+  hdrs = [
+    "ast_printer.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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/generator",
+    "//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/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "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 = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
new file mode 100644
index 0000000..0db88d9
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
@@ -0,0 +1,106 @@
+# 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 = "ir_to_program",
+  srcs = [
+    "ir_to_program.cc",
+    "rename_conflicts.cc",
+  ],
+  hdrs = [
+    "ir_to_program.h",
+    "rename_conflicts.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/resolver",
+    "//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",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "inlining_test.cc",
+    "ir_to_program_test.cc",
+    "ir_to_program_test.h",
+    "rename_conflicts_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/intrinsic/data",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir:test",
+    "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl/ast",
+    "//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",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cfg b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cfg
deleted file mode 100644
index 0ca3ed3..0000000
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "condition": "tint_build_ir"
-}
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 ef8b494..ef8913c 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
@@ -21,11 +21,9 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_wgsl_writer_ir_to_program
 # Kind:      lib
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_ir_to_program lib
   lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -38,6 +36,9 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
@@ -58,18 +59,9 @@
   tint_utils_traits
 )
 
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_wgsl_writer_ir_to_program lib
-    tint_lang_core_ir
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
-if(TINT_BUILD_IR)
 ################################################################################
 # Target:    tint_lang_wgsl_writer_ir_to_program_test
 # Kind:      test
-# Condition: TINT_BUILD_IR
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_ir_to_program_test test
   lang/wgsl/writer/ir_to_program/inlining_test.cc
@@ -82,11 +74,16 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_intrinsic_data
+  tint_lang_core_ir
+  tint_lang_core_ir_test
   tint_lang_core_type
   tint_lang_wgsl_ast
   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
   tint_utils_ice
@@ -105,13 +102,3 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_writer_ir_to_program_test test
   "gtest"
 )
-
-if(TINT_BUILD_IR)
-  tint_target_add_dependencies(tint_lang_wgsl_writer_ir_to_program_test test
-    tint_lang_core_ir
-    tint_lang_core_ir_test
-    tint_lang_wgsl_writer_ir_to_program
-  )
-endif(TINT_BUILD_IR)
-
-endif(TINT_BUILD_IR)
\ No newline at end of file
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 e19abe9..5dc5e9a 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
@@ -28,23 +28,65 @@
 if (tint_build_unittests) {
   import("//testing/test.gni")
 }
-if (tint_build_ir) {
-  libtint_source_set("ir_to_program") {
+
+libtint_source_set("ir_to_program") {
+  sources = [
+    "ir_to_program.cc",
+    "ir_to_program.h",
+    "rename_conflicts.cc",
+    "rename_conflicts.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/intrinsic/data",
+    "${tint_src_dir}/lang/core/ir",
+    "${tint_src_dir}/lang/core/type",
+    "${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/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
+if (tint_build_unittests) {
+  tint_unittests_source_set("unittests") {
+    testonly = true
     sources = [
-      "ir_to_program.cc",
-      "ir_to_program.h",
-      "rename_conflicts.cc",
-      "rename_conflicts.h",
+      "inlining_test.cc",
+      "ir_to_program_test.cc",
+      "ir_to_program_test.h",
+      "rename_conflicts_test.cc",
     ]
     deps = [
+      "${tint_src_dir}:gmock_and_gtest",
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/intrinsic/data",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/ir:unittests",
       "${tint_src_dir}/lang/core/type",
       "${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",
+      "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
@@ -59,54 +101,5 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
-
-    if (tint_build_ir) {
-      deps += [ "${tint_src_dir}/lang/core/ir" ]
-    }
-  }
-}
-if (tint_build_unittests) {
-  if (tint_build_ir) {
-    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",
-      ]
-      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/ast",
-        "${tint_src_dir}/lang/wgsl/program",
-        "${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_ir) {
-        deps += [
-          "${tint_src_dir}/lang/core/ir",
-          "${tint_src_dir}/lang/core/ir:unittests",
-          "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
-        ]
-      }
-    }
   }
 }
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
index 57620ed..fcabf9a 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
@@ -19,6 +19,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/disassembler.h"
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/type/matrix.h"
 
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
new file mode 100644
index 0000000..6ec7c13
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
@@ -0,0 +1,59 @@
+# 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 = "syntax_tree_printer",
+  srcs = [
+    "syntax_tree_printer.cc",
+  ],
+  hdrs = [
+    "syntax_tree_printer.h",
+  ],
+  deps = [
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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/generator",
+    "//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/strconv",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/lang/wgsl/writer/writer.cc b/src/tint/lang/wgsl/writer/writer.cc
index 0c08e52..1a7204a 100644
--- a/src/tint/lang/wgsl/writer/writer.cc
+++ b/src/tint/lang/wgsl/writer/writer.cc
@@ -29,10 +29,6 @@
 Result<Output, std::string> Generate(const Program* program, const Options& options) {
     (void)options;
 
-    if (!program->IsValid()) {
-        return std::string("input program is not valid");
-    }
-
     Output output;
 #if TINT_BUILD_SYNTAX_TREE_WRITER
     if (options.use_syntax_tree_writer) {
diff --git a/src/tint/tint.natvis b/src/tint/tint.natvis
index 47198c0..dbc090a 100644
--- a/src/tint/tint.natvis
+++ b/src/tint/tint.natvis
@@ -100,14 +100,20 @@
 		<DisplayString Condition="params.size()  > 3">fn {symbol}({*params[0]} {*params[1]} {*params[2]} {params.size()-3} more...) -> {*return_type} {*body}</DisplayString>
 	</Type>
 
-	<Type Name="tint::ast::IdentifierExpression">
-		<!--the ",sb" specifier removes the double quotes on the displayed string -->
+	<Type Name="tint::ast::Identifier">
 		<DisplayString>{symbol}</DisplayString>
 		<Expand>
 			<Item Name="symbol">symbol</Item>
 		</Expand>
 	</Type>
 
+	<Type Name="tint::ast::IdentifierExpression">
+		<DisplayString>{*identifier}</DisplayString>
+		<Expand>
+			<Item Name="identifier">identifier</Item>
+		</Expand>
+	</Type>
+
 	<Type Name="tint::ast::IndexAccessorExpression">
 		<DisplayString>{*object}[{*index}]</DisplayString>
 		<Expand>
@@ -117,9 +123,8 @@
 	</Type>
 
 	<Type Name="tint::ast::MemberAccessorExpression">
-		<DisplayString>{*structure}.{*member}</DisplayString>
+		<DisplayString>{*member}</DisplayString>
 		<Expand>
-			<Item Name="structure">*structure</Item>
 			<Item Name="member">*member</Item>
 		</Expand>
 	</Type>
@@ -179,12 +184,12 @@
 	</Type>
 
 	<Type Name="tint::ast::CallExpression">
-		<DisplayString Condition="args.Length() == 0">{target}()</DisplayString>
-		<DisplayString Condition="args.Length() == 1">{target}({*args[0]})</DisplayString>
-		<DisplayString Condition="args.Length() == 2">{target}({*args[0]}, {*args[1]})</DisplayString>
-		<DisplayString Condition="args.Length() == 3">{target}({*args[0]}, {*args[1]}, {*args[2]})</DisplayString>
-		<DisplayString Condition="args.Length() == 4">{target}({*args[0]}, {*args[1]}, {*args[2]}, {*args[3]})</DisplayString>
-		<DisplayString Condition="args.Length()  > 4">{target}({*args[0]}, {*args[1]}, {*args[2]}, {args.Length()-3} more...)</DisplayString>
+		<DisplayString Condition="args.Length() == 0">{*target}()</DisplayString>
+		<DisplayString Condition="args.Length() == 1">{*target}({*args[0]})</DisplayString>
+		<DisplayString Condition="args.Length() == 2">{*target}({*args[0]}, {*args[1]})</DisplayString>
+		<DisplayString Condition="args.Length() == 3">{*target}({*args[0]}, {*args[1]}, {*args[2]})</DisplayString>
+		<DisplayString Condition="args.Length() == 4">{*target}({*args[0]}, {*args[1]}, {*args[2]}, {*args[3]})</DisplayString>
+		<DisplayString Condition="args.Length()  > 4">{*target}({*args[0]}, {*args[1]}, {*args[2]}, {args.Length()-3} more...)</DisplayString>
 		<!-- TODO: add more overloads -->
 	</Type>
 
@@ -211,22 +216,6 @@
 		<DisplayString>{name}</DisplayString>
 	</Type>
 
-	<Type Name="tint::ast::Bool">
-		<DisplayString>bool</DisplayString>
-	</Type>
-
-	<Type Name="tint::ast::I32">
-		<DisplayString>i32</DisplayString>
-	</Type>
-
-	<Type Name="tint::ast::U32">
-		<DisplayString>u32</DisplayString>
-	</Type>
-
-	<Type Name="tint::ast::F32">
-		<DisplayString>f32</DisplayString>
-	</Type>
-
 	<!--=================================================================-->
 	<!-- sem -->
 	<!--=================================================================-->
@@ -247,12 +236,20 @@
 		<DisplayString>f32</DisplayString>
 	</Type>
 
+	<Type Name="tint::core::type::I32">
+		<DisplayString>i32</DisplayString>
+	</Type>
+
+	<Type Name="tint::core::type::U32">
+		<DisplayString>u32</DisplayString>
+	</Type>
+
 	<Type Name="tint::core::type::Vector">
 		<DisplayString>vec{width_}&lt;{*subtype_}&gt;</DisplayString>
 	</Type>
 
-	<Type Name="tint::core::connstant::Value">
-		<DisplayString>Type={*Type()} Value={Value()}</DisplayString>
+	<Type Name="tint::core::constant::Value">
+		<DisplayString>Type={*Type()} Value={InternalValue()}</DisplayString>
 	</Type>
 
 	<Type Name="tint::sem::ValueExpression">
diff --git a/src/tint/utils/BUILD.bazel b/src/tint/utils/BUILD.bazel
new file mode 100644
index 0000000..9f81589
--- /dev/null
+++ b/src/tint/utils/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/utils/cli/BUILD.bazel b/src/tint/utils/cli/BUILD.bazel
new file mode 100644
index 0000000..3b9fcb0
--- /dev/null
+++ b/src/tint/utils/cli/BUILD.bazel
@@ -0,0 +1,72 @@
+# 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 = "cli",
+  srcs = [
+    "cli.cc",
+  ],
+  hdrs = [
+    "cli.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//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/strconv",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "cli_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/cli",
+    "//src/tint/utils/containers",
+    "//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/strconv",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/command/BUILD.bazel b/src/tint/utils/command/BUILD.bazel
new file mode 100644
index 0000000..8f4d72e
--- /dev/null
+++ b/src/tint/utils/command/BUILD.bazel
@@ -0,0 +1,115 @@
+# 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 = "command",
+  srcs = [
+  ] + select({
+    ":_not_is_linux__and__not_is_mac__and__not_is_win_": [
+      "command_other.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":is_linux_or_is_mac": [
+      "command_posix.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":is_win": [
+      "command_windows.cc",
+    ],
+    "//conditions:default": [],
+  }),
+  hdrs = [
+    "command.h",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "//src/tint/utils/text",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "command_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/command",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "is_linux",
+  actual = "//src/tint:is_linux_true",
+)
+
+alias(
+  name = "_not_is_linux_",
+  actual = "//src/tint:is_linux_false",
+)
+
+alias(
+  name = "is_mac",
+  actual = "//src/tint:is_mac_true",
+)
+
+alias(
+  name = "_not_is_mac_",
+  actual = "//src/tint:is_mac_false",
+)
+
+alias(
+  name = "is_win",
+  actual = "//src/tint:is_win_true",
+)
+
+alias(
+  name = "_not_is_win_",
+  actual = "//src/tint:is_win_false",
+)
+
+selects.config_setting_group(
+    name = "is_linux_or_is_mac",
+    match_any = [
+        "is_linux",
+        "is_mac",
+    ],
+)
+
+selects.config_setting_group(
+    name = "_not_is_linux__and__not_is_mac__and__not_is_win_",
+    match_all = [
+        ":_not_is_linux_",
+        ":_not_is_mac_",
+        ":_not_is_win_",
+    ],
+)
+
diff --git a/src/tint/utils/containers/BUILD.bazel b/src/tint/utils/containers/BUILD.bazel
new file mode 100644
index 0000000..c64b88a
--- /dev/null
+++ b/src/tint/utils/containers/BUILD.bazel
@@ -0,0 +1,102 @@
+# 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 = "containers",
+  srcs = [
+    "containers.cc",
+  ],
+  hdrs = [
+    "bitset.h",
+    "enum_set.h",
+    "hashmap.h",
+    "hashmap_base.h",
+    "hashset.h",
+    "map.h",
+    "predicates.h",
+    "reverse.h",
+    "scope_stack.h",
+    "slice.h",
+    "transform.h",
+    "unique_allocator.h",
+    "unique_vector.h",
+    "vector.h",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "bitset_test.cc",
+    "enum_set_test.cc",
+    "hashmap_test.cc",
+    "hashset_test.cc",
+    "map_test.cc",
+    "predicates_test.cc",
+    "reverse_test.cc",
+    "scope_stack_test.cc",
+    "slice_test.cc",
+    "transform_test.cc",
+    "unique_allocator_test.cc",
+    "unique_vector_test.cc",
+    "vector_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/type",
+    "//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",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/containers/vector.h b/src/tint/utils/containers/vector.h
index 4f38c32..e2e4f67 100644
--- a/src/tint/utils/containers/vector.h
+++ b/src/tint/utils/containers/vector.h
@@ -558,9 +558,9 @@
             }
         }
 
-        /// Frees `data`, if not nullptr and isn't a pointer to #small_arr
+        /// Frees `data`, if isn't a pointer to #small_arr
         void Free(T* data) const {
-            if (data && data != small_arr[0].Get()) {
+            if (data != small_arr[0].Get()) {
                 delete[] Bitcast<TStorage*>(data);
             }
         }
@@ -580,12 +580,8 @@
             slice.cap = new_cap;
         }
 
-        /// Frees `data`, if not nullptr.
-        void Free(T* data) const {
-            if (data) {
-                delete[] Bitcast<TStorage*>(data);
-            }
-        }
+        /// Frees `data`.
+        void Free(T* data) const { delete[] Bitcast<TStorage*>(data); }
 
         /// Indicates whether the slice structure can be std::move()d.
         /// @returns true
diff --git a/src/tint/utils/debug/BUILD.bazel b/src/tint/utils/debug/BUILD.bazel
new file mode 100644
index 0000000..aca04a5
--- /dev/null
+++ b/src/tint/utils/debug/BUILD.bazel
@@ -0,0 +1,39 @@
+# 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 = "debug",
+  srcs = [
+    "debugger.cc",
+  ],
+  hdrs = [
+    "debugger.h",
+  ],
+  deps = [
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/diagnostic/BUILD.bazel b/src/tint/utils/diagnostic/BUILD.bazel
new file mode 100644
index 0000000..8d4b83a
--- /dev/null
+++ b/src/tint/utils/diagnostic/BUILD.bazel
@@ -0,0 +1,127 @@
+# 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 = "diagnostic",
+  srcs = [
+    "diagnostic.cc",
+    "formatter.cc",
+    "printer.cc",
+    "source.cc",
+  ] + select({
+    ":_not_is_linux__and__not_is_mac__and__not_is_win_": [
+      "printer_other.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":is_linux_or_is_mac": [
+      "printer_posix.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":is_win": [
+      "printer_windows.cc",
+    ],
+    "//conditions:default": [],
+  }),
+  hdrs = [
+    "diagnostic.h",
+    "formatter.h",
+    "printer.h",
+    "source.h",
+  ],
+  deps = [
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "diagnostic_test.cc",
+    "formatter_test.cc",
+    "printer_test.cc",
+    "source_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "is_linux",
+  actual = "//src/tint:is_linux_true",
+)
+
+alias(
+  name = "_not_is_linux_",
+  actual = "//src/tint:is_linux_false",
+)
+
+alias(
+  name = "is_mac",
+  actual = "//src/tint:is_mac_true",
+)
+
+alias(
+  name = "_not_is_mac_",
+  actual = "//src/tint:is_mac_false",
+)
+
+alias(
+  name = "is_win",
+  actual = "//src/tint:is_win_true",
+)
+
+alias(
+  name = "_not_is_win_",
+  actual = "//src/tint:is_win_false",
+)
+
+selects.config_setting_group(
+    name = "is_linux_or_is_mac",
+    match_any = [
+        "is_linux",
+        "is_mac",
+    ],
+)
+
+selects.config_setting_group(
+    name = "_not_is_linux__and__not_is_mac__and__not_is_win_",
+    match_all = [
+        ":_not_is_linux_",
+        ":_not_is_mac_",
+        ":_not_is_win_",
+    ],
+)
+
diff --git a/src/tint/utils/file/BUILD.bazel b/src/tint/utils/file/BUILD.bazel
new file mode 100644
index 0000000..d75fc5b
--- /dev/null
+++ b/src/tint/utils/file/BUILD.bazel
@@ -0,0 +1,117 @@
+# 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 = "file",
+  srcs = [
+  ] + select({
+    ":_not_is_linux__and__not_is_mac__and__not_is_win_": [
+      "tmpfile_other.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":is_linux_or_is_mac": [
+      "tmpfile_posix.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":is_win": [
+      "tmpfile_windows.cc",
+    ],
+    "//conditions:default": [],
+  }),
+  hdrs = [
+    "tmpfile.h",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/text",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "tmpfile_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/file",
+    "//src/tint/utils/text",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "is_linux",
+  actual = "//src/tint:is_linux_true",
+)
+
+alias(
+  name = "_not_is_linux_",
+  actual = "//src/tint:is_linux_false",
+)
+
+alias(
+  name = "is_mac",
+  actual = "//src/tint:is_mac_true",
+)
+
+alias(
+  name = "_not_is_mac_",
+  actual = "//src/tint:is_mac_false",
+)
+
+alias(
+  name = "is_win",
+  actual = "//src/tint:is_win_true",
+)
+
+alias(
+  name = "_not_is_win_",
+  actual = "//src/tint:is_win_false",
+)
+
+selects.config_setting_group(
+    name = "is_linux_or_is_mac",
+    match_any = [
+        "is_linux",
+        "is_mac",
+    ],
+)
+
+selects.config_setting_group(
+    name = "_not_is_linux__and__not_is_mac__and__not_is_win_",
+    match_all = [
+        ":_not_is_linux_",
+        ":_not_is_mac_",
+        ":_not_is_win_",
+    ],
+)
+
diff --git a/src/tint/utils/generator/BUILD.bazel b/src/tint/utils/generator/BUILD.bazel
new file mode 100644
index 0000000..7885b9e
--- /dev/null
+++ b/src/tint/utils/generator/BUILD.bazel
@@ -0,0 +1,45 @@
+# 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 = "generator",
+  srcs = [
+    "text_generator.cc",
+  ],
+  hdrs = [
+    "text_generator.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/ice/BUILD.bazel b/src/tint/utils/ice/BUILD.bazel
new file mode 100644
index 0000000..8789ad0
--- /dev/null
+++ b/src/tint/utils/ice/BUILD.bazel
@@ -0,0 +1,55 @@
+# 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 = "ice",
+  srcs = [
+    "ice.cc",
+  ],
+  hdrs = [
+    "ice.h",
+  ],
+  deps = [
+    "//src/tint/utils/debug",
+    "//src/tint/utils/macros",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "ice_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/id/BUILD.bazel b/src/tint/utils/id/BUILD.bazel
new file mode 100644
index 0000000..b556170
--- /dev/null
+++ b/src/tint/utils/id/BUILD.bazel
@@ -0,0 +1,43 @@
+# 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 = "id",
+  srcs = [
+    "generation_id.cc",
+  ],
+  hdrs = [
+    "generation_id.h",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/macros/BUILD.bazel b/src/tint/utils/macros/BUILD.bazel
new file mode 100644
index 0000000..e3a081b
--- /dev/null
+++ b/src/tint/utils/macros/BUILD.bazel
@@ -0,0 +1,57 @@
+# 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 = "macros",
+  srcs = [
+    "macros.cc",
+  ],
+  hdrs = [
+    "compiler.h",
+    "concat.h",
+    "defer.h",
+    "foreach.h",
+    "scoped_assignment.h",
+  ],
+  deps = [
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "defer_test.cc",
+    "scoped_assignment_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/macros/compiler.h b/src/tint/utils/macros/compiler.h
index c279f53..46236ae 100644
--- a/src/tint/utils/macros/compiler.h
+++ b/src/tint/utils/macros/compiler.h
@@ -60,7 +60,7 @@
 #define TINT_DISABLE_WARNING_WEAK_VTABLES _Pragma("clang diagnostic ignored \"-Wweak-vtables\"")
 #define TINT_DISABLE_WARNING_FLOAT_EQUAL _Pragma("clang diagnostic ignored \"-Wfloat-equal\"")
 #define TINT_DISABLE_WARNING_DEPRECATED /* currently no-op */
-#define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER                     \
+#define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER \
     _Pragma("clang diagnostic ignored \"-Wreserved-identifier\"")
 
 // clang-format off
diff --git a/src/tint/utils/math/BUILD.bazel b/src/tint/utils/math/BUILD.bazel
new file mode 100644
index 0000000..451ac32
--- /dev/null
+++ b/src/tint/utils/math/BUILD.bazel
@@ -0,0 +1,62 @@
+# 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 = "math",
+  srcs = [
+    "math.cc",
+  ],
+  hdrs = [
+    "crc32.h",
+    "hash.h",
+    "math.h",
+  ],
+  deps = [
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "crc32_test.cc",
+    "hash_test.cc",
+    "math_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/memory/BUILD.bazel b/src/tint/utils/memory/BUILD.bazel
new file mode 100644
index 0000000..e419f7b
--- /dev/null
+++ b/src/tint/utils/memory/BUILD.bazel
@@ -0,0 +1,58 @@
+# 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 = "memory",
+  srcs = [
+    "memory.cc",
+  ],
+  hdrs = [
+    "bitcast.h",
+    "block_allocator.h",
+    "bump_allocator.h",
+  ],
+  deps = [
+    "//src/tint/utils/math",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "bitcast_test.cc",
+    "block_allocator_test.cc",
+    "bump_allocator_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/reflection/BUILD.bazel b/src/tint/utils/reflection/BUILD.bazel
new file mode 100644
index 0000000..0e1a025
--- /dev/null
+++ b/src/tint/utils/reflection/BUILD.bazel
@@ -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.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 = "reflection",
+  srcs = [
+    "reflection.cc",
+  ],
+  hdrs = [
+    "reflection.h",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "reflection_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "//src/tint/utils/reflection",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/result/BUILD.bazel b/src/tint/utils/result/BUILD.bazel
new file mode 100644
index 0000000..56456e4
--- /dev/null
+++ b/src/tint/utils/result/BUILD.bazel
@@ -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.
+
+################################################################################
+# 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 = "result",
+  srcs = [
+    "result.cc",
+  ],
+  hdrs = [
+    "result.h",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "result_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/result",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/rtti/BUILD.bazel b/src/tint/utils/rtti/BUILD.bazel
new file mode 100644
index 0000000..e724929
--- /dev/null
+++ b/src/tint/utils/rtti/BUILD.bazel
@@ -0,0 +1,77 @@
+# 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 = "rtti",
+  srcs = [
+    "castable.cc",
+  ],
+  hdrs = [
+    "castable.h",
+    "switch.h",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "castable_test.cc",
+    "switch_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "bench",
+  srcs = [
+    "switch_bench.cc",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/socket/BUILD.bazel b/src/tint/utils/socket/BUILD.bazel
new file mode 100644
index 0000000..efbb0ee
--- /dev/null
+++ b/src/tint/utils/socket/BUILD.bazel
@@ -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.
+
+################################################################################
+# 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 = "socket",
+  srcs = [
+    "socket.cc",
+  ],
+  hdrs = [
+    "rwmutex.h",
+    "socket.h",
+  ],
+  deps = [
+    "//src/tint/utils/macros",
+  ] + select({
+    ":is_win": [
+      
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "is_win",
+  actual = "//src/tint:is_win_true",
+)
+
diff --git a/src/tint/utils/strconv/BUILD.bazel b/src/tint/utils/strconv/BUILD.bazel
new file mode 100644
index 0000000..74331d3
--- /dev/null
+++ b/src/tint/utils/strconv/BUILD.bazel
@@ -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.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 = "strconv",
+  srcs = [
+    "float_to_string.cc",
+    "parse_num.cc",
+  ],
+  hdrs = [
+    "float_to_string.h",
+    "parse_num.h",
+  ],
+  deps = [
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/result",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@abseil_cpp//absl/strings",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "float_to_string_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/memory",
+    "//src/tint/utils/strconv",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/symbol/BUILD.bazel b/src/tint/utils/symbol/BUILD.bazel
new file mode 100644
index 0000000..5b914d3
--- /dev/null
+++ b/src/tint/utils/symbol/BUILD.bazel
@@ -0,0 +1,73 @@
+# 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 = "symbol",
+  srcs = [
+    "symbol.cc",
+    "symbol_table.cc",
+  ],
+  hdrs = [
+    "symbol.h",
+    "symbol_table.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "symbol_table_test.cc",
+    "symbol_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
index 8326ad6..9da4e6c 100644
--- a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
+++ b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
@@ -88,7 +88,7 @@
 {{- range $i, $p := .Parameters }}
   {
     /* [{{$i}}] */
-    /* usage */ ParameterUsage::
+    /* usage */ core::ParameterUsage::
 {{-   if $p.Usage }}k{{PascalCase $p.Usage}}
 {{-   else        }}kNone
 {{-   end }},
@@ -136,14 +136,17 @@
 static_assert(TemplateNumberIndex::CanIndex(kTemplateNumbers),
               "TemplateNumberIndex is not large enough to index kTemplateNumbers");
 
-constexpr constant::Eval::Function kConstEvalFunctions[] = {
-{{- range $i, $f := .ConstEvalFunctions }}
-  /* [{{$i}}] */ &constant::Eval::{{template "ExpandName" $f}},
-{{- end }}
+{{- if .ConstEvalFunctions}}
+{{/* newline */}}
+constexpr core::constant::Eval::Function kConstEvalFunctions[] = {
+{{-   range $i, $f := .ConstEvalFunctions }}
+  /* [{{$i}}] */ &core::constant::Eval::{{template "OperatorName" $f}},
+{{-   end }}
 };
 
 static_assert(ConstEvalFunctionIndex::CanIndex(kConstEvalFunctions),
               "ConstEvalFunctionIndex is not large enough to index kConstEvalFunctions");
+{{- end}}
 
 constexpr OverloadInfo kOverloads[] = {
 {{- range $i, $o := .Overloads }}
@@ -203,52 +206,65 @@
 {{- end }}
 };
 
+{{- if .UnaryOperators}}
+{{/* newline */}}
 constexpr IntrinsicInfo kUnaryOperators[] = {
-{{- range $i, $o := .UnaryOperators }}
+{{-   range $i, $o := .UnaryOperators }}
   {
     /* [{{$i}}] */
-{{-   range $o.OverloadDescriptions }}
+{{-     range $o.OverloadDescriptions }}
     /* {{.}} */
-{{-   end }}
+{{-     end }}
     /* num overloads */ {{$o.NumOverloads}},
     /* overloads */ OverloadIndex({{$o.OverloadsOffset}}),
   },
-{{- end }}
+{{-   end }}
 };
-
-{{- range $i, $o := .UnaryOperators }}
-constexpr uint8_t kUnaryOperator{{ template "ExpandName" $o.Name}} = {{$i}};
 {{- end }}
 
+{{- $unary_ops := Map }}
+{{- range $i, $o := .UnaryOperators }}
+{{-   $unary_ops.Put $o.Name true }}
+constexpr uint8_t kUnaryOperator{{ template "OperatorName" $o.Name}} = {{$i}};
+{{- end }}
+
+{{- if .BinaryOperators}}
+{{/* newline */}}
 constexpr IntrinsicInfo kBinaryOperators[] = {
-{{- range $i, $o := .BinaryOperators }}
+{{-   range $i, $o := .BinaryOperators }}
   {
     /* [{{$i}}] */
-{{-   range $o.OverloadDescriptions }}
+{{-     range $o.OverloadDescriptions }}
     /* {{.}} */
-{{-   end }}
+{{-     end }}
     /* num overloads */ {{$o.NumOverloads}},
     /* overloads */ OverloadIndex({{$o.OverloadsOffset}}),
   },
-{{- end }}
+{{-   end }}
 };
-
-{{- range $i, $o := .BinaryOperators }}
-constexpr uint8_t kBinaryOperator{{ template "ExpandName" $o.Name}} = {{$i}};
 {{- end }}
 
+{{- $bin_ops := Map }}
+{{- range $i, $o := .BinaryOperators }}
+{{-   $bin_ops.Put $o.Name true }}
+constexpr uint8_t kBinaryOperator{{ template "OperatorName" $o.Name}} = {{$i}};
+{{- end }}
+
+{{- if .ConstructorsAndConverters}}
+{{/* newline */}}
 constexpr IntrinsicInfo kConstructorsAndConverters[] = {
-{{- range $i, $o := .ConstructorsAndConverters }}
+{{-   range $i, $o := .ConstructorsAndConverters }}
   {
     /* [{{$i}}] */
-{{-   range $o.OverloadDescriptions }}
+{{-     range $o.OverloadDescriptions }}
     /* {{.}} */
-{{-   end }}
+{{-     end }}
     /* num overloads */ {{$o.NumOverloads}},
     /* overloads */ OverloadIndex({{$o.OverloadsOffset}}),
   },
-{{- end }}
+{{-   end }}
 };
+{{- end }}
 
 // clang-format on
 
@@ -263,30 +279,17 @@
   /* number_matchers */ kNumberMatchers,
   /* parameters */ kParameters,
   /* overloads */ kOverloads,
-  /* const_eval_functions */ kConstEvalFunctions,
-  /* ctor_conv */ kConstructorsAndConverters,
+  /* const_eval_functions */ {{if .ConstEvalFunctions}}kConstEvalFunctions{{else}}Empty{{end}},
+  /* ctor_conv */ {{if .ConstructorsAndConverters}}kConstructorsAndConverters{{else}}Empty{{end}},
   /* builtins */ kBuiltins,
-  /* binary_plus */ kBinaryOperators[kBinaryOperatorPlus],
-  /* binary_minus */ kBinaryOperators[kBinaryOperatorMinus],
-  /* binary_star */ kBinaryOperators[kBinaryOperatorStar],
-  /* binary_divide */ kBinaryOperators[kBinaryOperatorDivide],
-  /* binary_modulo */ kBinaryOperators[kBinaryOperatorModulo],
-  /* binary_xor */ kBinaryOperators[kBinaryOperatorXor],
-  /* binary_and */ kBinaryOperators[kBinaryOperatorAnd],
-  /* binary_or */ kBinaryOperators[kBinaryOperatorOr],
-  /* binary_logical_and */ kBinaryOperators[kBinaryOperatorLogicalAnd],
-  /* binary_logical_or */ kBinaryOperators[kBinaryOperatorLogicalOr],
-  /* binary_equal */ kBinaryOperators[kBinaryOperatorEqual],
-  /* binary_not_equal */ kBinaryOperators[kBinaryOperatorNotEqual],
-  /* binary_less_than */ kBinaryOperators[kBinaryOperatorLessThan],
-  /* binary_greater_than */ kBinaryOperators[kBinaryOperatorGreaterThan],
-  /* binary_less_than_equal */ kBinaryOperators[kBinaryOperatorLessThanEqual],
-  /* binary_greater_than_equal */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
-  /* binary_shift_left */ kBinaryOperators[kBinaryOperatorShiftLeft],
-  /* binary_shift_right */ kBinaryOperators[kBinaryOperatorShiftRight],
-  /* unary_not */ kUnaryOperators[kUnaryOperatorNot],
-  /* unary_complement */ kUnaryOperators[kUnaryOperatorComplement],
-  /* unary_minus */ kUnaryOperators[kUnaryOperatorMinus],
+{{- range $op := List "+" "-" "*" "/" "%" "^" "&" "|" "&&" "||" "==" "!=" "<" ">" "<=" ">=" "<<" ">>"}}
+{{-   $N := Eval "OperatorName" $op }}
+  /* binary '{{$op}}' */ {{if $bin_ops.Get .}}kBinaryOperators[kBinaryOperator{{$N}}]{{else}}tint::core::intrinsic::kNoOverloads{{end}},
+{{- end}}
+{{- range $op := List "!" "~" "-"}}
+{{-   $N := Eval "OperatorName" $op }}
+  /* unary '{{$op}}' */ {{if $unary_ops.Get .}}kUnaryOperators[kUnaryOperator{{$N}}]{{else}}tint::core::intrinsic::kNoOverloads{{end}},
+{{- end}}
 };
 
 {{  end -}}
@@ -312,7 +315,7 @@
       return nullptr;
     }
 {{- end  }}
-    return Build{{$name}}(state{{range .TemplateParams}}, {{.GetName}}{{end}});
+    return Build{{$name}}(state, ty{{range .TemplateParams}}, {{.GetName}}{{end}});
   },
 /* string */ [](MatchState*{{if .TemplateParams}} state{{end}}) -> std::string {
 {{- range .TemplateParams }}
@@ -343,7 +346,7 @@
 /* match */ [](MatchState& state, const Type* ty) -> const Type* {
 {{- range .PrecedenceSortedTypes }}
     if (Match{{PascalCase .Name}}(state, ty)) {
-      return Build{{PascalCase .Name}}(state);
+      return Build{{PascalCase .Name}}(state, ty);
     }
 {{- end }}
     return nullptr;
@@ -376,16 +379,16 @@
 {{-   $option := index .Options 0 }}
 {{-   $entry := printf "k%v" (PascalCase $option.Name) -}}
 /* match */ [](MatchState&, Number number) -> Number {
-    if (number.IsAny() || number.Value() == static_cast<uint32_t>({{$enum}}::{{$entry}})) {
-      return Number(static_cast<uint32_t>({{$enum}}::{{$entry}}));
+    if (number.IsAny() || number.Value() == static_cast<uint32_t>(core::{{$enum}}::{{$entry}})) {
+      return Number(static_cast<uint32_t>(core::{{$enum}}::{{$entry}}));
     }
     return Number::invalid;
   },
 {{- else -}}
 /* match */ [](MatchState&, Number number) -> Number {
-    switch (static_cast<{{$enum}}>(number.Value())) {
+    switch (static_cast<core::{{$enum}}>(number.Value())) {
 {{-   range .Options }}
-      case {{$enum}}::k{{PascalCase .Name}}:
+      case core::{{$enum}}::k{{PascalCase .Name}}:
 {{-   end }}
         return number;
       default:
@@ -520,7 +523,7 @@
 
 
 {{- /* ------------------------------------------------------------------ */ -}}
-{{-                           define "ExpandName"                            -}}
+{{-                          define "OperatorName"                           -}}
 {{- /* ------------------------------------------------------------------ */ -}}
 {{-        if eq . "<<" -}}ShiftLeft
 {{-   else if eq . "&"  -}}And
diff --git a/src/tint/utils/text/BUILD.bazel b/src/tint/utils/text/BUILD.bazel
new file mode 100644
index 0000000..3601cf6
--- /dev/null
+++ b/src/tint/utils/text/BUILD.bazel
@@ -0,0 +1,72 @@
+# 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 = "text",
+  srcs = [
+    "string.cc",
+    "string_stream.cc",
+    "unicode.cc",
+  ],
+  hdrs = [
+    "string.h",
+    "string_stream.h",
+    "unicode.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/traits",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "string_stream_test.cc",
+    "string_test.cc",
+    "unicode_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/traits/BUILD.bazel b/src/tint/utils/traits/BUILD.bazel
new file mode 100644
index 0000000..791ab99
--- /dev/null
+++ b/src/tint/utils/traits/BUILD.bazel
@@ -0,0 +1,52 @@
+# 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 = "traits",
+  srcs = [
+    "traits.cc",
+  ],
+  hdrs = [
+    "traits.h",
+  ],
+  deps = [
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "traits_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+