Import Tint changes from Dawn

Changes:
  - 7e0d9e6843de383e721d2c269d6a64e3537a244b [tint] Handle empty tables in intrinsic table data template by Ben Clayton <bclayton@google.com>
  - 3c54ba593c7811c6ddcf5f61f8610187d210aa16 [tint][wgsl] Move LanguageFeature to a separate target by Ben Clayton <bclayton@google.com>
  - 2e26e7a6f8f48e67d099c3237f53da5708197fc6 [tint][msl][ir] Use ValueToLet IR transform by Ben Clayton <bclayton@google.com>
  - d9ffa54c787dbd790f4896bd522121a5c40b4ef4 [tint][ir] Add ValueToLet transform by Ben Clayton <bclayton@google.com>
  - e6d2cd002804c1543b3ca60a25715d04a4e76c98 [tint][spirv] Add a SPIR-V writer IR fuzzer. by Ben Clayton <bclayton@google.com>
  - fb6a9bcd2ce8771d696ee86ec41753b9ab5600aa [tint][ir] Remove default parameters for Operand() and Re... by Ben Clayton <bclayton@google.com>
  - da8a08aefec265090b9aedbbeaea919b81395904 [tint][ir] Change Instruction::Result() return type by Ben Clayton <bclayton@google.com>
  - 08396a3b9ac5c58fc0e86e3c3b53869228c8d6f9 [tint][ir] Add Builder overloads that take the type as a ... by Ben Clayton <bclayton@google.com>
  - 6a235972b207be3b9e6395d02aff71141562885b [tint][cmake] Pass -Wno-gnu-zero-variadic-macro-arguments by Ben Clayton <bclayton@google.com>
  - 84cd0f83d5f3661bd4d2f10287775e778589c14e [tint][ir] Always emit ir::Let for WGSL lets by Ben Clayton <bclayton@google.com>
  - 7e6e44fbf9d65645056e8759bddcff259884fa11 [tint][wgsl] Migrate IR roundtrip fuzzer to be an IR fuzzer by Ben Clayton <bclayton@google.com>
  - 339941969ddb111159ba1696728fdfe6e01783d4 [tint][fuzz] Add an IR fuzzer framework by Ben Clayton <bclayton@google.com>
  - bfa79963bcdda05644bce8857f13b980f76655b1 [tint][spirv] Add a SPIR-V writer fuzzer from AST by Ben Clayton <bclayton@google.com>
  - 881eff88cf623325bbb891a0c07c1ef0e0a612d2 [tint][spirv] Add validate library by Ben Clayton <bclayton@google.com>
  - 5ee5f187deed9ef125de1e6177d95fdf7beb2967 [ir][msl] Run existing transforms which are required. by dan sinclair <dsinclair@chromium.org>
  - 571cc4420ff93b2ff9ed49ef2c076191563c87df Remove chromium_experimental_read_write_storage_texture f... by Jiawei Shao <jiawei.shao@intel.com>
  - 2a21ac0c3b4f4b18afb69aefbf7a390915b13ef2 [ir][msl] Add support for fragment inputs by dan sinclair <dsinclair@chromium.org>
  - 71da3e8aa054ec1e8d36f65a07b975ad8d02de0f [ir][msl] Add thread and threadgroup position. by dan sinclair <dsinclair@chromium.org>
  - 9ffc1d92cb4b6a599dc8803a9d84f28821c5b27d [ir][msl] Fix const of non-entrypoint pointers by dan sinclair <dsinclair@chromium.org>
  - b4697b7658625f704b18f25f291a7e7557e90537 [ir][msl] Emit some core builtins. by dan sinclair <dsinclair@chromium.org>
  - 050f08c3f5c84ba3721b26a6765e6084ce497f1e Fix TINT_FOREACH_13, _14 and _15 by Jiawei Shao <jiawei.shao@intel.com>
  - 9aa5b553322a54c0ecd3c9eb71bf2d12ad8932b6 [tint] Fix Bazel config_setting_group definition by André Cruz <andre@cabine.org>
  - aa5190b63b384a21e1eab9769a7069fe850a72fc [ir] Lower IR before dumping. by dan sinclair <dsinclair@chromium.org>
  - 9cad74f4170393ee91dead059cd57b86bce70cb4 [ir][msl] Emit swizzle, load/store vector element instruc... by dan sinclair <dsinclair@chromium.org>
  - af2c7345c2db8227dd83558eeb0095eb5af20db8 [ir] Rename InstructionResult::Source. by dan sinclair <dsinclair@chromium.org>
  - 2e2bf672c68214f87aec7b8dfdefef943d67c23a [ir][msl] Emit switch instructions by dan sinclair <dsinclair@chromium.org>
  - 3b4bf191b3afa8902d3a39c21e1bca76a8246752 [ir][msl] Emit loops by dan sinclair <dsinclair@chromium.org>
  - b4ec73fd56384fd586caec2ae80af3b0fa144776 [ir][msl] Emit unary instructions by dan sinclair <dsinclair@chromium.org>
  - 99204da719f0f1f76b79afb306ac6d5fa81269e2 [ir][msl] Emit function parameters by dan sinclair <dsinclair@chromium.org>
  - a5460d0c97c7c07a49e65fdbfeed1af6ff41f7e4 [tint][utils] Fix diagnostic initializer_list ctor by Ben Clayton <bclayton@google.com>
  - 624e8dd3ae8c510f85929e5e17db8591e19e2fb6 Add user call tests by dan sinclair <dsinclair@chromium.org>
  - 5c69ff44659e270f174e132064ac0a2eaaab5fb0 [tint] Add tint_build_is_<OS> defines and build flags by Ben Clayton <bclayton@google.com>
  - 1cc545189477077687e73b77aed757ad341bef8a [ir][msl] Emit ir::UserCall by dan sinclair <dsinclair@chromium.org>
  - 3505ed4c5fc1eb6532562b14a77627249839843e Add struct index accessor tests by dan sinclair <dsinclair@chromium.org>
  - 48318cd9d31a766fa5f60bca330ae65cc53a3c9c [ir][msl] Emit ir::Access by dan sinclair <dsinclair@chromium.org>
  - 3aaaaf476d6cdc5cd859c1ba3a8b924c91efd88c [tint][wgsl][fuzz] Parse base64 encoded comments by Ben Clayton <bclayton@google.com>
  - c97af9022e8442a2dbdea6716e190a04f3c18028 [tint][utils] Add DecodeBase64FromComments() by Ben Clayton <bclayton@google.com>
  - b421e4039ddf914ca55a1be0468b138bf7dc188c [tint][wgsl][fuzz] Fix static init ordering bug by Ben Clayton <bclayton@google.com>
  - ff67199151e64f4dc1f13a96aea80dd50c43d669 [tint][spirv] Add GenerateBindings() overload for IR modu... by Ben Clayton <bclayton@google.com>
  - f0973d594afb2563f044085764c448042ab6b1e1 [tint][utils] Add DecodeBase64() helper by Ben Clayton <bclayton@google.com>
  - 63eb5c8c26eef4692a2e99698e0b902cea82ecd0 [tint][utils] Add byte decoder by Ben Clayton <bclayton@google.com>
  - e988749ee42d12307483ea2fc6b89895845012e6 [tint][wgsl][lower] Skip over dead instructions by Ben Clayton <bclayton@google.com>
  - b098803b006d7d1bb7797fa08644acc9d31b6e1b [tint] Rename wgsl/wgsl_fuzz.* -> wgsl/fuzz.* by Ben Clayton <bclayton@google.com>
  - 1b48e6b4e1c7f0192554d3225959b26b524acc9d [tint][utils] Add byte reader utility by Ben Clayton <bclayton@google.com>
  - cea718c62b756d4cfce155460d5e629ca6918cf8 [ir][msl] Emit bitcast by dan sinclair <dsinclair@chromium.org>
  - 1fa6a4beb24243ad86bba906eca9c09e4cbb200d [ir][msl] Emit entry point stage name by dan sinclair <dsinclair@chromium.org>
  - 5555694d23e1507c24c76827003d765f6593ecf2 [ir][msl] Fixup binary emission by dan sinclair <dsinclair@chromium.org>
  - 24b81095d4bd758b87fb548c2ca1a3dcae2a2d74 [ir] Move use of IR decision higher in the stack. by dan sinclair <dsinclair@chromium.org>
  - 58794b5dfa9d81ef14122e3fbc85382664541b20 [ir][msl] Simplify expression emission in MSL printer. by dan sinclair <dsinclair@chromium.org>
  - 7bbe4c1696f2433d7228877d451b812e23c4adff [ir][msl] Add `construct` to MSL IR printer by dan sinclair <dsinclair@chromium.org>
  - 019146fbc300c5bd704dae7e198714ff774236d3 [ir] Add an IR option into the ICE machinery. by dan sinclair <dsinclair@chromium.org>
  - acef3106912878e0b93c0f969c80020853fcbbb1 [tint][ir] Add ConstPropagatingPtr - const-propagating ptr by Ben Clayton <bclayton@google.com>
  - eee3ad04f703c90c7af1a8f0ea358231247f955a [ir][msl] Add `discard` to MSL IR printer by dan sinclair <dsinclair@chromium.org>
  - 37e9d111b375bd014cb0e1236436b8eeea8da45a [tint] Namespace the strconv files by dan sinclair <dsinclair@chromium.org>
  - 1e7b312706ad214eb09007f77521488f0a409991 [tint][ir] Use 'const' on non-mutating IR consumers by Ben Clayton <bclayton@google.com>
  - c33fdbb0a4f4a8301551019596101c05e38249b3 Remove chromium_experimental_read_write_storage_texture by Jiawei Shao <jiawei.shao@intel.com>
  - ead8a043e377273b24e6c788007e6a7d15cf1093 [tint][ir] Add 'const' accessors by Ben Clayton <bclayton@google.com>
  - 1c483bfabb8685b5c1ff8c50b4d41677708b0d53 [spirv-writer] Fix atomics after discard by James Price <jrprice@google.com>
  - 69298a3abdb3e788e05931e961cb2337aab70c23 [spirv-writer] Fix implicit blocks with results by James Price <jrprice@google.com>
  - ffbcc3584f07dae21ff696bbdc9fdd26127e425f [tint][wgsl] Move RenameConflicts out of IrToProgram by Ben Clayton <bclayton@google.com>
  - a166dcb470df81ce26f1fde0fc3bc071c90aca87 [tint][ir] Simplify ir::Switch case generation by Ben Clayton <bclayton@google.com>
  - a89d6647f696b240bfd2861d79c7dba7d856dcb5 [tint][ir] Refactor Instruction result methods by Ben Clayton <bclayton@google.com>
  - fb07fa9e8877d3f7934ec972a4e7bd3a44e0a78c [build] Add guards for protobuf by Ben Clayton <bclayton@google.com>
  - e6744dbe784dbba68a866c3e6cf219ac616bfc40 [tint][utils] Add Vector(const Slice<const T>&) constructor by Ben Clayton <bclayton@google.com>
  - 0e21d289202eea9f08d14ca32cf3cdfa2cac8cca [ir] Fix Std140 for non-decomposed matrices by James Price <jrprice@google.com>
  - b9d3e1c8448ac3dc37fa2eac24d668dc1199b928 [tint][ir] Remove Block::HasTerminator() by Ben Clayton <bclayton@google.com>
  - 2e4b301ab516935dd2afc7e6e161e628a7ab2c43 [tint][utils] Vector improvements by Ben Clayton <bclayton@google.com>
  - 96687ce6fe09763f997a9ee5c345d04162a87451 [tint][ir] Remove Usage::Hasher by Ben Clayton <bclayton@google.com>
  - a86a8230eab47ce501c4bb4eb01b6cad5a9f456f [tint][utils] Change SymbolTable::Wrap() to be a static m... by Ben Clayton <bclayton@google.com>
  - 095c7c067792c0dc868a84f72b356723a43235d0 [tint][ir] Remove Module::disassembly_file by Ben Clayton <bclayton@google.com>
  - f2de7ead699d34b1ef8aa43398d5488ba67f018b [tint] Add build support for protobufs by Ben Clayton <bclayton@google.com>
  - 429e97b6d3f8caddd198154d0ad63c7ed5e96615 [tint][utils] Remove stray '+' by Ben Clayton <bclayton@google.com>
  - d67c378f26a756649984b6bd0eda1d752377e7e1 Revert "[msl] Remove PixelLocal as printer supported exte... by Ben Clayton <bclayton@google.com>
  - a87d06317a67c25c0676fc47fff87d9146622b5e Tint/HLSL: Support translating pixel local variables on H... by Jiawei Shao <jiawei.shao@intel.com>
  - 5896e7501bf3a0dccb12f146949195d461d4a71e [tint] Namespace the socket files by dan sinclair <dsinclair@chromium.org>
  - e5f014098bf1706bbf04f5a507ee8b8d04cbfba5 [msl] Remove PixelLocal as printer supported extension by Ben Clayton <bclayton@google.com>
  - 99672e2bf1edb05cc52a2fa5e9128952fd3a93e8 Use result type to handle diagnostics by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 7e0d9e6843de383e721d2c269d6a64e3537a244b
Change-Id: Ic95378ba1d4e9efdc50c770088125baebd62efa5
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/161020
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index d083bef..6f78959 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -108,6 +108,24 @@
     defines += [ "TINT_BUILD_SYNTAX_TREE_WRITER=0" ]
   }
 
+  if (tint_build_is_win) {
+    defines += [ "TINT_BUILD_IS_WIN=1" ]
+  } else {
+    defines += [ "TINT_BUILD_IS_WIN=0" ]
+  }
+
+  if (tint_build_is_mac) {
+    defines += [ "TINT_BUILD_IS_MAC=1" ]
+  } else {
+    defines += [ "TINT_BUILD_IS_MAC=0" ]
+  }
+
+  if (tint_build_is_linux) {
+    defines += [ "TINT_BUILD_IS_LINUX=1" ]
+  } else {
+    defines += [ "TINT_BUILD_IS_LINUX=0" ]
+  }
+
   include_dirs = [
     "${tint_root_dir}/",
     "${tint_root_dir}/include/",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 3a47bd1..208a231 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -30,14 +30,14 @@
 ################################################################################
 if(NOT TINT_BUILD_AS_OTHER_OS)
   if(APPLE)  # Must come before UNIX
-    set(IS_MAC TRUE)
-    set(IS_MAC TRUE)
+    set(TINT_BUILD_IS_MAC TRUE)
+    set(TINT_BUILD_IS_MAC TRUE)
   elseif(UNIX)
-    set(IS_LINUX TRUE)
-    set(IS_LINUX TRUE)
+    set(TINT_BUILD_IS_LINUX TRUE)
+    set(TINT_BUILD_IS_LINUX TRUE)
   elseif(WIN32)
-    set(IS_WIN TRUE)
-    set(IS_WIN TRUE)
+    set(TINT_BUILD_IS_WIN TRUE)
+    set(TINT_BUILD_IS_WIN TRUE)
   endif()
 endif()
 
@@ -47,15 +47,19 @@
 function(tint_core_compile_options TARGET)
   target_include_directories(${TARGET} PUBLIC "${TINT_ROOT_SOURCE_DIR}")
   target_include_directories(${TARGET} PUBLIC "${TINT_ROOT_SOURCE_DIR}/include")
-  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_READER=$<BOOL:${TINT_BUILD_SPV_READER}>)
-  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
-  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_VALIDATOR=$<BOOL:${TINT_BUILD_GLSL_VALIDATOR}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_HLSL_WRITER=$<BOOL:${TINT_BUILD_HLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IS_LINUX=$<BOOL:${TINT_BUILD_IS_LINUX}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IS_MAC=$<BOOL:${TINT_BUILD_IS_MAC}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IS_WIN=$<BOOL:${TINT_BUILD_IS_WIN}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_MSL_WRITER=$<BOOL:${TINT_BUILD_MSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_READER=$<BOOL:${TINT_BUILD_SPV_READER}>)
   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_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
+
 
   if(TINT_BUILD_FUZZERS)
     target_compile_options(${TARGET} PRIVATE "-fsanitize=fuzzer")
@@ -83,10 +87,11 @@
     -Wno-c++98-compat
     -Wno-c++98-compat-pedantic
     -Wno-format-pedantic
+    -Wno-gnu-zero-variadic-macro-arguments
     -Wno-poison-system-directories
     -Wno-return-std-move-in-c++11
-    -Wno-unknown-warning-option
     -Wno-undefined-var-template
+    -Wno-unknown-warning-option
     -Wno-unsafe-buffer-usage
     -Wno-used-but-marked-unused
     -Weverything
@@ -177,6 +182,26 @@
   target_include_directories(${TARGET} PRIVATE "${TINT_SPIRV_TOOLS_DIR}/include")
 endfunction()
 
+function(tint_lib_compile_options TARGET)
+  if (TINT_ENABLE_INSTALL)
+    install(TARGETS ${TARGET}
+      LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+      ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    )
+  endif()
+  tint_default_compile_options(${TARGET})
+endfunction()
+
+function(tint_proto_compile_options TARGET)
+  if (TINT_ENABLE_INSTALL)
+    install(TARGETS ${TARGET}
+      LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+      ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    )
+  endif()
+  tint_core_compile_options(${TARGET})
+endfunction()
+
 function(tint_test_compile_options TARGET)
   tint_default_compile_options(${TARGET})
   set_target_properties(${TARGET} PROPERTIES FOLDER "Tests")
@@ -324,7 +349,7 @@
     if(TINT_BUILD_CMD_TOOLS)
       set(IS_ENABLED TRUE PARENT_SCOPE)
     endif()
-  elseif(${KIND} STREQUAL lib)
+  elseif((${KIND} STREQUAL lib) OR (${KIND} STREQUAL proto))
     set(IS_ENABLED TRUE PARENT_SCOPE)
   elseif((${KIND} STREQUAL test) OR (${KIND} STREQUAL test_cmd))
     if(TINT_BUILD_TESTS)
@@ -363,13 +388,12 @@
 
   if(${KIND} STREQUAL lib)
     add_library(${TARGET} STATIC EXCLUDE_FROM_ALL)
-    if (TINT_ENABLE_INSTALL)
-      install(TARGETS ${TARGET}
-        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
-        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
-      )
-    endif()
-    tint_default_compile_options(${TARGET})
+    tint_lib_compile_options(${TARGET})
+  elseif(${KIND} STREQUAL proto)
+    add_library(${TARGET} STATIC EXCLUDE_FROM_ALL)
+    list(APPEND TINT_PROTO_TARGETS ${TARGET})
+    set(TINT_PROTO_TARGETS ${TINT_PROTO_TARGETS} PARENT_SCOPE)
+    tint_proto_compile_options(${TARGET})
   elseif(${KIND} STREQUAL cmd)
     add_executable(${TARGET})
     tint_default_compile_options(${TARGET})
@@ -611,6 +635,19 @@
 
 
 ################################################################################
+# Generate protobuf sources
+################################################################################
+foreach(PROTO_TARGET ${TINT_PROTO_TARGETS})
+  generate_protos(
+    TARGET ${PROTO_TARGET}
+    PROTOC_OUT_DIR "${DAWN_BUILD_GEN_DIR}/src/tint/")
+  target_include_directories(${PROTO_TARGET} PRIVATE "${DAWN_BUILD_GEN_DIR}/src/tint/")
+  target_include_directories(${PROTO_TARGET} PUBLIC "${DAWN_BUILD_GEN_DIR}")
+  target_link_libraries(${PROTO_TARGET} libprotobuf)
+endforeach()
+
+
+################################################################################
 # Bespoke target settings
 ################################################################################
 if (MSVC)
diff --git a/src/tint/api/BUILD.bazel b/src/tint/api/BUILD.bazel
index 8853b01..22d06eb 100644
--- a/src/tint/api/BUILD.bazel
+++ b/src/tint/api/BUILD.bazel
@@ -56,6 +56,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/api/BUILD.cmake b/src/tint/api/BUILD.cmake
index 570309b..dea891a 100644
--- a/src/tint/api/BUILD.cmake
+++ b/src/tint/api/BUILD.cmake
@@ -58,6 +58,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/api/BUILD.gn b/src/tint/api/BUILD.gn
index 87b72d7..4c3586f 100644
--- a/src/tint/api/BUILD.gn
+++ b/src/tint/api/BUILD.gn
@@ -55,6 +55,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
     "${tint_src_dir}/utils/containers",
diff --git a/src/tint/api/options/pixel_local.h b/src/tint/api/options/pixel_local.h
index 720fcb6..d8f22ce 100644
--- a/src/tint/api/options/pixel_local.h
+++ b/src/tint/api/options/pixel_local.h
@@ -39,8 +39,21 @@
     /// Index of pixel_local structure member index to attachment index
     std::unordered_map<uint32_t, uint32_t> attachments;
 
+    /// The supported pixel local storage attachment format
+    enum class TexelFormat : uint8_t {
+        kR32Sint,
+        kR32Uint,
+        kR32Float,
+        kUndefined,
+    };
+    /// Index of pixel_local structure member index to pixel local storage attachment format
+    std::unordered_map<uint32_t, TexelFormat> attachment_formats;
+
+    /// The bind group index of all pixel local storage attachments
+    uint32_t pixel_local_group_index;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(attachments);
+    TINT_REFLECT(attachments, attachment_formats, pixel_local_group_index);
 };
 
 }  // namespace tint
diff --git a/src/tint/cmd/bench/BUILD.bazel b/src/tint/cmd/bench/BUILD.bazel
index 9a81f43..df4a15f 100644
--- a/src/tint/cmd/bench/BUILD.bazel
+++ b/src/tint/cmd/bench/BUILD.bazel
@@ -53,6 +53,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/cmd/bench/BUILD.cmake b/src/tint/cmd/bench/BUILD.cmake
index 50d1d83..0af9aa2 100644
--- a/src/tint/cmd/bench/BUILD.cmake
+++ b/src/tint/cmd/bench/BUILD.cmake
@@ -129,6 +129,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/cmd/bench/BUILD.gn b/src/tint/cmd/bench/BUILD.gn
index ae16dad..5143ce9 100644
--- a/src/tint/cmd/bench/BUILD.gn
+++ b/src/tint/cmd/bench/BUILD.gn
@@ -58,6 +58,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/cmd/common/BUILD.bazel b/src/tint/cmd/common/BUILD.bazel
index b15b369..acade5c 100644
--- a/src/tint/cmd/common/BUILD.bazel
+++ b/src/tint/cmd/common/BUILD.bazel
@@ -57,6 +57,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -113,6 +114,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/cmd/common/BUILD.cmake b/src/tint/cmd/common/BUILD.cmake
index 7545e63..b795733 100644
--- a/src/tint/cmd/common/BUILD.cmake
+++ b/src/tint/cmd/common/BUILD.cmake
@@ -56,6 +56,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -116,6 +117,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/cmd/common/BUILD.gn b/src/tint/cmd/common/BUILD.gn
index a6980f0..99229d6 100644
--- a/src/tint/cmd/common/BUILD.gn
+++ b/src/tint/cmd/common/BUILD.gn
@@ -60,6 +60,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
@@ -111,6 +112,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/cmd/fuzz/BUILD.cmake b/src/tint/cmd/fuzz/BUILD.cmake
index 65795ac..65ca459 100644
--- a/src/tint/cmd/fuzz/BUILD.cmake
+++ b/src/tint/cmd/fuzz/BUILD.cmake
@@ -34,4 +34,5 @@
 #                       Do not modify this file directly
 ################################################################################
 
+include(cmd/fuzz/ir/BUILD.cmake)
 include(cmd/fuzz/wgsl/BUILD.cmake)
diff --git a/src/tint/cmd/fuzz/ir/BUILD.bazel b/src/tint/cmd/fuzz/ir/BUILD.bazel
new file mode 100644
index 0000000..7c71462
--- /dev/null
+++ b/src/tint/cmd/fuzz/ir/BUILD.bazel
@@ -0,0 +1,44 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/cmd/fuzz/ir/BUILD.cmake b/src/tint/cmd/fuzz/ir/BUILD.cmake
new file mode 100644
index 0000000..96fbb4b
--- /dev/null
+++ b/src/tint/cmd/fuzz/ir/BUILD.cmake
@@ -0,0 +1,80 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_cmd_fuzz_ir_fuzz
+# Kind:      fuzz
+################################################################################
+tint_add_target(tint_cmd_fuzz_ir_fuzz fuzz
+  cmd/fuzz/ir/fuzz.cc
+  cmd/fuzz/ir/fuzz.h
+)
+
+tint_target_add_dependencies(tint_cmd_fuzz_ir_fuzz fuzz
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_common
+  tint_lang_wgsl_features
+  tint_lang_wgsl_helpers
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_bytes
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_cmd_fuzz_ir_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
diff --git a/src/tint/cmd/fuzz/ir/BUILD.gn b/src/tint/cmd/fuzz/ir/BUILD.gn
new file mode 100644
index 0000000..9063f19
--- /dev/null
+++ b/src/tint/cmd/fuzz/ir/BUILD.gn
@@ -0,0 +1,81 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+tint_fuzz_source_set("fuzz") {
+  sources = [
+    "fuzz.cc",
+    "fuzz.h",
+  ]
+  deps = [
+    "${tint_src_dir}/api/common",
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/ir",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/lang/wgsl",
+    "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
+    "${tint_src_dir}/lang/wgsl/helpers",
+    "${tint_src_dir}/lang/wgsl/program",
+    "${tint_src_dir}/lang/wgsl/sem",
+    "${tint_src_dir}/utils/bytes",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+
+  if (tint_build_wgsl_reader) {
+    deps += [
+      "${tint_src_dir}/cmd/fuzz/wgsl:fuzz",
+      "${tint_src_dir}/lang/wgsl/reader",
+    ]
+  }
+}
diff --git a/src/tint/cmd/fuzz/ir/fuzz.cc b/src/tint/cmd/fuzz/ir/fuzz.cc
new file mode 100644
index 0000000..fc96dcc
--- /dev/null
+++ b/src/tint/cmd/fuzz/ir/fuzz.cc
@@ -0,0 +1,100 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+
+#include "src/tint/utils/containers/vector.h"
+
+#if TINT_BUILD_WGSL_READER
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
+#include "src/tint/lang/wgsl/ast/enable.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
+#include "src/tint/lang/wgsl/reader/reader.h"
+#endif
+
+#include "src/tint/lang/core/ir/validator.h"
+
+#if TINT_BUILD_WGSL_READER
+namespace tint::fuzz::ir {
+namespace {
+
+bool IsUnsupported(const ast::Enable* enable) {
+    for (auto ext : enable->extensions) {
+        switch (ext->name) {
+            case tint::wgsl::Extension::kChromiumExperimentalDp4A:
+            case tint::wgsl::Extension::kChromiumExperimentalFullPtrParameters:
+            case tint::wgsl::Extension::kChromiumExperimentalPixelLocal:
+            case tint::wgsl::Extension::kChromiumExperimentalPushConstant:
+            case tint::wgsl::Extension::kChromiumInternalDualSourceBlending:
+            case tint::wgsl::Extension::kChromiumInternalRelaxedUniformLayout:
+                return true;
+            default:
+                break;
+        }
+    }
+    return false;
+}
+
+}  // namespace
+
+void Register(const IRFuzzer& fuzzer) {
+    wgsl::Register({
+        fuzzer.name,
+        [fn = fuzzer.fn](const Program& program, Slice<const std::byte> data) {
+            if (program.AST().Enables().Any(IsUnsupported)) {
+                return;
+            }
+
+            auto transformed = tint::wgsl::ApplySubstituteOverrides(program);
+            auto& src = transformed ? transformed.value() : program;
+            if (!src.IsValid()) {
+                return;
+            }
+
+            auto ir = tint::wgsl::reader::ProgramToLoweredIR(src);
+            if (!ir) {
+                return;
+            }
+
+            if (auto val = core::ir::Validate(ir.Get()); !val) {
+                TINT_ICE() << val.Failure();
+                return;
+            }
+
+            return fn(ir.Get(), data);
+        },
+    });
+}
+
+}  // namespace tint::fuzz::ir
+
+#else
+
+void tint::fuzz::ir::Register([[maybe_unused]] const IRFuzzer&) {}
+
+#endif
diff --git a/src/tint/cmd/fuzz/ir/fuzz.h b/src/tint/cmd/fuzz/ir/fuzz.h
new file mode 100644
index 0000000..897f6e1
--- /dev/null
+++ b/src/tint/cmd/fuzz/ir/fuzz.h
@@ -0,0 +1,89 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_CMD_FUZZ_IR_FUZZ_H_
+#define SRC_TINT_CMD_FUZZ_IR_FUZZ_H_
+
+#include <functional>
+#include <string>
+#include <tuple>
+#include <utility>
+
+#include "src/tint/utils/bytes/decoder.h"
+#include "src/tint/utils/containers/slice.h"
+#include "src/tint/utils/macros/static_init.h"
+
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::fuzz::ir {
+
+/// IRFuzzer describes a fuzzer function that takes a IR module as input
+struct IRFuzzer {
+    /// @param name the name of the fuzzer
+    /// @param fn the fuzzer function
+    /// @returns an IRFuzzer that invokes the function @p fn with the IR module, along with any
+    /// additional arguments which are deserialized from the fuzzer input.
+    template <typename... ARGS>
+    static IRFuzzer Create(std::string_view name, void (*fn)(core::ir::Module&, ARGS...)) {
+        if constexpr (sizeof...(ARGS) > 0) {
+            auto fn_with_decode = [fn](core::ir::Module& module, Slice<const std::byte> data) {
+                bytes::Reader reader{data};
+                if (auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader)) {
+                    auto all_args =
+                        std::tuple_cat(std::tuple<core::ir::Module&>{module}, data_args.Get());
+                    std::apply(*fn, all_args);
+                }
+            };
+            return IRFuzzer{name, std::move(fn_with_decode)};
+        } else {
+            return IRFuzzer{
+                name,
+                [fn](core::ir::Module& module, Slice<const std::byte>) { fn(module); },
+            };
+        }
+    }
+
+    /// Name of the fuzzer function
+    std::string_view name;
+    /// The fuzzer function
+    std::function<void(core::ir::Module&, Slice<const std::byte> data)> fn;
+};
+
+/// Registers the fuzzer function with the IR fuzzer executable.
+/// @param fuzzer the fuzzer
+void Register(const IRFuzzer& fuzzer);
+
+/// TINT_IR_MODULE_FUZZER registers the fuzzer function.
+#define TINT_IR_MODULE_FUZZER(FUNCTION) \
+    TINT_STATIC_INIT(                   \
+        ::tint::fuzz::ir::Register(::tint::fuzz::ir::IRFuzzer::Create(#FUNCTION, FUNCTION)))
+
+}  // namespace tint::fuzz::ir
+
+#endif  // SRC_TINT_CMD_FUZZ_IR_FUZZ_H_
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.bazel b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
index 4c77253..c3cbe15 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.bazel
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
@@ -38,6 +38,11 @@
 load("@bazel_skylib//lib:selects.bzl", "selects")
 
 alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+alias(
   name = "tint_build_wgsl_reader",
   actual = "//src/tint:tint_build_wgsl_reader_true",
 )
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.cmake b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
index eb85247..4da9274 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.cmake
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
@@ -45,6 +45,7 @@
 )
 
 tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+  tint_cmd_fuzz_ir_fuzz
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -54,6 +55,7 @@
   tint_lang_wgsl_program_fuzz
   tint_lang_wgsl_sem
   tint_lang_wgsl_fuzz
+  tint_utils_bytes
   tint_utils_cli
   tint_utils_containers
   tint_utils_diagnostic
@@ -62,6 +64,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_strconv
@@ -70,6 +73,12 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_lang_spirv_writer_fuzz
+  )
+endif(TINT_BUILD_SPV_WRITER)
+
 if(TINT_BUILD_WGSL_READER)
   tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
     tint_cmd_fuzz_wgsl_fuzz
@@ -93,8 +102,8 @@
 # Condition: TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_cmd_fuzz_wgsl_fuzz fuzz
-  cmd/fuzz/wgsl/wgsl_fuzz.cc
-  cmd/fuzz/wgsl/wgsl_fuzz.h
+  cmd/fuzz/wgsl/fuzz.cc
+  cmd/fuzz/wgsl/fuzz.h
 )
 
 tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz fuzz
@@ -106,8 +115,10 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
+  tint_utils_bytes
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.gn b/src/tint/cmd/fuzz/wgsl/BUILD.gn
index b55e57f..f4bec4d 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.gn
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.gn
@@ -40,8 +40,8 @@
 if (tint_build_wgsl_reader) {
   tint_fuzz_source_set("fuzz") {
     sources = [
-      "wgsl_fuzz.cc",
-      "wgsl_fuzz.h",
+      "fuzz.cc",
+      "fuzz.h",
     ]
     deps = [
       "${tint_src_dir}:thread",
@@ -53,8 +53,10 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/bytes",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
@@ -80,6 +82,7 @@
     output_name = "tint_wgsl_fuzzer"
     sources = [ "main_fuzz.cc" ]
     deps = [
+      "${tint_src_dir}/cmd/fuzz/ir:fuzz",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/type",
@@ -89,6 +92,7 @@
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/program:fuzz",
       "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/bytes",
       "${tint_src_dir}/utils/cli",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -97,6 +101,7 @@
       "${tint_src_dir}/utils/macros",
       "${tint_src_dir}/utils/math",
       "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
       "${tint_src_dir}/utils/result",
       "${tint_src_dir}/utils/rtti",
       "${tint_src_dir}/utils/strconv",
@@ -105,6 +110,10 @@
       "${tint_src_dir}/utils/traits",
     ]
 
+    if (tint_build_spv_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/writer:fuzz" ]
+    }
+
     if (tint_build_wgsl_reader) {
       deps += [
         "${tint_src_dir}/cmd/fuzz/wgsl:fuzz",
diff --git a/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.cc b/src/tint/cmd/fuzz/wgsl/fuzz.cc
similarity index 79%
rename from src/tint/cmd/fuzz/wgsl/wgsl_fuzz.cc
rename to src/tint/cmd/fuzz/wgsl/fuzz.cc
index 69d3587..d6b90ec 100644
--- a/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.cc
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
 
 #include <iostream>
 #include <thread>
@@ -38,7 +38,14 @@
 namespace tint::fuzz::wgsl {
 namespace {
 
-Vector<ProgramFuzzer, 32> fuzzers;
+/// @returns a reference to the static list of registered ProgramFuzzers.
+/// @note this is not a global, as the static initializers that register fuzzers may be called
+/// before this vector is constructed.
+Vector<ProgramFuzzer, 32>& Fuzzers() {
+    static Vector<ProgramFuzzer, 32> fuzzers;
+    return fuzzers;
+}
+
 thread_local std::string_view currently_running;
 
 [[noreturn]] void TintInternalCompilerErrorReporter(const tint::InternalCompilerError& err) {
@@ -50,15 +57,15 @@
 }  // namespace
 
 void Register(const ProgramFuzzer& fuzzer) {
-    fuzzers.Push(fuzzer);
+    Fuzzers().Push(fuzzer);
 }
 
-void Run(std::string_view wgsl, const Options& options) {
+void Run(std::string_view wgsl, Slice<const std::byte> data, const Options& options) {
     tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
 
     // Ensure that fuzzers are sorted. Without this, the fuzzers may be registered in any order,
     // leading to non-determinism, which we must avoid.
-    TINT_STATIC_INIT(fuzzers.Sort([](auto& a, auto& b) { return a.name < b.name; }));
+    TINT_STATIC_INIT(Fuzzers().Sort([](auto& a, auto& b) { return a.name < b.name; }));
 
     // Create a Source::File to hand to the parser.
     tint::Source::File file("test.wgsl", wgsl);
@@ -71,14 +78,14 @@
 
     // Run each of the program fuzzer functions
     if (options.run_concurrently) {
-        size_t n = fuzzers.Length();
+        size_t n = Fuzzers().Length();
         tint::Vector<std::thread, 32> threads;
         threads.Resize(n);
         for (size_t i = 0; i < n; i++) {
-            threads[i] = std::thread([i, &program] {
-                auto& fuzzer = fuzzers[i];
+            threads[i] = std::thread([i, &program, &data] {
+                auto& fuzzer = Fuzzers()[i];
                 currently_running = fuzzer.name;
-                fuzzer.fn(program);
+                fuzzer.fn(program, data);
             });
         }
         for (auto& thread : threads) {
@@ -86,9 +93,9 @@
         }
     } else {
         TINT_DEFER(currently_running = "");
-        for (auto& fuzzer : fuzzers) {
+        for (auto& fuzzer : Fuzzers()) {
             currently_running = fuzzer.name;
-            fuzzer.fn(program);
+            fuzzer.fn(program, data);
         }
     }
 }
diff --git a/src/tint/cmd/fuzz/wgsl/fuzz.h b/src/tint/cmd/fuzz/wgsl/fuzz.h
new file mode 100644
index 0000000..744ceb5
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.h
@@ -0,0 +1,98 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_CMD_FUZZ_WGSL_FUZZ_H_
+#define SRC_TINT_CMD_FUZZ_WGSL_FUZZ_H_
+
+#include <string>
+#include <tuple>
+#include <utility>
+
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/utils/bytes/decoder.h"
+#include "src/tint/utils/containers/slice.h"
+#include "src/tint/utils/macros/static_init.h"
+#include "src/tint/utils/reflection/reflection.h"
+
+namespace tint::fuzz::wgsl {
+
+/// ProgramFuzzer describes a fuzzer function that takes a WGSL program as input
+struct ProgramFuzzer {
+    /// @param name the name of the fuzzer
+    /// @param fn the fuzzer function
+    /// @returns a ProgramFuzzer that invokes the function @p fn with the Program, along with any
+    /// additional arguments which are deserialized from the fuzzer input.
+    template <typename... ARGS>
+    static ProgramFuzzer Create(std::string_view name, void (*fn)(const Program&, ARGS...)) {
+        if constexpr (sizeof...(ARGS) > 0) {
+            auto fn_with_decode = [fn](const Program& program, Slice<const std::byte> data) {
+                bytes::Reader reader{data};
+                if (auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader)) {
+                    auto all_args =
+                        std::tuple_cat(std::tuple<const Program&>{program}, data_args.Get());
+                    std::apply(*fn, all_args);
+                }
+            };
+            return ProgramFuzzer{name, std::move(fn_with_decode)};
+        } else {
+            return ProgramFuzzer{
+                name,
+                [fn](const Program& program, Slice<const std::byte>) { fn(program); },
+            };
+        }
+    }
+
+    /// Name of the fuzzer function
+    std::string_view name;
+    /// The fuzzer function
+    std::function<void(const Program&, Slice<const std::byte> data)> fn;
+};
+
+/// Options for Run()
+struct Options {
+    /// If true, the fuzzers will be run concurrently on separate threads.
+    bool run_concurrently = false;
+};
+
+/// Runs all the registered WGSL fuzzers with the supplied WGSL
+/// @param wgsl the input WGSL
+/// @param data additional data used for fuzzing
+/// @param options the options for running the fuzzers
+void Run(std::string_view wgsl, Slice<const std::byte> data, const Options& options);
+
+/// Registers the fuzzer function with the WGSL fuzzer executable.
+/// @param fuzzer the fuzzer
+void Register(const ProgramFuzzer& fuzzer);
+
+/// TINT_WGSL_PROGRAM_FUZZER registers the fuzzer function to run as part of `tint_wgsl_fuzzer`
+#define TINT_WGSL_PROGRAM_FUZZER(FUNCTION)         \
+    TINT_STATIC_INIT(::tint::fuzz::wgsl::Register( \
+        ::tint::fuzz::wgsl::ProgramFuzzer::Create(#FUNCTION, FUNCTION)))
+
+}  // namespace tint::fuzz::wgsl
+
+#endif  // SRC_TINT_CMD_FUZZ_WGSL_FUZZ_H_
diff --git a/src/tint/cmd/fuzz/wgsl/main_fuzz.cc b/src/tint/cmd/fuzz/wgsl/main_fuzz.cc
index fa27edc..1edcd04 100644
--- a/src/tint/cmd/fuzz/wgsl/main_fuzz.cc
+++ b/src/tint/cmd/fuzz/wgsl/main_fuzz.cc
@@ -27,19 +27,22 @@
 
 #include <iostream>
 
-#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
 #include "src/tint/utils/cli/cli.h"
+#include "src/tint/utils/macros/defer.h"
+#include "src/tint/utils/text/base64.h"
 
 namespace {
 
 tint::fuzz::wgsl::Options options;
 
-}
+}  // namespace
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* input, size_t size) {
     if (size > 0) {
-        std::string_view wgsl(reinterpret_cast<const char*>(data), size);
-        tint::fuzz::wgsl::Run(wgsl, options);
+        std::string_view wgsl(reinterpret_cast<const char*>(input), size);
+        auto data = tint::DecodeBase64FromComments(wgsl);
+        tint::fuzz::wgsl::Run(wgsl, data.Slice(), options);
     }
     return 0;
 }
diff --git a/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h b/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h
deleted file mode 100644
index 909c394..0000000
--- a/src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2023 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#ifndef SRC_TINT_CMD_FUZZ_WGSL_WGSL_FUZZ_H_
-#define SRC_TINT_CMD_FUZZ_WGSL_WGSL_FUZZ_H_
-
-#include <string>
-
-#include "src/tint/lang/wgsl/program/program.h"
-#include "src/tint/utils/containers/slice.h"
-#include "src/tint/utils/macros/static_init.h"
-
-namespace tint::fuzz::wgsl {
-
-/// ProgramFuzzer describes a fuzzer function that takes a WGSL program as input
-struct ProgramFuzzer {
-    /// The function signature
-    using Fn = void(const Program&);
-
-    /// Name of the fuzzer function
-    std::string_view name;
-    /// The fuzzer function pointer
-    Fn* fn = nullptr;
-};
-
-/// Options for Run()
-struct Options {
-    /// If true, the fuzzers will be run concurrently on separate threads.
-    bool run_concurrently = false;
-};
-
-/// Runs all the registered WGSL fuzzers with the supplied WGSL
-/// @param wgsl the input WGSL
-/// @param options the options for running the fuzzers
-void Run(std::string_view wgsl, const Options& options);
-
-/// Registers the fuzzer function with the WGSL fuzzer executable.
-/// @param fuzzer the fuzzer
-void Register(const ProgramFuzzer& fuzzer);
-
-/// TINT_WGSL_PROGRAM_FUZZER registers the fuzzer function to run as part of `tint_wgsl_fuzzer`
-#define TINT_WGSL_PROGRAM_FUZZER(FUNCTION) \
-    TINT_STATIC_INIT(::tint::fuzz::wgsl::Register({#FUNCTION, FUNCTION}))
-
-}  // namespace tint::fuzz::wgsl
-
-#endif  // SRC_TINT_CMD_FUZZ_WGSL_WGSL_FUZZ_H_
diff --git a/src/tint/cmd/info/BUILD.bazel b/src/tint/cmd/info/BUILD.bazel
index a71fc36..c19a5da 100644
--- a/src/tint/cmd/info/BUILD.bazel
+++ b/src/tint/cmd/info/BUILD.bazel
@@ -51,6 +51,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/cmd/info/BUILD.cmake b/src/tint/cmd/info/BUILD.cmake
index 8da6ae4..74d28e0 100644
--- a/src/tint/cmd/info/BUILD.cmake
+++ b/src/tint/cmd/info/BUILD.cmake
@@ -52,6 +52,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
diff --git a/src/tint/cmd/info/BUILD.gn b/src/tint/cmd/info/BUILD.gn
index ac7005b..6f3a945 100644
--- a/src/tint/cmd/info/BUILD.gn
+++ b/src/tint/cmd/info/BUILD.gn
@@ -51,6 +51,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/cmd/loopy/BUILD.bazel b/src/tint/cmd/loopy/BUILD.bazel
index 6054294..7d42293 100644
--- a/src/tint/cmd/loopy/BUILD.bazel
+++ b/src/tint/cmd/loopy/BUILD.bazel
@@ -55,6 +55,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
diff --git a/src/tint/cmd/loopy/BUILD.cmake b/src/tint/cmd/loopy/BUILD.cmake
index af48744..f72d122 100644
--- a/src/tint/cmd/loopy/BUILD.cmake
+++ b/src/tint/cmd/loopy/BUILD.cmake
@@ -56,6 +56,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
diff --git a/src/tint/cmd/loopy/BUILD.gn b/src/tint/cmd/loopy/BUILD.gn
index 3e7fa3d..4879c29 100644
--- a/src/tint/cmd/loopy/BUILD.gn
+++ b/src/tint/cmd/loopy/BUILD.gn
@@ -55,6 +55,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/helpers",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
diff --git a/src/tint/cmd/loopy/main.cc b/src/tint/cmd/loopy/main.cc
index 6cc2317..6fcd4ac 100644
--- a/src/tint/cmd/loopy/main.cc
+++ b/src/tint/cmd/loopy/main.cc
@@ -50,7 +50,7 @@
 #endif  // TINT_BUILD_SPV_READER
 
 #if TINT_BUILD_SPV_WRITER
-#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
+#include "src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
diff --git a/src/tint/cmd/remote_compile/main.cc b/src/tint/cmd/remote_compile/main.cc
index 1cade2d..60a0464 100644
--- a/src/tint/cmd/remote_compile/main.cc
+++ b/src/tint/cmd/remote_compile/main.cc
@@ -83,14 +83,14 @@
 constexpr uint32_t kProtocolVersion = 1;
 
 /// Supported shader source languages
-enum SourceLanguage {
+enum SourceLanguage : uint8_t {
     MSL,
 };
 
 /// Stream is a serialization wrapper around a socket
 struct Stream {
     /// The underlying socket
-    Socket* const socket;
+    tint::socket::Socket* const socket;
     /// Error state
     std::string error;
 
@@ -187,7 +187,7 @@
 /// Base class for all messages
 struct Message {
     /// The type of the message
-    enum class Type {
+    enum class Type : uint8_t {
         ConnectionRequest,
         ConnectionResponse,
         CompileRequest,
@@ -384,8 +384,7 @@
                 file = args[1];
                 break;
             default:
-                std::cerr << "expected 1 or 2 arguments, got " << args.size() << std::endl
-                          << std::endl;
+                std::cerr << "expected 1 or 2 arguments, got " << args.size() << "\n\n";
                 ShowUsage();
         }
         if (address.empty() || file.empty()) {
@@ -402,12 +401,12 @@
 }
 
 bool RunServer(std::string port) {
-    auto server_socket = Socket::Listen("", port.c_str());
+    auto server_socket = tint::socket::Socket::Listen("", port.c_str());
     if (!server_socket) {
-        std::cout << "Failed to listen on port " << port << std::endl;
+        std::cout << "Failed to listen on port " << port << "\n";
         return false;
     }
-    std::cout << "Listening on port " << port.c_str() << "..." << std::endl;
+    std::cout << "Listening on port " << port.c_str() << "...\n";
     while (auto conn = server_socket->Accept()) {
         std::thread([=] {
             DEBUG("Client connected...");
@@ -472,16 +471,16 @@
     // Read the file
     std::ifstream input(file, std::ios::binary);
     if (!input) {
-        std::cerr << "Couldn't open '" << file << "'" << std::endl;
+        std::cerr << "Couldn't open '" << file << "'\n";
         return false;
     }
     std::string source((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
 
     constexpr const int timeout_ms = 10000;
     DEBUG("Connecting to %s:%s...", address.c_str(), port.c_str());
-    auto conn = Socket::Connect(address.c_str(), port.c_str(), timeout_ms);
+    auto conn = tint::socket::Socket::Connect(address.c_str(), port.c_str(), timeout_ms);
     if (!conn) {
-        std::cerr << "Connection failed" << std::endl;
+        std::cerr << "Connection failed\n";
         return false;
     }
 
@@ -490,22 +489,22 @@
     DEBUG("Sending connection request...");
     auto conn_resp = Send(stream, ConnectionRequest{kProtocolVersion});
     if (!stream.error.empty()) {
-        std::cerr << stream.error << std::endl;
+        std::cerr << stream.error << "\n";
         return false;
     }
     if (!conn_resp.error.empty()) {
-        std::cerr << conn_resp.error << std::endl;
+        std::cerr << conn_resp.error << "\n";
         return false;
     }
     DEBUG("Connection established. Requesting compile...");
     auto comp_resp =
         Send(stream, CompileRequest{SourceLanguage::MSL, version_major, version_minor, source});
     if (!stream.error.empty()) {
-        std::cerr << stream.error << std::endl;
+        std::cerr << stream.error << "\n";
         return false;
     }
     if (!comp_resp.error.empty()) {
-        std::cerr << comp_resp.error << std::endl;
+        std::cerr << comp_resp.error << "\n";
         return false;
     }
     DEBUG("Compilation successful");
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index c883c12..6f1e558 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -52,6 +52,7 @@
     "//src/tint/lang/core:test",
     "//src/tint/lang/spirv/ir:test",
     "//src/tint/lang/wgsl/ast:test",
+    "//src/tint/lang/wgsl/features:test",
     "//src/tint/lang/wgsl/helpers:test",
     "//src/tint/lang/wgsl/program:test",
     "//src/tint/lang/wgsl/reader/lower:test",
@@ -60,6 +61,7 @@
     "//src/tint/lang/wgsl/writer/ir_to_program:test",
     "//src/tint/lang/wgsl/writer/raise:test",
     "//src/tint/lang/wgsl:test",
+    "//src/tint/utils/bytes:test",
     "//src/tint/utils/cli:test",
     "//src/tint/utils/command:test",
     "//src/tint/utils/containers:test",
@@ -123,6 +125,11 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "//src/tint/lang/spirv/validate:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_spv_writer": [
       "//src/tint/lang/spirv/writer/ast_printer:test",
       "//src/tint/lang/spirv/writer/common:test",
@@ -193,6 +200,14 @@
 )
 
 selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
+selects.config_setting_group(
     name = "tint_build_glsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
     match_all = [
         ":tint_build_glsl_writer",
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index f994d98..670c21f 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -53,6 +53,7 @@
   tint_lang_core_test
   tint_lang_spirv_ir_test
   tint_lang_wgsl_ast_test
+  tint_lang_wgsl_features_test
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program_test
   tint_lang_wgsl_reader_lower_test
@@ -61,6 +62,7 @@
   tint_lang_wgsl_writer_ir_to_program_test
   tint_lang_wgsl_writer_raise_test
   tint_lang_wgsl_test
+  tint_utils_bytes_test
   tint_utils_cli_test
   tint_utils_command_test
   tint_utils_containers_test
@@ -136,6 +138,12 @@
   )
 endif(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER)
 
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_spirv_validate_test
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_spirv_writer_ast_printer_test
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index 774b68a..17410ae 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -59,6 +59,7 @@
       "${tint_src_dir}/lang/spirv/ir:unittests",
       "${tint_src_dir}/lang/wgsl:unittests",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
+      "${tint_src_dir}/lang/wgsl/features:unittests",
       "${tint_src_dir}/lang/wgsl/helpers:unittests",
       "${tint_src_dir}/lang/wgsl/program:unittests",
       "${tint_src_dir}/lang/wgsl/reader/lower:unittests",
@@ -66,6 +67,7 @@
       "${tint_src_dir}/lang/wgsl/sem:unittests",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program:unittests",
       "${tint_src_dir}/lang/wgsl/writer/raise:unittests",
+      "${tint_src_dir}/utils/bytes:unittests",
       "${tint_src_dir}/utils/cli:unittests",
       "${tint_src_dir}/utils/command:unittests",
       "${tint_src_dir}/utils/containers:unittests",
@@ -129,6 +131,10 @@
       deps += [ "${tint_src_dir}/lang/spirv/reader/ast_parser:unittests" ]
     }
 
+    if (tint_build_spv_reader || tint_build_spv_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/validate:unittests" ]
+    }
+
     if (tint_build_spv_writer) {
       deps += [
         "${tint_src_dir}/lang/spirv/writer:unittests",
diff --git a/src/tint/cmd/tint/BUILD.bazel b/src/tint/cmd/tint/BUILD.bazel
index 1abb879..b6fb729 100644
--- a/src/tint/cmd/tint/BUILD.bazel
+++ b/src/tint/cmd/tint/BUILD.bazel
@@ -56,6 +56,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
@@ -102,11 +103,6 @@
     ],
     "//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",
     ],
diff --git a/src/tint/cmd/tint/BUILD.cmake b/src/tint/cmd/tint/BUILD.cmake
index bd4e4ed..9f35d05 100644
--- a/src/tint/cmd/tint/BUILD.cmake
+++ b/src/tint/cmd/tint/BUILD.cmake
@@ -57,6 +57,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
@@ -108,12 +109,6 @@
   )
 endif(TINT_BUILD_MSL_WRITER)
 
-if(TINT_BUILD_SPV_READER)
-  tint_target_add_dependencies(tint_cmd_tint_cmd cmd
-    tint_lang_spirv_reader
-  )
-endif(TINT_BUILD_SPV_READER)
-
 if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
   tint_target_add_external_dependencies(tint_cmd_tint_cmd cmd
     "spirv-tools"
diff --git a/src/tint/cmd/tint/BUILD.gn b/src/tint/cmd/tint/BUILD.gn
index 4bb12ce..c5b628c 100644
--- a/src/tint/cmd/tint/BUILD.gn
+++ b/src/tint/cmd/tint/BUILD.gn
@@ -56,6 +56,7 @@
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/ast/transform",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/helpers",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
@@ -105,10 +106,6 @@
     ]
   }
 
-  if (tint_build_spv_reader) {
-    deps += [ "${tint_src_dir}/lang/spirv/reader" ]
-  }
-
   if (tint_build_spv_reader || tint_build_spv_writer) {
     deps += [
       "${tint_spirv_tools_dir}:spvtools_headers",
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 7ae374d..5ba2c6e 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -27,12 +27,9 @@
 
 #include <charconv>
 #include <cstdio>
-#include <fstream>
 #include <iostream>
-#include <limits>
 #include <memory>
 #include <optional>
-#include <sstream>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -63,17 +60,13 @@
 #include "src/tint/utils/text/string.h"
 #include "src/tint/utils/text/string_stream.h"
 
-#if TINT_BUILD_SPV_READER
-#include "src/tint/lang/spirv/reader/reader.h"
-#endif  // TINT_BUILD_SPV_READER
-
 #if TINT_BUILD_WGSL_READER
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
 #endif  // TINT_BUILD_WGSL_READER
 
 #if TINT_BUILD_SPV_WRITER
-#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
+#include "src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
@@ -134,10 +127,10 @@
 
 /// Prints the given hash value in a format string that the end-to-end test runner can parse.
 [[maybe_unused]] void PrintHash(uint32_t hash) {
-    std::cout << "<<HASH: 0x" << std::hex << hash << ">>" << std::endl;
+    std::cout << "<<HASH: 0x" << std::hex << hash << ">>\n";
 }
 
-enum class Format {
+enum class Format : uint8_t {
     kUnknown,
     kNone,
     kSpirv,
@@ -375,6 +368,22 @@
 attachment.
 )");
 
+    auto& pixel_local_attachment_formats =
+        options.Add<StringOption>("pixel_local_attachment_formats",
+                                  R"(Pixel local storage attachment formats, comma-separated
+Each binding is of the form MEMBER_INDEX=ATTACHMENT_FORMAT,
+where MEMBER_INDEX is the pixel-local structure member
+index and ATTACHMENT_FORMAT is the format of the emitted
+attachment, which can only be one of the below value:
+R32Sint, R32Uint, R32Float.
+)");
+
+    auto& pixel_local_group_index = options.Add<ValueOption<uint32_t>>(
+        "pixel_local_group_index",
+        R"(The bind group index of the pixel local attachments (default 0).
+)",
+        Default{0});
+
     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)");
@@ -408,7 +417,7 @@
 
     auto result = options.Parse(arguments);
     if (!result) {
-        std::cerr << result.Failure() << std::endl;
+        std::cerr << result.Failure() << "\n";
         show_usage();
         return false;
     }
@@ -424,7 +433,7 @@
                 std::cerr << "override values must be of the form IDENTIFIER=VALUE";
                 return false;
             }
-            auto value = tint::ParseNumber<double>(parts[1]);
+            auto value = tint::strconv::ParseNumber<double>(parts[1]);
             if (!value) {
                 std::cerr << "invalid override value: " << parts[1];
                 return false;
@@ -437,19 +446,19 @@
         auto binding_points = tint::Split(*hlsl_rc_bp.value, ",");
         if (binding_points.Length() != 2) {
             std::cerr << "Invalid binding point for " << hlsl_rc_bp.name << ": "
-                      << *hlsl_rc_bp.value << std::endl;
+                      << *hlsl_rc_bp.value << "\n";
             return false;
         }
-        auto group = tint::ParseUint32(binding_points[0]);
+        auto group = tint::strconv::ParseUint32(binding_points[0]);
         if (!group) {
             std::cerr << "Invalid group for " << hlsl_rc_bp.name << ": " << binding_points[0]
-                      << std::endl;
+                      << "\n";
             return false;
         }
-        auto binding = tint::ParseUint32(binding_points[1]);
+        auto binding = tint::strconv::ParseUint32(binding_points[1]);
         if (!binding) {
             std::cerr << "Invalid binding for " << hlsl_rc_bp.name << ": " << binding_points[1]
-                      << std::endl;
+                      << "\n";
             return false;
         }
         opts->hlsl_root_constant_binding_point = tint::BindingPoint{group.Get(), binding.Get()};
@@ -461,19 +470,19 @@
             auto values = tint::Split(binding, "=");
             if (values.Length() != 2) {
                 std::cerr << "Invalid binding " << pixel_local_attachments.name << ": " << binding
-                          << std::endl;
+                          << "\n";
                 return false;
             }
-            auto member_index = tint::ParseUint32(values[0]);
+            auto member_index = tint::strconv::ParseUint32(values[0]);
             if (!member_index) {
                 std::cerr << "Invalid member index for " << pixel_local_attachments.name << ": "
-                          << values[0] << std::endl;
+                          << values[0] << "\n";
                 return false;
             }
-            auto attachment_index = tint::ParseUint32(values[1]);
+            auto attachment_index = tint::strconv::ParseUint32(values[1]);
             if (!attachment_index) {
                 std::cerr << "Invalid attachment index for " << pixel_local_attachments.name << ": "
-                          << values[1] << std::endl;
+                          << values[1] << "\n";
                 return false;
             }
             opts->pixel_local_options.attachments.emplace(member_index.Get(),
@@ -481,10 +490,47 @@
         }
     }
 
+    if (pixel_local_group_index.value.has_value()) {
+        opts->pixel_local_options.pixel_local_group_index = *pixel_local_group_index.value;
+    }
+
+    if (pixel_local_attachment_formats.value.has_value()) {
+        auto binding_formats = tint::Split(*pixel_local_attachment_formats.value, ",");
+        for (auto& binding_format : binding_formats) {
+            auto values = tint::Split(binding_format, "=");
+            if (values.Length() != 2) {
+                std::cerr << "Invalid binding format " << pixel_local_attachment_formats.name
+                          << ": " << binding_format << std::endl;
+                return false;
+            }
+            auto member_index = tint::strconv::ParseUint32(values[0]);
+            if (!member_index) {
+                std::cerr << "Invalid member index for " << pixel_local_attachment_formats.name
+                          << ": " << values[0] << std::endl;
+                return false;
+            }
+            auto format = values[1];
+            tint::PixelLocalOptions::TexelFormat texel_format =
+                tint::PixelLocalOptions::TexelFormat::kUndefined;
+            if (format == "R32Sint") {
+                texel_format = tint::PixelLocalOptions::TexelFormat::kR32Sint;
+            } else if (format == "R32Uint") {
+                texel_format = tint::PixelLocalOptions::TexelFormat::kR32Uint;
+            } else if (format == "R32Float") {
+                texel_format = tint::PixelLocalOptions::TexelFormat::kR32Float;
+            } else {
+                std::cerr << "Invalid texel format for " << pixel_local_attachments.name << ": "
+                          << format << std::endl;
+                return false;
+            }
+            opts->pixel_local_options.attachment_formats.emplace(member_index.Get(), texel_format);
+        }
+    }
+
     auto files = result.Get();
     if (files.Length() > 1) {
         std::cerr << "More than one input file specified: "
-                  << tint::Join(Transform(files, tint::Quote), ", ") << std::endl;
+                  << tint::Join(Transform(files, tint::Quote), ", ") << "\n";
         return false;
     }
     if (files.Length() == 1) {
@@ -514,7 +560,7 @@
         file = fopen(output_file.c_str(), mode.c_str());
 #endif
         if (!file) {
-            std::cerr << "Could not open file " << output_file << " for writing" << std::endl;
+            std::cerr << "Could not open file " << output_file << " for writing\n";
             return false;
         }
     }
@@ -523,9 +569,9 @@
         fwrite(buffer.data(), sizeof(typename ContainerT::value_type), buffer.size(), file);
     if (buffer.size() != written) {
         if (use_stdout) {
-            std::cerr << "Could not write all output to standard output" << std::endl;
+            std::cerr << "Could not write all output to standard output\n";
         } else {
-            std::cerr << "Could not write to file " << output_file << std::endl;
+            std::cerr << "Could not write to file " << output_file << "\n";
             fclose(file);
         }
         return false;
@@ -571,7 +617,7 @@
     if (!tools.Disassemble(
             data, &result,
             SPV_BINARY_TO_TEXT_OPTION_INDENT | SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) {
-        std::cerr << spv_errors << std::endl;
+        std::cerr << spv_errors << "\n";
     }
     return result;
 }
@@ -588,12 +634,23 @@
     gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.bindings = tint::spirv::writer::GenerateBindings(program);
-    gen_options.use_tint_ir = options.use_ir;
 
-    auto result = tint::spirv::writer::Generate(program, gen_options);
+    tint::Result<tint::spirv::writer::Output> result;
+    if (options.use_ir) {
+        // Convert the AST program to an IR module.
+        auto ir = tint::wgsl::reader::ProgramToLoweredIR(program);
+        if (!ir) {
+            std::cerr << "Failed to generate IR: " << ir << "\n";
+            return false;
+        }
+        result = tint::spirv::writer::Generate(ir.Get(), gen_options);
+    } else {
+        result = tint::spirv::writer::Generate(program, gen_options);
+    }
+
     if (!result) {
         tint::cmd::PrintWGSL(std::cerr, program);
-        std::cerr << "Failed to generate: " << result.Failure() << std::endl;
+        std::cerr << "Failed to generate: " << result.Failure() << "\n";
         return false;
     }
 
@@ -617,7 +674,7 @@
         spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
         tools.SetMessageConsumer(
             [](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) {
-                std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl;
+                std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << "\n";
             });
         if (!tools.Validate(result.Get().spirv.data(), result.Get().spirv.size(),
                             spvtools::ValidatorOptions())) {
@@ -645,7 +702,7 @@
     tint::wgsl::writer::Options gen_options;
     auto result = tint::wgsl::writer::Generate(program, gen_options);
     if (!result) {
-        std::cerr << "Failed to generate: " << result.Failure() << std::endl;
+        std::cerr << "Failed to generate: " << result.Failure() << "\n";
         return false;
     }
 
@@ -701,7 +758,6 @@
 
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::msl::writer::Options gen_options;
-    gen_options.use_tint_ir = options.use_ir;
     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;
@@ -711,10 +767,23 @@
                                                                           0);
     gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(tint::BindingPoint{0, 1},
                                                                           1);
-    auto result = tint::msl::writer::Generate(*input_program, gen_options);
+
+    tint::Result<tint::msl::writer::Output> result;
+    if (options.use_ir) {
+        // Convert the AST program to an IR module.
+        auto ir = tint::wgsl::reader::ProgramToLoweredIR(program);
+        if (!ir) {
+            std::cerr << "Failed to generate IR: " << ir << "\n";
+            return false;
+        }
+        result = tint::msl::writer::Generate(ir.Get(), gen_options);
+    } else {
+        result = tint::msl::writer::Generate(*input_program, gen_options);
+    }
+
     if (!result) {
         tint::cmd::PrintWGSL(std::cerr, program);
-        std::cerr << "Failed to generate: " << result.Failure() << std::endl;
+        std::cerr << "Failed to generate: " << result.Failure() << "\n";
         return false;
     }
 
@@ -760,7 +829,7 @@
         }
 #endif  // __APPLE__
         if (res.failed) {
-            std::cerr << res.output << std::endl;
+            std::cerr << res.output << "\n";
             return false;
         }
     }
@@ -782,6 +851,7 @@
     gen_options.external_texture_options.bindings_map =
         tint::cmd::GenerateExternalTextureBindings(program);
     gen_options.root_constant_binding_point = options.hlsl_root_constant_binding_point;
+    gen_options.pixel_local_options = options.pixel_local_options;
     auto result = tint::hlsl::writer::Generate(program, gen_options);
     if (!result) {
         tint::cmd::PrintWGSL(std::cerr, program);
@@ -887,7 +957,7 @@
 #else
     (void)program;
     (void)options;
-    std::cerr << "HLSL writer not enabled in tint build" << std::endl;
+    std::cerr << "HLSL writer not enabled in tint build\n";
     return false;
 #endif  // TINT_BUILD_HLSL_WRITER
 }
@@ -914,7 +984,7 @@
         auto result = tint::glsl::writer::Generate(prg, gen_options, entry_point_name);
         if (!result) {
             tint::cmd::PrintWGSL(std::cerr, prg);
-            std::cerr << "Failed to generate: " << result.Failure() << std::endl;
+            std::cerr << "Failed to generate: " << result.Failure() << "\n";
             return false;
         }
 
@@ -1019,17 +1089,18 @@
                  const auto& name = override.key;
                  const auto& value = override.value;
                  if (name.empty()) {
-                     std::cerr << "empty override name" << std::endl;
+                     std::cerr << "empty override name\n";
                      return false;
                  }
-                 if (auto num = tint::ParseNumber<decltype(tint::OverrideId::value)>(name)) {
+                 if (auto num =
+                         tint::strconv::ParseNumber<decltype(tint::OverrideId::value)>(name)) {
                      tint::OverrideId id{num.Get()};
                      values.emplace(id, value);
                  } else {
                      auto override_names = inspector.GetNamedOverrideIds();
                      auto it = override_names.find(name);
                      if (it == override_names.end()) {
-                         std::cerr << "unknown override '" << name << "'" << std::endl;
+                         std::cerr << "unknown override '" << name << "'\n";
                          return false;
                      }
                      values.emplace(it->second, value);
@@ -1046,7 +1117,7 @@
     auto transform_names = [&] {
         tint::StringStream names;
         for (auto& t : transforms) {
-            names << "   " << t.name << std::endl;
+            names << "   " << t.name << "\n";
         }
         return names.str();
     };
@@ -1083,22 +1154,22 @@
         gen_options.use_syntax_tree_writer = true;
         auto result = tint::wgsl::writer::Generate(info.program, gen_options);
         if (!result) {
-            std::cerr << "Failed to dump AST: " << result.Failure() << std::endl;
+            std::cerr << "Failed to dump AST: " << result.Failure() << "\n";
         } else {
-            std::cout << result->wgsl << std::endl;
+            std::cout << result->wgsl << "\n";
         }
     }
 #endif  // TINT_BUILD_SYNTAX_TREE_WRITER
 
 #if TINT_BUILD_WGSL_READER
     if (options.dump_ir) {
-        auto result = tint::wgsl::reader::ProgramToIR(info.program);
+        auto result = tint::wgsl::reader::ProgramToLoweredIR(info.program);
         if (!result) {
-            std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
+            std::cerr << "Failed to build IR from program: " << result.Failure() << "\n";
         } else {
             auto mod = result.Move();
             if (options.dump_ir) {
-                std::cout << tint::core::ir::Disassemble(mod) << std::endl;
+                std::cout << tint::core::ir::Disassemble(mod) << "\n";
             }
         }
     }
@@ -1159,8 +1230,8 @@
             }
         }
 
-        std::cerr << "Unknown transform: " << name << std::endl;
-        std::cerr << "Available transforms: " << std::endl << transform_names() << std::endl;
+        std::cerr << "Unknown transform: " << name << "\n";
+        std::cerr << "Available transforms: \n" << transform_names() << "\n";
         return false;
     };
 
@@ -1189,7 +1260,7 @@
     auto program = transform_manager.Run(info.program, std::move(transform_inputs), outputs);
     if (!program.IsValid()) {
         tint::cmd::PrintWGSL(std::cerr, program);
-        std::cerr << program.Diagnostics() << std::endl;
+        std::cerr << program.Diagnostics() << "\n";
         return 1;
     }
 
@@ -1214,7 +1285,7 @@
         case Format::kNone:
             break;
         default:
-            std::cerr << "Unknown output format specified" << std::endl;
+            std::cerr << "Unknown output format specified\n";
             return 1;
     }
     if (!success) {
diff --git a/src/tint/externals.json b/src/tint/externals.json
index a7631a5..dd5d1df 100644
--- a/src/tint/externals.json
+++ b/src/tint/externals.json
@@ -17,7 +17,7 @@
         "IncludePatterns": [
             "Metal/Metal.h"
         ],
-        "Condition": "is_mac",
+        "Condition": "tint_build_is_mac",
     },
     "thread": {
         "IncludePatterns": [
@@ -64,6 +64,6 @@
         "IncludePatterns": [
             "winsock2.h"
         ],
-        "Condition": "is_win",
+        "Condition": "tint_build_is_win",
     },
-}
\ No newline at end of file
+}
diff --git a/src/tint/flags.bzl b/src/tint/flags.bzl
index 08b5d60..b607cf8 100644
--- a/src/tint/flags.bzl
+++ b/src/tint/flags.bzl
@@ -35,7 +35,7 @@
 
 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.
+    'tint_build_is_<os>_true' and 'tint_build_is_<os>_false' targets.
 
     The OS flag can be specified on the command line with '--//src/tint:os=<os>'
     """
@@ -55,13 +55,13 @@
 
     for os in OSes:
         native.config_setting(
-            name = "is_{}_true".format(os),
+            name = "tint_build_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],
+            name = "tint_build_is_{}_false".format(os),
+            match_any = [ "tint_build_is_{}_true".format(other) for other in OSes if other != os],
             visibility = ["//visibility:public"],
         )
 
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index de7e8f6..be08086 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -55,7 +55,7 @@
 #include "src/tint/utils/math/hash.h"
 
 #if TINT_BUILD_SPV_WRITER
-#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
+#include "src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h"
 #endif  // TINT_BUILD_SPV_WRITER
 
 #if TINT_BUILD_MSL_WRITER
@@ -364,12 +364,6 @@
         }
         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.
             if (auto flattened = tint::wgsl::FlattenBindings(program)) {
diff --git a/src/tint/lang/core/BUILD.bazel b/src/tint/lang/core/BUILD.bazel
index ee003f9..a26c37f 100644
--- a/src/tint/lang/core/BUILD.bazel
+++ b/src/tint/lang/core/BUILD.bazel
@@ -107,6 +107,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/core/BUILD.cmake b/src/tint/lang/core/BUILD.cmake
index 6ce30bf..a5c1610 100644
--- a/src/tint/lang/core/BUILD.cmake
+++ b/src/tint/lang/core/BUILD.cmake
@@ -111,6 +111,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/core/BUILD.gn b/src/tint/lang/core/BUILD.gn
index 011fb58..88de4fd 100644
--- a/src/tint/lang/core/BUILD.gn
+++ b/src/tint/lang/core/BUILD.gn
@@ -108,6 +108,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/core/access_test.cc b/src/tint/lang/core/access_test.cc
index 5b7116b..69c05b2 100644
--- a/src/tint/lang/core/access_test.cc
+++ b/src/tint/lang/core/access_test.cc
@@ -86,7 +86,7 @@
 TEST_P(AccessPrintTest, Print) {
     Access value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, AccessPrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/address_space_test.cc b/src/tint/lang/core/address_space_test.cc
index 38ab91c..0f63446 100644
--- a/src/tint/lang/core/address_space_test.cc
+++ b/src/tint/lang/core/address_space_test.cc
@@ -101,7 +101,7 @@
 TEST_P(AddressSpacePrintTest, Print) {
     AddressSpace value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, AddressSpacePrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/attribute_test.cc b/src/tint/lang/core/attribute_test.cc
index ac6b338..10d8e72 100644
--- a/src/tint/lang/core/attribute_test.cc
+++ b/src/tint/lang/core/attribute_test.cc
@@ -146,7 +146,7 @@
 TEST_P(AttributePrintTest, Print) {
     Attribute value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, AttributePrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/builtin_type_test.cc b/src/tint/lang/core/builtin_type_test.cc
index 32284c7..8fd8d6a 100644
--- a/src/tint/lang/core/builtin_type_test.cc
+++ b/src/tint/lang/core/builtin_type_test.cc
@@ -462,7 +462,7 @@
 TEST_P(BuiltinTypePrintTest, Print) {
     BuiltinType value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, BuiltinTypePrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/builtin_value_test.cc b/src/tint/lang/core/builtin_value_test.cc
index 721aacf..220aab8 100644
--- a/src/tint/lang/core/builtin_value_test.cc
+++ b/src/tint/lang/core/builtin_value_test.cc
@@ -138,7 +138,7 @@
 TEST_P(BuiltinValuePrintTest, Print) {
     BuiltinValue value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, BuiltinValuePrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/constant/BUILD.bazel b/src/tint/lang/core/constant/BUILD.bazel
index dfdb0d1..b7967f5 100644
--- a/src/tint/lang/core/constant/BUILD.bazel
+++ b/src/tint/lang/core/constant/BUILD.bazel
@@ -108,6 +108,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
diff --git a/src/tint/lang/core/constant/BUILD.cmake b/src/tint/lang/core/constant/BUILD.cmake
index 0a8076d..3e86cd2 100644
--- a/src/tint/lang/core/constant/BUILD.cmake
+++ b/src/tint/lang/core/constant/BUILD.cmake
@@ -107,6 +107,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
diff --git a/src/tint/lang/core/constant/BUILD.gn b/src/tint/lang/core/constant/BUILD.gn
index 64d4cce..39c11d9 100644
--- a/src/tint/lang/core/constant/BUILD.gn
+++ b/src/tint/lang/core/constant/BUILD.gn
@@ -109,6 +109,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
diff --git a/src/tint/lang/core/interpolation_sampling_test.cc b/src/tint/lang/core/interpolation_sampling_test.cc
index be7a47e..6d658b2 100644
--- a/src/tint/lang/core/interpolation_sampling_test.cc
+++ b/src/tint/lang/core/interpolation_sampling_test.cc
@@ -95,7 +95,7 @@
 TEST_P(InterpolationSamplingPrintTest, Print) {
     InterpolationSampling value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases,
diff --git a/src/tint/lang/core/interpolation_type_test.cc b/src/tint/lang/core/interpolation_type_test.cc
index 8d3ac8b..5d93e72 100644
--- a/src/tint/lang/core/interpolation_type_test.cc
+++ b/src/tint/lang/core/interpolation_type_test.cc
@@ -89,7 +89,7 @@
 TEST_P(InterpolationTypePrintTest, Print) {
     InterpolationType value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, InterpolationTypePrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/intrinsic/BUILD.bazel b/src/tint/lang/core/intrinsic/BUILD.bazel
index b1ba629..ea936ea 100644
--- a/src/tint/lang/core/intrinsic/BUILD.bazel
+++ b/src/tint/lang/core/intrinsic/BUILD.bazel
@@ -86,6 +86,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
diff --git a/src/tint/lang/core/intrinsic/BUILD.cmake b/src/tint/lang/core/intrinsic/BUILD.cmake
index d10b9fe..4a6f199 100644
--- a/src/tint/lang/core/intrinsic/BUILD.cmake
+++ b/src/tint/lang/core/intrinsic/BUILD.cmake
@@ -85,6 +85,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
diff --git a/src/tint/lang/core/intrinsic/BUILD.gn b/src/tint/lang/core/intrinsic/BUILD.gn
index 8294126..011a65a 100644
--- a/src/tint/lang/core/intrinsic/BUILD.gn
+++ b/src/tint/lang/core/intrinsic/BUILD.gn
@@ -85,6 +85,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
diff --git a/src/tint/lang/core/ir/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel
index a07a52e..3c62fe9 100644
--- a/src/tint/lang/core/ir/BUILD.bazel
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -113,6 +113,7 @@
     "exit_switch.h",
     "function.h",
     "function_param.h",
+    "ice.h",
     "if.h",
     "instruction.h",
     "instruction_result.h",
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index 028790d..19c6a8a 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -89,6 +89,7 @@
   lang/core/ir/function.h
   lang/core/ir/function_param.cc
   lang/core/ir/function_param.h
+  lang/core/ir/ice.h
   lang/core/ir/if.cc
   lang/core/ir/if.h
   lang/core/ir/instruction.cc
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index 0e76537..9bcb139 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -92,6 +92,7 @@
     "function.h",
     "function_param.cc",
     "function_param.h",
+    "ice.h",
     "if.cc",
     "if.h",
     "instruction.cc",
diff --git a/src/tint/lang/core/ir/access.cc b/src/tint/lang/core/ir/access.cc
index adb651c..c469a5d 100644
--- a/src/tint/lang/core/ir/access.cc
+++ b/src/tint/lang/core/ir/access.cc
@@ -46,7 +46,7 @@
 Access::~Access() = default;
 
 Access* Access::Clone(CloneContext& ctx) {
-    auto new_result = ctx.Clone(Result());
+    auto new_result = ctx.Clone(Result(0));
     auto obj = ctx.Remap(Object());
     auto indices = ctx.Remap<Access::kDefaultNumOperands>(Indices());
     return ctx.ir.instructions.Create<Access>(new_result, obj, indices);
diff --git a/src/tint/lang/core/ir/access.h b/src/tint/lang/core/ir/access.h
index 274fa6b..0bf2caa 100644
--- a/src/tint/lang/core/ir/access.h
+++ b/src/tint/lang/core/ir/access.h
@@ -57,15 +57,23 @@
     /// @returns the object used for the access
     Value* Object() { return operands_[kObjectOperandOffset]; }
 
+    /// @returns the object used for the access
+    const Value* Object() const { return operands_[kObjectOperandOffset]; }
+
     /// Adds the given index to the end of the access chain
     /// @param idx the index to add
     void AddIndex(Value* idx) { AddOperand(operands_.Length(), idx); }
 
     /// @returns the accessor indices
-    tint::Slice<Value*> Indices() { return operands_.Slice().Offset(kIndicesOperandOffset); }
+    tint::Slice<Value* const> Indices() { return operands_.Slice().Offset(kIndicesOperandOffset); }
+
+    /// @returns the accessor indices
+    tint::Slice<const Value* const> Indices() const {
+        return operands_.Slice().Offset(kIndicesOperandOffset);
+    }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "access"; }
+    std::string FriendlyName() const override { return "access"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/access_test.cc b/src/tint/lang/core/ir/access_test.cc
index be295e8..f3be39c 100644
--- a/src/tint/lang/core/ir/access_test.cc
+++ b/src/tint/lang/core/ir/access_test.cc
@@ -45,7 +45,7 @@
     auto* idx = b.Constant(u32(1));
     auto* a = b.Access(ty.i32(), var, idx);
 
-    EXPECT_THAT(var->Result()->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
+    EXPECT_THAT(var->Result(0)->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
     EXPECT_THAT(idx->Usages(), testing::UnorderedElementsAre(Usage{a, 1u}));
 }
 
@@ -55,11 +55,10 @@
     auto* idx = b.Constant(u32(1));
     auto* a = b.Access(ty.i32(), var, idx);
 
-    EXPECT_TRUE(a->HasResults());
-    EXPECT_FALSE(a->HasMultiResults());
+    EXPECT_EQ(a->Results().Length(), 1u);
 
-    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
-    EXPECT_EQ(a, a->Result()->Source());
+    EXPECT_TRUE(a->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(a, a->Result(0)->Instruction());
 }
 
 TEST_F(IR_AccessTest, Fail_NullType) {
@@ -85,8 +84,8 @@
 
     EXPECT_NE(a, new_a);
 
-    EXPECT_NE(a->Result(), new_a->Result());
-    EXPECT_EQ(type, new_a->Result()->Type());
+    EXPECT_NE(a->Result(0), new_a->Result(0));
+    EXPECT_EQ(type, new_a->Result(0)->Type());
 
     EXPECT_NE(nullptr, new_a->Object());
     EXPECT_EQ(a->Object(), new_a->Object());
diff --git a/src/tint/lang/core/ir/binary.cc b/src/tint/lang/core/ir/binary.cc
index c5a8e7c..41c9972 100644
--- a/src/tint/lang/core/ir/binary.cc
+++ b/src/tint/lang/core/ir/binary.cc
@@ -43,7 +43,7 @@
 Binary::~Binary() = default;
 
 Binary* Binary::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* lhs = ctx.Remap(LHS());
     auto* rhs = ctx.Remap(RHS());
     return ctx.ir.instructions.Create<Binary>(new_result, op_, lhs, rhs);
diff --git a/src/tint/lang/core/ir/binary.h b/src/tint/lang/core/ir/binary.h
index 117dfbf..6490be9 100644
--- a/src/tint/lang/core/ir/binary.h
+++ b/src/tint/lang/core/ir/binary.h
@@ -79,16 +79,22 @@
     Binary* Clone(CloneContext& ctx) override;
 
     /// @returns the binary operator
-    BinaryOp Op() { return op_; }
+    BinaryOp Op() const { return op_; }
 
     /// @returns the left-hand-side value for the instruction
     Value* LHS() { return operands_[kLhsOperandOffset]; }
 
+    /// @returns the left-hand-side value for the instruction
+    const Value* LHS() const { return operands_[kLhsOperandOffset]; }
+
     /// @returns the right-hand-side value for the instruction
     Value* RHS() { return operands_[kRhsOperandOffset]; }
 
+    /// @returns the right-hand-side value for the instruction
+    const Value* RHS() const { return operands_[kRhsOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "binary"; }
+    std::string FriendlyName() const override { return "binary"; }
 
   private:
     BinaryOp op_;
diff --git a/src/tint/lang/core/ir/binary_test.cc b/src/tint/lang/core/ir/binary_test.cc
index 0eae85d..d468a61 100644
--- a/src/tint/lang/core/ir/binary_test.cc
+++ b/src/tint/lang/core/ir/binary_test.cc
@@ -53,10 +53,9 @@
 TEST_F(IR_BinaryTest, Result) {
     auto* a = b.Add(mod.Types().i32(), 4_i, 2_i);
 
-    EXPECT_TRUE(a->HasResults());
-    EXPECT_FALSE(a->HasMultiResults());
-    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
-    EXPECT_EQ(a, a->Result()->Source());
+    EXPECT_EQ(a->Results().Length(), 1u);
+    EXPECT_TRUE(a->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(a, a->Result(0)->Instruction());
 }
 
 TEST_F(IR_BinaryTest, CreateAnd) {
@@ -396,7 +395,7 @@
 
     EXPECT_NE(inst, c);
 
-    EXPECT_EQ(mod.Types().i32(), c->Result()->Type());
+    EXPECT_EQ(mod.Types().i32(), c->Result(0)->Type());
     EXPECT_EQ(BinaryOp::kAnd, c->Op());
 
     auto new_lhs = c->LHS()->As<Constant>()->Value();
diff --git a/src/tint/lang/core/ir/bitcast.cc b/src/tint/lang/core/ir/bitcast.cc
index 62a40d7..26031a47 100644
--- a/src/tint/lang/core/ir/bitcast.cc
+++ b/src/tint/lang/core/ir/bitcast.cc
@@ -42,7 +42,7 @@
 Bitcast::~Bitcast() = default;
 
 Bitcast* Bitcast::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* val = ctx.Remap(Val());
     return ctx.ir.instructions.Create<Bitcast>(new_result, val);
 }
diff --git a/src/tint/lang/core/ir/bitcast.h b/src/tint/lang/core/ir/bitcast.h
index 11998fe..3526681 100644
--- a/src/tint/lang/core/ir/bitcast.h
+++ b/src/tint/lang/core/ir/bitcast.h
@@ -53,8 +53,11 @@
     /// @returns the operand value
     Value* Val() { return operands_[kValueOperandOffset]; }
 
+    /// @returns the operand value
+    const Value* Val() const { return operands_[kValueOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "bitcast"; }
+    std::string FriendlyName() const override { return "bitcast"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/bitcast_test.cc b/src/tint/lang/core/ir/bitcast_test.cc
index 3075df1..833f460 100644
--- a/src/tint/lang/core/ir/bitcast_test.cc
+++ b/src/tint/lang/core/ir/bitcast_test.cc
@@ -45,7 +45,7 @@
     auto* inst = b.Bitcast(mod.Types().i32(), 4_i);
 
     ASSERT_TRUE(inst->Is<ir::Bitcast>());
-    ASSERT_NE(inst->Result()->Type(), nullptr);
+    ASSERT_NE(inst->Result(0)->Type(), nullptr);
 
     auto args = inst->Args();
     ASSERT_EQ(args.Length(), 1u);
@@ -58,10 +58,9 @@
 TEST_F(IR_BitcastTest, Result) {
     auto* a = b.Bitcast(mod.Types().i32(), 4_i);
 
-    EXPECT_TRUE(a->HasResults());
-    EXPECT_FALSE(a->HasMultiResults());
-    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
-    EXPECT_EQ(a, a->Result()->Source());
+    EXPECT_EQ(a->Results().Length(), 1u);
+    EXPECT_TRUE(a->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(a, a->Result(0)->Instruction());
 }
 
 TEST_F(IR_BitcastTest, Bitcast_Usage) {
@@ -90,7 +89,7 @@
 
     EXPECT_NE(inst, n);
 
-    EXPECT_EQ(mod.Types().i32(), n->Result()->Type());
+    EXPECT_EQ(mod.Types().i32(), n->Result(0)->Type());
 
     auto new_val = n->Val()->As<Constant>()->Value();
     ASSERT_TRUE(new_val->Is<core::constant::Scalar<i32>>());
diff --git a/src/tint/lang/core/ir/block.h b/src/tint/lang/core/ir/block.h
index 6273b29..02cb037 100644
--- a/src/tint/lang/core/ir/block.h
+++ b/src/tint/lang/core/ir/block.h
@@ -32,6 +32,7 @@
 
 #include "src/tint/lang/core/ir/instruction.h"
 #include "src/tint/lang/core/ir/terminator.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/containers/vector.h"
 
 // Forward declarations
@@ -58,33 +59,34 @@
     /// @param out the block to clone into
     virtual void CloneInto(CloneContext& ctx, Block* out);
 
-    /// @returns true if this is block has a terminator instruction
-    bool HasTerminator() {
-        return instructions_.last != nullptr && instructions_.last->Is<ir::Terminator>();
-    }
+    /// @return the terminator instruction for this block, or nullptr if this block does not end in
+    /// a terminator.
+    ir::Terminator* Terminator() { return tint::As<ir::Terminator>(instructions_.last.Get()); }
 
-    /// @return the terminator instruction for this block
-    ir::Terminator* Terminator() {
-        if (!HasTerminator()) {
-            return nullptr;
-        }
-        return instructions_.last->As<ir::Terminator>();
+    /// @return the terminator instruction for this block, or nullptr if this block does not end in
+    /// a terminator.
+    const ir::Terminator* Terminator() const {
+        return tint::As<ir::Terminator>(instructions_.last.Get());
     }
 
     /// @returns the instructions in the block
-    Instruction* Instructions() { return instructions_.first; }
+    Instruction* Instructions() { return instructions_.first.Get(); }
+
+    /// @returns the instructions in the block
+    const Instruction* Instructions() const { return instructions_.first.Get(); }
 
     /// Iterator for the instructions inside a block
+    template <typename T>
     class Iterator {
       public:
         /// Constructor
         /// @param inst the instruction to start iterating from
-        explicit Iterator(Instruction* inst) : inst_(inst) {}
+        explicit Iterator(T* inst) : inst_(inst) {}
         ~Iterator() = default;
 
         /// Dereference operator
         /// @returns the instruction for this iterator
-        Instruction* operator*() const { return inst_; }
+        T* operator*() const { return inst_; }
 
         /// Comparison operator
         /// @param itr to compare against
@@ -104,21 +106,35 @@
         }
 
       private:
-        Instruction* inst_ = nullptr;
+        T* inst_ = nullptr;
     };
 
     /// @returns the iterator pointing to the start of the instruction list
-    Iterator begin() { return Iterator{instructions_.first}; }
+    Iterator<Instruction> begin() { return Iterator<Instruction>{instructions_.first}; }
 
     /// @returns the ending iterator
-    Iterator end() { return Iterator{nullptr}; }
+    Iterator<Instruction> end() { return Iterator<Instruction>{nullptr}; }
+
+    /// @returns the iterator pointing to the start of the instruction list
+    Iterator<const Instruction> begin() const {
+        return Iterator<const Instruction>{instructions_.first};
+    }
+
+    /// @returns the ending iterator
+    Iterator<const Instruction> end() const { return Iterator<const Instruction>{nullptr}; }
 
     /// @returns the first instruction in the instruction list
     Instruction* Front() { return instructions_.first; }
 
+    /// @returns the first instruction in the instruction list
+    const Instruction* Front() const { return instructions_.first; }
+
     /// @returns the last instruction in the instruction list
     Instruction* Back() { return instructions_.last; }
 
+    /// @returns the last instruction in the instruction list
+    const Instruction* Back() const { return instructions_.last; }
+
     /// Adds the instruction to the beginning of the block
     /// @param inst the instruction to add
     /// @returns the instruction to allow calls to be chained
@@ -144,14 +160,17 @@
     void Remove(Instruction* inst);
 
     /// @returns true if the block contains no instructions
-    bool IsEmpty() { return Length() == 0; }
+    bool IsEmpty() const { return Length() == 0; }
 
     /// @returns the number of instructions in the block
-    size_t Length() { return instructions_.count; }
+    size_t Length() const { return instructions_.count; }
 
     /// @return the parent instruction that owns this block
     ControlInstruction* Parent() { return parent_; }
 
+    /// @return the parent instruction that owns this block
+    const ControlInstruction* Parent() const { return parent_; }
+
     /// @param parent the parent instruction that owns this block
     void SetParent(ControlInstruction* parent) { parent_ = parent; }
 
@@ -160,12 +179,12 @@
 
   private:
     struct {
-        Instruction* first = nullptr;
-        Instruction* last = nullptr;
+        ConstPropagatingPtr<Instruction> first;
+        ConstPropagatingPtr<Instruction> last;
         size_t count = 0;
     } instructions_;
 
-    ControlInstruction* parent_ = nullptr;
+    ConstPropagatingPtr<ControlInstruction> parent_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/block_param.h b/src/tint/lang/core/ir/block_param.h
index 3ab0c26..009e022 100644
--- a/src/tint/lang/core/ir/block_param.h
+++ b/src/tint/lang/core/ir/block_param.h
@@ -42,7 +42,7 @@
     ~BlockParam() override;
 
     /// @returns the type of the var
-    const core::type::Type* Type() override { return type_; }
+    const core::type::Type* Type() const override { return type_; }
 
     /// @copydoc Instruction::Clone()
     BlockParam* Clone(CloneContext& ctx) override;
diff --git a/src/tint/lang/core/ir/block_test.cc b/src/tint/lang/core/ir/block_test.cc
index d48b7c7..8e2f3b6 100644
--- a/src/tint/lang/core/ir/block_test.cc
+++ b/src/tint/lang/core/ir/block_test.cc
@@ -35,65 +35,65 @@
 using namespace tint::core::number_suffixes;  // NOLINT
 using IR_BlockTest = IRTestHelper;
 
-TEST_F(IR_BlockTest, HasTerminator_Empty) {
+TEST_F(IR_BlockTest, Terminator_Empty) {
     auto* blk = b.Block();
-    EXPECT_FALSE(blk->HasTerminator());
+    EXPECT_EQ(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_None) {
+TEST_F(IR_BlockTest, Terminator_None) {
     auto* blk = b.Block();
     blk->Append(b.Add(mod.Types().i32(), 1_u, 2_u));
-    EXPECT_FALSE(blk->HasTerminator());
+    EXPECT_EQ(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_BreakIf) {
+TEST_F(IR_BlockTest, Terminator_BreakIf) {
     auto* blk = b.Block();
     auto* loop = b.Loop();
     blk->Append(b.BreakIf(loop, true));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_Continue) {
+TEST_F(IR_BlockTest, Terminator_Continue) {
     auto* blk = b.Block();
     auto* loop = b.Loop();
     blk->Append(b.Continue(loop));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_ExitIf) {
+TEST_F(IR_BlockTest, Terminator_ExitIf) {
     auto* blk = b.Block();
     auto* if_ = b.If(true);
     blk->Append(b.ExitIf(if_));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_ExitLoop) {
+TEST_F(IR_BlockTest, Terminator_ExitLoop) {
     auto* blk = b.Block();
     auto* loop = b.Loop();
     blk->Append(b.ExitLoop(loop));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_ExitSwitch) {
+TEST_F(IR_BlockTest, Terminator_ExitSwitch) {
     auto* blk = b.Block();
     auto* s = b.Switch(1_u);
     blk->Append(b.ExitSwitch(s));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_NextIteration) {
+TEST_F(IR_BlockTest, Terminator_NextIteration) {
     auto* blk = b.Block();
     auto* loop = b.Loop();
     blk->Append(b.NextIteration(loop));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
-TEST_F(IR_BlockTest, HasTerminator_Return) {
+TEST_F(IR_BlockTest, Terminator_Return) {
     auto* f = b.Function("myFunc", mod.Types().void_());
 
     auto* blk = b.Block();
     blk->Append(b.Return(f));
-    EXPECT_TRUE(blk->HasTerminator());
+    EXPECT_NE(blk->Terminator(), nullptr);
 }
 
 TEST_F(IR_BlockTest, Append) {
diff --git a/src/tint/lang/core/ir/break_if.h b/src/tint/lang/core/ir/break_if.h
index b2b1b5e..bc8dbdf 100644
--- a/src/tint/lang/core/ir/break_if.h
+++ b/src/tint/lang/core/ir/break_if.h
@@ -32,6 +32,7 @@
 
 #include "src/tint/lang/core/ir/terminator.h"
 #include "src/tint/lang/core/ir/value.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/rtti/castable.h"
 
 // Forward declarations
@@ -60,22 +61,26 @@
     /// @copydoc Instruction::Clone()
     BreakIf* Clone(CloneContext& ctx) override;
 
-    /// @returns the MultiInBlock arguments
-    tint::Slice<Value* const> Args() override {
-        return operands_.Slice().Offset(kArgsOperandOffset);
-    }
+    /// @returns the offset of the arguments in Operands()
+    size_t ArgsOperandOffset() const override { return kArgsOperandOffset; }
 
     /// @returns the break condition
     Value* Condition() { return operands_[kConditionOperandOffset]; }
 
+    /// @returns the break condition
+    const Value* Condition() const { return operands_[kConditionOperandOffset]; }
+
     /// @returns the loop containing the break-if
     ir::Loop* Loop() { return loop_; }
 
+    /// @returns the loop containing the break-if
+    const ir::Loop* Loop() const { return loop_; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "break_if"; }
+    std::string FriendlyName() const override { return "break_if"; }
 
   private:
-    ir::Loop* loop_ = nullptr;
+    ConstPropagatingPtr<ir::Loop> loop_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/break_if_test.cc b/src/tint/lang/core/ir/break_if_test.cc
index df13922..9b36ad1 100644
--- a/src/tint/lang/core/ir/break_if_test.cc
+++ b/src/tint/lang/core/ir/break_if_test.cc
@@ -57,8 +57,7 @@
     auto* arg2 = b.Constant(2_u);
 
     auto* brk = b.BreakIf(loop, cond, arg1, arg2);
-    EXPECT_FALSE(brk->HasResults());
-    EXPECT_FALSE(brk->HasMultiResults());
+    EXPECT_TRUE(brk->Results().IsEmpty());
 }
 
 TEST_F(IR_BreakIfTest, Fail_NullLoop) {
diff --git a/src/tint/lang/core/ir/builder.cc b/src/tint/lang/core/ir/builder.cc
index f6f94ef..9315f8a 100644
--- a/src/tint/lang/core/ir/builder.cc
+++ b/src/tint/lang/core/ir/builder.cc
@@ -66,15 +66,25 @@
     return Append(ir.instructions.Create<ir::Loop>(Block(), MultiInBlock(), MultiInBlock()));
 }
 
-Block* Builder::Case(ir::Switch* s, VectorRef<Switch::CaseSelector> selectors) {
+Block* Builder::Case(ir::Switch* s, VectorRef<ir::Constant*> values) {
     auto* block = Block();
-    s->Cases().Push(Switch::Case{std::move(selectors), block});
+
+    Switch::Case c;
+    c.block = block;
+    for (auto* value : values) {
+        c.selectors.Push(Switch::CaseSelector{value});
+    }
+    s->Cases().Push(std::move(c));
     block->SetParent(s);
     return block;
 }
 
-Block* Builder::Case(ir::Switch* s, std::initializer_list<Switch::CaseSelector> selectors) {
-    return Case(s, Vector<Switch::CaseSelector, 4>(selectors));
+Block* Builder::DefaultCase(ir::Switch* s) {
+    return Case(s, Vector<ir::Constant*, 1>{nullptr});
+}
+
+Block* Builder::Case(ir::Switch* s, std::initializer_list<ir::Constant*> selectors) {
+    return Case(s, Vector<ir::Constant*, 4>(selectors));
 }
 
 ir::Discard* Builder::Discard() {
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 6020902..2a89fa9 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -268,17 +268,22 @@
         return Append(ir.instructions.Create<ir::Switch>(cond_val));
     }
 
-    /// Creates a case for the switch @p s with the given selectors
+    /// Creates a default case for the switch @p s
     /// @param s the switch to create the case into
-    /// @param selectors the case selectors for the case statement
     /// @returns the start block for the case instruction
-    ir::Block* Case(ir::Switch* s, VectorRef<Switch::CaseSelector> selectors);
+    ir::Block* DefaultCase(ir::Switch* s);
 
     /// Creates a case for the switch @p s with the given selectors
     /// @param s the switch to create the case into
-    /// @param selectors the case selectors for the case statement
+    /// @param values the case selector values for the case statement
     /// @returns the start block for the case instruction
-    ir::Block* Case(ir::Switch* s, std::initializer_list<Switch::CaseSelector> selectors);
+    ir::Block* Case(ir::Switch* s, VectorRef<ir::Constant*> values);
+
+    /// Creates a case for the switch @p s with the given selectors
+    /// @param s the switch to create the case into
+    /// @param values the case selector values for the case statement
+    /// @returns the start block for the case instruction
+    ir::Block* Case(ir::Switch* s, std::initializer_list<ir::Constant*> values);
 
     /// Creates a new ir::Constant
     /// @param val the constant value
@@ -396,8 +401,9 @@
                 return in;  /// Pass-through
             } else if constexpr (is_instruction) {
                 /// Extract the first result from the instruction
-                TINT_ASSERT(in->HasResults() && !in->HasMultiResults());
-                return in->Result();
+                auto results = in->Results();
+                TINT_ASSERT(results.Length() == 1);
+                return results[0];
             }
         } else if constexpr (is_numeric) {
             /// Creates a value from the given number
@@ -454,6 +460,17 @@
         return Binary(BinaryOp::kAnd, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an And operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* And(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return And(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Or operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -464,6 +481,17 @@
         return Binary(BinaryOp::kOr, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Or operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Or(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Or(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Xor operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -474,6 +502,17 @@
         return Binary(BinaryOp::kXor, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Xor operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Xor(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Xor(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Equal operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -484,6 +523,17 @@
         return Binary(BinaryOp::kEqual, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Equal operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Equal(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Equal(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an NotEqual operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -494,6 +544,17 @@
         return Binary(BinaryOp::kNotEqual, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an NotEqual operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* NotEqual(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return NotEqual(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an LessThan operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -504,6 +565,17 @@
         return Binary(BinaryOp::kLessThan, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an LessThan operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* LessThan(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return LessThan(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an GreaterThan operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -514,6 +586,17 @@
         return Binary(BinaryOp::kGreaterThan, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an GreaterThan operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* GreaterThan(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return GreaterThan(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an LessThanEqual operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -525,6 +608,17 @@
                       std::forward<RHS>(rhs));
     }
 
+    /// Creates an LessThanEqual operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* LessThanEqual(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return LessThanEqual(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an GreaterThanEqual operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -536,6 +630,17 @@
                       std::forward<RHS>(rhs));
     }
 
+    /// Creates an GreaterThanEqual operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* GreaterThanEqual(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return GreaterThanEqual(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an ShiftLeft operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -546,6 +651,17 @@
         return Binary(BinaryOp::kShiftLeft, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an ShiftLeft operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* ShiftLeft(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return ShiftLeft(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an ShiftRight operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -556,6 +672,17 @@
         return Binary(BinaryOp::kShiftRight, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an ShiftRight operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* ShiftRight(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return ShiftRight(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Add operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -566,6 +693,17 @@
         return Binary(BinaryOp::kAdd, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Add operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Add(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Add(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Subtract operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -576,6 +714,17 @@
         return Binary(BinaryOp::kSubtract, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Subtract operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Subtract(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Subtract(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Multiply operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -586,6 +735,17 @@
         return Binary(BinaryOp::kMultiply, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Multiply operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Multiply(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Multiply(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Divide operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -596,6 +756,17 @@
         return Binary(BinaryOp::kDivide, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Divide operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Divide(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Divide(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an Modulo operation
     /// @param type the result type of the expression
     /// @param lhs the lhs of the add
@@ -606,6 +777,17 @@
         return Binary(BinaryOp::kModulo, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
+    /// Creates an Modulo operation
+    /// @tparam TYPE the result type of the expression
+    /// @param lhs the lhs of the add
+    /// @param rhs the rhs of the add
+    /// @returns the operation
+    template <typename TYPE, typename LHS, typename RHS>
+    ir::Binary* Modulo(LHS&& lhs, RHS&& rhs) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Modulo(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
+    }
+
     /// Creates an op for `op val`
     /// @param op the unary operator
     /// @param type the result type of the binary expression
@@ -617,6 +799,17 @@
         return Append(ir.instructions.Create<ir::Unary>(InstructionResult(type), op, value));
     }
 
+    /// Creates an op for `op val`
+    /// @param op the unary operator
+    /// @tparam TYPE the result type of the binary expression
+    /// @param val the value of the operation
+    /// @returns the operation
+    template <typename TYPE, typename VAL>
+    ir::Unary* Unary(UnaryOp op, VAL&& val) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Unary(op, type, std::forward<VAL>(val));
+    }
+
     /// Creates a Complement operation
     /// @param type the result type of the expression
     /// @param val the value
@@ -626,6 +819,16 @@
         return Unary(ir::UnaryOp::kComplement, type, std::forward<VAL>(val));
     }
 
+    /// Creates a Complement operation
+    /// @tparam TYPE the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    template <typename TYPE, typename VAL>
+    ir::Unary* Complement(VAL&& val) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Complement(type, std::forward<VAL>(val));
+    }
+
     /// Creates a Negation operation
     /// @param type the result type of the expression
     /// @param val the value
@@ -635,6 +838,16 @@
         return Unary(ir::UnaryOp::kNegation, type, std::forward<VAL>(val));
     }
 
+    /// Creates a Negation operation
+    /// @tparam TYPE the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    template <typename TYPE, typename VAL>
+    ir::Unary* Negation(VAL&& val) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Negation(type, std::forward<VAL>(val));
+    }
+
     /// Creates a Not operation
     /// @param type the result type of the expression
     /// @param val the value
@@ -648,6 +861,16 @@
         }
     }
 
+    /// Creates a Not operation
+    /// @tparam TYPE the result type of the expression
+    /// @param val the value
+    /// @returns the operation
+    template <typename TYPE, typename VAL>
+    ir::Binary* Not(VAL&& val) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Not(type, std::forward<VAL>(val));
+    }
+
     /// Creates a bitcast instruction
     /// @param type the result type of the bitcast
     /// @param val the value being bitcast
@@ -658,6 +881,17 @@
         return Append(ir.instructions.Create<ir::Bitcast>(InstructionResult(type), value));
     }
 
+    /// Creates a bitcast instruction
+    /// @tparam TYPE the result type of the bitcast
+    /// @param val the value being bitcast
+    /// @returns the instruction
+    template <typename TYPE, typename VAL>
+    ir::Bitcast* Bitcast(VAL&& val) {
+        auto* type = ir.Types().Get<TYPE>();
+        auto* value = Value(std::forward<VAL>(val));
+        return Bitcast(type, value);
+    }
+
     /// Creates a discard instruction
     /// @returns the instruction
     ir::Discard* Discard();
@@ -682,6 +916,18 @@
                                                            Values(std::forward<ARGS>(args)...)));
     }
 
+    /// Creates a user function call instruction
+    /// @tparam TYPE the return type of the call
+    /// @param func the function to call
+    /// @param args the call arguments
+    /// @returns the instruction
+    template <typename TYPE, typename... ARGS>
+    ir::UserCall* Call(ir::Function* func, ARGS&&... args) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Append(ir.instructions.Create<ir::UserCall>(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
@@ -694,6 +940,18 @@
     }
 
     /// Creates a core builtin call instruction
+    /// @tparam TYPE the return type of the call
+    /// @param func the builtin function to call
+    /// @param args the call arguments
+    /// @returns the instruction
+    template <typename TYPE, typename... ARGS>
+    ir::CoreBuiltinCall* Call(core::BuiltinFn func, ARGS&&... args) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Append(ir.instructions.Create<ir::CoreBuiltinCall>(
+            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
@@ -820,7 +1078,7 @@
         }
         auto* var = Var(name, ir.Types().ptr(SPACE, val->Type(), ACCESS));
         var->SetInitializer(val);
-        ir.SetName(var->Result(), name);
+        ir.SetName(var->Result(0), name);
         return var;
     }
 
@@ -857,7 +1115,16 @@
             return nullptr;
         }
         auto* let = Append(ir.instructions.Create<ir::Let>(InstructionResult(val->Type()), val));
-        ir.SetName(let->Result(), name);
+        ir.SetName(let->Result(0), name);
+        return let;
+    }
+
+    /// Creates a new `let` declaration, with an unassigned value
+    /// @param type the let type
+    /// @returns the instruction
+    ir::Let* Let(const type::Type* type) {
+        auto* let = ir.instructions.Create<ir::Let>(InstructionResult(type), nullptr);
+        Append(let);
         return let;
     }
 
@@ -980,6 +1247,16 @@
     /// @returns the value
     ir::FunctionParam* FunctionParam(std::string_view name, const core::type::Type* type);
 
+    /// Creates a new `FunctionParam` with a name.
+    /// @tparam TYPE the parameter type
+    /// @param name the parameter name
+    /// @returns the value
+    template <typename TYPE>
+    ir::FunctionParam* FunctionParam(std::string_view name) {
+        auto* type = ir.Types().Get<TYPE>();
+        return FunctionParam(name, type);
+    }
+
     /// Creates a new `Access`
     /// @param type the return type
     /// @param object the object being accessed
@@ -1019,6 +1296,16 @@
                                                           Vector<uint32_t, 4>(indices)));
     }
 
+    /// Name names the value or instruction with @p name
+    /// @param name the new name for the value or instruction
+    /// @param object the value or instruction
+    /// @return @p object
+    template <typename OBJECT>
+    OBJECT* Name(std::string_view name, OBJECT* object) {
+        ir.SetName(object, name);
+        return object;
+    }
+
     /// Creates a terminate invocation instruction
     /// @returns the instruction
     ir::TerminateInvocation* TerminateInvocation();
diff --git a/src/tint/lang/core/ir/builtin_call.h b/src/tint/lang/core/ir/builtin_call.h
index 73ed85a..0351ff4 100644
--- a/src/tint/lang/core/ir/builtin_call.h
+++ b/src/tint/lang/core/ir/builtin_call.h
@@ -47,10 +47,10 @@
     ~BuiltinCall() override;
 
     /// @returns the identifier for the function
-    virtual size_t FuncId() = 0;
+    virtual size_t FuncId() const = 0;
 
     /// @returns the table data to validate this builtin
-    virtual const core::intrinsic::TableData& TableData() = 0;
+    virtual const core::intrinsic::TableData& TableData() const = 0;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/call.h b/src/tint/lang/core/ir/call.h
index 6df2c85..b56a092 100644
--- a/src/tint/lang/core/ir/call.h
+++ b/src/tint/lang/core/ir/call.h
@@ -38,8 +38,16 @@
   public:
     ~Call() override;
 
+    /// @returns the offset of the arguments in Operands()
+    virtual size_t ArgsOperandOffset() const { return 0; }
+
     /// @returns the call arguments
-    virtual tint::Slice<Value*> Args() { return operands_.Slice(); }
+    tint::Slice<Value* const> Args() { return operands_.Slice().Offset(ArgsOperandOffset()); }
+
+    /// @returns the call arguments
+    tint::Slice<const Value* const> Args() const {
+        return operands_.Slice().Offset(ArgsOperandOffset());
+    }
 
     /// Append a new argument to the argument list for this call instruction.
     /// @param arg the argument value to append
diff --git a/src/tint/lang/core/ir/clone_context.h b/src/tint/lang/core/ir/clone_context.h
index 9c09f98..221fb83 100644
--- a/src/tint/lang/core/ir/clone_context.h
+++ b/src/tint/lang/core/ir/clone_context.h
@@ -28,6 +28,7 @@
 #ifndef SRC_TINT_LANG_CORE_IR_CLONE_CONTEXT_H_
 #define SRC_TINT_LANG_CORE_IR_CLONE_CONTEXT_H_
 
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/containers/transform.h"
 #include "src/tint/utils/traits/traits.h"
@@ -63,6 +64,14 @@
         return result;
     }
 
+    /// Performs a clone of @p what.
+    /// @param what the item to clone
+    /// @return the cloned item
+    template <typename T>
+    T* Clone(ConstPropagatingPtr<T>& what) {
+        return Clone(what.Get());
+    }
+
     /// Performs a clone of all the elements in @p what.
     /// @param what the elements to clone
     /// @return the cloned elements
@@ -98,6 +107,14 @@
         return what;
     }
 
+    /// Obtains the (potentially) remapped pointer to @p what
+    /// @param what the item
+    /// @return the cloned item for @p what, or the original pointer if @p what has not been cloned.
+    template <typename T>
+    T* Remap(ConstPropagatingPtr<T>& what) {
+        return Remap(what.Get());
+    }
+
     /// Obtains the (potentially) remapped pointer of all the elements in @p what.
     /// @param what the item
     /// @return the remapped elements
diff --git a/src/tint/lang/core/ir/constant.h b/src/tint/lang/core/ir/constant.h
index 3b29da9..c5fb574 100644
--- a/src/tint/lang/core/ir/constant.h
+++ b/src/tint/lang/core/ir/constant.h
@@ -42,10 +42,10 @@
     ~Constant() override;
 
     /// @returns the constants value
-    const core::constant::Value* Value() { return value_; }
+    const core::constant::Value* Value() const { return value_; }
 
     /// @returns the type of the constant
-    const core::type::Type* Type() override { return value_->Type(); }
+    const core::type::Type* Type() const override { return value_->Type(); }
 
     /// @copydoc Value::Clone()
     Constant* Clone(CloneContext& ctx) override;
diff --git a/src/tint/lang/core/ir/construct.cc b/src/tint/lang/core/ir/construct.cc
index d3a81b9..fd67aee 100644
--- a/src/tint/lang/core/ir/construct.cc
+++ b/src/tint/lang/core/ir/construct.cc
@@ -44,7 +44,7 @@
 Construct::~Construct() = default;
 
 Construct* Construct::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto args = ctx.Remap<Construct::kDefaultNumOperands>(Args());
     return ctx.ir.instructions.Create<Construct>(new_result, args);
 }
diff --git a/src/tint/lang/core/ir/construct.h b/src/tint/lang/core/ir/construct.h
index 0bbcbe3..ff9ca22 100644
--- a/src/tint/lang/core/ir/construct.h
+++ b/src/tint/lang/core/ir/construct.h
@@ -51,7 +51,7 @@
     Construct* Clone(CloneContext& ctx) override;
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "construct"; }
+    std::string FriendlyName() const override { return "construct"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/construct_test.cc b/src/tint/lang/core/ir/construct_test.cc
index 51cf309..04c68dc 100644
--- a/src/tint/lang/core/ir/construct_test.cc
+++ b/src/tint/lang/core/ir/construct_test.cc
@@ -51,10 +51,9 @@
     auto* arg2 = b.Constant(false);
     auto* c = b.Construct(mod.Types().f32(), arg1, arg2);
 
-    EXPECT_TRUE(c->HasResults());
-    EXPECT_FALSE(c->HasMultiResults());
-    EXPECT_TRUE(c->Result()->Is<InstructionResult>());
-    EXPECT_EQ(c, c->Result()->Source());
+    EXPECT_EQ(c->Results().Length(), 1u);
+    EXPECT_TRUE(c->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(c, c->Result(0)->Instruction());
 }
 
 TEST_F(IR_ConstructTest, Fail_NullType) {
@@ -75,8 +74,8 @@
     auto* new_c = clone_ctx.Clone(c);
 
     EXPECT_NE(c, new_c);
-    EXPECT_NE(c->Result(), new_c->Result());
-    EXPECT_EQ(mod.Types().f32(), new_c->Result()->Type());
+    EXPECT_NE(c->Result(0), new_c->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_c->Result(0)->Type());
 
     auto args = new_c->Args();
     EXPECT_EQ(2u, args.Length());
@@ -92,8 +91,8 @@
     auto* c = b.Construct(mod.Types().f32());
 
     auto* new_c = clone_ctx.Clone(c);
-    EXPECT_NE(c->Result(), new_c->Result());
-    EXPECT_EQ(mod.Types().f32(), new_c->Result()->Type());
+    EXPECT_NE(c->Result(0), new_c->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_c->Result(0)->Type());
     EXPECT_TRUE(new_c->Args().IsEmpty());
 }
 
diff --git a/src/tint/lang/core/ir/continue.h b/src/tint/lang/core/ir/continue.h
index ff6e999..6233282 100644
--- a/src/tint/lang/core/ir/continue.h
+++ b/src/tint/lang/core/ir/continue.h
@@ -31,6 +31,7 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/terminator.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/rtti/castable.h"
 
 // Forward declarations
@@ -58,11 +59,14 @@
     /// @returns the loop owning the continue block
     ir::Loop* Loop() { return loop_; }
 
+    /// @returns the loop owning the continue block
+    const ir::Loop* Loop() const { return loop_; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "continue"; }
+    std::string FriendlyName() const override { return "continue"; }
 
   private:
-    ir::Loop* loop_ = nullptr;
+    ConstPropagatingPtr<ir::Loop> loop_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/continue_test.cc b/src/tint/lang/core/ir/continue_test.cc
index a25d232..5346bde 100644
--- a/src/tint/lang/core/ir/continue_test.cc
+++ b/src/tint/lang/core/ir/continue_test.cc
@@ -55,8 +55,7 @@
 
     auto* brk = b.Continue(loop, arg1, arg2);
 
-    EXPECT_FALSE(brk->HasResults());
-    EXPECT_FALSE(brk->HasMultiResults());
+    EXPECT_TRUE(brk->Results().IsEmpty());
 }
 
 TEST_F(IR_ContinueTest, Fail_NullLoop) {
diff --git a/src/tint/lang/core/ir/control_instruction.h b/src/tint/lang/core/ir/control_instruction.h
index 0bd994b..cb172db 100644
--- a/src/tint/lang/core/ir/control_instruction.h
+++ b/src/tint/lang/core/ir/control_instruction.h
@@ -59,13 +59,13 @@
     void SetResults(VectorRef<InstructionResult*> values) {
         for (auto* value : results_) {
             if (value) {
-                value->SetSource(nullptr);
+                value->SetInstruction(nullptr);
             }
         }
         results_ = std::move(values);
         for (auto* value : results_) {
             if (value) {
-                value->SetSource(this);
+                value->SetInstruction(this);
             }
         }
     }
diff --git a/src/tint/lang/core/ir/convert.cc b/src/tint/lang/core/ir/convert.cc
index 92639f6..4a479de 100644
--- a/src/tint/lang/core/ir/convert.cc
+++ b/src/tint/lang/core/ir/convert.cc
@@ -44,7 +44,7 @@
 Convert::~Convert() = default;
 
 Convert* Convert::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* val = ctx.Remap(Args()[0]);
     return ctx.ir.instructions.Create<Convert>(new_result, val);
 }
diff --git a/src/tint/lang/core/ir/convert.h b/src/tint/lang/core/ir/convert.h
index 7e8d47c..20350aa 100644
--- a/src/tint/lang/core/ir/convert.h
+++ b/src/tint/lang/core/ir/convert.h
@@ -52,7 +52,7 @@
     Convert* Clone(CloneContext& ctx) override;
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "convert"; }
+    std::string FriendlyName() const override { return "convert"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/convert_test.cc b/src/tint/lang/core/ir/convert_test.cc
index fad55b5..ad5f53d 100644
--- a/src/tint/lang/core/ir/convert_test.cc
+++ b/src/tint/lang/core/ir/convert_test.cc
@@ -48,10 +48,9 @@
 TEST_F(IR_ConvertTest, Results) {
     auto* c = b.Convert(mod.Types().i32(), 1_u);
 
-    EXPECT_TRUE(c->HasResults());
-    EXPECT_FALSE(c->HasMultiResults());
-    EXPECT_TRUE(c->Result()->Is<InstructionResult>());
-    EXPECT_EQ(c->Result()->Source(), c);
+    EXPECT_EQ(c->Results().Length(), 1u);
+    EXPECT_TRUE(c->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(c->Result(0)->Instruction(), c);
 }
 
 TEST_F(IR_ConvertTest, Clone) {
@@ -60,8 +59,8 @@
     auto* new_c = clone_ctx.Clone(c);
 
     EXPECT_NE(c, new_c);
-    EXPECT_NE(c->Result(), new_c->Result());
-    EXPECT_EQ(mod.Types().f32(), new_c->Result()->Type());
+    EXPECT_NE(c->Result(0), new_c->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_c->Result(0)->Type());
 
     auto args = new_c->Args();
     EXPECT_EQ(1u, args.Length());
diff --git a/src/tint/lang/core/ir/core_builtin_call.cc b/src/tint/lang/core/ir/core_builtin_call.cc
index 3dd8483..33e38b5 100644
--- a/src/tint/lang/core/ir/core_builtin_call.cc
+++ b/src/tint/lang/core/ir/core_builtin_call.cc
@@ -48,7 +48,7 @@
 CoreBuiltinCall::~CoreBuiltinCall() = default;
 
 CoreBuiltinCall* CoreBuiltinCall::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto args = ctx.Remap<CoreBuiltinCall::kDefaultNumOperands>(Args());
     return ctx.ir.instructions.Create<CoreBuiltinCall>(new_result, func_, args);
 }
diff --git a/src/tint/lang/core/ir/core_builtin_call.h b/src/tint/lang/core/ir/core_builtin_call.h
index b102071..9e7e9f0 100644
--- a/src/tint/lang/core/ir/core_builtin_call.h
+++ b/src/tint/lang/core/ir/core_builtin_call.h
@@ -54,16 +54,16 @@
     CoreBuiltinCall* Clone(CloneContext& ctx) override;
 
     /// @returns the builtin function
-    core::BuiltinFn Func() { return func_; }
+    core::BuiltinFn Func() const { return func_; }
 
     /// @returns the identifier for the function
-    size_t FuncId() override { return static_cast<size_t>(func_); }
+    size_t FuncId() const override { return static_cast<size_t>(func_); }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return core::str(func_); }
+    std::string FriendlyName() const override { return core::str(func_); }
 
     /// @returns the table data to validate this builtin
-    const core::intrinsic::TableData& TableData() override {
+    const core::intrinsic::TableData& TableData() const override {
         return core::intrinsic::Dialect::kData;
     }
 
diff --git a/src/tint/lang/core/ir/core_builtin_call_test.cc b/src/tint/lang/core/ir/core_builtin_call_test.cc
index 447788a..05965f8 100644
--- a/src/tint/lang/core/ir/core_builtin_call_test.cc
+++ b/src/tint/lang/core/ir/core_builtin_call_test.cc
@@ -50,10 +50,9 @@
     auto* arg2 = b.Constant(2_u);
     auto* builtin = b.Call(mod.Types().f32(), core::BuiltinFn::kAbs, arg1, arg2);
 
-    EXPECT_TRUE(builtin->HasResults());
-    EXPECT_FALSE(builtin->HasMultiResults());
-    EXPECT_TRUE(builtin->Result()->Is<InstructionResult>());
-    EXPECT_EQ(builtin->Result()->Source(), builtin);
+    EXPECT_EQ(builtin->Results().Length(), 1u);
+    EXPECT_TRUE(builtin->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(builtin->Result(0)->Instruction(), builtin);
 }
 
 TEST_F(IR_CoreBuiltinCallTest, Fail_NullType) {
@@ -92,8 +91,8 @@
     auto* new_b = clone_ctx.Clone(builtin);
 
     EXPECT_NE(builtin, new_b);
-    EXPECT_NE(builtin->Result(), new_b->Result());
-    EXPECT_EQ(mod.Types().f32(), new_b->Result()->Type());
+    EXPECT_NE(builtin->Result(0), new_b->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_b->Result(0)->Type());
 
     EXPECT_EQ(core::BuiltinFn::kAbs, new_b->Func());
 
@@ -111,8 +110,8 @@
     auto* builtin = b.Call(mod.Types().f32(), core::BuiltinFn::kAbs);
 
     auto* new_b = clone_ctx.Clone(builtin);
-    EXPECT_NE(builtin->Result(), new_b->Result());
-    EXPECT_EQ(mod.Types().f32(), new_b->Result()->Type());
+    EXPECT_NE(builtin->Result(0), new_b->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_b->Result(0)->Type());
 
     EXPECT_EQ(core::BuiltinFn::kAbs, new_b->Func());
 
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 09d5c75..728004c 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -87,11 +87,11 @@
 
 }  // namespace
 
-std::string Disassemble(Module& mod) {
+std::string Disassemble(const Module& mod) {
     return Disassembler{mod}.Disassemble();
 }
 
-Disassembler::Disassembler(Module& mod) : mod_(mod) {}
+Disassembler::Disassembler(const Module& mod) : mod_(mod) {}
 
 Disassembler::~Disassembler() = default;
 
@@ -108,12 +108,12 @@
     current_output_start_pos_ = out_.tellp();
 }
 
-size_t Disassembler::IdOf(Block* node) {
+size_t Disassembler::IdOf(const Block* node) {
     TINT_ASSERT(node);
     return block_ids_.GetOrCreate(node, [&] { return block_ids_.Count(); });
 }
 
-std::string Disassembler::IdOf(Value* value) {
+std::string Disassembler::IdOf(const Value* value) {
     TINT_ASSERT(value);
     return value_ids_.GetOrCreate(value, [&] {
         if (auto sym = mod_.NameOf(value)) {
@@ -132,7 +132,7 @@
     });
 }
 
-std::string Disassembler::NameOf(If* inst) {
+std::string Disassembler::NameOf(const If* inst) {
     if (!inst) {
         return "undef";
     }
@@ -140,7 +140,7 @@
     return if_names_.GetOrCreate(inst, [&] { return "if_" + std::to_string(if_names_.Count()); });
 }
 
-std::string Disassembler::NameOf(Loop* inst) {
+std::string Disassembler::NameOf(const Loop* inst) {
     if (!inst) {
         return "undef";
     }
@@ -149,7 +149,7 @@
                                    [&] { return "loop_" + std::to_string(loop_names_.Count()); });
 }
 
-std::string Disassembler::NameOf(Switch* inst) {
+std::string Disassembler::NameOf(const Switch* inst) {
     if (!inst) {
         return "undef";
     }
@@ -174,13 +174,13 @@
         EmitLine();
     }
 
-    for (auto* func : mod_.functions) {
+    for (auto& func : mod_.functions) {
         EmitFunction(func);
     }
     return out_.str();
 }
 
-void Disassembler::EmitBlock(Block* blk, std::string_view comment /* = "" */) {
+void Disassembler::EmitBlock(const Block* blk, std::string_view comment /* = "" */) {
     Indent();
 
     SourceMarker sm(this);
@@ -229,7 +229,7 @@
     }
 }
 
-void Disassembler::EmitParamAttributes(FunctionParam* p) {
+void Disassembler::EmitParamAttributes(const FunctionParam* p) {
     if (!p->Invariant() && !p->Location().has_value() && !p->BindingPoint().has_value() &&
         !p->Builtin().has_value()) {
         return;
@@ -266,7 +266,7 @@
     out_ << "]";
 }
 
-void Disassembler::EmitReturnAttributes(Function* func) {
+void Disassembler::EmitReturnAttributes(const Function* func) {
     if (!func->ReturnInvariant() && !func->ReturnLocation().has_value() &&
         !func->ReturnBuiltin().has_value()) {
         return;
@@ -298,7 +298,7 @@
     out_ << "]";
 }
 
-void Disassembler::EmitFunction(Function* func) {
+void Disassembler::EmitFunction(const Function* func) {
     in_function_ = true;
 
     std::string fn_id = IdOf(func);
@@ -358,17 +358,17 @@
     EmitLine();
 }
 
-void Disassembler::EmitValueWithType(Instruction* val) {
+void Disassembler::EmitValueWithType(const Instruction* val) {
     SourceMarker sm(this);
-    if (val->Result()) {
-        EmitValueWithType(val->Result());
+    if (val->Result(0)) {
+        EmitValueWithType(val->Result(0));
     } else {
         out_ << "undef";
     }
-    sm.StoreResult(Usage{val, 0});
+    sm.StoreResult(IndexedValue{val, 0});
 }
 
-void Disassembler::EmitValueWithType(Value* val) {
+void Disassembler::EmitValueWithType(const Value* val) {
     if (!val) {
         out_ << "undef";
         return;
@@ -378,10 +378,10 @@
     out_ << ":" << val->Type()->FriendlyName();
 }
 
-void Disassembler::EmitValue(Value* val) {
+void Disassembler::EmitValue(const Value* val) {
     tint::Switch(
         val,
-        [&](ir::Constant* constant) {
+        [&](const ir::Constant* constant) {
             std::function<void(const core::constant::Value*)> emit =
                 [&](const core::constant::Value* c) {
                     tint::Switch(
@@ -427,10 +427,12 @@
                 };
             emit(constant->Value());
         },
-        [&](ir::InstructionResult* rv) { out_ << "%" << IdOf(rv); },
-        [&](ir::BlockParam* p) { out_ << "%" << IdOf(p) << ":" << p->Type()->FriendlyName(); },
-        [&](ir::FunctionParam* p) { out_ << "%" << IdOf(p); },
-        [&](ir::Function* f) { out_ << "%" << IdOf(f); },
+        [&](const ir::InstructionResult* rv) { out_ << "%" << IdOf(rv); },
+        [&](const ir::BlockParam* p) {
+            out_ << "%" << IdOf(p) << ":" << p->Type()->FriendlyName();
+        },
+        [&](const ir::FunctionParam* p) { out_ << "%" << IdOf(p); },
+        [&](const ir::Function* f) { out_ << "%" << IdOf(f); },
         [&](Default) {
             if (val == nullptr) {
                 out_ << "undef";
@@ -440,13 +442,13 @@
         });
 }
 
-void Disassembler::EmitInstructionName(Instruction* inst) {
+void Disassembler::EmitInstructionName(const Instruction* inst) {
     SourceMarker sm(this);
     out_ << inst->FriendlyName();
     sm.Store(inst);
 }
 
-void Disassembler::EmitInstruction(Instruction* inst) {
+void Disassembler::EmitInstruction(const Instruction* inst) {
     TINT_DEFER(EmitLine());
 
     if (!inst->Alive()) {
@@ -456,26 +458,26 @@
         return;
     }
     tint::Switch(
-        inst,                               //
-        [&](Switch* s) { EmitSwitch(s); },  //
-        [&](If* i) { EmitIf(i); },          //
-        [&](Loop* l) { EmitLoop(l); },      //
-        [&](Binary* b) { EmitBinary(b); },  //
-        [&](Unary* u) { EmitUnary(u); },    //
-        [&](Discard* d) { EmitInstructionName(d); },
-        [&](Store* s) {
+        inst,                                     //
+        [&](const Switch* s) { EmitSwitch(s); },  //
+        [&](const If* i) { EmitIf(i); },          //
+        [&](const Loop* l) { EmitLoop(l); },      //
+        [&](const Binary* b) { EmitBinary(b); },  //
+        [&](const Unary* u) { EmitUnary(u); },    //
+        [&](const Discard* d) { EmitInstructionName(d); },
+        [&](const Store* s) {
             EmitInstructionName(s);
             out_ << " ";
             EmitOperand(s, Store::kToOperandOffset);
             out_ << ", ";
             EmitOperand(s, Store::kFromOperandOffset);
         },
-        [&](StoreVectorElement* s) {
+        [&](const StoreVectorElement* s) {
             EmitInstructionName(s);
             out_ << " ";
             EmitOperandList(s);
         },
-        [&](UserCall* uc) {
+        [&](const UserCall* uc) {
             EmitValueWithType(uc);
             out_ << " = ";
             EmitInstructionName(uc);
@@ -486,7 +488,7 @@
             }
             EmitOperandList(uc, UserCall::kArgsOperandOffset);
         },
-        [&](Var* v) {
+        [&](const Var* v) {
             EmitValueWithType(v);
             out_ << " = ";
             EmitInstructionName(v);
@@ -519,7 +521,7 @@
                 out_ << " @builtin(" << v->Attributes().builtin.value() << ")";
             }
         },
-        [&](Swizzle* s) {
+        [&](const Swizzle* s) {
             EmitValueWithType(s);
             out_ << " = ";
             EmitInstructionName(s);
@@ -543,7 +545,7 @@
                 }
             }
         },
-        [&](Terminator* b) { EmitTerminator(b); },
+        [&](const Terminator* b) { EmitTerminator(b); },
         [&](Default) {
             EmitValueWithType(inst);
             out_ << " = ";
@@ -572,13 +574,13 @@
     }
 }
 
-void Disassembler::EmitOperand(Instruction* inst, size_t index) {
-    SourceMarker condMarker(this);
+void Disassembler::EmitOperand(const Instruction* inst, size_t index) {
+    SourceMarker marker(this);
     EmitValue(inst->Operands()[index]);
-    condMarker.Store(Usage{inst, static_cast<uint32_t>(index)});
+    marker.Store(IndexedValue{inst, static_cast<uint32_t>(index)});
 }
 
-void Disassembler::EmitOperandList(Instruction* inst, size_t start_index /* = 0 */) {
+void Disassembler::EmitOperandList(const Instruction* inst, size_t start_index /* = 0 */) {
     for (size_t i = start_index, n = inst->Operands().Length(); i < n; i++) {
         if (i != start_index) {
             out_ << ", ";
@@ -587,17 +589,16 @@
     }
 }
 
-void Disassembler::EmitIf(If* if_) {
+void Disassembler::EmitIf(const If* if_) {
     SourceMarker sm(this);
-    if (if_->HasResults()) {
-        auto res = if_->Results();
-        for (size_t i = 0; i < res.Length(); ++i) {
+    if (auto results = if_->Results(); !results.IsEmpty()) {
+        for (size_t i = 0; i < results.Length(); ++i) {
             if (i > 0) {
                 out_ << ", ";
             }
             SourceMarker rs(this);
-            EmitValueWithType(res[i]);
-            rs.StoreResult(Usage{if_, i});
+            EmitValueWithType(results[i]);
+            rs.StoreResult(IndexedValue{if_, i});
         }
         out_ << " = ";
     }
@@ -625,7 +626,7 @@
     if (has_false) {
         ScopedIndent si(indent_size_);
         EmitBlock(if_->False(), "false");
-    } else if (if_->HasResults()) {
+    } else if (auto results = if_->Results(); !results.IsEmpty()) {
         ScopedIndent si(indent_size_);
         Indent();
         out_ << "# implicit false block: exit_if undef";
@@ -639,7 +640,7 @@
     out_ << "}";
 }
 
-void Disassembler::EmitLoop(Loop* l) {
+void Disassembler::EmitLoop(const Loop* l) {
     Vector<std::string, 3> parts;
     if (!l->Initializer()->IsEmpty()) {
         parts.Push("i: %b" + std::to_string(IdOf(l->Initializer())));
@@ -650,15 +651,14 @@
         parts.Push("c: %b" + std::to_string(IdOf(l->Continuing())));
     }
     SourceMarker sm(this);
-    if (l->HasResults()) {
-        auto res = l->Results();
-        for (size_t i = 0; i < res.Length(); ++i) {
+    if (auto results = l->Results(); !results.IsEmpty()) {
+        for (size_t i = 0; i < results.Length(); ++i) {
             if (i > 0) {
                 out_ << ", ";
             }
             SourceMarker rs(this);
-            EmitValueWithType(res[i]);
-            rs.StoreResult(Usage{l, i});
+            EmitValueWithType(results[i]);
+            rs.StoreResult(IndexedValue{l, i});
         }
         out_ << " = ";
     }
@@ -688,17 +688,16 @@
     out_ << "}";
 }
 
-void Disassembler::EmitSwitch(Switch* s) {
+void Disassembler::EmitSwitch(const Switch* s) {
     SourceMarker sm(this);
-    if (s->HasResults()) {
-        auto res = s->Results();
-        for (size_t i = 0; i < res.Length(); ++i) {
+    if (auto results = s->Results(); !results.IsEmpty()) {
+        for (size_t i = 0; i < results.Length(); ++i) {
             if (i > 0) {
                 out_ << ", ";
             }
             SourceMarker rs(this);
-            EmitValueWithType(res[i]);
-            rs.StoreResult(Usage{s, i});
+            EmitValueWithType(results[i]);
+            rs.StoreResult(IndexedValue{s, i});
         }
         out_ << " = ";
     }
@@ -721,7 +720,7 @@
                 EmitValue(selector.val);
             }
         }
-        out_ << ", %b" << IdOf(c.Block()) << ")";
+        out_ << ", %b" << IdOf(c.block) << ")";
     }
     out_ << "]";
     sm.Store(s);
@@ -731,50 +730,50 @@
 
     for (auto& c : s->Cases()) {
         ScopedIndent si(indent_size_);
-        EmitBlock(c.Block(), "case");
+        EmitBlock(c.block, "case");
     }
 
     Indent();
     out_ << "}";
 }
 
-void Disassembler::EmitTerminator(Terminator* b) {
+void Disassembler::EmitTerminator(const Terminator* b) {
     SourceMarker sm(this);
     size_t args_offset = 0;
     tint::Switch(
         b,
-        [&](ir::Return*) {
+        [&](const ir::Return*) {
             out_ << "ret";
-            args_offset = ir::Return::kArgOperandOffset;
+            args_offset = ir::Return::kArgsOperandOffset;
         },
-        [&](ir::Continue* cont) {
+        [&](const ir::Continue* cont) {
             out_ << "continue %b" << IdOf(cont->Loop()->Continuing());
             args_offset = ir::Continue::kArgsOperandOffset;
         },
-        [&](ir::ExitIf*) {
+        [&](const ir::ExitIf*) {
             out_ << "exit_if";
             args_offset = ir::ExitIf::kArgsOperandOffset;
         },
-        [&](ir::ExitSwitch*) {
+        [&](const ir::ExitSwitch*) {
             out_ << "exit_switch";
             args_offset = ir::ExitSwitch::kArgsOperandOffset;
         },
-        [&](ir::ExitLoop*) {
+        [&](const ir::ExitLoop*) {
             out_ << "exit_loop";
             args_offset = ir::ExitLoop::kArgsOperandOffset;
         },
-        [&](ir::NextIteration* ni) {
+        [&](const ir::NextIteration* ni) {
             out_ << "next_iteration %b" << IdOf(ni->Loop()->Body());
             args_offset = ir::NextIteration::kArgsOperandOffset;
         },
-        [&](ir::Unreachable*) { out_ << "unreachable"; },
-        [&](ir::BreakIf* bi) {
+        [&](const ir::Unreachable*) { out_ << "unreachable"; },
+        [&](const ir::BreakIf* bi) {
             out_ << "break_if ";
             EmitValue(bi->Condition());
             out_ << " %b" << IdOf(bi->Loop()->Body());
             args_offset = ir::BreakIf::kArgsOperandOffset;
         },
-        [&](ir::TerminateInvocation*) { out_ << "terminate_invocation"; },
+        [&](const ir::TerminateInvocation*) { out_ << "terminate_invocation"; },
         [&](Default) { out_ << "unknown terminator " << b->TypeInfo().name; });
 
     if (!b->Args().IsEmpty()) {
@@ -784,14 +783,14 @@
     sm.Store(b);
 
     tint::Switch(
-        b,                                                                  //
-        [&](ir::ExitIf* e) { out_ << "  # " << NameOf(e->If()); },          //
-        [&](ir::ExitSwitch* e) { out_ << "  # " << NameOf(e->Switch()); },  //
-        [&](ir::ExitLoop* e) { out_ << "  # " << NameOf(e->Loop()); }       //
+        b,                                                                        //
+        [&](const ir::ExitIf* e) { out_ << "  # " << NameOf(e->If()); },          //
+        [&](const ir::ExitSwitch* e) { out_ << "  # " << NameOf(e->Switch()); },  //
+        [&](const ir::ExitLoop* e) { out_ << "  # " << NameOf(e->Loop()); }       //
     );
 }
 
-void Disassembler::EmitValueList(tint::Slice<Value* const> values) {
+void Disassembler::EmitValueList(tint::Slice<const Value* const> values) {
     for (size_t i = 0, n = values.Length(); i < n; i++) {
         if (i > 0) {
             out_ << ", ";
@@ -800,7 +799,7 @@
     }
 }
 
-void Disassembler::EmitBinary(Binary* b) {
+void Disassembler::EmitBinary(const Binary* b) {
     SourceMarker sm(this);
     EmitValueWithType(b);
     out_ << " = ";
@@ -860,7 +859,7 @@
     sm.Store(b);
 }
 
-void Disassembler::EmitUnary(Unary* u) {
+void Disassembler::EmitUnary(const Unary* u) {
     SourceMarker sm(this);
     EmitValueWithType(u);
     out_ << " = ";
diff --git a/src/tint/lang/core/ir/disassembler.h b/src/tint/lang/core/ir/disassembler.h
index d26d98c..8f15e95 100644
--- a/src/tint/lang/core/ir/disassembler.h
+++ b/src/tint/lang/core/ir/disassembler.h
@@ -51,14 +51,32 @@
 
 /// @returns the disassembly for the module @p mod
 /// @param mod the module to disassemble
-std::string Disassemble(Module& mod);
+std::string Disassemble(const Module& mod);
 
 /// Helper class to disassemble the IR
 class Disassembler {
   public:
+    /// A reference to an instruction's operand or result.
+    struct IndexedValue {
+        /// The instruction that is using the value;
+        const Instruction* instruction = nullptr;
+        /// The index of the operand that is the value being used.
+        size_t index = 0u;
+
+        /// @returns the hash code of the IndexedValue
+        size_t HashCode() const { return Hash(instruction, index); }
+
+        /// An equality helper for IndexedValue.
+        /// @param other the IndexedValue to compare against
+        /// @returns true if the two IndexedValues are equal
+        bool operator==(const IndexedValue& other) const {
+            return instruction == other.instruction && index == other.index;
+        }
+    };
+
     /// Constructor
     /// @param mod the module
-    explicit Disassembler(Module& mod);
+    explicit Disassembler(const Module& mod);
     ~Disassembler();
 
     /// Returns the module as a string
@@ -70,41 +88,45 @@
 
     /// @param inst the instruction to retrieve
     /// @returns the source for the instruction
-    Source InstructionSource(Instruction* inst) {
+    Source InstructionSource(const Instruction* inst) {
         return instruction_to_src_.Get(inst).value_or(Source{});
     }
 
     /// @param operand the operand to retrieve
     /// @returns the source for the operand
-    Source OperandSource(Usage operand) { return operand_to_src_.Get(operand).value_or(Source{}); }
+    Source OperandSource(IndexedValue operand) {
+        return operand_to_src_.Get(operand).value_or(Source{});
+    }
 
     /// @param result the result to retrieve
     /// @returns the source for the result
-    Source ResultSource(Usage result) { return result_to_src_.Get(result).value_or(Source{}); }
+    Source ResultSource(IndexedValue result) {
+        return result_to_src_.Get(result).value_or(Source{});
+    }
 
     /// @param blk teh block to retrieve
     /// @returns the source for the block
-    Source BlockSource(Block* blk) { return block_to_src_.Get(blk).value_or(Source{}); }
+    Source BlockSource(const Block* blk) { return block_to_src_.Get(blk).value_or(Source{}); }
 
     /// Stores the given @p src location for @p inst instruction
     /// @param inst the instruction to store
     /// @param src the source location
-    void SetSource(Instruction* inst, Source src) { instruction_to_src_.Add(inst, src); }
+    void SetSource(const Instruction* inst, Source src) { instruction_to_src_.Add(inst, src); }
 
     /// Stores the given @p src location for @p blk block
     /// @param blk the block to store
     /// @param src the source location
-    void SetSource(Block* blk, Source src) { block_to_src_.Add(blk, src); }
+    void SetSource(const Block* blk, Source src) { block_to_src_.Add(blk, src); }
 
     /// Stores the given @p src location for @p op operand
     /// @param op the operand to store
     /// @param src the source location
-    void SetSource(Usage op, Source src) { operand_to_src_.Add(op, src); }
+    void SetSource(IndexedValue op, Source src) { operand_to_src_.Add(op, src); }
 
     /// Stores the given @p src location for @p result
     /// @param result the result to store
     /// @param src the source location
-    void SetResultSource(Usage result, Source src) { result_to_src_.Add(result, src); }
+    void SetResultSource(IndexedValue result, Source src) { result_to_src_.Add(result, src); }
 
     /// @returns the source location for the current emission location
     Source::Location MakeCurrentLocation();
@@ -115,13 +137,13 @@
         explicit SourceMarker(Disassembler* d) : dis_(d), begin_(dis_->MakeCurrentLocation()) {}
         ~SourceMarker() = default;
 
-        void Store(Instruction* inst) { dis_->SetSource(inst, MakeSource()); }
+        void Store(const Instruction* inst) { dis_->SetSource(inst, MakeSource()); }
 
-        void Store(Block* blk) { dis_->SetSource(blk, MakeSource()); }
+        void Store(const Block* blk) { dis_->SetSource(blk, MakeSource()); }
 
-        void Store(Usage operand) { dis_->SetSource(operand, MakeSource()); }
+        void Store(IndexedValue operand) { dis_->SetSource(operand, MakeSource()); }
 
-        void StoreResult(Usage result) { dis_->SetResultSource(result, MakeSource()); }
+        void StoreResult(IndexedValue result) { dis_->SetResultSource(result, MakeSource()); }
 
         Source MakeSource() const {
             return Source(Source::Range(begin_, dis_->MakeCurrentLocation()));
@@ -134,39 +156,39 @@
 
     StringStream& Indent();
 
-    size_t IdOf(Block* blk);
-    std::string IdOf(Value* node);
-    std::string NameOf(If* inst);
-    std::string NameOf(Loop* inst);
-    std::string NameOf(Switch* inst);
+    size_t IdOf(const Block* blk);
+    std::string IdOf(const Value* node);
+    std::string NameOf(const If* inst);
+    std::string NameOf(const Loop* inst);
+    std::string NameOf(const Switch* inst);
 
-    void EmitBlock(Block* blk, std::string_view comment = "");
-    void EmitFunction(Function* func);
-    void EmitParamAttributes(FunctionParam* p);
-    void EmitReturnAttributes(Function* func);
+    void EmitBlock(const Block* blk, std::string_view comment = "");
+    void EmitFunction(const Function* func);
+    void EmitParamAttributes(const FunctionParam* p);
+    void EmitReturnAttributes(const Function* func);
     void EmitBindingPoint(BindingPoint p);
     void EmitLocation(Location loc);
-    void EmitInstruction(Instruction* inst);
-    void EmitValueWithType(Instruction* val);
-    void EmitValueWithType(Value* val);
-    void EmitValue(Value* val);
-    void EmitValueList(tint::Slice<ir::Value* const> values);
-    void EmitBinary(Binary* b);
-    void EmitUnary(Unary* b);
-    void EmitTerminator(Terminator* b);
-    void EmitSwitch(Switch* s);
-    void EmitLoop(Loop* l);
-    void EmitIf(If* i);
+    void EmitInstruction(const Instruction* inst);
+    void EmitValueWithType(const Instruction* val);
+    void EmitValueWithType(const Value* val);
+    void EmitValue(const Value* val);
+    void EmitValueList(tint::Slice<const ir::Value* const> values);
+    void EmitBinary(const Binary* b);
+    void EmitUnary(const Unary* b);
+    void EmitTerminator(const Terminator* b);
+    void EmitSwitch(const Switch* s);
+    void EmitLoop(const Loop* l);
+    void EmitIf(const If* i);
     void EmitStructDecl(const core::type::Struct* str);
     void EmitLine();
-    void EmitOperand(Instruction* inst, size_t index);
-    void EmitOperandList(Instruction* inst, size_t start_index = 0);
-    void EmitInstructionName(Instruction* inst);
+    void EmitOperand(const Instruction* inst, size_t index);
+    void EmitOperandList(const Instruction* inst, size_t start_index = 0);
+    void EmitInstructionName(const Instruction* inst);
 
-    Module& mod_;
+    const Module& mod_;
     StringStream out_;
-    Hashmap<Block*, size_t, 32> block_ids_;
-    Hashmap<Value*, std::string, 32> value_ids_;
+    Hashmap<const Block*, size_t, 32> block_ids_;
+    Hashmap<const Value*, std::string, 32> value_ids_;
     Hashset<std::string, 32> ids_;
     uint32_t indent_size_ = 0;
     bool in_function_ = false;
@@ -174,13 +196,13 @@
     uint32_t current_output_line_ = 1;
     uint32_t current_output_start_pos_ = 0;
 
-    Hashmap<Block*, Source, 8> block_to_src_;
-    Hashmap<Instruction*, Source, 8> instruction_to_src_;
-    Hashmap<Usage, Source, 8, Usage::Hasher> operand_to_src_;
-    Hashmap<Usage, Source, 8, Usage::Hasher> result_to_src_;
-    Hashmap<If*, std::string, 8> if_names_;
-    Hashmap<Loop*, std::string, 8> loop_names_;
-    Hashmap<Switch*, std::string, 8> switch_names_;
+    Hashmap<const Block*, Source, 8> block_to_src_;
+    Hashmap<const Instruction*, Source, 8> instruction_to_src_;
+    Hashmap<IndexedValue, Source, 8> operand_to_src_;
+    Hashmap<IndexedValue, Source, 8> result_to_src_;
+    Hashmap<const If*, std::string, 8> if_names_;
+    Hashmap<const Loop*, std::string, 8> loop_names_;
+    Hashmap<const Switch*, std::string, 8> switch_names_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/discard.h b/src/tint/lang/core/ir/discard.h
index 31a805d..62e20f7 100644
--- a/src/tint/lang/core/ir/discard.h
+++ b/src/tint/lang/core/ir/discard.h
@@ -46,7 +46,7 @@
     Discard* Clone(CloneContext& ctx) override;
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "discard"; }
+    std::string FriendlyName() const override { return "discard"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/discard_test.cc b/src/tint/lang/core/ir/discard_test.cc
index bc9bc09..098b99f 100644
--- a/src/tint/lang/core/ir/discard_test.cc
+++ b/src/tint/lang/core/ir/discard_test.cc
@@ -43,8 +43,7 @@
 TEST_F(IR_DiscardTest, Result) {
     auto* inst = b.Discard();
 
-    EXPECT_FALSE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Results().IsEmpty());
 }
 
 TEST_F(IR_DiscardTest, Clone) {
diff --git a/src/tint/lang/core/ir/exit.h b/src/tint/lang/core/ir/exit.h
index 8a3d448..f75da6f 100644
--- a/src/tint/lang/core/ir/exit.h
+++ b/src/tint/lang/core/ir/exit.h
@@ -29,6 +29,7 @@
 #define SRC_TINT_LANG_CORE_IR_EXIT_H_
 
 #include "src/tint/lang/core/ir/terminator.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 
 // Forward declarations
 namespace tint::core::ir {
@@ -48,13 +49,16 @@
     /// @return the control instruction that this exit is associated with
     ir::ControlInstruction* ControlInstruction() { return ctrl_inst_; }
 
+    /// @return the control instruction that this exit is associated with
+    const ir::ControlInstruction* ControlInstruction() const { return ctrl_inst_; }
+
   protected:
     /// Sets control instruction that this exit is associated with
     /// @param ctrl_inst the new ControlInstruction that this exit is associated with
     void SetControlInstruction(ir::ControlInstruction* ctrl_inst);
 
   private:
-    ir::ControlInstruction* ctrl_inst_ = nullptr;
+    ConstPropagatingPtr<ir::ControlInstruction> ctrl_inst_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_if.cc b/src/tint/lang/core/ir/exit_if.cc
index 373a682..74cd394 100644
--- a/src/tint/lang/core/ir/exit_if.cc
+++ b/src/tint/lang/core/ir/exit_if.cc
@@ -59,4 +59,8 @@
     return static_cast<ir::If*>(ControlInstruction());
 }
 
+const ir::If* ExitIf::If() const {
+    return static_cast<const ir::If*>(ControlInstruction());
+}
+
 }  // 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 be83148..3b6f568 100644
--- a/src/tint/lang/core/ir/exit_if.h
+++ b/src/tint/lang/core/ir/exit_if.h
@@ -62,8 +62,11 @@
     /// @returns the if being exited
     ir::If* If();
 
+    /// @returns the if being exited
+    const ir::If* If() const;
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "exit_if"; }
+    std::string FriendlyName() const override { return "exit_if"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_if_test.cc b/src/tint/lang/core/ir/exit_if_test.cc
index ad838f9..7eb07ce 100644
--- a/src/tint/lang/core/ir/exit_if_test.cc
+++ b/src/tint/lang/core/ir/exit_if_test.cc
@@ -44,7 +44,7 @@
 
     EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{e, 0u}));
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 1u}));
-    EXPECT_EQ(if_->Result(), nullptr);
+    EXPECT_EQ(if_->Result(0), nullptr);
 }
 
 TEST_F(IR_ExitIfTest, Result) {
@@ -53,8 +53,7 @@
     auto* if_ = b.If(true);
     auto* e = b.ExitIf(if_, arg1, arg2);
 
-    EXPECT_FALSE(e->HasResults());
-    EXPECT_FALSE(e->HasMultiResults());
+    EXPECT_TRUE(e->Results().IsEmpty());
 }
 
 TEST_F(IR_ExitIfTest, Destroy) {
diff --git a/src/tint/lang/core/ir/exit_loop.cc b/src/tint/lang/core/ir/exit_loop.cc
index 1d06680..fe8da1b 100644
--- a/src/tint/lang/core/ir/exit_loop.cc
+++ b/src/tint/lang/core/ir/exit_loop.cc
@@ -60,4 +60,8 @@
     return static_cast<ir::Loop*>(ControlInstruction());
 }
 
+const ir::Loop* ExitLoop::Loop() const {
+    return static_cast<const ir::Loop*>(ControlInstruction());
+}
+
 }  // 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 83bb20e..0686fc2 100644
--- a/src/tint/lang/core/ir/exit_loop.h
+++ b/src/tint/lang/core/ir/exit_loop.h
@@ -62,8 +62,11 @@
     /// @returns the loop being exited
     ir::Loop* Loop();
 
+    /// @returns the loop being exited
+    const ir::Loop* Loop() const;
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "exit_loop"; }
+    std::string FriendlyName() const override { return "exit_loop"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_loop_test.cc b/src/tint/lang/core/ir/exit_loop_test.cc
index 2f25903..18c59a3 100644
--- a/src/tint/lang/core/ir/exit_loop_test.cc
+++ b/src/tint/lang/core/ir/exit_loop_test.cc
@@ -44,7 +44,7 @@
 
     EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{e, 0u}));
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 1u}));
-    EXPECT_EQ(loop->Result(), nullptr);
+    EXPECT_EQ(loop->Result(0), nullptr);
 }
 
 TEST_F(IR_ExitLoopTest, Destroy) {
diff --git a/src/tint/lang/core/ir/exit_switch.cc b/src/tint/lang/core/ir/exit_switch.cc
index fe7ef6c..5d90811 100644
--- a/src/tint/lang/core/ir/exit_switch.cc
+++ b/src/tint/lang/core/ir/exit_switch.cc
@@ -59,4 +59,8 @@
     return static_cast<ir::Switch*>(ControlInstruction());
 }
 
+const ir::Switch* ExitSwitch::Switch() const {
+    return static_cast<const ir::Switch*>(ControlInstruction());
+}
+
 }  // 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 3b614f1..878f73b 100644
--- a/src/tint/lang/core/ir/exit_switch.h
+++ b/src/tint/lang/core/ir/exit_switch.h
@@ -62,8 +62,11 @@
     /// @returns the switch being exited
     ir::Switch* Switch();
 
+    /// @returns the switch being exited
+    const ir::Switch* Switch() const;
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "exit_switch"; }
+    std::string FriendlyName() const override { return "exit_switch"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_switch_test.cc b/src/tint/lang/core/ir/exit_switch_test.cc
index 222cc3c..2fc1f77 100644
--- a/src/tint/lang/core/ir/exit_switch_test.cc
+++ b/src/tint/lang/core/ir/exit_switch_test.cc
@@ -44,7 +44,7 @@
 
     EXPECT_THAT(arg1->Usages(), testing::UnorderedElementsAre(Usage{e, 0u}));
     EXPECT_THAT(arg2->Usages(), testing::UnorderedElementsAre(Usage{e, 1u}));
-    EXPECT_EQ(switch_->Result(), nullptr);
+    EXPECT_EQ(switch_->Result(0), nullptr);
 }
 
 TEST_F(IR_ExitSwitchTest, Result) {
@@ -53,8 +53,7 @@
     auto* switch_ = b.Switch(true);
     auto* e = b.ExitSwitch(switch_, arg1, arg2);
 
-    EXPECT_FALSE(e->HasResults());
-    EXPECT_FALSE(e->HasMultiResults());
+    EXPECT_TRUE(e->Results().IsEmpty());
 }
 
 TEST_F(IR_ExitSwitchTest, Destroy) {
diff --git a/src/tint/lang/core/ir/function.h b/src/tint/lang/core/ir/function.h
index 33639bb..1937673 100644
--- a/src/tint/lang/core/ir/function.h
+++ b/src/tint/lang/core/ir/function.h
@@ -36,6 +36,7 @@
 #include "src/tint/lang/core/ir/location.h"
 #include "src/tint/lang/core/ir/value.h"
 #include "src/tint/lang/core/type/type.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/ice/ice.h"
 
 // Forward declarations
@@ -88,7 +89,7 @@
     void SetStage(PipelineStage stage) { pipeline_stage_ = stage; }
 
     /// @returns the function pipeline stage
-    PipelineStage Stage() { return pipeline_stage_; }
+    PipelineStage Stage() const { return pipeline_stage_; }
 
     /// Sets the workgroup size
     /// @param x the x size
@@ -100,10 +101,10 @@
     void ClearWorkgroupSize() { workgroup_size_ = {}; }
 
     /// @returns the workgroup size information
-    std::optional<std::array<uint32_t, 3>> WorkgroupSize() { return workgroup_size_; }
+    std::optional<std::array<uint32_t, 3>> WorkgroupSize() const { return workgroup_size_; }
 
     /// @returns the return type for the function
-    const core::type::Type* ReturnType() { return return_.type; }
+    const core::type::Type* ReturnType() const { return return_.type; }
 
     /// Sets the return attributes
     /// @param builtin the builtin to set
@@ -112,7 +113,7 @@
         return_.builtin = builtin;
     }
     /// @returns the return builtin attribute
-    std::optional<enum ReturnBuiltin> ReturnBuiltin() { return return_.builtin; }
+    std::optional<enum ReturnBuiltin> ReturnBuiltin() const { return return_.builtin; }
     /// Clears the return builtin attribute.
     void ClearReturnBuiltin() { return_.builtin = {}; }
 
@@ -123,7 +124,7 @@
         return_.location = {loc, interp};
     }
     /// @returns the return location
-    std::optional<Location> ReturnLocation() { return return_.location; }
+    std::optional<Location> ReturnLocation() const { return return_.location; }
     /// Clears the return location attribute.
     void ClearReturnLocation() { return_.location = {}; }
 
@@ -131,7 +132,7 @@
     /// @param val the invariant value to set
     void SetReturnInvariant(bool val) { return_.invariant = val; }
     /// @returns the return invariant value
-    bool ReturnInvariant() { return return_.invariant; }
+    bool ReturnInvariant() const { return return_.invariant; }
 
     /// Sets the function parameters
     /// @param params the function parameters
@@ -144,15 +145,22 @@
     /// @returns the function parameters
     const VectorRef<FunctionParam*> Params() { return params_; }
 
+    /// @returns the function parameters
+    VectorRef<const FunctionParam*> Params() const { return params_; }
+
     /// Sets the root block for the function
     /// @param target the root block
     void SetBlock(Block* target) {
         TINT_ASSERT(target != nullptr);
         block_ = target;
     }
+
     /// @returns the function root block
     ir::Block* Block() { return block_; }
 
+    /// @returns the function root block
+    const ir::Block* Block() const { return block_; }
+
     /// Destroys the function and all of its instructions.
     void Destroy() override;
 
@@ -168,7 +176,7 @@
     } return_;
 
     Vector<FunctionParam*, 1> params_;
-    ir::Block* block_ = nullptr;
+    ConstPropagatingPtr<ir::Block> block_;
 };
 
 /// @param value the enum value
diff --git a/src/tint/lang/core/ir/function_param.h b/src/tint/lang/core/ir/function_param.h
index 0955ef3..4f8e22c 100644
--- a/src/tint/lang/core/ir/function_param.h
+++ b/src/tint/lang/core/ir/function_param.h
@@ -78,7 +78,7 @@
     ~FunctionParam() override;
 
     /// @returns the type of the var
-    const core::type::Type* Type() override { return type_; }
+    const core::type::Type* Type() const override { return type_; }
 
     /// @copydoc Value::Clone()
     FunctionParam* Clone(CloneContext& ctx) override;
@@ -90,7 +90,7 @@
         builtin_ = val;
     }
     /// @returns the builtin set for the parameter
-    std::optional<FunctionParam::Builtin> Builtin() { return builtin_; }
+    std::optional<FunctionParam::Builtin> Builtin() const { return builtin_; }
     /// Clears the builtin attribute.
     void ClearBuiltin() { builtin_ = {}; }
 
@@ -98,7 +98,7 @@
     /// @param val the value to set for invariant
     void SetInvariant(bool val) { invariant_ = val; }
     /// @returns true if parameter is invariant
-    bool Invariant() { return invariant_; }
+    bool Invariant() const { return invariant_; }
 
     /// Sets the location
     /// @param loc the location value
@@ -107,7 +107,7 @@
         location_ = {loc, interpolation};
     }
     /// @returns the location if `Attributes` contains `kLocation`
-    std::optional<struct Location> Location() { return location_; }
+    std::optional<struct Location> Location() const { return location_; }
     /// Clears the location attribute.
     void ClearLocation() { location_ = {}; }
 
@@ -116,7 +116,7 @@
     /// @param binding the binding
     void SetBindingPoint(uint32_t group, uint32_t binding) { binding_point_ = {group, binding}; }
     /// @returns the binding points if `Attributes` contains `kBindingPoint`
-    std::optional<struct BindingPoint>& BindingPoint() { return binding_point_; }
+    std::optional<struct BindingPoint> BindingPoint() const { return binding_point_; }
 
   private:
     const core::type::Type* type_ = nullptr;
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/core/ir/ice.h
similarity index 65%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/core/ir/ice.h
index 6f0f657..40ef1dd 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/core/ir/ice.h
@@ -25,28 +25,12 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_LANG_CORE_IR_ICE_H_
+#define SRC_TINT_LANG_CORE_IR_ICE_H_
 
-#include <string>
+#include "src/tint/lang/core/ir/disassembler.h"
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
+/// Emit an ICE message with the disassembly of `mod` attached.
+#define TINT_IR_ICE(mod) TINT_ICE() << tint::core::ir::Disassembler{mod}.Disassemble() << "\n"
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
-}
-
-namespace tint::wgsl::writer {
-
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
-
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_LANG_CORE_IR_ICE_H_
diff --git a/src/tint/lang/core/ir/if.h b/src/tint/lang/core/ir/if.h
index ccf8c1d..cc03e63 100644
--- a/src/tint/lang/core/ir/if.h
+++ b/src/tint/lang/core/ir/if.h
@@ -31,6 +31,7 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/control_instruction.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 
 // Forward declarations
 namespace tint::core::ir {
@@ -76,18 +77,27 @@
     /// @returns the if condition
     Value* Condition() { return operands_[kConditionOperandOffset]; }
 
+    /// @returns the if condition
+    const Value* Condition() const { return operands_[kConditionOperandOffset]; }
+
     /// @returns the true block
     ir::Block* True() { return true_; }
 
+    /// @returns the true block
+    const ir::Block* True() const { return true_; }
+
     /// @returns the false block
     ir::Block* False() { return false_; }
 
+    /// @returns the false block
+    const ir::Block* False() const { return false_; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "if"; }
+    std::string FriendlyName() const override { return "if"; }
 
   private:
-    ir::Block* true_ = nullptr;
-    ir::Block* false_ = nullptr;
+    ConstPropagatingPtr<ir::Block> true_;
+    ConstPropagatingPtr<ir::Block> false_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/if_test.cc b/src/tint/lang/core/ir/if_test.cc
index 3d3ea8b..6d609fd 100644
--- a/src/tint/lang/core/ir/if_test.cc
+++ b/src/tint/lang/core/ir/if_test.cc
@@ -45,8 +45,7 @@
 TEST_F(IR_IfTest, Result) {
     auto* if_ = b.If(b.Constant(true));
 
-    EXPECT_FALSE(if_->HasResults());
-    EXPECT_FALSE(if_->HasMultiResults());
+    EXPECT_TRUE(if_->Results().IsEmpty());
 }
 
 TEST_F(IR_IfTest, Parent) {
diff --git a/src/tint/lang/core/ir/instruction.cc b/src/tint/lang/core/ir/instruction.cc
index 3778221..69d3f77 100644
--- a/src/tint/lang/core/ir/instruction.cc
+++ b/src/tint/lang/core/ir/instruction.cc
@@ -44,7 +44,7 @@
         Remove();
     }
     for (auto* result : Results()) {
-        result->SetSource(nullptr);
+        result->SetInstruction(nullptr);
         result->Destroy();
     }
     flags_.Add(Flag::kDead);
diff --git a/src/tint/lang/core/ir/instruction.h b/src/tint/lang/core/ir/instruction.h
index c15e6c6..886df26 100644
--- a/src/tint/lang/core/ir/instruction.h
+++ b/src/tint/lang/core/ir/instruction.h
@@ -32,6 +32,7 @@
 
 #include "src/tint/lang/core/ir/instruction_result.h"
 #include "src/tint/lang/core/ir/value.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/containers/enum_set.h"
 #include "src/tint/utils/rtti/castable.h"
 
@@ -57,24 +58,21 @@
     /// @returns the operands of the instruction
     virtual VectorRef<ir::Value*> Operands() = 0;
 
-    /// @returns true if the instruction has result values
-    virtual bool HasResults() { return false; }
-    /// @returns true if the instruction has multiple values
-    virtual bool HasMultiResults() { return false; }
-
-    /// @returns the first result. Returns `nullptr` if there are no results, or if ther are
-    /// multi-results
-    virtual InstructionResult* Result() { return nullptr; }
+    /// @returns the operands of the instruction
+    virtual VectorRef<const ir::Value*> Operands() const = 0;
 
     /// @returns the result values for this instruction
-    virtual VectorRef<InstructionResult*> Results() { return tint::Empty; }
+    virtual VectorRef<InstructionResult*> Results() = 0;
+
+    /// @returns the result values for this instruction
+    virtual VectorRef<const InstructionResult*> Results() const = 0;
 
     /// Removes the instruction from the block, and destroys all the result values.
     /// The result values must not be in use.
     virtual void Destroy();
 
     /// @returns the friendly name for the instruction
-    virtual std::string FriendlyName() = 0;
+    virtual std::string FriendlyName() const = 0;
 
     /// @param ctx the CloneContext used to clone this instruction
     /// @returns a clone of this instruction
@@ -94,6 +92,9 @@
     /// @returns the block that owns this instruction
     ir::Block* Block() { return block_; }
 
+    /// @returns the block that owns this instruction
+    const ir::Block* Block() const { return block_; }
+
     /// Adds the new instruction before the given instruction in the owning block
     /// @param before the instruction to insert before
     void InsertBefore(Instruction* before);
@@ -106,18 +107,42 @@
     /// Removes this instruction from the owning block
     void Remove();
 
+    /// @param idx the index of the operand
+    /// @returns the operand with index @p idx, or `nullptr` if there are no operands or the index
+    /// is out of bounds.
+    Value* Operand(size_t idx) {
+        auto res = Operands();
+        return idx < res.Length() ? res[idx] : nullptr;
+    }
+
+    /// @param idx the index of the operand
+    /// @returns the operand with index @p idx, or `nullptr` if there are no operands or the index
+    /// is out of bounds.
+    const Value* Operand(size_t idx) const {
+        auto res = Operands();
+        return idx < res.Length() ? res[idx] : nullptr;
+    }
+
     /// @param idx the index of the result
     /// @returns the result with index @p idx, or `nullptr` if there are no results or the index is
     /// out of bounds.
-    Value* Result(size_t idx) {
+    InstructionResult* Result(size_t idx) {
+        auto res = Results();
+        return idx < res.Length() ? res[idx] : nullptr;
+    }
+
+    /// @param idx the index of the result
+    /// @returns the result with index @p idx, or `nullptr` if there are no results or the index is
+    /// out of bounds.
+    const InstructionResult* Result(size_t idx) const {
         auto res = Results();
         return idx < res.Length() ? res[idx] : nullptr;
     }
 
     /// Pointer to the next instruction in the list
-    Instruction* next = nullptr;
+    ConstPropagatingPtr<Instruction> next;
     /// Pointer to the previous instruction in the list
-    Instruction* prev = nullptr;
+    ConstPropagatingPtr<Instruction> prev;
 
   protected:
     /// Flags applied to an Instruction
@@ -132,7 +157,7 @@
     Instruction();
 
     /// The block that owns this instruction
-    ir::Block* block_ = nullptr;
+    ConstPropagatingPtr<ir::Block> block_;
 
     /// Bitset of instruction flags
     tint::EnumSet<Flag> flags_;
diff --git a/src/tint/lang/core/ir/instruction_result.cc b/src/tint/lang/core/ir/instruction_result.cc
index 496c82b..2aa0fc7 100644
--- a/src/tint/lang/core/ir/instruction_result.cc
+++ b/src/tint/lang/core/ir/instruction_result.cc
@@ -44,12 +44,12 @@
 InstructionResult::~InstructionResult() = default;
 
 void InstructionResult::Destroy() {
-    TINT_ASSERT(source_ == nullptr);
+    TINT_ASSERT(instruction_ == nullptr);
     Base::Destroy();
 }
 
 InstructionResult* InstructionResult::Clone(CloneContext& ctx) {
-    // Do not clone the `Source`. It will be set when this result is placed in the new parent
+    // Do not clone the `Instruction`. It will be set when this result is placed in the new parent
     // instruction.
     return ctx.ir.values.Create<InstructionResult>(type_);
 }
diff --git a/src/tint/lang/core/ir/instruction_result.h b/src/tint/lang/core/ir/instruction_result.h
index 8ba3878..2b6b442 100644
--- a/src/tint/lang/core/ir/instruction_result.h
+++ b/src/tint/lang/core/ir/instruction_result.h
@@ -47,7 +47,7 @@
     void Destroy() override;
 
     /// @returns the type of the value
-    const core::type::Type* Type() override { return type_; }
+    const core::type::Type* Type() const override { return type_; }
 
     /// @copydoc Value::Clone()
     InstructionResult* Clone(CloneContext& ctx) override;
@@ -56,15 +56,18 @@
     /// @param type the new type of the value
     void SetType(const core::type::Type* type) { type_ = type; }
 
-    /// Sets the source instruction for this value
+    /// Sets the instruction for this value
     /// @param inst the instruction to set
-    void SetSource(Instruction* inst) { source_ = inst; }
+    void SetInstruction(Instruction* inst) { instruction_ = inst; }
 
-    /// @returns the source instruction, if any
-    Instruction* Source() { return source_; }
+    /// @returns the instruction, if any
+    ir::Instruction* Instruction() { return instruction_; }
+
+    /// @returns the instruction, if any
+    const ir::Instruction* Instruction() const { return instruction_; }
 
   private:
-    Instruction* source_ = nullptr;
+    ir::Instruction* instruction_ = nullptr;
     const core::type::Type* type_ = nullptr;
 };
 
diff --git a/src/tint/lang/core/ir/instruction_result_test.cc b/src/tint/lang/core/ir/instruction_result_test.cc
index 71212db..534f347 100644
--- a/src/tint/lang/core/ir/instruction_result_test.cc
+++ b/src/tint/lang/core/ir/instruction_result_test.cc
@@ -37,23 +37,23 @@
 
 using IR_InstructionResultTest = IRTestHelper;
 
-TEST_F(IR_InstructionResultTest, Destroy_HasSource) {
+TEST_F(IR_InstructionResultTest, Destroy_HasInstruction) {
     EXPECT_FATAL_FAILURE(
         {
             Module mod;
             Builder b{mod};
-            auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result();
+            auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result(0);
             val->Destroy();
         },
         "");
 }
 
 TEST_F(IR_InstructionResultTest, Clone) {
-    auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result();
+    auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result(0);
     auto* new_res = clone_ctx.Clone(val);
 
     EXPECT_NE(val, new_res);
-    EXPECT_EQ(nullptr, new_res->Source());
+    EXPECT_EQ(nullptr, new_res->Instruction());
     EXPECT_EQ(mod.Types().i32(), new_res->Type());
 }
 
diff --git a/src/tint/lang/core/ir/let.cc b/src/tint/lang/core/ir/let.cc
index e6b2ea3..ddbf9a3 100644
--- a/src/tint/lang/core/ir/let.cc
+++ b/src/tint/lang/core/ir/let.cc
@@ -43,7 +43,7 @@
 Let::~Let() = default;
 
 Let* Let::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* val = ctx.Remap(Value());
     auto* new_let = ctx.ir.instructions.Create<Let>(new_result, val);
 
diff --git a/src/tint/lang/core/ir/let.h b/src/tint/lang/core/ir/let.h
index f983a2c..a2fe222 100644
--- a/src/tint/lang/core/ir/let.h
+++ b/src/tint/lang/core/ir/let.h
@@ -49,11 +49,17 @@
     /// @copydoc Instruction::Clone()
     Let* Clone(CloneContext& ctx) override;
 
+    /// @param value the new let value
+    void SetValue(ir::Value* value) { SetOperand(kValueOperandOffset, value); }
+
     /// @returns the value
     ir::Value* Value() { return operands_[kValueOperandOffset]; }
 
+    /// @returns the value
+    const ir::Value* Value() const { return operands_[kValueOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "let"; }
+    std::string FriendlyName() const override { return "let"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/let_test.cc b/src/tint/lang/core/ir/let_test.cc
index 59c5947..78b9d5e 100644
--- a/src/tint/lang/core/ir/let_test.cc
+++ b/src/tint/lang/core/ir/let_test.cc
@@ -55,11 +55,10 @@
 TEST_F(IR_LetTest, Results) {
     auto* value = b.Constant(1_f);
     auto* let = b.Let("l", value);
-    EXPECT_TRUE(let->HasResults());
-    EXPECT_FALSE(let->HasMultiResults());
-    EXPECT_TRUE(let->Result()->Is<InstructionResult>());
-    EXPECT_EQ(let->Result()->Source(), let);
-    EXPECT_EQ(let->Result()->Type(), value->Type());
+    EXPECT_EQ(let->Results().Length(), 1u);
+    EXPECT_TRUE(let->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(let->Result(0)->Instruction(), let);
+    EXPECT_EQ(let->Result(0)->Type(), value->Type());
 }
 
 TEST_F(IR_LetTest, Clone) {
@@ -69,15 +68,15 @@
     auto* new_let = clone_ctx.Clone(let);
 
     EXPECT_NE(let, new_let);
-    EXPECT_NE(nullptr, new_let->Result());
-    EXPECT_NE(let->Result(), new_let->Result());
+    EXPECT_NE(nullptr, new_let->Result(0));
+    EXPECT_NE(let->Result(0), new_let->Result(0));
 
     auto new_val = new_let->Value()->As<Constant>()->Value();
     ASSERT_TRUE(new_val->Is<core::constant::Scalar<f32>>());
     EXPECT_FLOAT_EQ(4_f, new_val->As<core::constant::Scalar<f32>>()->ValueAs<f32>());
 
     EXPECT_EQ(std::string("l"), mod.NameOf(new_let).Name());
-    EXPECT_EQ(std::string("l"), mod.NameOf(new_let->Result()).Name());
+    EXPECT_EQ(std::string("l"), mod.NameOf(new_let->Result(0)).Name());
 }
 
 }  // namespace
diff --git a/src/tint/lang/core/ir/load.cc b/src/tint/lang/core/ir/load.cc
index 2d63594..ba9f6d3 100644
--- a/src/tint/lang/core/ir/load.cc
+++ b/src/tint/lang/core/ir/load.cc
@@ -49,7 +49,7 @@
 Load::~Load() = default;
 
 Load* Load::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* from = ctx.Remap(From());
     return ctx.ir.instructions.Create<Load>(new_result, from);
 }
diff --git a/src/tint/lang/core/ir/load.h b/src/tint/lang/core/ir/load.h
index daab943..167fa87 100644
--- a/src/tint/lang/core/ir/load.h
+++ b/src/tint/lang/core/ir/load.h
@@ -54,8 +54,11 @@
     /// @returns the value being loaded from
     Value* From() { return operands_[kFromOperandOffset]; }
 
+    /// @returns the value being loaded from
+    const Value* From() const { return operands_[kFromOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "load"; }
+    std::string FriendlyName() const override { return "load"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load_test.cc b/src/tint/lang/core/ir/load_test.cc
index ccb77dc..1550b14 100644
--- a/src/tint/lang/core/ir/load_test.cc
+++ b/src/tint/lang/core/ir/load_test.cc
@@ -45,13 +45,13 @@
     auto* inst = b.Load(var);
 
     ASSERT_TRUE(inst->Is<Load>());
-    ASSERT_EQ(inst->From(), var->Result());
-    EXPECT_EQ(inst->Result()->Type(), store_type);
+    ASSERT_EQ(inst->From(), var->Result(0));
+    EXPECT_EQ(inst->Result(0)->Type(), store_type);
 
     auto* result = inst->From()->As<InstructionResult>();
     ASSERT_NE(result, nullptr);
-    ASSERT_TRUE(result->Source()->Is<ir::Var>());
-    EXPECT_EQ(result->Source(), var);
+    ASSERT_TRUE(result->Instruction()->Is<ir::Var>());
+    EXPECT_EQ(result->Instruction(), var);
 }
 
 TEST_F(IR_LoadTest, Usage) {
@@ -66,10 +66,9 @@
     auto* var = b.Var(ty.ptr<function, i32>());
     auto* inst = b.Load(var);
 
-    EXPECT_TRUE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
-    EXPECT_TRUE(inst->Result()->Is<InstructionResult>());
-    EXPECT_EQ(inst->Result()->Source(), inst);
+    EXPECT_EQ(inst->Results().Length(), 1u);
+    EXPECT_TRUE(inst->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(inst->Result(0)->Instruction(), inst);
 }
 
 TEST_F(IR_LoadTest, Fail_NonPtr_Builder) {
@@ -90,10 +89,10 @@
     auto* new_inst = clone_ctx.Clone(inst);
 
     EXPECT_NE(inst, new_inst);
-    EXPECT_NE(nullptr, new_inst->Result());
-    EXPECT_NE(inst->Result(), new_inst->Result());
+    EXPECT_NE(nullptr, new_inst->Result(0));
+    EXPECT_NE(inst->Result(0), new_inst->Result(0));
 
-    EXPECT_EQ(new_var->Result(), new_inst->From());
+    EXPECT_EQ(new_var->Result(0), new_inst->From());
 }
 
 }  // namespace
diff --git a/src/tint/lang/core/ir/load_vector_element.cc b/src/tint/lang/core/ir/load_vector_element.cc
index 525043b..4db0a53 100644
--- a/src/tint/lang/core/ir/load_vector_element.cc
+++ b/src/tint/lang/core/ir/load_vector_element.cc
@@ -45,7 +45,7 @@
 LoadVectorElement::~LoadVectorElement() = default;
 
 LoadVectorElement* LoadVectorElement::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* from = ctx.Remap(From());
     auto* index = ctx.Remap(Index());
     return ctx.ir.instructions.Create<LoadVectorElement>(new_result, from, index);
diff --git a/src/tint/lang/core/ir/load_vector_element.h b/src/tint/lang/core/ir/load_vector_element.h
index e883c79..f8cbb37 100644
--- a/src/tint/lang/core/ir/load_vector_element.h
+++ b/src/tint/lang/core/ir/load_vector_element.h
@@ -57,11 +57,17 @@
     /// @returns the vector pointer value
     ir::Value* From() { return operands_[kFromOperandOffset]; }
 
+    /// @returns the vector pointer value
+    const ir::Value* From() const { return operands_[kFromOperandOffset]; }
+
     /// @returns the new vector element index
     ir::Value* Index() { return operands_[kIndexOperandOffset]; }
 
+    /// @returns the new vector element index
+    const ir::Value* Index() const { return operands_[kIndexOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "load_vector_element"; }
+    std::string FriendlyName() const override { return "load_vector_element"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load_vector_element_test.cc b/src/tint/lang/core/ir/load_vector_element_test.cc
index 78bcd44..0f69ec1 100644
--- a/src/tint/lang/core/ir/load_vector_element_test.cc
+++ b/src/tint/lang/core/ir/load_vector_element_test.cc
@@ -44,7 +44,7 @@
     auto* inst = b.LoadVectorElement(from, 2_i);
 
     ASSERT_TRUE(inst->Is<LoadVectorElement>());
-    ASSERT_EQ(inst->From(), from->Result());
+    ASSERT_EQ(inst->From(), from->Result(0));
 
     ASSERT_TRUE(inst->Index()->Is<Constant>());
     auto index = inst->Index()->As<Constant>()->Value();
@@ -67,8 +67,7 @@
     auto* from = b.Var(ty.ptr<private_, vec3<i32>>());
     auto* inst = b.LoadVectorElement(from, 2_i);
 
-    EXPECT_TRUE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_EQ(inst->Results().Length(), 1u);
 }
 
 TEST_F(IR_LoadVectorElementTest, Clone) {
@@ -79,10 +78,10 @@
     auto* new_inst = clone_ctx.Clone(inst);
 
     EXPECT_NE(inst, new_inst);
-    EXPECT_NE(nullptr, new_inst->Result());
-    EXPECT_NE(inst->Result(), new_inst->Result());
+    EXPECT_NE(nullptr, new_inst->Result(0));
+    EXPECT_NE(inst->Result(0), new_inst->Result(0));
 
-    EXPECT_EQ(new_from->Result(), new_inst->From());
+    EXPECT_EQ(new_from->Result(0), new_inst->From());
 
     auto new_idx = new_inst->Index()->As<Constant>()->Value();
     ASSERT_TRUE(new_idx->Is<core::constant::Scalar<i32>>());
diff --git a/src/tint/lang/core/ir/loop.cc b/src/tint/lang/core/ir/loop.cc
index efb0cd7..a5e3519 100644
--- a/src/tint/lang/core/ir/loop.cc
+++ b/src/tint/lang/core/ir/loop.cc
@@ -85,7 +85,7 @@
 }
 
 bool Loop::HasInitializer() {
-    return initializer_->HasTerminator();
+    return initializer_->Terminator() != nullptr;
 }
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/loop.h b/src/tint/lang/core/ir/loop.h
index 906032f..c22b8b9 100644
--- a/src/tint/lang/core/ir/loop.h
+++ b/src/tint/lang/core/ir/loop.h
@@ -31,6 +31,7 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/control_instruction.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 
 // Forward declarations
 namespace tint::core::ir {
@@ -87,6 +88,9 @@
     /// @returns the switch initializer block
     ir::Block* Initializer() { return initializer_; }
 
+    /// @returns the switch initializer block
+    const ir::Block* Initializer() const { return initializer_; }
+
     /// @returns true if the loop uses an initializer block. If true, then the Loop first branches
     /// to the initializer block, otherwise it first branches to the body block.
     bool HasInitializer();
@@ -94,16 +98,22 @@
     /// @returns the switch start block
     ir::MultiInBlock* Body() { return body_; }
 
+    /// @returns the switch start block
+    const ir::MultiInBlock* Body() const { return body_; }
+
     /// @returns the switch continuing block
     ir::MultiInBlock* Continuing() { return continuing_; }
 
+    /// @returns the switch continuing block
+    const ir::MultiInBlock* Continuing() const { return continuing_; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "loop"; }
+    std::string FriendlyName() const override { return "loop"; }
 
   private:
-    ir::Block* initializer_ = nullptr;
-    ir::MultiInBlock* body_ = nullptr;
-    ir::MultiInBlock* continuing_ = nullptr;
+    ConstPropagatingPtr<ir::Block> initializer_;
+    ConstPropagatingPtr<ir::MultiInBlock> body_;
+    ConstPropagatingPtr<ir::MultiInBlock> continuing_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/loop_test.cc b/src/tint/lang/core/ir/loop_test.cc
index df6abcc..b8721e2 100644
--- a/src/tint/lang/core/ir/loop_test.cc
+++ b/src/tint/lang/core/ir/loop_test.cc
@@ -44,8 +44,7 @@
 
 TEST_F(IR_LoopTest, Result) {
     auto* loop = b.Loop();
-    EXPECT_FALSE(loop->HasResults());
-    EXPECT_FALSE(loop->HasMultiResults());
+    EXPECT_TRUE(loop->Results().IsEmpty());
 }
 
 TEST_F(IR_LoopTest, Fail_NullInitializerBlock) {
@@ -83,7 +82,7 @@
     auto* new_loop = clone_ctx.Clone(loop);
 
     EXPECT_NE(loop, new_loop);
-    EXPECT_FALSE(new_loop->HasResults());
+    EXPECT_TRUE(new_loop->Results().IsEmpty());
     EXPECT_EQ(0u, new_loop->Exits().Count());
     EXPECT_NE(nullptr, new_loop->Initializer());
     EXPECT_NE(loop->Initializer(), new_loop->Initializer());
diff --git a/src/tint/lang/core/ir/module.cc b/src/tint/lang/core/ir/module.cc
index a6f2c5b..4738c14 100644
--- a/src/tint/lang/core/ir/module.cc
+++ b/src/tint/lang/core/ir/module.cc
@@ -41,18 +41,20 @@
 
 Module& Module::operator=(Module&&) = default;
 
-Symbol Module::NameOf(Instruction* inst) {
-    TINT_ASSERT(inst->HasResults() && !inst->HasMultiResults());
-    return NameOf(inst->Result());
+Symbol Module::NameOf(const Instruction* inst) const {
+    if (inst->Results().Length() != 1) {
+        return Symbol{};
+    }
+    return NameOf(inst->Result(0));
 }
 
-Symbol Module::NameOf(Value* value) {
+Symbol Module::NameOf(const Value* value) const {
     return value_to_name_.Get(value).value_or(Symbol{});
 }
 
 void Module::SetName(Instruction* inst, std::string_view name) {
-    TINT_ASSERT(inst->HasResults() && !inst->HasMultiResults());
-    return SetName(inst->Result(), name);
+    TINT_ASSERT(inst->Results().Length() == 1);
+    return SetName(inst->Result(0), name);
 }
 
 void Module::SetName(Value* value, std::string_view name) {
@@ -65,4 +67,8 @@
     value_to_name_.Replace(value, name);
 }
 
+void Module::ClearName(Value* value) {
+    value_to_name_.Remove(value);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/module.h b/src/tint/lang/core/ir/module.h
index ac7b99b..fba89fb 100644
--- a/src/tint/lang/core/ir/module.h
+++ b/src/tint/lang/core/ir/module.h
@@ -38,6 +38,7 @@
 #include "src/tint/lang/core/ir/instruction.h"
 #include "src/tint/lang/core/ir/value.h"
 #include "src/tint/lang/core/type/manager.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/diagnostic/source.h"
 #include "src/tint/utils/id/generation_id.h"
@@ -53,7 +54,7 @@
     GenerationID prog_id_;
 
     /// Map of value to name
-    Hashmap<Value*, Symbol, 32> value_to_name_;
+    Hashmap<const Value*, Symbol, 32> value_to_name_;
 
   public:
     /// Constructor
@@ -71,12 +72,12 @@
 
     /// @param inst the instruction
     /// @return the name of the given instruction, or an invalid symbol if the instruction is not
-    /// named. Requires that the instruction only has a single return value.
-    Symbol NameOf(Instruction* inst);
+    /// named or does not have a single return value.
+    Symbol NameOf(const Instruction* inst) const;
 
     /// @param value the value
     /// @return the name of the given value, or an invalid symbol if the value is not named.
-    Symbol NameOf(Value* value);
+    Symbol NameOf(const Value* value) const;
 
     /// @param inst the instruction to set the name of
     /// @param name the desired name of the value. May be suffixed on collision.
@@ -91,9 +92,16 @@
     /// @param name the desired name of the value
     void SetName(Value* value, Symbol name);
 
+    /// Removes the name from @p value
+    /// @param value the value to remove the name from
+    void ClearName(Value* value);
+
     /// @return the type manager for the module
     core::type::Manager& Types() { return constant_values.types; }
 
+    /// @return the type manager for the module
+    const core::type::Manager& Types() const { return constant_values.types; }
+
     /// The block allocator
     BlockAllocator<Block> blocks;
 
@@ -107,19 +115,16 @@
     BlockAllocator<Value> values;
 
     /// List of functions in the program
-    Vector<Function*, 8> functions;
+    Vector<ConstPropagatingPtr<Function>, 8> functions;
 
     /// The block containing module level declarations, if any exist.
-    Block* root_block = nullptr;
+    ConstPropagatingPtr<Block> root_block;
 
     /// The symbol table for the module
     SymbolTable symbols{prog_id_};
 
     /// The map of core::constant::Value to their ir::Constant.
     Hashmap<const core::constant::Value*, ir::Constant*, 16> constants;
-
-    /// If the module generated a validation error, will store the file for the disassembly text.
-    std::unique_ptr<Source::File> disassembly_file;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/multi_in_block.h b/src/tint/lang/core/ir/multi_in_block.h
index ed2b117..76915f1 100644
--- a/src/tint/lang/core/ir/multi_in_block.h
+++ b/src/tint/lang/core/ir/multi_in_block.h
@@ -31,11 +31,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/ir/block.h"
-
-// Forward declarations
-namespace tint::core::ir {
-class BlockParam;
-}
+#include "src/tint/lang/core/ir/block_param.h"
 
 namespace tint::core::ir {
 
@@ -63,7 +59,10 @@
     void SetParams(std::initializer_list<BlockParam*> params);
 
     /// @returns the params to the block
-    const Vector<BlockParam*, 2>& Params() { return params_; }
+    VectorRef<BlockParam*> Params() { return params_; }
+
+    /// @returns the params to the block
+    VectorRef<const BlockParam*> Params() const { return params_; }
 
     /// @returns branches made to this block by sibling blocks
     const VectorRef<ir::Terminator*> InboundSiblingBranches() { return inbound_sibling_branches_; }
diff --git a/src/tint/lang/core/ir/next_iteration.h b/src/tint/lang/core/ir/next_iteration.h
index f14be17..f859ee0 100644
--- a/src/tint/lang/core/ir/next_iteration.h
+++ b/src/tint/lang/core/ir/next_iteration.h
@@ -31,6 +31,7 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/terminator.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 #include "src/tint/utils/rtti/castable.h"
 
 // Forward declarations
@@ -58,11 +59,14 @@
     /// @returns the loop being iterated
     ir::Loop* Loop() { return loop_; }
 
+    /// @returns the loop being iterated
+    const ir::Loop* Loop() const { return loop_; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "next_iteration"; }
+    std::string FriendlyName() const override { return "next_iteration"; }
 
   private:
-    ir::Loop* loop_ = nullptr;
+    ConstPropagatingPtr<ir::Loop> loop_;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/next_iteration_test.cc b/src/tint/lang/core/ir/next_iteration_test.cc
index 75cdca1..249bd63 100644
--- a/src/tint/lang/core/ir/next_iteration_test.cc
+++ b/src/tint/lang/core/ir/next_iteration_test.cc
@@ -48,8 +48,7 @@
 TEST_F(IR_NextIterationTest, Result) {
     auto* inst = b.NextIteration(b.Loop());
 
-    EXPECT_FALSE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Results().IsEmpty());
 }
 
 TEST_F(IR_NextIterationTest, Clone) {
diff --git a/src/tint/lang/core/ir/operand_instruction.h b/src/tint/lang/core/ir/operand_instruction.h
index 624eeb8..2b3f83a 100644
--- a/src/tint/lang/core/ir/operand_instruction.h
+++ b/src/tint/lang/core/ir/operand_instruction.h
@@ -94,25 +94,29 @@
     /// @returns the operands of the instruction
     VectorRef<ir::Value*> Operands() override { return operands_; }
 
-    /// @returns true if the instruction has result values
-    bool HasResults() override { return !results_.IsEmpty(); }
-    /// @returns true if the instruction has multiple values
-    bool HasMultiResults() override { return results_.Length() > 1; }
-
-    /// @returns the first result. Returns `nullptr` if there are no results, or if ther are
-    /// multi-results
-    InstructionResult* Result() override {
-        if (!HasResults() || HasMultiResults()) {
-            return nullptr;
-        }
-        return results_[0];
-    }
-
-    using Instruction::Result;
+    /// @returns the operands of the instruction
+    VectorRef<const ir::Value*> Operands() const override { return operands_; }
 
     /// @returns the result values for this instruction
     VectorRef<InstructionResult*> Results() override { return results_; }
 
+    /// @returns the result values for this instruction
+    VectorRef<const InstructionResult*> Results() const override { return results_; }
+
+    /// @param idx the index of the result
+    /// @returns the result with index @p idx, or `nullptr` if there are no results or the index is
+    /// out of bounds.
+    InstructionResult* Result(size_t idx) {
+        return idx < results_.Length() ? results_[idx] : nullptr;
+    }
+
+    /// @param idx the index of the result
+    /// @returns the result with index @p idx, or `nullptr` if there are no results or the index is
+    /// out of bounds.
+    const InstructionResult* Result(size_t idx) const {
+        return idx < results_.Length() ? results_[idx] : nullptr;
+    }
+
   protected:
     /// Append a new operand to the operand list for this instruction.
     /// @param idx the index the operand should be at
@@ -141,7 +145,7 @@
     /// @param value the value to append
     void AddResult(InstructionResult* value) {
         if (value) {
-            value->SetSource(this);
+            value->SetInstruction(this);
         }
         results_.Push(value);
     }
diff --git a/src/tint/lang/core/ir/operand_instruction_test.cc b/src/tint/lang/core/ir/operand_instruction_test.cc
index b66a600..b82c9d5 100644
--- a/src/tint/lang/core/ir/operand_instruction_test.cc
+++ b/src/tint/lang/core/ir/operand_instruction_test.cc
@@ -45,14 +45,14 @@
     EXPECT_EQ(inst->Block(), block);
     EXPECT_THAT(lhs->Usages(), testing::ElementsAre(Usage{inst, 0u}));
     EXPECT_THAT(rhs->Usages(), testing::ElementsAre(Usage{inst, 1u}));
-    EXPECT_TRUE(inst->Result()->Alive());
+    EXPECT_TRUE(inst->Result(0)->Alive());
 
     inst->Destroy();
 
     EXPECT_EQ(inst->Block(), nullptr);
     EXPECT_TRUE(lhs->Usages().IsEmpty());
     EXPECT_TRUE(rhs->Usages().IsEmpty());
-    EXPECT_FALSE(inst->Result()->Alive());
+    EXPECT_FALSE(inst->Result(0)->Alive());
 }
 
 TEST_F(IR_OperandInstructionTest, ClearOperands_WithNullOperand) {
@@ -63,7 +63,7 @@
 
     inst->Destroy();
     EXPECT_EQ(inst->Block(), nullptr);
-    EXPECT_FALSE(inst->Result()->Alive());
+    EXPECT_FALSE(inst->Result(0)->Alive());
 }
 
 TEST_F(IR_OperandInstructionTest, SetOperands_WithNullOperand) {
diff --git a/src/tint/lang/core/ir/return.cc b/src/tint/lang/core/ir/return.cc
index a854d62..f105afc 100644
--- a/src/tint/lang/core/ir/return.cc
+++ b/src/tint/lang/core/ir/return.cc
@@ -43,7 +43,7 @@
 
 Return::Return(Function* func, ir::Value* arg) {
     AddOperand(Return::kFunctionOperandOffset, func);
-    AddOperand(Return::kArgOperandOffset, arg);
+    AddOperand(Return::kArgsOperandOffset, arg);
 }
 
 Return::~Return() = default;
@@ -56,7 +56,11 @@
     return ctx.ir.instructions.Create<Return>(fn);
 }
 
-Function* Return::Func() const {
+Function* Return::Func() {
+    return tint::As<Function>(operands_[kFunctionOperandOffset]);
+}
+
+const Function* Return::Func() const {
     return tint::As<Function>(operands_[kFunctionOperandOffset]);
 }
 
diff --git a/src/tint/lang/core/ir/return.h b/src/tint/lang/core/ir/return.h
index 412369f..4fcdc86 100644
--- a/src/tint/lang/core/ir/return.h
+++ b/src/tint/lang/core/ir/return.h
@@ -47,7 +47,7 @@
     static constexpr size_t kFunctionOperandOffset = 0;
 
     /// The offset in Operands() for the return argument
-    static constexpr size_t kArgOperandOffset = 1;
+    static constexpr size_t kArgsOperandOffset = 1;
 
     /// Constructor (no return value)
     /// @param func the function being returned
@@ -64,24 +64,25 @@
     Return* Clone(CloneContext& ctx) override;
 
     /// @returns the function being returned
-    Function* Func() const;
+    Function* Func();
+
+    /// @returns the function being returned
+    const Function* Func() const;
 
     /// @returns the return value, or nullptr
     ir::Value* Value() const {
-        return operands_.Length() > kArgOperandOffset ? operands_[kArgOperandOffset] : nullptr;
+        return operands_.Length() > kArgsOperandOffset ? operands_[kArgsOperandOffset] : nullptr;
     }
 
     /// Sets the return value
     /// @param val the new return value
-    void SetValue(ir::Value* val) { SetOperand(kArgOperandOffset, val); }
+    void SetValue(ir::Value* val) { SetOperand(kArgsOperandOffset, val); }
 
-    /// @returns the return arguments
-    tint::Slice<ir::Value* const> Args() override {
-        return operands_.Slice().Offset(kArgOperandOffset);
-    }
+    /// @returns the offset of the arguments in Operands()
+    size_t ArgsOperandOffset() const override { return kArgsOperandOffset; }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "return"; }
+    std::string FriendlyName() const override { return "return"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/return_test.cc b/src/tint/lang/core/ir/return_test.cc
index f019437..dfd088b 100644
--- a/src/tint/lang/core/ir/return_test.cc
+++ b/src/tint/lang/core/ir/return_test.cc
@@ -64,14 +64,12 @@
 
     {
         auto* ret1 = b.Return(vfunc);
-        EXPECT_FALSE(ret1->HasResults());
-        EXPECT_FALSE(ret1->HasMultiResults());
+        EXPECT_TRUE(ret1->Results().IsEmpty());
     }
 
     {
         auto* ret2 = b.Return(ifunc, b.Constant(42_i));
-        EXPECT_FALSE(ret2->HasResults());
-        EXPECT_FALSE(ret2->HasMultiResults());
+        EXPECT_TRUE(ret2->Results().IsEmpty());
     }
 }
 
diff --git a/src/tint/lang/core/ir/store.h b/src/tint/lang/core/ir/store.h
index a5d8444..9ed7cc5 100644
--- a/src/tint/lang/core/ir/store.h
+++ b/src/tint/lang/core/ir/store.h
@@ -56,11 +56,17 @@
     /// @returns the value being stored too
     Value* To() { return operands_[kToOperandOffset]; }
 
+    /// @returns the value being stored too
+    const Value* To() const { return operands_[kToOperandOffset]; }
+
     /// @returns the value being stored
     Value* From() { return operands_[kFromOperandOffset]; }
 
+    /// @returns the value being stored
+    const Value* From() const { return operands_[kFromOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "store"; }
+    std::string FriendlyName() const override { return "store"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store_test.cc b/src/tint/lang/core/ir/store_test.cc
index aff0b12..28e4d85 100644
--- a/src/tint/lang/core/ir/store_test.cc
+++ b/src/tint/lang/core/ir/store_test.cc
@@ -44,7 +44,7 @@
     auto* inst = b.Store(to, 4_i);
 
     ASSERT_TRUE(inst->Is<Store>());
-    ASSERT_EQ(inst->To(), to->Result());
+    ASSERT_EQ(inst->To(), to->Result(0));
 
     ASSERT_TRUE(inst->From()->Is<Constant>());
     auto lhs = inst->From()->As<Constant>()->Value();
@@ -67,8 +67,7 @@
     auto* to = b.Var(ty.ptr<private_, i32>());
     auto* inst = b.Store(to, 4_i);
 
-    EXPECT_FALSE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Results().IsEmpty());
 }
 
 TEST_F(IR_StoreTest, Clone) {
@@ -79,7 +78,7 @@
     auto* new_s = clone_ctx.Clone(s);
 
     EXPECT_NE(s, new_s);
-    EXPECT_EQ(new_v->Result(), new_s->To());
+    EXPECT_EQ(new_v->Result(0), new_s->To());
 
     auto new_from = new_s->From()->As<Constant>()->Value();
     ASSERT_TRUE(new_from->Is<core::constant::Scalar<i32>>());
diff --git a/src/tint/lang/core/ir/store_vector_element.h b/src/tint/lang/core/ir/store_vector_element.h
index f76ec79..2ef3895 100644
--- a/src/tint/lang/core/ir/store_vector_element.h
+++ b/src/tint/lang/core/ir/store_vector_element.h
@@ -60,14 +60,23 @@
     /// @returns the vector pointer value
     ir::Value* To() { return operands_[kToOperandOffset]; }
 
+    /// @returns the vector pointer value
+    const ir::Value* To() const { return operands_[kToOperandOffset]; }
+
     /// @returns the new vector element index
     ir::Value* Index() { return operands_[kIndexOperandOffset]; }
 
+    /// @returns the new vector element index
+    const ir::Value* Index() const { return operands_[kIndexOperandOffset]; }
+
     /// @returns the new vector element value
     ir::Value* Value() { return operands_[kValueOperandOffset]; }
 
+    /// @returns the new vector element value
+    const ir::Value* Value() const { return operands_[kValueOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "store_vector_element"; }
+    std::string FriendlyName() const override { return "store_vector_element"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store_vector_element_test.cc b/src/tint/lang/core/ir/store_vector_element_test.cc
index 74664ba..971b12b 100644
--- a/src/tint/lang/core/ir/store_vector_element_test.cc
+++ b/src/tint/lang/core/ir/store_vector_element_test.cc
@@ -44,7 +44,7 @@
     auto* inst = b.StoreVectorElement(to, 2_i, 4_i);
 
     ASSERT_TRUE(inst->Is<StoreVectorElement>());
-    ASSERT_EQ(inst->To(), to->Result());
+    ASSERT_EQ(inst->To(), to->Result(0));
 
     ASSERT_TRUE(inst->Index()->Is<Constant>());
     auto index = inst->Index()->As<Constant>()->Value();
@@ -75,8 +75,7 @@
     auto* to = b.Var(ty.ptr<private_, vec3<i32>>());
     auto* inst = b.StoreVectorElement(to, 2_i, 4_i);
 
-    EXPECT_FALSE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Results().IsEmpty());
 }
 
 TEST_F(IR_StoreVectorElementTest, Clone) {
@@ -87,7 +86,7 @@
     auto* new_inst = clone_ctx.Clone(inst);
 
     EXPECT_NE(inst, new_inst);
-    EXPECT_EQ(new_to->Result(), new_inst->To());
+    EXPECT_EQ(new_to->Result(0), new_inst->To());
 
     auto new_idx = new_inst->Index()->As<Constant>()->Value();
     ASSERT_TRUE(new_idx->Is<core::constant::Scalar<i32>>());
diff --git a/src/tint/lang/core/ir/switch.cc b/src/tint/lang/core/ir/switch.cc
index 3880fa0..30e08bf 100644
--- a/src/tint/lang/core/ir/switch.cc
+++ b/src/tint/lang/core/ir/switch.cc
@@ -27,6 +27,8 @@
 
 #include "src/tint/lang/core/ir/switch.h"
 
+#include <utility>
+
 #include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
@@ -45,7 +47,7 @@
 
 void Switch::ForeachBlock(const std::function<void(ir::Block*)>& cb) {
     for (auto& c : cases_) {
-        cb(c.Block());
+        cb(c.block);
     }
 }
 
@@ -55,17 +57,17 @@
     ctx.Replace(this, new_switch);
 
     new_switch->cases_.Reserve(cases_.Length());
-    for (const auto& cse : cases_) {
+    for (auto& cse : cases_) {
         Switch::Case new_case{};
         new_case.block = ctx.ir.blocks.Create<ir::Block>();
         cse.block->CloneInto(ctx, new_case.block);
 
         new_case.selectors.Reserve(cse.selectors.Length());
-        for (const auto& sel : cse.selectors) {
+        for (auto& sel : cse.selectors) {
             auto* new_val = sel.val ? ctx.Clone(sel.val) : nullptr;
             new_case.selectors.Push(Switch::CaseSelector{new_val});
         }
-        new_switch->cases_.Push(new_case);
+        new_switch->cases_.Push(std::move(new_case));
     }
 
     new_switch->SetResults(ctx.Clone(results_));
diff --git a/src/tint/lang/core/ir/switch.h b/src/tint/lang/core/ir/switch.h
index 4ca3965..3b44161 100644
--- a/src/tint/lang/core/ir/switch.h
+++ b/src/tint/lang/core/ir/switch.h
@@ -29,8 +29,10 @@
 #define SRC_TINT_LANG_CORE_IR_SWITCH_H_
 
 #include <string>
+#include <utility>
 
 #include "src/tint/lang/core/ir/control_instruction.h"
+#include "src/tint/utils/containers/const_propagating_ptr.h"
 
 // Forward declarations
 namespace tint::core::ir {
@@ -64,21 +66,19 @@
     /// A case selector
     struct CaseSelector {
         /// @returns true if this is a default selector
-        bool IsDefault() { return val == nullptr; }
+        bool IsDefault() const { return val == nullptr; }
 
         /// The selector value, or nullptr if this is the default selector
-        Constant* val = nullptr;
+        ConstPropagatingPtr<Constant> val;
     };
 
     /// A case label in the struct
     struct Case {
         /// The case selector for this node
         Vector<CaseSelector, 4> selectors;
-        /// The case block.
-        ir::Block* block = nullptr;
 
-        /// @returns the case block
-        ir::Block* Block() { return block; }
+        /// The case block.
+        ConstPropagatingPtr<ir::Block> block;
     };
 
     /// Constructor
@@ -95,11 +95,17 @@
     /// @returns the switch cases
     Vector<Case, 4>& Cases() { return cases_; }
 
+    /// @returns the switch cases
+    VectorRef<Case> Cases() const { return cases_; }
+
     /// @returns the condition
     Value* Condition() { return operands_[kConditionOperandOffset]; }
 
+    /// @returns the condition
+    const Value* Condition() const { return operands_[kConditionOperandOffset]; }
+
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "switch"; }
+    std::string FriendlyName() const override { return "switch"; }
 
   private:
     Vector<Case, 4> cases_;
diff --git a/src/tint/lang/core/ir/switch_test.cc b/src/tint/lang/core/ir/switch_test.cc
index f71b256..7391458 100644
--- a/src/tint/lang/core/ir/switch_test.cc
+++ b/src/tint/lang/core/ir/switch_test.cc
@@ -47,21 +47,19 @@
 TEST_F(IR_SwitchTest, Results) {
     auto* cond = b.Constant(true);
     auto* switch_ = b.Switch(cond);
-    EXPECT_FALSE(switch_->HasResults());
-    EXPECT_FALSE(switch_->HasMultiResults());
+    EXPECT_TRUE(switch_->Results().IsEmpty());
 }
 
 TEST_F(IR_SwitchTest, Parent) {
     auto* switch_ = b.Switch(1_i);
-    b.Case(switch_, {Switch::CaseSelector{nullptr}});
-    EXPECT_THAT(switch_->Cases().Front().Block()->Parent(), switch_);
+    b.DefaultCase(switch_);
+    EXPECT_THAT(switch_->Cases().Front().block->Parent(), switch_);
 }
 
 TEST_F(IR_SwitchTest, Clone) {
     auto* switch_ = b.Switch(1_i);
-    switch_->Cases().Push(
-        Switch::Case{{Switch::CaseSelector{}, Switch::CaseSelector{b.Constant(2_i)}}, b.Block()});
-    switch_->Cases().Push(Switch::Case{{Switch::CaseSelector{b.Constant(3_i)}}, b.Block()});
+    b.Case(switch_, {nullptr, b.Constant(2_i)});
+    b.Case(switch_, {b.Constant(3_i)});
 
     auto* new_switch = clone_ctx.Clone(switch_);
 
@@ -103,9 +101,8 @@
     {
         auto* switch_ = b.Switch(1_i);
 
-        auto* blk = b.Block();
+        auto* blk = b.Case(switch_, {b.Constant(3_i)});
         b.Append(blk, [&] { b.ExitSwitch(switch_); });
-        switch_->Cases().Push(Switch::Case{{Switch::CaseSelector{b.Constant(3_i)}}, blk});
         new_switch = clone_ctx.Clone(switch_);
     }
 
@@ -122,9 +119,8 @@
         auto* switch_ = b.Switch(1_i);
         switch_->SetResults(Vector{r0, r1});
 
-        auto* blk = b.Block();
+        auto* blk = b.Case(switch_, Vector{b.Constant(3_i)});
         b.Append(blk, [&] { b.ExitSwitch(switch_, b.Constant(42_i), b.Constant(42_f)); });
-        switch_->Cases().Push(Switch::Case{{Switch::CaseSelector{b.Constant(3_i)}}, blk});
         new_switch = clone_ctx.Clone(switch_);
     }
 
diff --git a/src/tint/lang/core/ir/swizzle.cc b/src/tint/lang/core/ir/swizzle.cc
index 2c10907..1a1e6dc 100644
--- a/src/tint/lang/core/ir/swizzle.cc
+++ b/src/tint/lang/core/ir/swizzle.cc
@@ -53,7 +53,7 @@
 Swizzle::~Swizzle() = default;
 
 Swizzle* Swizzle::Clone(CloneContext& ctx) {
-    auto* result = ctx.Clone(Result());
+    auto* result = ctx.Clone(Result(0));
     auto* obj = ctx.Remap(Object());
     return ctx.ir.instructions.Create<Swizzle>(result, obj, indices_);
 }
diff --git a/src/tint/lang/core/ir/swizzle.h b/src/tint/lang/core/ir/swizzle.h
index afda992..1e5b0fc 100644
--- a/src/tint/lang/core/ir/swizzle.h
+++ b/src/tint/lang/core/ir/swizzle.h
@@ -54,11 +54,14 @@
     /// @returns the object used for the access
     Value* Object() { return operands_[kObjectOperandOffset]; }
 
+    /// @returns the object used for the access
+    const Value* Object() const { return operands_[kObjectOperandOffset]; }
+
     /// @returns the swizzle indices
-    VectorRef<uint32_t> Indices() { return indices_; }
+    VectorRef<uint32_t> Indices() const { return indices_; }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "swizzle"; }
+    std::string FriendlyName() const override { return "swizzle"; }
 
   private:
     Vector<uint32_t, 4> indices_;
diff --git a/src/tint/lang/core/ir/swizzle_test.cc b/src/tint/lang/core/ir/swizzle_test.cc
index 153db7b..e66a578 100644
--- a/src/tint/lang/core/ir/swizzle_test.cc
+++ b/src/tint/lang/core/ir/swizzle_test.cc
@@ -42,17 +42,16 @@
     auto* var = b.Var(ty.ptr<function, i32>());
     auto* a = b.Swizzle(mod.Types().i32(), var, {1u});
 
-    EXPECT_THAT(var->Result()->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
+    EXPECT_THAT(var->Result(0)->Usages(), testing::UnorderedElementsAre(Usage{a, 0u}));
 }
 
 TEST_F(IR_SwizzleTest, Results) {
     auto* var = b.Var(ty.ptr<function, i32>());
     auto* a = b.Swizzle(mod.Types().i32(), var, {1u});
 
-    EXPECT_TRUE(a->HasResults());
-    EXPECT_FALSE(a->HasMultiResults());
-    EXPECT_TRUE(a->Result()->Is<InstructionResult>());
-    EXPECT_EQ(a->Result()->Source(), a);
+    EXPECT_EQ(a->Results().Length(), 1u);
+    EXPECT_TRUE(a->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(a->Result(0)->Instruction(), a);
 }
 
 TEST_F(IR_SwizzleTest, Fail_NullType) {
@@ -107,10 +106,10 @@
     auto* new_s = clone_ctx.Clone(s);
 
     EXPECT_NE(s, new_s);
-    EXPECT_NE(nullptr, new_s->Result());
-    EXPECT_NE(s->Result(), new_s->Result());
+    EXPECT_NE(nullptr, new_s->Result(0));
+    EXPECT_NE(s->Result(0), new_s->Result(0));
 
-    EXPECT_EQ(new_var->Result(), new_s->Object());
+    EXPECT_EQ(new_var->Result(0), new_s->Object());
 
     EXPECT_EQ(1u, new_s->Indices().Length());
     EXPECT_EQ(2u, new_s->Indices().Front());
diff --git a/src/tint/lang/core/ir/terminate_invocation.h b/src/tint/lang/core/ir/terminate_invocation.h
index 7697c78..b0308bb 100644
--- a/src/tint/lang/core/ir/terminate_invocation.h
+++ b/src/tint/lang/core/ir/terminate_invocation.h
@@ -43,7 +43,7 @@
     TerminateInvocation* Clone(CloneContext& ctx) override;
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "terminate_invocation"; }
+    std::string FriendlyName() const override { return "terminate_invocation"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/terminator.h b/src/tint/lang/core/ir/terminator.h
index 1f12541..f02c7b2 100644
--- a/src/tint/lang/core/ir/terminator.h
+++ b/src/tint/lang/core/ir/terminator.h
@@ -44,8 +44,16 @@
   public:
     ~Terminator() override;
 
-    /// @returns the terminator arguments
-    virtual tint::Slice<Value* const> Args() { return operands_.Slice(); }
+    /// @returns the offset of the arguments in Operands()
+    virtual size_t ArgsOperandOffset() const { return 0; }
+
+    /// @returns the call arguments
+    tint::Slice<Value* const> Args() { return operands_.Slice().Offset(ArgsOperandOffset()); }
+
+    /// @returns the call arguments
+    tint::Slice<const Value* const> Args() const {
+        return operands_.Slice().Offset(ArgsOperandOffset());
+    }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index 8870a6b..b75b761 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -54,6 +54,7 @@
     "robustness.cc",
     "shader_io.cc",
     "std140.cc",
+    "value_to_let.cc",
     "vectorize_scalar_matrix_constructors.cc",
     "zero_init_workgroup_memory.cc",
   ],
@@ -73,6 +74,7 @@
     "robustness.h",
     "shader_io.h",
     "std140.h",
+    "value_to_let.h",
     "vectorize_scalar_matrix_constructors.h",
     "zero_init_workgroup_memory.h",
   ],
@@ -120,6 +122,7 @@
     "preserve_padding_test.cc",
     "robustness_test.cc",
     "std140_test.cc",
+    "value_to_let_test.cc",
     "vectorize_scalar_matrix_constructors_test.cc",
     "zero_init_workgroup_memory_test.cc",
   ] + select({
@@ -144,6 +147,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/lang/wgsl/writer/ir_to_program",
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 3465e11..76a01e3 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -69,6 +69,8 @@
   lang/core/ir/transform/shader_io.h
   lang/core/ir/transform/std140.cc
   lang/core/ir/transform/std140.h
+  lang/core/ir/transform/value_to_let.cc
+  lang/core/ir/transform/value_to_let.h
   lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
   lang/core/ir/transform/vectorize_scalar_matrix_constructors.h
   lang/core/ir/transform/zero_init_workgroup_memory.cc
@@ -118,6 +120,7 @@
   lang/core/ir/transform/preserve_padding_test.cc
   lang/core/ir/transform/robustness_test.cc
   lang/core/ir/transform/std140_test.cc
+  lang/core/ir/transform/value_to_let_test.cc
   lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc
   lang/core/ir/transform/zero_init_workgroup_memory_test.cc
 )
@@ -134,6 +137,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_lang_wgsl_writer_ir_to_program
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index 7343823..fbb325a 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -74,6 +74,8 @@
     "shader_io.h",
     "std140.cc",
     "std140.h",
+    "value_to_let.cc",
+    "value_to_let.h",
     "vectorize_scalar_matrix_constructors.cc",
     "vectorize_scalar_matrix_constructors.h",
     "zero_init_workgroup_memory.cc",
@@ -120,6 +122,7 @@
       "preserve_padding_test.cc",
       "robustness_test.cc",
       "std140_test.cc",
+      "value_to_let_test.cc",
       "vectorize_scalar_matrix_constructors_test.cc",
       "zero_init_workgroup_memory_test.cc",
     ]
@@ -136,6 +139,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
diff --git a/src/tint/lang/core/ir/transform/add_empty_entry_point.cc b/src/tint/lang/core/ir/transform/add_empty_entry_point.cc
index 4c86d58..d1b35b4 100644
--- a/src/tint/lang/core/ir/transform/add_empty_entry_point.cc
+++ b/src/tint/lang/core/ir/transform/add_empty_entry_point.cc
@@ -38,7 +38,7 @@
 namespace {
 
 void Run(ir::Module& ir) {
-    for (auto* func : ir.functions) {
+    for (auto& func : ir.functions) {
         if (func->Stage() != Function::PipelineStage::kUndefined) {
             return;
         }
diff --git a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
index 5d81e6b..0d9b9fa 100644
--- a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
@@ -63,7 +63,7 @@
                 if (!var) {
                     continue;
                 }
-                auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+                auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
                 if (!ptr) {
                     continue;
                 }
@@ -80,7 +80,7 @@
         }
 
         // Find function parameters that need to be replaced.
-        for (auto* func : ir.functions) {
+        for (auto& func : ir.functions) {
             for (uint32_t index = 0; index < func->Params().Length(); index++) {
                 auto* param = func->Params()[index];
                 auto* storage_texture = param->Type()->As<core::type::StorageTexture>();
@@ -108,7 +108,7 @@
         }
 
         // Replace all uses of the old variable with the new one.
-        ReplaceUses(old_var->Result(), new_var->Result());
+        ReplaceUses(old_var->Result(0), new_var->Result(0));
     }
 
     /// Replace a function parameter with one that uses rgba8unorm instead of bgra8unorm.
@@ -147,7 +147,7 @@
                     // Replace load instructions with new ones that have the updated type.
                     auto* new_load = b.Load(new_value);
                     new_load->InsertBefore(load);
-                    ReplaceUses(load->Result(), new_load->Result());
+                    ReplaceUses(load->Result(0), new_load->Result(0));
                     load->Destroy();
                 },
                 [&](CoreBuiltinCall* call) {
@@ -160,14 +160,14 @@
                         auto* value = call->Args()[index];
                         auto* swizzle = b.Swizzle(value->Type(), value, Vector{2u, 1u, 0u, 3u});
                         swizzle->InsertBefore(call);
-                        call->SetOperand(index, swizzle->Result());
+                        call->SetOperand(index, swizzle->Result(0));
                     } else if (call->Func() == core::BuiltinFn::kTextureLoad) {
                         // 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());
+                            b.Swizzle(call->Result(0)->Type(), nullptr, Vector{2u, 1u, 0u, 3u});
+                        call->Result(0)->ReplaceAllUsesWith(swizzle->Result(0));
                         swizzle->InsertAfter(call);
-                        swizzle->SetOperand(Swizzle::kObjectOperandOffset, call->Result());
+                        swizzle->SetOperand(Swizzle::kObjectOperandOffset, call->Result(0));
                     }
                 },
                 [&](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 a51835b..6357040 100644
--- a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill_test.cc
+++ b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill_test.cc
@@ -72,7 +72,7 @@
     auto* value = b.FunctionParam("value", ty.vec4<f32>());
     func->SetParams({value, coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         b.Call(ty.void_(), core::BuiltinFn::kTextureStore, load, coords, value);
         b.Return(func);
     });
@@ -145,7 +145,7 @@
     auto* value = b.FunctionParam("value", ty.vec4<f32>());
     func->SetParams({value, coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         b.Call(ty.void_(), core::BuiltinFn::kTextureStore, load, coords, value);
         b.Return(func);
     });
@@ -252,7 +252,7 @@
         auto* value = b.FunctionParam("value", ty.vec4<f32>());
         foo->SetParams({coords, value});
         b.Append(foo->Block(), [&] {
-            auto* load = b.Load(var->Result());
+            auto* load = b.Load(var->Result(0));
             b.Call(ty.void_(), bar, load, coords, value);
             b.Return(foo);
         });
@@ -342,9 +342,9 @@
         auto* value = b.FunctionParam("value", ty.vec4<f32>());
         foo->SetParams({coords, value});
         b.Append(foo->Block(), [&] {
-            auto* load_a = b.Load(var_a->Result());
-            auto* load_b = b.Load(var_b->Result());
-            auto* load_c = b.Load(var_c->Result());
+            auto* load_a = b.Load(var_a->Result(0));
+            auto* load_b = b.Load(var_b->Result(0));
+            auto* load_c = b.Load(var_c->Result(0));
             b.Call(ty.void_(), bar, load_a, load_b, load_c, coords, value);
             b.Return(foo);
         });
@@ -440,11 +440,11 @@
         auto* value = b.FunctionParam("value", ty.vec4<f32>());
         foo->SetParams({coords, value});
         b.Append(foo->Block(), [&] {
-            auto* load_a = b.Load(var_a->Result());
+            auto* load_a = b.Load(var_a->Result(0));
             b.Call(ty.void_(), core::BuiltinFn::kTextureStore, load_a, coords, value);
-            auto* load_b = b.Load(var_a->Result());
+            auto* load_b = b.Load(var_a->Result(0));
             b.Call(ty.void_(), bar, load_b, coords, value);
-            auto* load_c = b.Load(var_a->Result());
+            auto* load_c = b.Load(var_a->Result(0));
             b.Call(ty.void_(), bar, load_c, coords, value);
             b.Return(foo);
         });
@@ -527,7 +527,7 @@
     auto* value = b.FunctionParam("value", ty.vec4<f32>());
     func->SetParams({value, coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         b.Call(ty.void_(), core::BuiltinFn::kTextureStore, load, coords, index, value);
         b.Return(func);
     });
@@ -578,7 +578,7 @@
 
     auto* func = b.Function("foo", ty.vec2<u32>());
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* dims = b.Call(ty.vec2<u32>(), core::BuiltinFn::kTextureDimensions, load);
         b.Return(func, dims);
         mod.SetName(dims, "dims");
@@ -631,7 +631,7 @@
     auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
     func->SetParams({coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load, coords);
         b.Return(func, result);
         mod.SetName(result, "result");
@@ -685,7 +685,7 @@
     auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
     func->SetParams({coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load, coords);
         b.Call(ty.void_(), core::BuiltinFn::kTextureStore, load, coords, result);
         b.Return(func);
diff --git a/src/tint/lang/core/ir/transform/binary_polyfill.cc b/src/tint/lang/core/ir/transform/binary_polyfill.cc
index 276bec4..ba175c6 100644
--- a/src/tint/lang/core/ir/transform/binary_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/binary_polyfill.cc
@@ -76,7 +76,7 @@
                     case BinaryOp::kDivide:
                     case BinaryOp::kModulo:
                         if (config.int_div_mod &&
-                            binary->Result()->Type()->is_integer_scalar_or_vector()) {
+                            binary->Result(0)->Type()->is_integer_scalar_or_vector()) {
                             worklist.Push(binary);
                         }
                         break;
@@ -109,12 +109,12 @@
             }
             TINT_ASSERT_OR_RETURN(replacement);
 
-            if (replacement != binary->Result()) {
+            if (replacement != binary->Result(0)) {
                 // Replace the old binary instruction result with the new value.
-                if (auto name = ir.NameOf(binary->Result())) {
+                if (auto name = ir.NameOf(binary->Result(0))) {
                     ir.SetName(replacement, name);
                 }
-                binary->Result()->ReplaceAllUsesWith(replacement);
+                binary->Result(0)->ReplaceAllUsesWith(replacement);
                 binary->Destroy();
             }
         }
@@ -150,7 +150,7 @@
     /// @param binary the binary instruction
     /// @returns the replacement value
     ir::Value* IntDivMod(ir::Binary* binary) {
-        auto* result_ty = binary->Result()->Type();
+        auto* result_ty = binary->Result(0)->Type();
         bool is_div = binary->Op() == BinaryOp::kDivide;
         bool is_signed = result_ty->is_signed_integer_scalar_or_vector();
 
@@ -197,7 +197,7 @@
 
                 if (binary->Op() == BinaryOp::kDivide) {
                     // Perform the divide with the modified RHS.
-                    b.Return(func, b.Divide(result_ty, lhs, rhs_or_one)->Result());
+                    b.Return(func, b.Divide(result_ty, lhs, rhs_or_one)->Result(0));
                 } else if (binary->Op() == BinaryOp::kModulo) {
                     // Calculate the modulo manually, as modulo with negative operands is undefined
                     // behavior for many backends:
@@ -205,7 +205,7 @@
                     auto* whole = b.Divide(result_ty, lhs, rhs_or_one);
                     auto* remainder =
                         b.Subtract(result_ty, lhs, b.Multiply(result_ty, whole, rhs_or_one));
-                    b.Return(func, remainder->Result());
+                    b.Return(func, remainder->Result(0));
                 }
             });
             return func;
@@ -214,7 +214,7 @@
         /// Helper to splat a value to match the vector width of the result type if necessary.
         auto maybe_splat = [&](ir::Value* value) -> ir::Value* {
             if (value->Type()->Is<type::Scalar>() && result_ty->Is<core::type::Vector>()) {
-                return b.Construct(result_ty, value)->Result();
+                return b.Construct(result_ty, value)->Result(0);
             }
             return value;
         };
@@ -224,7 +224,7 @@
         b.InsertBefore(binary, [&] {
             auto* lhs = maybe_splat(binary->LHS());
             auto* rhs = maybe_splat(binary->RHS());
-            result = b.Call(result_ty, helper, lhs, rhs)->Result();
+            result = b.Call(result_ty, helper, lhs, rhs)->Result(0);
         });
         return result;
     }
@@ -238,8 +238,8 @@
         auto* mask = b.Constant(u32(lhs->Type()->DeepestElement()->Size() * 8 - 1));
         auto* masked = b.And(rhs->Type(), rhs, MatchWidth(mask, rhs->Type()));
         masked->InsertBefore(binary);
-        binary->SetOperand(ir::Binary::kRhsOperandOffset, masked->Result());
-        return binary->Result();
+        binary->SetOperand(ir::Binary::kRhsOperandOffset, masked->Result(0));
+        return binary->Result(0);
     }
 };
 
diff --git a/src/tint/lang/core/ir/transform/block_decorated_structs.cc b/src/tint/lang/core/ir/transform/block_decorated_structs.cc
index 32cbbd5..91d4b3d 100644
--- a/src/tint/lang/core/ir/transform/block_decorated_structs.cc
+++ b/src/tint/lang/core/ir/transform/block_decorated_structs.cc
@@ -56,7 +56,7 @@
         if (!var) {
             continue;
         }
-        auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+        auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
         if (!ptr || !core::IsHostShareable(ptr->AddressSpace())) {
             continue;
         }
@@ -65,7 +65,7 @@
 
     // Now process the buffer variables.
     for (auto* var : buffer_variables) {
-        auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+        auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
         auto* store_ty = ptr->StoreType();
 
         if (auto* str = store_ty->As<core::type::Struct>(); str && !str->HasFixedFootprint()) {
@@ -93,10 +93,10 @@
         // Replace uses of the old variable.
         // The structure has been wrapped, so replace all uses of the old variable with a member
         // accessor on the new variable.
-        var->Result()->ReplaceAllUsesWith([&](Usage use) -> Value* {
-            auto* access = builder.Access(var->Result()->Type(), new_var, 0_u);
+        var->Result(0)->ReplaceAllUsesWith([&](Usage use) -> Value* {
+            auto* access = builder.Access(var->Result(0)->Type(), new_var, 0_u);
             access->InsertBefore(use.instruction);
-            return access->Result();
+            return access->Result(0);
         });
 
         var->Destroy();
diff --git a/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc b/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc
index dfe8d6a..e681183 100644
--- a/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc
+++ b/src/tint/lang/core/ir/transform/block_decorated_structs_test.cc
@@ -256,7 +256,7 @@
 
     auto* func = b.Function("foo", ty.u32());
     b.Append(func->Block(), [&] {
-        auto* let_root = b.Let("root", buffer->Result());
+        auto* let_root = b.Let("root", buffer->Result(0));
         auto* let_arr = b.Let("arr", b.Access(ty.ptr(storage, ty.array<i32>()), let_root, 1_u));
         auto* length = b.Call(ty.u32(), core::BuiltinFn::kArrayLength, let_arr);
         b.Return(func, length);
@@ -343,7 +343,7 @@
     buffer_a->SetBindingPoint(0, 0);
     buffer_b->SetBindingPoint(0, 1);
     buffer_c->SetBindingPoint(0, 2);
-    auto* root = mod.root_block;
+    auto* root = mod.root_block.Get();
     root->Append(buffer_a);
     root->Append(buffer_b);
     root->Append(buffer_c);
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill.cc b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
index b59f6e4..07f299c 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
@@ -70,7 +70,7 @@
                 switch (builtin->Func()) {
                     case core::BuiltinFn::kClamp:
                         if (config.clamp_int &&
-                            builtin->Result()->Type()->is_integer_scalar_or_vector()) {
+                            builtin->Result(0)->Type()->is_integer_scalar_or_vector()) {
                             worklist.Push(builtin);
                         }
                         break;
@@ -161,12 +161,12 @@
             }
             TINT_ASSERT_OR_RETURN(replacement);
 
-            if (replacement != builtin->Result()) {
+            if (replacement != builtin->Result(0)) {
                 // Replace the old builtin call result with the new value.
-                if (auto name = ir.NameOf(builtin->Result())) {
+                if (auto name = ir.NameOf(builtin->Result(0))) {
                     ir.SetName(replacement, name);
                 }
-                builtin->Result()->ReplaceAllUsesWith(replacement);
+                builtin->Result(0)->ReplaceAllUsesWith(replacement);
                 builtin->Destroy();
             }
         }
@@ -201,7 +201,7 @@
     /// @param call the builtin call instruction
     /// @returns the replacement value
     ir::Value* ClampInt(ir::CoreBuiltinCall* call) {
-        auto* type = call->Result()->Type();
+        auto* type = call->Result(0)->Type();
         auto* e = call->Args()[0];
         auto* low = call->Args()[1];
         auto* high = call->Args()[2];
@@ -210,7 +210,7 @@
         b.InsertBefore(call, [&] {
             auto* max = b.Call(type, core::BuiltinFn::kMax, e, low);
             auto* min = b.Call(type, core::BuiltinFn::kMin, max, high);
-            result = min->Result();
+            result = min->Result(0);
         });
         return result;
     }
@@ -247,20 +247,20 @@
 
             auto* x = input;
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                x = b.Bitcast(uint_ty, x)->Result();
+                x = b.Bitcast(uint_ty, x)->Result(0);
             }
             auto* b16 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(16),
                                b.LessThanEqual(bool_ty, x, V(0x0000ffff)));
-            x = b.ShiftLeft(uint_ty, x, b16)->Result();
+            x = b.ShiftLeft(uint_ty, x, b16)->Result(0);
             auto* b8 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(8),
                               b.LessThanEqual(bool_ty, x, V(0x00ffffff)));
-            x = b.ShiftLeft(uint_ty, x, b8)->Result();
+            x = b.ShiftLeft(uint_ty, x, b8)->Result(0);
             auto* b4 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(4),
                               b.LessThanEqual(bool_ty, x, V(0x0fffffff)));
-            x = b.ShiftLeft(uint_ty, x, b4)->Result();
+            x = b.ShiftLeft(uint_ty, x, b4)->Result(0);
             auto* b2 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(2),
                               b.LessThanEqual(bool_ty, x, V(0x3fffffff)));
-            x = b.ShiftLeft(uint_ty, x, b2)->Result();
+            x = b.ShiftLeft(uint_ty, x, b2)->Result(0);
             auto* b1 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(1),
                               b.LessThanEqual(bool_ty, x, V(0x7fffffff)));
             auto* b0 =
@@ -270,9 +270,9 @@
                                 b.Or(uint_ty, b8,
                                      b.Or(uint_ty, b4, b.Or(uint_ty, b2, b.Or(uint_ty, b1, b0))))),
                            b0)
-                         ->Result();
+                         ->Result(0);
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result();
+                result = b.Bitcast(result_ty, result)->Result(0);
             }
         });
         return result;
@@ -310,20 +310,20 @@
 
             auto* x = input;
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                x = b.Bitcast(uint_ty, x)->Result();
+                x = b.Bitcast(uint_ty, x)->Result(0);
             }
             auto* b16 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(16),
                                b.Equal(bool_ty, b.And(uint_ty, x, V(0x0000ffff)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b16)->Result();
+            x = b.ShiftRight(uint_ty, x, b16)->Result(0);
             auto* b8 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(8),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x000000ff)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b8)->Result();
+            x = b.ShiftRight(uint_ty, x, b8)->Result(0);
             auto* b4 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(4),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x0000000f)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b4)->Result();
+            x = b.ShiftRight(uint_ty, x, b4)->Result(0);
             auto* b2 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(2),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000003)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b2)->Result();
+            x = b.ShiftRight(uint_ty, x, b2)->Result(0);
             auto* b1 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(1),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000001)), V(0)));
             auto* b0 =
@@ -332,9 +332,9 @@
                            b.Or(uint_ty, b16,
                                 b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1)))),
                            b0)
-                         ->Result();
+                         ->Result(0);
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result();
+                result = b.Bitcast(result_ty, result)->Result(0);
             }
         });
         return result;
@@ -359,10 +359,10 @@
                     auto* o = b.Call(ty.u32(), core::BuiltinFn::kMin, offset, 32_u);
                     auto* c = b.Call(ty.u32(), core::BuiltinFn::kMin, count,
                                      b.Subtract(ty.u32(), 32_u, o));
-                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 1, o->Result());
-                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 2, c->Result());
+                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 1, o->Result(0));
+                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 2, c->Result(0));
                 });
-                return call->Result();
+                return call->Result(0);
             }
             default:
                 TINT_UNIMPLEMENTED() << "extractBits polyfill level";
@@ -402,33 +402,33 @@
 
             auto* x = input;
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                x = b.Bitcast(uint_ty, x)->Result();
+                x = b.Bitcast(uint_ty, x)->Result(0);
                 auto* inverted = b.Complement(uint_ty, x);
                 x = b.Call(uint_ty, core::BuiltinFn::kSelect, inverted, x,
                            b.LessThan(bool_ty, x, V(0x80000000)))
-                        ->Result();
+                        ->Result(0);
             }
             auto* b16 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(16), V(0),
                                b.Equal(bool_ty, b.And(uint_ty, x, V(0xffff0000)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b16)->Result();
+            x = b.ShiftRight(uint_ty, x, b16)->Result(0);
             auto* b8 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(8), V(0),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x0000ff00)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b8)->Result();
+            x = b.ShiftRight(uint_ty, x, b8)->Result(0);
             auto* b4 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(4), V(0),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x000000f0)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b4)->Result();
+            x = b.ShiftRight(uint_ty, x, b4)->Result(0);
             auto* b2 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(2), V(0),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x0000000c)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b2)->Result();
+            x = b.ShiftRight(uint_ty, x, b2)->Result(0);
             auto* b1 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(1), V(0),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000002)), V(0)));
             result = b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1))))
-                         ->Result();
+                         ->Result(0);
             result = b.Call(uint_ty, core::BuiltinFn::kSelect, result, V(0xffffffff),
                             b.Equal(bool_ty, x, V(0)))
-                         ->Result();
+                         ->Result(0);
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result();
+                result = b.Bitcast(result_ty, result)->Result(0);
             }
         });
         return result;
@@ -466,29 +466,29 @@
 
             auto* x = input;
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                x = b.Bitcast(uint_ty, x)->Result();
+                x = b.Bitcast(uint_ty, x)->Result(0);
             }
             auto* b16 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(16),
                                b.Equal(bool_ty, b.And(uint_ty, x, V(0x0000ffff)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b16)->Result();
+            x = b.ShiftRight(uint_ty, x, b16)->Result(0);
             auto* b8 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(8),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x000000ff)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b8)->Result();
+            x = b.ShiftRight(uint_ty, x, b8)->Result(0);
             auto* b4 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(4),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x0000000f)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b4)->Result();
+            x = b.ShiftRight(uint_ty, x, b4)->Result(0);
             auto* b2 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(2),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000003)), V(0)));
-            x = b.ShiftRight(uint_ty, x, b2)->Result();
+            x = b.ShiftRight(uint_ty, x, b2)->Result(0);
             auto* b1 = b.Call(uint_ty, core::BuiltinFn::kSelect, V(0), V(1),
                               b.Equal(bool_ty, b.And(uint_ty, x, V(0x00000001)), V(0)));
             result = b.Or(uint_ty, b16, b.Or(uint_ty, b8, b.Or(uint_ty, b4, b.Or(uint_ty, b2, b1))))
-                         ->Result();
+                         ->Result(0);
             result = b.Call(uint_ty, core::BuiltinFn::kSelect, result, V(0xffffffff),
                             b.Equal(bool_ty, x, V(0)))
-                         ->Result();
+                         ->Result(0);
             if (result_ty->is_signed_integer_scalar_or_vector()) {
-                result = b.Bitcast(result_ty, result)->Result();
+                result = b.Bitcast(result_ty, result)->Result(0);
             }
         });
         return result;
@@ -513,10 +513,10 @@
                     auto* o = b.Call(ty.u32(), core::BuiltinFn::kMin, offset, 32_u);
                     auto* c = b.Call(ty.u32(), core::BuiltinFn::kMin, count,
                                      b.Subtract(ty.u32(), 32_u, o));
-                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 2, o->Result());
-                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 3, c->Result());
+                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 2, o->Result(0));
+                    call->SetOperand(ir::CoreBuiltinCall::kArgsOperandOffset + 3, c->Result(0));
                 });
-                return call->Result();
+                return call->Result(0);
             }
             default:
                 TINT_UNIMPLEMENTED() << "insertBits polyfill level";
@@ -529,7 +529,7 @@
     /// @returns the replacement value
     ir::Value* Saturate(ir::CoreBuiltinCall* call) {
         // Replace `saturate(x)` with `clamp(x, 0., 1.)`.
-        auto* type = call->Result()->Type();
+        auto* type = call->Result(0)->Type();
         ir::Constant* zero = nullptr;
         ir::Constant* one = nullptr;
         if (type->DeepestElement()->Is<core::type::F32>()) {
@@ -541,7 +541,7 @@
         }
         auto* clamp = b.Call(type, core::BuiltinFn::kClamp, Vector{call->Args()[0], zero, one});
         clamp->InsertBefore(call);
-        return clamp->Result();
+        return clamp->Result(0);
     }
 
     /// Polyfill a `textureSampleBaseClampToEdge()` builtin call for 2D F32 textures.
@@ -567,7 +567,7 @@
                 b.Call(vec2f, core::BuiltinFn::kClamp, coords, half_texel, one_minus_half_texel);
             result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureSampleLevel, texture, sampler,
                             clamped, 0_f)
-                         ->Result();
+                         ->Result(0);
         });
         return result;
     }
diff --git a/src/tint/lang/core/ir/transform/combine_access_instructions.cc b/src/tint/lang/core/ir/transform/combine_access_instructions.cc
index 856d4cb..da304ea 100644
--- a/src/tint/lang/core/ir/transform/combine_access_instructions.cc
+++ b/src/tint/lang/core/ir/transform/combine_access_instructions.cc
@@ -52,7 +52,7 @@
             if (auto* access = inst->As<ir::Access>(); access && access->Alive()) {
                 // Look for places where the result of this access instruction is used as a base
                 // pointer for another access instruction.
-                access->Result()->ForEachUse([&](Usage use) {
+                access->Result(0)->ForEachUse([&](Usage use) {
                     auto* child = use.instruction->As<ir::Access>();
                     if (child && use.operand_index == ir::Access::kObjectOperandOffset) {
                         // Push the indices of the parent access instruction into the child.
@@ -69,7 +69,7 @@
                 });
 
                 // If there are no other uses of the access instruction, remove it.
-                if (access->Result()->Usages().IsEmpty()) {
+                if (access->Result(0)->Usages().IsEmpty()) {
                     access->Destroy();
                 }
             }
diff --git a/src/tint/lang/core/ir/transform/conversion_polyfill.cc b/src/tint/lang/core/ir/transform/conversion_polyfill.cc
index 62af849..f99ea69 100644
--- a/src/tint/lang/core/ir/transform/conversion_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/conversion_polyfill.cc
@@ -74,7 +74,7 @@
             }
             if (auto* convert = inst->As<ir::Convert>()) {
                 auto* src_ty = convert->Args()[0]->Type();
-                auto* res_ty = convert->Result()->Type();
+                auto* res_ty = convert->Result(0)->Type();
                 if (config.ftoi &&                          //
                     src_ty->is_float_scalar_or_vector() &&  //
                     res_ty->is_integer_scalar_or_vector()) {
@@ -88,10 +88,10 @@
             auto* replacement = ftoi(convert);
 
             // Replace the old conversion instruction result with the new value.
-            if (auto name = ir.NameOf(convert->Result())) {
+            if (auto name = ir.NameOf(convert->Result(0))) {
                 ir.SetName(replacement, name);
             }
-            convert->Result()->ReplaceAllUsesWith(replacement);
+            convert->Result(0)->ReplaceAllUsesWith(replacement);
             convert->Destroy();
         }
     }
@@ -101,7 +101,7 @@
     /// @param convert the conversion instruction
     /// @returns the replacement value
     ir::Value* ftoi(ir::Convert* convert) {
-        auto* res_ty = convert->Result()->Type();
+        auto* res_ty = convert->Result(0)->Type();
         auto* src_ty = convert->Args()[0]->Type();
         auto* src_el_ty = src_ty->DeepestElement();
 
@@ -187,7 +187,7 @@
                 auto* select_high = b.Call(res_ty, core::BuiltinFn::kSelect, limits.high_limit_i,
                                            select_low, high_cond);
 
-                b.Return(func, select_high->Result());
+                b.Return(func, select_high->Result(0));
             });
             return func;
         });
@@ -195,7 +195,7 @@
         // Call the helper function, splatting the arguments to match the target vector width.
         auto* call = b.Call(res_ty, helper, convert->Args()[0]);
         call->InsertBefore(convert);
-        return call->Result();
+        return call->Result(0);
     }
 
     /// Return a type with element type @p type that has the same number of vector components as
diff --git a/src/tint/lang/core/ir/transform/demote_to_helper.cc b/src/tint/lang/core/ir/transform/demote_to_helper.cc
index 6e9953c..89bb789 100644
--- a/src/tint/lang/core/ir/transform/demote_to_helper.cc
+++ b/src/tint/lang/core/ir/transform/demote_to_helper.cc
@@ -66,7 +66,7 @@
         // Check each function for discard instructions, potentially inside other functions called
         // (transitively) by the function.
         Vector<Function*, 4> to_process;
-        for (auto* func : ir.functions) {
+        for (auto& func : ir.functions) {
             // If the function contains a discard (directly or indirectly), we need to process it.
             if (HasDiscard(func)) {
                 to_process.Push(func);
@@ -145,11 +145,12 @@
             // Move the original instruction into the if-true block.
             auto* result = ifelse->True()->Append(inst);
 
-            TINT_ASSERT(!inst->HasMultiResults());
-            if (inst->HasResults() && !inst->Result()->Type()->Is<core::type::Void>()) {
+            auto results = inst->Results();
+            TINT_ASSERT(results.Length() < 2);
+            if (!results.IsEmpty() && !results[0]->Type()->Is<core::type::Void>()) {
                 // The original instruction had a result, so return it from the if instruction.
-                ifelse->SetResults(Vector{b.InstructionResult(inst->Result()->Type())});
-                inst->Result()->ReplaceAllUsesWith(ifelse->Result());
+                ifelse->SetResults(Vector{b.InstructionResult(results[0]->Type())});
+                results[0]->ReplaceAllUsesWith(ifelse->Result(0));
                 ifelse->True()->Append(b.ExitIf(ifelse, result));
             } else {
                 ifelse->True()->Append(b.ExitIf(ifelse));
@@ -160,7 +161,7 @@
         for (auto* inst = *block->begin(); inst;) {
             // As we're (potentially) modifying the block that we're iterating over, grab a pointer
             // to the next instruction before we make any changes.
-            auto* next = inst->next;
+            auto* next = inst->next.Get();
             TINT_DEFER(inst = next);
 
             tint::Switch(
@@ -203,6 +204,10 @@
                 [&](ControlInstruction* ctrl) {
                     // Recurse into control instructions.
                     ctrl->ForeachBlock([&](Block* blk) { ProcessBlock(blk); });
+                },
+                [&](BuiltinCall*) {
+                    // TODO(crbug.com/tint/2102): Catch this with the validator instead.
+                    TINT_UNREACHABLE() << "unexpected non-core instruction";
                 });
         }
     }
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access.cc b/src/tint/lang/core/ir/transform/direct_variable_access.cc
index 6a264b9..0c2c27b 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.cc
@@ -239,7 +239,8 @@
     /// Process the module.
     void Process() {
         // Make a copy of all the functions in the IR module.
-        auto input_fns = ir.functions;
+        // Use transform to convert from ConstPropagatingPtr<Function> to Function*
+        auto input_fns = Transform<8>(ir.functions.Slice(), [](auto& fn) { return fn.Get(); });
 
         // Populate #need_forking
         GatherFnsThatNeedForking();
@@ -258,7 +259,7 @@
     /// Populates #need_forking with all the functions that have pointer parameters which need
     /// transforming. These functions will be replaced with variants based on the access shapes.
     void GatherFnsThatNeedForking() {
-        for (auto* fn : ir.functions) {
+        for (auto& fn : ir.functions) {
             for (auto* param : fn->Params()) {
                 if (ParamNeedsTransforming(param)) {
                     need_forking.Add(fn, fn_info_allocator.Create());
@@ -271,7 +272,7 @@
     /// Adjusts the calls of all the functions that make calls to #need_forking, which aren't in
     /// #need_forking themselves. This populates #variants_to_build with the called functions.
     void BuildRootFns() {
-        for (auto* fn : ir.functions) {
+        for (auto& fn : ir.functions) {
             if (!need_forking.Contains(fn)) {
                 TransformCalls(fn);
             }
@@ -330,7 +331,7 @@
                         if (size_t array_len = chain.indices.Length(); array_len > 0) {
                             auto* array = ty.array(ty.u32(), static_cast<uint32_t>(array_len));
                             auto* indices = b.Construct(array, std::move(chain.indices));
-                            new_args.Push(indices->Result());
+                            new_args.Push(indices->Result(0));
                         }
                         // Record the parameter shape for the variant's signature.
                         signature.Add(i, chain.shape);
@@ -409,7 +410,7 @@
                 value,  //
                 [&](InstructionResult* res) {
                     // value was emitted by an instruction
-                    auto* inst = res->Source();
+                    auto* inst = res->Instruction();
                     return tint::Switch(
                         inst,
                         [&](Access* access) {
@@ -436,7 +437,7 @@
                                 // Array or matrix access.
                                 // Convert index to u32 if it isn't already.
                                 if (!idx->Type()->Is<type::U32>()) {
-                                    idx = b.Convert(ty.u32(), idx)->Result();
+                                    idx = b.Convert(ty.u32(), idx)->Result(0);
                                 }
 
                                 ops.Push(IndexAccess{});
@@ -455,7 +456,7 @@
                                 chain.indices.Push(idx);
                             }
 
-                            TINT_ASSERT(obj_ty == access->Result()->Type()->UnwrapPtr());
+                            TINT_ASSERT(obj_ty == access->Result(0)->Type()->UnwrapPtr());
                             return access->Object();
                         },
                         [&](Var* var) {
@@ -466,9 +467,9 @@
                             } else {
                                 // Root pointer is a function-scope 'var'
                                 chain.shape.root =
-                                    RootPtrParameter{var->Result()->Type()->As<type::Pointer>()};
+                                    RootPtrParameter{var->Result(0)->Type()->As<type::Pointer>()};
                             }
-                            chain.root_ptr = var->Result();
+                            chain.root_ptr = var->Result(0);
                             return nullptr;
                         },
                         [&](Let* let) { return let->Value(); },  //
@@ -524,7 +525,7 @@
                     root_ptr = root_ptr_param;
                 } else if (auto* global = std::get_if<RootModuleScopeVar>(&shape->root)) {
                     // Root pointer is a module-scope var
-                    root_ptr = global->var->Result();
+                    root_ptr = global->var->Result(0);
                 } else {
                     TINT_ICE() << "unhandled AccessShape root variant";
                 }
@@ -555,12 +556,12 @@
                         return b.Constant(u32(m->member->Index()));
                     }
                     auto* access = b.Access(ty.u32(), indices_param, u32(index_index++));
-                    return access->Result();
+                    return access->Result(0);
                 });
                 auto* access = b.Access(old_param->Type(), root_ptr, std::move(chain));
 
                 // Replace the now removed parameter value with the access instruction
-                old_param->ReplaceAllUsesWith(access->Result());
+                old_param->ReplaceAllUsesWith(access->Result(0));
                 old_param->Destroy();
             }
 
@@ -574,7 +575,7 @@
     /// @param input_fns the content of #ir.functions before transformation began.
     void EmitFunctions(VectorRef<Function*> input_fns) {
         ir.functions.Clear();
-        for (auto* fn : input_fns) {
+        for (auto& fn : input_fns) {
             if (auto info = need_forking.Get(fn)) {
                 fn->Destroy();
                 for (auto variant : (*info)->ordered_variants) {
@@ -648,7 +649,7 @@
                 return;  // Only instructions can be removed.
             }
             value = tint::Switch(
-                inst_res->Source(),  //
+                inst_res->Instruction(),  //
                 [&](Access* access) {
                     TINT_DEFER(access->Destroy());
                     return access->Object();
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc b/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
index f72262b..7b8a407 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
@@ -2168,7 +2168,7 @@
 }
 
 fn f() {
-  len_S();
+  let n = len_S();
 }
 )";
 
@@ -2200,7 +2200,7 @@
 }
 
 fn f() {
-  load_W();
+  let v = load_W();
 }
 )";
 
@@ -2369,33 +2369,33 @@
 fn b() {
   let I = 3i;
   let J = 4i;
-  fn_u_U();
-  fn_u_U_str_i();
-  fn_u_U_arr_X(array<u32, 1u>(u32(0i)));
-  fn_u_U_arr_X(array<u32, 1u>(u32(1i)));
-  fn_u_U_arr_X(array<u32, 1u>(u32(I)));
-  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
-  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
-  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
-  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
-  fn_s_S();
-  fn_s_S_str_i();
-  fn_s_S_arr_X(array<u32, 1u>(u32(0i)));
-  fn_s_S_arr_X(array<u32, 1u>(u32(1i)));
-  fn_s_S_arr_X(array<u32, 1u>(u32(I)));
-  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
-  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
-  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
-  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
-  fn_w_W();
-  fn_w_W_str_i();
-  fn_w_W_arr_X(array<u32, 1u>(u32(0i)));
-  fn_w_W_arr_X(array<u32, 1u>(u32(1i)));
-  fn_w_W_arr_X(array<u32, 1u>(u32(I)));
-  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
-  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
-  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
-  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
+  let u = fn_u_U();
+  let u_str = fn_u_U_str_i();
+  let u_arr0 = fn_u_U_arr_X(array<u32, 1u>(u32(0i)));
+  let u_arr1 = fn_u_U_arr_X(array<u32, 1u>(u32(1i)));
+  let u_arrI = fn_u_U_arr_X(array<u32, 1u>(u32(I)));
+  let u_arr1_arr0 = fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
+  let u_arr2_arrI = fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
+  let u_arrI_arr2 = fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
+  let u_arrI_arrJ = fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
+  let s = fn_s_S();
+  let s_str = fn_s_S_str_i();
+  let s_arr0 = fn_s_S_arr_X(array<u32, 1u>(u32(0i)));
+  let s_arr1 = fn_s_S_arr_X(array<u32, 1u>(u32(1i)));
+  let s_arrI = fn_s_S_arr_X(array<u32, 1u>(u32(I)));
+  let s_arr1_arr0 = fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
+  let s_arr2_arrI = fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
+  let s_arrI_arr2 = fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
+  let s_arrI_arrJ = fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
+  let w = fn_w_W();
+  let w_str = fn_w_W_str_i();
+  let w_arr0 = fn_w_W_arr_X(array<u32, 1u>(u32(0i)));
+  let w_arr1 = fn_w_W_arr_X(array<u32, 1u>(u32(1i)));
+  let w_arrI = fn_w_W_arr_X(array<u32, 1u>(u32(I)));
+  let w_arr1_arr0 = fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
+  let w_arr2_arrI = fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
+  let w_arrI_arr2 = fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
+  let w_arrI_arrJ = fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
 }
 )";
 
@@ -2436,7 +2436,7 @@
 }
 
 fn c() {
-  b_S_X(array<u32, 1u>(u32(42i)));
+  let v = b_S_X(array<u32, 1u>(u32(42i)));
 }
 )";
 
@@ -2481,7 +2481,7 @@
 }
 
 fn c() {
-  b_S_X(array<u32, 1u>(u32(42i)));
+  let v = b_S_X(array<u32, 1u>(u32(42i)));
 }
 )";
 
@@ -2525,7 +2525,7 @@
 }
 
 fn c() {
-  b_S_X_U_X(array<u32, 1u>(u32(42i)), array<u32, 1u>(u32(24i)));
+  let v = b_S_X_U_X(array<u32, 1u>(u32(42i)), array<u32, 1u>(u32(24i)));
 }
 )";
 
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
index 7766c59..d9118c1 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
@@ -84,7 +84,7 @@
                 if (!var) {
                     continue;
                 }
-                auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+                auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
                 if (ptr->StoreType()->Is<core::type::ExternalTexture>()) {
                     ReplaceVar(var);
                     to_remove.Push(var);
@@ -97,7 +97,7 @@
 
         // Find function parameters that need to be replaced.
         auto functions = ir.functions;
-        for (auto* func : functions) {
+        for (auto& func : functions) {
             for (uint32_t index = 0; index < func->Params().Length(); index++) {
                 auto* param = func->Params()[index];
                 if (param->Type()->Is<core::type::ExternalTexture>()) {
@@ -148,8 +148,8 @@
         }
 
         // Replace all uses of the old variable with the new ones.
-        ReplaceUses(old_var->Result(), plane_0->Result(), plane_1->Result(),
-                    external_texture_params->Result());
+        ReplaceUses(old_var->Result(0), plane_0->Result(0), plane_1->Result(0),
+                    external_texture_params->Result(0));
     }
 
     /// Replace an external texture function parameter.
@@ -208,11 +208,11 @@
                     Value* plane_1_load = nullptr;
                     Value* params_load = nullptr;
                     b.InsertBefore(load, [&] {
-                        plane_0_load = b.Load(plane_0)->Result();
-                        plane_1_load = b.Load(plane_1)->Result();
-                        params_load = b.Load(params)->Result();
+                        plane_0_load = b.Load(plane_0)->Result(0);
+                        plane_1_load = b.Load(plane_1)->Result(0);
+                        params_load = b.Load(params)->Result(0);
                     });
-                    ReplaceUses(load->Result(), plane_0_load, plane_1_load, params_load);
+                    ReplaceUses(load->Result(0), plane_0_load, plane_1_load, params_load);
                     load->Destroy();
                 },
                 [&](CoreBuiltinCall* call) {
@@ -225,14 +225,14 @@
                         if (coords->Type()->is_signed_integer_vector()) {
                             auto* convert = b.Convert(ty.vec2<u32>(), coords);
                             convert->InsertBefore(call);
-                            coords = convert->Result();
+                            coords = convert->Result(0);
                         }
 
                         // Call the `TextureLoadExternal()` helper function.
                         auto* helper = b.Call(ty.vec4<f32>(), TextureLoadExternal(), plane_0,
                                               plane_1, params, coords);
                         helper->InsertBefore(call);
-                        call->Result()->ReplaceAllUsesWith(helper->Result());
+                        call->Result(0)->ReplaceAllUsesWith(helper->Result(0));
                         call->Destroy();
                     } else if (call->Func() == core::BuiltinFn::kTextureSampleBaseClampToEdge) {
                         // Call the `TextureSampleExternal()` helper function.
@@ -241,7 +241,7 @@
                         auto* helper = b.Call(ty.vec4<f32>(), TextureSampleExternal(), plane_0,
                                               plane_1, params, sampler, coords);
                         helper->InsertBefore(call);
-                        call->Result()->ReplaceAllUsesWith(helper->Result());
+                        call->Result(0)->ReplaceAllUsesWith(helper->Result(0));
                         call->Destroy();
                     } else {
                         TINT_ICE() << "unhandled texture_external builtin call: " << call->Func();
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc b/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc
index 7ae0828..5e2336a 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture_test.cc
@@ -197,7 +197,7 @@
 
     auto* func = b.Function("foo", ty.vec2<u32>());
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* result = b.Call(ty.vec2<u32>(), core::BuiltinFn::kTextureDimensions, load);
         b.Return(func, result);
         mod.SetName(result, "result");
@@ -272,7 +272,7 @@
     auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
     func->SetParams({coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load, coords);
         b.Return(func, result);
         mod.SetName(result, "result");
@@ -416,7 +416,7 @@
     auto* coords = b.FunctionParam("coords", ty.vec2<i32>());
     func->SetParams({coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load, coords);
         b.Return(func, result);
         mod.SetName(result, "result");
@@ -562,7 +562,7 @@
     auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
     func->SetParams({sampler, coords});
     b.Append(func->Block(), [&] {
-        auto* load = b.Load(var->Result());
+        auto* load = b.Load(var->Result(0));
         auto* result = b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureSampleBaseClampToEdge, load,
                               sampler, coords);
         b.Return(func, result);
@@ -735,7 +735,7 @@
         auto* coords = b.FunctionParam("coords", ty.vec2<f32>());
         bar->SetParams({sampler, coords});
         b.Append(bar->Block(), [&] {
-            auto* load = b.Load(var->Result());
+            auto* load = b.Load(var->Result(0));
             auto* result = b.Call(ty.vec4<f32>(), foo, load, sampler, coords);
             b.Return(bar, result);
             mod.SetName(result, "result");
@@ -920,15 +920,15 @@
         auto* coords_f = b.FunctionParam("coords", ty.vec2<f32>());
         bar->SetParams({sampler, coords_f});
         b.Append(bar->Block(), [&] {
-            auto* load_a = b.Load(var->Result());
+            auto* load_a = b.Load(var->Result(0));
             b.Call(ty.vec2<u32>(), core::BuiltinFn::kTextureDimensions, load_a);
-            auto* load_b = b.Load(var->Result());
+            auto* load_b = b.Load(var->Result(0));
             b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureSampleBaseClampToEdge, load_b, sampler,
                    coords_f);
-            auto* load_c = b.Load(var->Result());
+            auto* load_c = b.Load(var->Result(0));
             b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureSampleBaseClampToEdge, load_c, sampler,
                    coords_f);
-            auto* load_d = b.Load(var->Result());
+            auto* load_d = b.Load(var->Result(0));
             auto* result_a = b.Call(ty.vec4<f32>(), foo, load_d, sampler, coords_f);
             auto* result_b = b.Call(ty.vec4<f32>(), foo, load_d, sampler, coords_f);
             b.Return(bar, b.Add(ty.vec4<f32>(), result_a, result_b));
@@ -1129,11 +1129,11 @@
     auto* coords = b.FunctionParam("coords", ty.vec2<u32>());
     foo->SetParams({coords});
     b.Append(foo->Block(), [&] {
-        auto* load_a = b.Load(var_a->Result());
+        auto* load_a = b.Load(var_a->Result(0));
         b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load_a, coords);
-        auto* load_b = b.Load(var_b->Result());
+        auto* load_b = b.Load(var_b->Result(0));
         b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load_b, coords);
-        auto* load_c = b.Load(var_c->Result());
+        auto* load_c = b.Load(var_c->Result(0));
         b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureLoad, load_c, coords);
         b.Return(foo);
     });
diff --git a/src/tint/lang/core/ir/transform/preserve_padding.cc b/src/tint/lang/core/ir/transform/preserve_padding.cc
index 0ac5f44..c9a87e8 100644
--- a/src/tint/lang/core/ir/transform/preserve_padding.cc
+++ b/src/tint/lang/core/ir/transform/preserve_padding.cc
@@ -139,7 +139,7 @@
                                 auto* el_ptr =
                                     b.Access(ty.ptr(storage, arr->ElemType()), target, idx);
                                 auto* el_value = b.Access(arr->ElemType(), value_param, idx);
-                                MakeStore(el_ptr->Result(), el_value->Result());
+                                MakeStore(el_ptr->Result(0), el_value->Result(0));
                             });
                     },
                     [&](const type::Matrix* mat) {
@@ -147,7 +147,7 @@
                             auto* col_ptr =
                                 b.Access(ty.ptr(storage, mat->ColumnType()), target, u32(i));
                             auto* col_value = b.Access(mat->ColumnType(), value_param, u32(i));
-                            MakeStore(col_ptr->Result(), col_value->Result());
+                            MakeStore(col_ptr->Result(0), col_value->Result(0));
                         }
                     },
                     [&](const type::Struct* str) {
@@ -156,7 +156,7 @@
                                                      u32(member->Index()));
                             auto* sub_value =
                                 b.Access(member->Type(), value_param, u32(member->Index()));
-                            MakeStore(sub_ptr->Result(), sub_value->Result());
+                            MakeStore(sub_ptr->Result(0), sub_value->Result(0));
                         }
                     });
 
diff --git a/src/tint/lang/core/ir/transform/robustness.cc b/src/tint/lang/core/ir/transform/robustness.cc
index e0e4839..25d9d1e 100644
--- a/src/tint/lang/core/ir/transform/robustness.cc
+++ b/src/tint/lang/core/ir/transform/robustness.cc
@@ -186,7 +186,7 @@
         if (auto* vec = value->Type()->As<type::Vector>()) {
             type = ty.vec(type, vec->Width());
         }
-        return b.Convert(type, value)->Result();
+        return b.Convert(type, value)->Result(0);
     }
 
     /// Clamp operand @p op_idx of @p inst to ensure it is within @p limit.
@@ -205,7 +205,7 @@
                                                   const_limit->Value()->ValueAs<uint32_t>())));
         } else {
             // Clamp it to the dynamic limit.
-            clamped_idx = b.Call(ty.u32(), core::BuiltinFn::kMin, CastToU32(idx), limit)->Result();
+            clamped_idx = b.Call(ty.u32(), core::BuiltinFn::kMin, CastToU32(idx), limit)->Result(0);
         }
 
         // Replace the index operand with the clamped version.
@@ -251,12 +251,12 @@
                         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();
+                        object = b.Access(arr_ptr, object, indices[0])->Result(0);
                     }
 
                     // Use the `arrayLength` builtin to get the limit of a runtime-sized array.
                     auto* length = b.Call(ty.u32(), core::BuiltinFn::kArrayLength, object);
-                    return b.Subtract(ty.u32(), length, b.Constant(1_u))->Result();
+                    return b.Subtract(ty.u32(), length, b.Constant(1_u))->Result(0);
                 });
 
             // If there's a dynamic limit that needs enforced, clamp the index operand.
@@ -284,7 +284,7 @@
             auto* num_levels = b.Call(ty.u32(), core::BuiltinFn::kTextureNumLevels, args[0]);
             auto* limit = b.Subtract(ty.u32(), num_levels, 1_u);
             clamped_level =
-                b.Call(ty.u32(), core::BuiltinFn::kMin, CastToU32(args[idx]), limit)->Result();
+                b.Call(ty.u32(), core::BuiltinFn::kMin, CastToU32(args[idx]), limit)->Result(0);
             call->SetOperand(CoreBuiltinCall::kArgsOperandOffset + idx, clamped_level);
         };
 
@@ -302,7 +302,7 @@
             auto* limit = b.Subtract(type, dims, one);
             call->SetOperand(
                 CoreBuiltinCall::kArgsOperandOffset + idx,
-                b.Call(type, core::BuiltinFn::kMin, CastToU32(args[idx]), limit)->Result());
+                b.Call(type, core::BuiltinFn::kMin, CastToU32(args[idx]), limit)->Result(0));
         };
 
         // Helper for clamping the array index.
@@ -311,7 +311,7 @@
             auto* limit = b.Subtract(ty.u32(), num_layers, 1_u);
             call->SetOperand(
                 CoreBuiltinCall::kArgsOperandOffset + idx,
-                b.Call(ty.u32(), core::BuiltinFn::kMin, CastToU32(args[idx]), limit)->Result());
+                b.Call(ty.u32(), core::BuiltinFn::kMin, CastToU32(args[idx]), limit)->Result(0));
         };
 
         // Select which arguments to clamp based on the function overload.
diff --git a/src/tint/lang/core/ir/transform/shader_io.cc b/src/tint/lang/core/ir/transform/shader_io.cc
index 6e5f8a1..ce24b54 100644
--- a/src/tint/lang/core/ir/transform/shader_io.cc
+++ b/src/tint/lang/core/ir/transform/shader_io.cc
@@ -152,7 +152,7 @@
         // Call the original function, passing it the inputs and capturing its return value.
         auto inner_call_args = BuildInnerCallArgs(wrapper);
         auto* inner_result = wrapper.Call(func->ReturnType(), func, std::move(inner_call_args));
-        SetOutputs(wrapper, inner_result->Result());
+        SetOutputs(wrapper, inner_result->Result(0));
         if (vertex_point_size_index) {
             backend->SetOutput(wrapper, vertex_point_size_index.value(), b.Constant(1_f));
         }
@@ -245,7 +245,7 @@
                 for (uint32_t i = 0; i < str->Members().Length(); i++) {
                     construct_args.Push(backend->GetInput(builder, input_idx++));
                 }
-                args.Push(builder.Construct(param->Type(), construct_args)->Result());
+                args.Push(builder.Construct(param->Type(), construct_args)->Result(0));
             } else {
                 args.Push(backend->GetInput(builder, input_idx++));
             }
@@ -261,7 +261,7 @@
         if (auto* str = inner_result->Type()->As<core::type::Struct>()) {
             for (auto* member : str->Members()) {
                 Value* from =
-                    builder.Access(member->Type(), inner_result, u32(member->Index()))->Result();
+                    builder.Access(member->Type(), inner_result, u32(member->Index()))->Result(0);
                 backend->SetOutput(builder, member->Index(), from);
             }
         } else if (!inner_result->Type()->Is<core::type::Void>()) {
@@ -286,7 +286,7 @@
 
     // Take a copy of the function list since the transform will add new functions to the module.
     auto functions = module.functions;
-    for (auto* func : functions) {
+    for (auto& func : functions) {
         // Only process entry points.
         if (func->Stage() == Function::PipelineStage::kUndefined) {
             continue;
diff --git a/src/tint/lang/core/ir/transform/std140.cc b/src/tint/lang/core/ir/transform/std140.cc
index 0f2b8f6..85a2036 100644
--- a/src/tint/lang/core/ir/transform/std140.cc
+++ b/src/tint/lang/core/ir/transform/std140.cc
@@ -79,7 +79,7 @@
             if (!var || !var->Alive()) {
                 continue;
             }
-            auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+            auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
             if (!ptr || ptr->AddressSpace() != core::AddressSpace::kUniform) {
                 continue;
             }
@@ -93,16 +93,16 @@
         for (auto* var : buffer_variables) {
             // Create a new variable with the modified store type.
             const auto& bp = var->BindingPoint();
-            auto* store_type = var->Result()->Type()->As<core::type::Pointer>()->StoreType();
+            auto* store_type = var->Result(0)->Type()->As<core::type::Pointer>()->StoreType();
             auto* new_var = b.Var(ty.ptr(uniform, RewriteType(store_type)));
             new_var->SetBindingPoint(bp->group, bp->binding);
             if (auto name = ir.NameOf(var)) {
-                ir.SetName(new_var->Result(), name);
+                ir.SetName(new_var->Result(0), name);
             }
 
             // Replace every instruction that uses the original variable.
-            var->Result()->ForEachUse(
-                [&](Usage use) { Replace(use.instruction, new_var->Result()); });
+            var->Result(0)->ForEachUse(
+                [&](Usage use) { Replace(use.instruction, new_var->Result(0)); });
 
             // Replace the original variable with the new variable.
             var->ReplaceWith(new_var);
@@ -201,9 +201,9 @@
         for (uint32_t i = 0; i < mat->columns(); i++) {
             indices.Back() = b.Constant(u32(first_column + i));
             auto* access = b.Access(ty.ptr(uniform, mat->ColumnType()), root, indices);
-            args.Push(b.Load(access->Result())->Result());
+            args.Push(b.Load(access->Result(0))->Result(0));
         }
-        return b.Construct(mat, std::move(args))->Result();
+        return b.Construct(mat, std::move(args))->Result(0);
     }
 
     /// Convert a value that may contain decomposed matrices to a value with the original type.
@@ -234,15 +234,15 @@
                                 Vector<Value*, 4> columns;
                                 for (uint32_t i = 0; i < mat->columns(); i++) {
                                     auto* extract = b.Access(mat->ColumnType(), input, u32(index));
-                                    columns.Push(extract->Result());
+                                    columns.Push(extract->Result(0));
                                     index++;
                                 }
-                                args.Push(b.Construct(mat, std::move(columns))->Result());
+                                args.Push(b.Construct(mat, std::move(columns))->Result(0));
                             } else {
                                 // Extract and convert the member.
                                 auto* type = input_str->Element(index);
                                 auto* extract = b.Access(type, input, u32(index));
-                                args.Push(Convert(extract->Result(), member->Type()));
+                                args.Push(Convert(extract->Result(0), member->Type()));
                                 index++;
                             }
                         }
@@ -254,7 +254,7 @@
                 });
 
                 // Call the helper function to convert the struct.
-                return b.Call(str, helper, source)->Result();
+                return b.Call(str, helper, source)->Result(0);
             },
             [&](const core::type::Array* arr) -> Value* {
                 // Create a loop that copies and converts each element of the array.
@@ -263,10 +263,10 @@
                 b.LoopRange(ty, 0_u, u32(arr->ConstantCount().value()), 1_u, [&](Value* idx) {
                     // Convert arr[idx] and store to new_arr[idx];
                     auto* to = b.Access(ty.ptr(function, arr->ElemType()), new_arr, idx);
-                    auto* from = b.Access(el_ty, source, idx)->Result();
+                    auto* from = b.Access(el_ty, source, idx)->Result(0);
                     b.Store(to, Convert(from, arr->ElemType()));
                 });
-                return b.Load(new_arr)->Result();
+                return b.Load(new_arr)->Result(0);
             },
             [&](Default) { return source; });
     }
@@ -309,37 +309,43 @@
                             current_type = ty.ptr(uniform, RewriteType(current_type));
                         }
                         auto* new_access = b.Access(current_type, replacement, std::move(indices));
-                        replacement = new_access->Result();
+                        replacement = new_access->Result(0);
                     }
 
                     // Replace every instruction that uses the original access instruction.
-                    access->Result()->ForEachUse(
+                    access->Result(0)->ForEachUse(
                         [&](Usage use) { Replace(use.instruction, replacement); });
                     access->Destroy();
                 },
                 [&](Load* load) {
                     if (!replacement->Type()->Is<core::type::Pointer>()) {
                         // We have already loaded to a value type, so this load just folds away.
-                        load->Result()->ReplaceAllUsesWith(replacement);
+                        load->Result(0)->ReplaceAllUsesWith(replacement);
                     } else {
                         // Load the decomposed value and then convert it to the original type.
                         auto* decomposed = b.Load(replacement);
-                        auto* converted = Convert(decomposed->Result(), load->Result()->Type());
-                        load->Result()->ReplaceAllUsesWith(converted);
+                        auto* converted = Convert(decomposed->Result(0), load->Result(0)->Type());
+                        load->Result(0)->ReplaceAllUsesWith(converted);
                     }
                     load->Destroy();
                 },
                 [&](LoadVectorElement* load) {
-                    // We should have loaded the decomposed matrix, reconstructed it, so this is now
-                    // extracting from a value type.
-                    TINT_ASSERT(!replacement->Type()->Is<core::type::Pointer>());
-                    auto* access = b.Access(load->Result()->Type(), replacement, load->Index());
-                    load->Result()->ReplaceAllUsesWith(access->Result());
-                    load->Destroy();
+                    if (!replacement->Type()->Is<core::type::Pointer>()) {
+                        // We have loaded a decomposed matrix and reconstructed it, so this is now
+                        // extracting from a value type.
+                        auto* access =
+                            b.Access(load->Result(0)->Type(), replacement, load->Index());
+                        load->Result(0)->ReplaceAllUsesWith(access->Result(0));
+                        load->Destroy();
+                    } else {
+                        // There was no decomposed matrix on the path to this instruction so just
+                        // update the source operand.
+                        load->SetOperand(LoadVectorElement::kFromOperandOffset, replacement);
+                    }
                 },
                 [&](Let* let) {
                     // Let instructions just fold away.
-                    let->Result()->ForEachUse(
+                    let->Result(0)->ForEachUse(
                         [&](Usage use) { Replace(use.instruction, replacement); });
                     let->Destroy();
                 });
diff --git a/src/tint/lang/core/ir/transform/std140_test.cc b/src/tint/lang/core/ir/transform/std140_test.cc
index 8415529..105f2cd 100644
--- a/src/tint/lang/core/ir/transform/std140_test.cc
+++ b/src/tint/lang/core/ir/transform/std140_test.cc
@@ -1483,6 +1483,283 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_Std140Test, NotAllMatricesDecomposed) {
+    auto* mat4x4 = ty.mat4x4<f32>();
+    auto* mat3x2 = ty.mat3x2<f32>();
+    auto* structure = ty.Struct(mod.symbols.New("MyStruct"), {
+                                                                 {mod.symbols.New("a"), mat4x4},
+                                                                 {mod.symbols.New("b"), mat3x2},
+                                                             });
+    structure->SetStructFlag(core::type::kBlock);
+
+    auto* buffer = b.Var("buffer", ty.ptr(uniform, structure));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    {
+        auto* func = b.Function("load_struct_a", mat4x4);
+        b.Append(func->Block(), [&] {
+            auto* load_struct = b.Load(buffer);
+            auto* extract_mat = b.Access(mat4x4, load_struct, 0_u);
+            b.Return(func, extract_mat);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_struct_b", mat3x2);
+        b.Append(func->Block(), [&] {
+            auto* load_struct = b.Load(buffer);
+            auto* extract_mat = b.Access(mat3x2, load_struct, 1_u);
+            b.Return(func, extract_mat);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_mat_a", ty.vec4<f32>());
+        b.Append(func->Block(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat4x4), buffer, 0_u);
+            auto* load_mat = b.Load(access_mat);
+            auto* extract_vec = b.Access(ty.vec4<f32>(), load_mat, 0_u);
+            b.Return(func, extract_vec);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_mat_b", ty.vec2<f32>());
+        b.Append(func->Block(), [&] {
+            auto* access_mat = b.Access(ty.ptr(uniform, mat3x2), buffer, 1_u);
+            auto* load_mat = b.Load(access_mat);
+            auto* extract_vec = b.Access(ty.vec2<f32>(), load_mat, 0_u);
+            b.Return(func, extract_vec);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_vec_a", ty.f32());
+        b.Append(func->Block(), [&] {
+            auto* access_vec = b.Access(ty.ptr(uniform, mat4x4->ColumnType()), buffer, 0_u, 1_u);
+            auto* load_vec = b.Load(access_vec);
+            auto* extract_el = b.Access(ty.f32(), load_vec, 1_u);
+            b.Return(func, extract_el);
+        });
+    }
+
+    {
+        auto* func = b.Function("load_vec_b", ty.f32());
+        b.Append(func->Block(), [&] {
+            auto* access_vec = b.Access(ty.ptr(uniform, mat3x2->ColumnType()), buffer, 1_u, 1_u);
+            auto* load_vec = b.Load(access_vec);
+            auto* extract_el = b.Access(ty.f32(), load_vec, 1_u);
+            b.Return(func, extract_el);
+        });
+    }
+
+    {
+        auto* func = b.Function("lve_a", ty.f32());
+        b.Append(func->Block(), [&] {
+            auto* access_vec = b.Access(ty.ptr(uniform, mat4x4->ColumnType()), buffer, 0_u, 1_u);
+            auto* lve = b.LoadVectorElement(access_vec, 1_u);
+            b.Return(func, lve);
+        });
+    }
+
+    {
+        auto* func = b.Function("lve_b", ty.f32());
+        b.Append(func->Block(), [&] {
+            auto* access_vec = b.Access(ty.ptr(uniform, mat3x2->ColumnType()), buffer, 1_u, 1_u);
+            auto* lve = b.LoadVectorElement(access_vec, 1_u);
+            b.Return(func, lve);
+        });
+    }
+
+    auto* src = R"(
+MyStruct = struct @align(16), @block {
+  a:mat4x4<f32> @offset(0)
+  b:mat3x2<f32> @offset(64)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct, read_write> = var @binding_point(0, 0)
+}
+
+%load_struct_a = func():mat4x4<f32> -> %b2 {
+  %b2 = block {
+    %3:MyStruct = load %buffer
+    %4:mat4x4<f32> = access %3, 0u
+    ret %4
+  }
+}
+%load_struct_b = func():mat3x2<f32> -> %b3 {
+  %b3 = block {
+    %6:MyStruct = load %buffer
+    %7:mat3x2<f32> = access %6, 1u
+    ret %7
+  }
+}
+%load_mat_a = func():vec4<f32> -> %b4 {
+  %b4 = block {
+    %9:ptr<uniform, mat4x4<f32>, read_write> = access %buffer, 0u
+    %10:mat4x4<f32> = load %9
+    %11:vec4<f32> = access %10, 0u
+    ret %11
+  }
+}
+%load_mat_b = func():vec2<f32> -> %b5 {
+  %b5 = block {
+    %13:ptr<uniform, mat3x2<f32>, read_write> = access %buffer, 1u
+    %14:mat3x2<f32> = load %13
+    %15:vec2<f32> = access %14, 0u
+    ret %15
+  }
+}
+%load_vec_a = func():f32 -> %b6 {
+  %b6 = block {
+    %17:ptr<uniform, vec4<f32>, read_write> = access %buffer, 0u, 1u
+    %18:vec4<f32> = load %17
+    %19:f32 = access %18, 1u
+    ret %19
+  }
+}
+%load_vec_b = func():f32 -> %b7 {
+  %b7 = block {
+    %21:ptr<uniform, vec2<f32>, read_write> = access %buffer, 1u, 1u
+    %22:vec2<f32> = load %21
+    %23:f32 = access %22, 1u
+    ret %23
+  }
+}
+%lve_a = func():f32 -> %b8 {
+  %b8 = block {
+    %25:ptr<uniform, vec4<f32>, read_write> = access %buffer, 0u, 1u
+    %26:f32 = load_vector_element %25, 1u
+    ret %26
+  }
+}
+%lve_b = func():f32 -> %b9 {
+  %b9 = block {
+    %28:ptr<uniform, vec2<f32>, read_write> = access %buffer, 1u, 1u
+    %29:f32 = load_vector_element %28, 1u
+    ret %29
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+MyStruct = struct @align(16), @block {
+  a:mat4x4<f32> @offset(0)
+  b:mat3x2<f32> @offset(64)
+}
+
+MyStruct_std140 = struct @align(16), @block {
+  a:mat4x4<f32> @offset(0)
+  b_col0:vec2<f32> @offset(64)
+  b_col1:vec2<f32> @offset(72)
+  b_col2:vec2<f32> @offset(80)
+}
+
+%b1 = block {  # root
+  %buffer:ptr<uniform, MyStruct_std140, read_write> = var @binding_point(0, 0)
+}
+
+%load_struct_a = func():mat4x4<f32> -> %b2 {
+  %b2 = block {
+    %3:MyStruct_std140 = load %buffer
+    %4:MyStruct = call %convert_MyStruct, %3
+    %6:mat4x4<f32> = access %4, 0u
+    ret %6
+  }
+}
+%load_struct_b = func():mat3x2<f32> -> %b3 {
+  %b3 = block {
+    %8:MyStruct_std140 = load %buffer
+    %9:MyStruct = call %convert_MyStruct, %8
+    %10:mat3x2<f32> = access %9, 1u
+    ret %10
+  }
+}
+%load_mat_a = func():vec4<f32> -> %b4 {
+  %b4 = block {
+    %12:ptr<uniform, mat4x4<f32>, read_write> = access %buffer, 0u
+    %13:mat4x4<f32> = load %12
+    %14:vec4<f32> = access %13, 0u
+    ret %14
+  }
+}
+%load_mat_b = func():vec2<f32> -> %b5 {
+  %b5 = block {
+    %16:ptr<uniform, vec2<f32>, read_write> = access %buffer, 1u
+    %17:vec2<f32> = load %16
+    %18:ptr<uniform, vec2<f32>, read_write> = access %buffer, 2u
+    %19:vec2<f32> = load %18
+    %20:ptr<uniform, vec2<f32>, read_write> = access %buffer, 3u
+    %21:vec2<f32> = load %20
+    %22:mat3x2<f32> = construct %17, %19, %21
+    %23:vec2<f32> = access %22, 0u
+    ret %23
+  }
+}
+%load_vec_a = func():f32 -> %b6 {
+  %b6 = block {
+    %25:ptr<uniform, vec4<f32>, read_write> = access %buffer, 0u, 1u
+    %26:vec4<f32> = load %25
+    %27:f32 = access %26, 1u
+    ret %27
+  }
+}
+%load_vec_b = func():f32 -> %b7 {
+  %b7 = block {
+    %29:ptr<uniform, vec2<f32>, read_write> = access %buffer, 1u
+    %30:vec2<f32> = load %29
+    %31:ptr<uniform, vec2<f32>, read_write> = access %buffer, 2u
+    %32:vec2<f32> = load %31
+    %33:ptr<uniform, vec2<f32>, read_write> = access %buffer, 3u
+    %34:vec2<f32> = load %33
+    %35:mat3x2<f32> = construct %30, %32, %34
+    %36:vec2<f32> = access %35, 1u
+    %37:f32 = access %36, 1u
+    ret %37
+  }
+}
+%lve_a = func():f32 -> %b8 {
+  %b8 = block {
+    %39:ptr<uniform, vec4<f32>, read_write> = access %buffer, 0u, 1u
+    %40:f32 = load_vector_element %39, 1u
+    ret %40
+  }
+}
+%lve_b = func():f32 -> %b9 {
+  %b9 = block {
+    %42:ptr<uniform, vec2<f32>, read_write> = access %buffer, 1u
+    %43:vec2<f32> = load %42
+    %44:ptr<uniform, vec2<f32>, read_write> = access %buffer, 2u
+    %45:vec2<f32> = load %44
+    %46:ptr<uniform, vec2<f32>, read_write> = access %buffer, 3u
+    %47:vec2<f32> = load %46
+    %48:mat3x2<f32> = construct %43, %45, %47
+    %49:vec2<f32> = access %48, 1u
+    %50:f32 = access %49, 1u
+    ret %50
+  }
+}
+%convert_MyStruct = func(%input:MyStruct_std140):MyStruct -> %b10 {
+  %b10 = block {
+    %52:mat4x4<f32> = access %input, 0u
+    %53:vec2<f32> = access %input, 1u
+    %54:vec2<f32> = access %input, 2u
+    %55:vec2<f32> = access %input, 3u
+    %56:mat3x2<f32> = construct %53, %54, %55
+    %57:MyStruct = construct %52, %56
+    ret %57
+  }
+}
+)";
+
+    Run(Std140);
+
+    EXPECT_EQ(expect, str());
+}
+
 TEST_F(IR_Std140Test, F16) {
     auto* structure =
         ty.Struct(mod.symbols.New("MyStruct"), {
diff --git a/src/tint/lang/core/ir/transform/value_to_let.cc b/src/tint/lang/core/ir/transform/value_to_let.cc
new file mode 100644
index 0000000..7580900
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/value_to_let.cc
@@ -0,0 +1,186 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/transform/value_to_let.h"
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+
+namespace {
+
+/// Access is an enumerator of memory access operations
+enum class Access : uint8_t { kLoad, kStore };
+/// Accesses is a set of of Access
+using Accesses = EnumSet<Access>;
+
+/// @returns the accesses that may be performed by the instruction @p inst
+Accesses AccessesFor(ir::Instruction* inst) {
+    return tint::Switch<Accesses>(
+        inst,                                                           //
+        [&](const ir::Load*) { return Access::kLoad; },                 //
+        [&](const ir::LoadVectorElement*) { return Access::kLoad; },    //
+        [&](const ir::Store*) { return Access::kStore; },               //
+        [&](const ir::StoreVectorElement*) { return Access::kStore; },  //
+        [&](const ir::Call*) {
+            return Accesses{Access::kLoad, Access::kStore};
+        },
+        [&](Default) { return Accesses{}; });
+}
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    Module& ir;
+
+    /// The IR builder.
+    Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// Process the module.
+    void Process() {
+        // Process each block.
+        for (auto* block : ir.blocks.Objects()) {
+            Process(block);
+        }
+    }
+
+  private:
+    void Process(ir::Block* block) {
+        // A set of possibly-inlinable values returned by a instructions that has not yet been
+        // marked-for or ruled-out-for inlining.
+        Hashset<ir::InstructionResult*, 32> pending_resolution;
+        // The accesses of the values in pending_resolution.
+        Access pending_access = Access::kLoad;
+
+        auto put_pending_in_lets = [&] {
+            for (auto* pending : pending_resolution) {
+                PutInLet(pending);
+            }
+            pending_resolution.Clear();
+        };
+
+        auto maybe_put_in_let = [&](auto* inst) {
+            if (auto* result = inst->Result(0)) {
+                auto& usages = result->Usages();
+                switch (usages.Count()) {
+                    case 0:  // No usage
+                        break;
+                    case 1: {  // Single usage
+                        auto* usage = (*usages.begin()).instruction;
+                        if (usage->Block() == inst->Block()) {
+                            // Usage in same block. Assign to pending_resolution, as we don't
+                            // know whether its safe to inline yet.
+                            pending_resolution.Add(result);
+                        } else {
+                            // Usage from another block. Cannot inline.
+                            inst = PutInLet(result);
+                        }
+                        break;
+                    }
+                    default:  // Value has multiple usages. Cannot inline.
+                        inst = PutInLet(result);
+                        break;
+                }
+            }
+        };
+
+        for (ir::Instruction* inst = block->Front(); inst; inst = inst->next) {
+            // This transform assumes that all multi-result instructions have been replaced
+            TINT_ASSERT(inst->Results().Length() < 2);
+
+            // The memory accesses of this instruction
+            auto accesses = AccessesFor(inst);
+
+            for (auto* operand : inst->Operands()) {
+                // If the operand is in pending_resolution, then we know it has a single use and
+                // because it hasn't been removed with put_pending_in_lets(), we know its safe to
+                // inline without breaking access ordering. By inlining the operand, we are pulling
+                // the operand's instruction into the same statement as this instruction, so this
+                // instruction adopts the access of the operand.
+                if (auto* result = As<InstructionResult>(operand)) {
+                    if (pending_resolution.Remove(result)) {
+                        // Var and Let are always statements, and so can never be inlined. As such,
+                        // they do not need to propagate the pending resolution through them.
+                        if (!inst->IsAnyOf<Var, Let>()) {
+                            accesses.Add(pending_access);
+                        }
+                    }
+                }
+            }
+
+            if (accesses.Contains(Access::kStore)) {  // Note: Also handles load + store
+                put_pending_in_lets();
+                maybe_put_in_let(inst);
+            } else if (accesses.Contains(Access::kLoad)) {
+                if (pending_access != Access::kLoad) {
+                    put_pending_in_lets();
+                    pending_access = Access::kLoad;
+                }
+                maybe_put_in_let(inst);
+            }
+        }
+    }
+
+    /// PutInLet places the value into a new 'let' instruction, immediately after the value's
+    /// instruction
+    /// @param value the value to place into the 'let'
+    /// @return the created 'let' instruction.
+    ir::Let* PutInLet(ir::InstructionResult* value) {
+        auto* inst = value->Instruction();
+        auto* let = b.Let(value->Type());
+        value->ReplaceAllUsesWith(let->Result(0));
+        let->SetValue(value);
+        let->InsertAfter(inst);
+        if (auto name = b.ir.NameOf(value); name.IsValid()) {
+            b.ir.SetName(let->Result(0), name);
+            b.ir.ClearName(value);
+        }
+        return let;
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> ValueToLet(Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "ValueToLet transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/core/ir/transform/value_to_let.h
similarity index 66%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/core/ir/transform/value_to_let.h
index 6f0f657..895bc09 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/core/ir/transform/value_to_let.h
@@ -25,12 +25,9 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_VALUE_TO_LET_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_VALUE_TO_LET_H_
 
-#include <string>
-
-#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -38,15 +35,20 @@
 class Module;
 }
 
-namespace tint::wgsl::writer {
+namespace tint::core::ir::transform {
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
+/// ValueToLet is a transform that moves "non-inlinable" instruction values to let instructions.
+/// An expression is considered "non-inlinable" if any of the the following are true:
+/// * The value has multiple uses.
+/// * The value's instruction is a load that when inlined would cross a store instruction.
+/// * The value's instruction is a store instruction that when inlined would cross a load or store
+///   instruction.
+/// * The value is used in a block different to the value's instruction.
+///
 /// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
+/// @returns error diagnostics on failure
+Result<SuccessType> ValueToLet(Module& module);
 
-}  // namespace tint::wgsl::writer
+}  // namespace tint::core::ir::transform
 
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_VALUE_TO_LET_H_
diff --git a/src/tint/lang/core/ir/transform/value_to_let_test.cc b/src/tint/lang/core/ir/transform/value_to_let_test.cc
new file mode 100644
index 0000000..91e04d9
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/value_to_let_test.cc
@@ -0,0 +1,637 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/transform/value_to_let.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using IR_ValueToLetTest = TransformTest;
+
+TEST_F(IR_ValueToLetTest, Empty) {
+    auto* expect = R"(
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_Blah) {
+    auto* func = b.Function("F", ty.void_());
+    b.Append(func->Block(), [&] { b.Return(func); });
+
+    auto* src = R"(
+%F = func():void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_Unsequenced) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Let("x", 1_i);
+        auto* y = b.Let("y", 2_i);
+        auto* z = b.Let("z", b.Add<i32>(x, y));
+        b.Return(fn, z);
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %x:i32 = let 1i
+    %y:i32 = let 2i
+    %4:i32 = add %x, %y
+    %z:i32 = let %4
+    ret %z
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+TEST_F(IR_ValueToLetTest, NoModify_SequencedValueUsedWithNonSequenced) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        // select is called with one, inlinable sequenced operand and two non-sequenced values.
+        auto* y = b.Name("y", b.Call<i32>(core::BuiltinFn::kSelect, 2_i, x, false));
+        b.Return(fn, y);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    %y:i32 = select 2i, %x, false
+    ret %y
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_Inlinable_NestedCalls) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        auto* y = b.Name("y", b.Call(rmw, x));
+        auto* z = b.Name("z", b.Call(rmw, y));
+        b.Return(fn, z);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    %y:i32 = call %rmw, %x
+    %z:i32 = call %rmw, %y
+    ret %z
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_LetUsedTwice) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        // No need to create more lets, as these are already in lets
+        auto* x = b.Let("x", b.Call(rmw, 1_i));
+        auto* y = b.Name("y", b.Add<i32>(x, x));
+        b.Return(fn, y);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = call %rmw, 1i
+    %x:i32 = let %8
+    %y:i32 = add %x, %x
+    ret %y
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NoModify_VarUsedTwice) {
+    auto* p = b.FunctionParam<ptr<function, i32, read_write>>("p");
+    auto* fn_g = b.Function("g", ty.i32());
+    fn_g->SetParams({p});
+    b.Append(fn_g->Block(), [&] { b.Return(fn_g, b.Load(p)); });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* x = b.Let("x", b.Call(fn_g, v));
+        auto* y = b.Let("y", b.Call(fn_g, v));
+        b.Return(fn, b.Add<i32>(x, y));
+    });
+
+    auto* src = R"(
+%g = func(%p:ptr<function, i32, read_write>):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = load %p
+    ret %3
+  }
+}
+%F = func():i32 -> %b2 {
+  %b2 = block {
+    %v:ptr<function, i32, read_write> = var
+    %6:i32 = call %g, %v
+    %x:i32 = let %6
+    %8:i32 = call %g, %v
+    %y:i32 = let %8
+    %10:i32 = add %x, %y
+    ret %10
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, VarLoadUsedTwice) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* l = b.Name("l", b.Load(v));
+        b.Return(fn, b.Add<i32>(l, l));
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %l:i32 = load %v
+    %4:i32 = add %l, %l
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %l:i32 = let %3
+    %5:i32 = add %l, %l
+    ret %5
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, VarLoad_ThenStore_ThenUse) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* l = b.Name("l", b.Load(v));
+        b.Store(v, 1_i);
+        b.Return(fn, l);
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %l:i32 = load %v
+    store %v, 1i
+    ret %l
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %l:i32 = let %3
+    store %v, 1i
+    ret %l
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, TwoCalls_ThenUseReturnValues) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        auto* y = b.Name("y", b.Call(rmw, 2_i));
+        auto* z = b.Name("z", b.Add<i32>(x, y));
+        b.Return(fn, z);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    %y:i32 = call %rmw, 2i
+    %z:i32 = add %x, %y
+    ret %z
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = call %rmw, 1i
+    %x:i32 = let %8
+    %y:i32 = call %rmw, 2i
+    %z:i32 = add %x, %y
+    ret %z
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, SequencedUsedInDifferentBlock) {
+    auto* i = b.Var<private_, i32>("i");
+    b.ir.root_block->Append(i);
+
+    auto* p = b.FunctionParam<i32>("p");
+    auto* rmw = b.Function("rmw", ty.i32());
+    rmw->SetParams({p});
+    b.Append(rmw->Block(), [&] {
+        auto* v = b.Let("v", b.Add<i32>(b.Load(i), p));
+        b.Store(i, v);
+        b.Return(rmw, v);
+    });
+
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* x = b.Name("x", b.Call(rmw, 1_i));
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&] {  //
+            b.Return(fn, x);
+        });
+        b.Return(fn, 2_i);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %x:i32 = call %rmw, 1i
+    if true [t: %b4] {  # if_1
+      %b4 = block {  # true
+        ret %x
+      }
+    }
+    ret 2i
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %i:ptr<private, i32, read_write> = var
+}
+
+%rmw = func(%p:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, %p
+    %v:i32 = let %5
+    store %i, %v
+    ret %v
+  }
+}
+%F = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = call %rmw, 1i
+    %x:i32 = let %8
+    if true [t: %b4] {  # if_1
+      %b4 = block {  # true
+        ret %x
+      }
+    }
+    ret 2i
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NameMe1) {
+    auto* fn = b.Function("F", ty.i32());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* x = b.Load(v);
+        auto* y = b.Add<i32>(x, 1_i);
+        b.Store(v, 2_i);
+        b.Return(fn, y);
+    });
+
+    auto* src = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %4:i32 = add %3, 1i
+    store %v, 2i
+    ret %4
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():i32 -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %3:i32 = load %v
+    %4:i32 = add %3, 1i
+    %5:i32 = let %4
+    store %v, 2i
+    ret %5
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_ValueToLetTest, NameMe2) {
+    auto* fn = b.Function("F", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* i = b.Name("i", b.Call<i32>(core::BuiltinFn::kMax, 1_i, 2_i));
+        auto* v = b.Var<function>("v", i);
+        auto* x = b.Name("x", b.Call<i32>(core::BuiltinFn::kMax, 3_i, 4_i));
+        auto* y = b.Name("y", b.Load(v));
+        auto* z = b.Name("z", b.Add<i32>(y, x));
+        b.Store(v, z);
+        b.Return(fn);
+    });
+
+    auto* src = R"(
+%F = func():void -> %b1 {
+  %b1 = block {
+    %i:i32 = max 1i, 2i
+    %v:ptr<function, i32, read_write> = var, %i
+    %x:i32 = max 3i, 4i
+    %y:i32 = load %v
+    %z:i32 = add %y, %x
+    store %v, %z
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%F = func():void -> %b1 {
+  %b1 = block {
+    %i:i32 = max 1i, 2i
+    %v:ptr<function, i32, read_write> = var, %i
+    %x:i32 = max 3i, 4i
+    %y:i32 = load %v
+    %z:i32 = add %y, %x
+    store %v, %z
+    ret
+  }
+}
+)";
+
+    Run(ValueToLet);
+
+    EXPECT_EQ(expect, str());
+
+    Run(ValueToLet);  // running a second time should be no-op
+
+    EXPECT_EQ(expect, str());
+}
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc b/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
index 68e365a..134844c 100644
--- a/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
+++ b/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.cc
@@ -57,7 +57,7 @@
         Vector<Construct*, 8> worklist;
         for (auto inst : ir.instructions.Objects()) {
             if (auto* construct = inst->As<Construct>(); construct && construct->Alive()) {
-                if (construct->Result()->Type()->As<type::Matrix>()) {
+                if (construct->Result(0)->Type()->As<type::Matrix>()) {
                     if (construct->Operands().Length() > 0 &&
                         construct->Operands()[0]->Type()->Is<type::Scalar>()) {
                         b.InsertBefore(construct, [&] {  //
@@ -72,7 +72,7 @@
     /// Replace a matrix construct instruction.
     /// @param construct the instruction to replace
     void ReplaceConstructor(Construct* construct) {
-        auto* mat = construct->Result()->Type()->As<type::Matrix>();
+        auto* mat = construct->Result(0)->Type()->As<type::Matrix>();
         auto* col = mat->ColumnType();
         const auto& scalars = construct->Operands();
 
@@ -83,12 +83,12 @@
             for (uint32_t r = 0; r < col->Width(); r++) {
                 values.Push(scalars[c * col->Width() + r]);
             }
-            columns.Push(b.Construct(col, std::move(values))->Result());
+            columns.Push(b.Construct(col, std::move(values))->Result(0));
         }
 
         // Construct the matrix from the column vectors and replace the original instruction.
-        auto* replacement = b.Construct(mat, std::move(columns))->Result();
-        construct->Result()->ReplaceAllUsesWith(replacement);
+        auto* replacement = b.Construct(mat, std::move(columns))->Result(0);
+        construct->Result(0)->ReplaceAllUsesWith(replacement);
         construct->Destroy();
     }
 };
diff --git a/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc b/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc
index 4f42888..826ba54 100644
--- a/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc
+++ b/src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors_test.cc
@@ -45,7 +45,7 @@
     auto* func = b.Function("foo", mat);
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -72,7 +72,7 @@
     func->SetParams({value});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, value);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -101,7 +101,7 @@
     func->SetParams({v1, v2, v3});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -131,7 +131,7 @@
     func->SetParams({v1, v2, v3, v4});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -172,7 +172,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -215,7 +215,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6, v7, v8});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -256,7 +256,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -301,7 +301,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6, v7, v8, v9});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8, v9);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -349,7 +349,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -393,7 +393,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6, v7, v8});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -442,7 +442,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -496,7 +496,7 @@
     b.Append(func->Block(), [&] {
         auto* construct =
             b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
@@ -542,7 +542,7 @@
     func->SetParams({v1, v2, v3, v4, v5, v6, v7, v8, v9});
     b.Append(func->Block(), [&] {
         auto* construct = b.Construct(mat, v1, v2, v3, v4, v5, v6, v7, v8, v9);
-        b.Return(func, construct->Result());
+        b.Return(func, construct->Result(0));
     });
 
     auto* src = R"(
diff --git a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc
index 927844b..4bfa2cf 100644
--- a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc
+++ b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory.cc
@@ -101,10 +101,10 @@
         uint32_t next_id = 0;
         for (auto inst : *ir.root_block) {
             if (auto* var = inst->As<Var>()) {
-                auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+                auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
                 if (ptr && ptr->AddressSpace() == core::AddressSpace::kWorkgroup) {
                     // Record the usage of the variable for each block that references it.
-                    var->Result()->ForEachUse([&](const Usage& use) {
+                    var->Result(0)->ForEachUse([&](const Usage& use) {
                         block_to_direct_vars.GetOrZero(use.instruction->Block())->Add(var);
                     });
                     var_to_id.Add(var, next_id++);
@@ -113,7 +113,7 @@
         }
 
         // Process each entry point function.
-        for (auto* func : ir.functions) {
+        for (auto& func : ir.functions) {
             if (func->Stage() == Function::PipelineStage::kCompute) {
                 ProcessEntryPoint(func);
             }
@@ -138,7 +138,7 @@
         // Build list of store descriptors for all workgroup variables.
         StoreMap stores;
         for (auto* var : sorted_vars) {
-            PrepareStores(var, var->Result()->Type()->UnwrapPtr(), 1, {}, stores);
+            PrepareStores(var, var->Result(0)->Type()->UnwrapPtr(), 1, {}, stores);
         }
 
         // Sort the iteration counts to get deterministic output in tests.
@@ -279,7 +279,7 @@
                                                             BuiltinValue::kLocalInvocationIndex) {
                         auto* access = b.Access(ty.u32(), param, u32(member->Index()));
                         access->InsertBefore(func->Block()->Front());
-                        return access->Result();
+                        return access->Result(0);
                     }
                 }
             } else {
@@ -305,7 +305,7 @@
     /// @param total_count the total number of elements that will be zeroed
     /// @param linear_index the linear index of the single element that will be zeroed
     void GenerateStore(const Store& store, uint32_t total_count, Value* linear_index) {
-        auto* to = store.var->Result();
+        auto* to = store.var->Result(0);
         if (!store.indices.IsEmpty()) {
             // Build the access indices to get to the target element.
             // We walk backwards along the index list so that adjacent invocation store to
@@ -319,10 +319,10 @@
                     auto array_index = std::get<ArrayIndex>(idx);
                     Value* index = linear_index;
                     if (count > 1) {
-                        index = b.Divide(ty.u32(), index, u32(count))->Result();
+                        index = b.Divide(ty.u32(), index, u32(count))->Result(0);
                     }
                     if (total_count > count * array_index.count) {
-                        index = b.Modulo(ty.u32(), index, u32(array_index.count))->Result();
+                        index = b.Modulo(ty.u32(), index, u32(array_index.count))->Result(0);
                     }
                     indices.Push(index);
                     count *= array_index.count;
@@ -332,7 +332,7 @@
                 }
             }
             indices.Reverse();
-            to = b.Access(ty.ptr(workgroup, store.store_type), to, indices)->Result();
+            to = b.Access(ty.ptr(workgroup, store.store_type), to, indices)->Result(0);
         }
 
         // Generate the store instruction.
diff --git a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
index 3546b1e..d0a972e 100644
--- a/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
+++ b/src/tint/lang/core/ir/transform/zero_init_workgroup_memory_test.cc
@@ -1579,7 +1579,7 @@
         auto* ifelse = b.If(true);
         b.Append(ifelse->True(), [&] {  //
             auto* sw = b.Switch(42_i);
-            auto* def_case = b.Case(sw, Vector{core::ir::Switch::CaseSelector()});
+            auto* def_case = b.DefaultCase(sw);
             b.Append(def_case, [&] {  //
                 auto* loop = b.Loop();
                 b.Append(loop->Body(), [&] {  //
@@ -1703,7 +1703,7 @@
         auto* ifelse = b.If(true);
         b.Append(ifelse->True(), [&] {  //
             auto* sw = b.Switch(42_i);
-            auto* def_case = b.Case(sw, Vector{core::ir::Switch::CaseSelector()});
+            auto* def_case = b.DefaultCase(sw);
             b.Append(def_case, [&] {  //
                 auto* loop = b.Loop();
                 b.Append(loop->Body(), [&] {  //
diff --git a/src/tint/lang/core/ir/traverse.h b/src/tint/lang/core/ir/traverse.h
index 56257bc..6e20a84 100644
--- a/src/tint/lang/core/ir/traverse.h
+++ b/src/tint/lang/core/ir/traverse.h
@@ -54,7 +54,7 @@
                 callback(as_t);
             }
             if (auto* ctrl = inst->As<ControlInstruction>()) {
-                if (auto* next = inst->next) {
+                if (Instruction* next = inst->next) {
                     queue.Push(next);  // Resume iteration of this block
                 }
 
diff --git a/src/tint/lang/core/ir/traverse_test.cc b/src/tint/lang/core/ir/traverse_test.cc
index 0fc7397..d792c24 100644
--- a/src/tint/lang/core/ir/traverse_test.cc
+++ b/src/tint/lang/core/ir/traverse_test.cc
@@ -71,10 +71,10 @@
         auto* switch_ = b.Switch(1_i);
         expect.Push(switch_);
 
-        auto* case_0 = b.Case(switch_, {Switch::CaseSelector{b.Constant(0_i)}});
+        auto* case_0 = b.Case(switch_, {b.Constant(0_i)});
         b.Append(case_0, [&] { expect.Push(b.Var<function, i32>()); });
 
-        auto* case_1 = b.Case(switch_, {Switch::CaseSelector{b.Constant(1_i)}});
+        auto* case_1 = b.Case(switch_, {b.Constant(1_i)});
         b.Append(case_1, [&] { expect.Push(b.Var<function, i32>()); });
 
         expect.Push(b.Var<function, i32>());
@@ -113,10 +113,10 @@
 
         auto* switch_ = b.Switch(1_i);
 
-        auto* case_0 = b.Case(switch_, {Switch::CaseSelector{b.Constant(0_i)}});
+        auto* case_0 = b.Case(switch_, {b.Constant(0_i)});
         b.Append(case_0, [&] { b.Var<function, i32>(); });
 
-        auto* case_1 = b.Case(switch_, {Switch::CaseSelector{b.Constant(1_i)}});
+        auto* case_1 = b.Case(switch_, {b.Constant(1_i)});
         b.Append(case_1, [&] { b.Var<function, i32>(); });
 
         b.Var<function, i32>();
diff --git a/src/tint/lang/core/ir/unary.cc b/src/tint/lang/core/ir/unary.cc
index 2b81d7b..b79a23c 100644
--- a/src/tint/lang/core/ir/unary.cc
+++ b/src/tint/lang/core/ir/unary.cc
@@ -42,7 +42,7 @@
 Unary::~Unary() = default;
 
 Unary* Unary::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* val = ctx.Remap(Val());
     return ctx.ir.instructions.Create<Unary>(new_result, op_, val);
 }
diff --git a/src/tint/lang/core/ir/unary.h b/src/tint/lang/core/ir/unary.h
index c25f5fc..af03473 100644
--- a/src/tint/lang/core/ir/unary.h
+++ b/src/tint/lang/core/ir/unary.h
@@ -60,11 +60,14 @@
     /// @returns the value for the instruction
     Value* Val() { return operands_[kValueOperandOffset]; }
 
+    /// @returns the value for the instruction
+    const Value* Val() const { return operands_[kValueOperandOffset]; }
+
     /// @returns the unary operator
-    UnaryOp Op() { return op_; }
+    UnaryOp Op() const { return op_; }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "unary"; }
+    std::string FriendlyName() const override { return "unary"; }
 
   private:
     UnaryOp op_;
diff --git a/src/tint/lang/core/ir/unary_test.cc b/src/tint/lang/core/ir/unary_test.cc
index bbdccdd..a984045 100644
--- a/src/tint/lang/core/ir/unary_test.cc
+++ b/src/tint/lang/core/ir/unary_test.cc
@@ -75,10 +75,9 @@
 
 TEST_F(IR_UnaryTest, Result) {
     auto* inst = b.Negation(mod.Types().i32(), 4_i);
-    EXPECT_TRUE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
-    EXPECT_TRUE(inst->Result()->Is<InstructionResult>());
-    EXPECT_EQ(inst->Result()->Source(), inst);
+    EXPECT_EQ(inst->Results().Length(), 1u);
+    EXPECT_TRUE(inst->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(inst->Result(0)->Instruction(), inst);
 }
 
 TEST_F(IR_UnaryTest, Fail_NullType) {
@@ -96,8 +95,8 @@
     auto* new_inst = clone_ctx.Clone(inst);
 
     EXPECT_NE(inst, new_inst);
-    EXPECT_NE(nullptr, new_inst->Result());
-    EXPECT_NE(inst->Result(), new_inst->Result());
+    EXPECT_NE(nullptr, new_inst->Result(0));
+    EXPECT_NE(inst->Result(0), new_inst->Result(0));
 
     EXPECT_EQ(UnaryOp::kComplement, new_inst->Op());
 
diff --git a/src/tint/lang/core/ir/unreachable.h b/src/tint/lang/core/ir/unreachable.h
index 577b2f9..43cddc9 100644
--- a/src/tint/lang/core/ir/unreachable.h
+++ b/src/tint/lang/core/ir/unreachable.h
@@ -43,7 +43,7 @@
     Unreachable* Clone(CloneContext& ctx) override;
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "unreachable"; }
+    std::string FriendlyName() const override { return "unreachable"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unreachable_test.cc b/src/tint/lang/core/ir/unreachable_test.cc
index 23359f3..97a56dd 100644
--- a/src/tint/lang/core/ir/unreachable_test.cc
+++ b/src/tint/lang/core/ir/unreachable_test.cc
@@ -43,8 +43,7 @@
 TEST_F(IR_UnreachableTest, Result) {
     auto* inst = b.Unreachable();
 
-    EXPECT_FALSE(inst->HasResults());
-    EXPECT_FALSE(inst->HasMultiResults());
+    EXPECT_TRUE(inst->Results().IsEmpty());
 }
 
 TEST_F(IR_UnreachableTest, Clone) {
diff --git a/src/tint/lang/core/ir/user_call.cc b/src/tint/lang/core/ir/user_call.cc
index 0407fa9..698cb62 100644
--- a/src/tint/lang/core/ir/user_call.cc
+++ b/src/tint/lang/core/ir/user_call.cc
@@ -46,7 +46,7 @@
 UserCall::~UserCall() = default;
 
 UserCall* UserCall::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* target = ctx.Remap(Target());
     auto args = ctx.Remap<UserCall::kDefaultNumOperands>(Args());
     return ctx.ir.instructions.Create<UserCall>(new_result, target, args);
diff --git a/src/tint/lang/core/ir/user_call.h b/src/tint/lang/core/ir/user_call.h
index 4f74824..4036208 100644
--- a/src/tint/lang/core/ir/user_call.h
+++ b/src/tint/lang/core/ir/user_call.h
@@ -55,8 +55,8 @@
     /// @copydoc Instruction::Clone()
     UserCall* Clone(CloneContext& ctx) override;
 
-    /// @returns the call arguments
-    tint::Slice<Value*> Args() override { return operands_.Slice().Offset(kArgsOperandOffset); }
+    /// @returns the offset of the arguments in Operands()
+    size_t ArgsOperandOffset() const override { return kArgsOperandOffset; }
 
     /// Replaces the call arguments to @p arguments
     /// @param arguments the new call arguments
@@ -65,12 +65,15 @@
     /// @returns the called function
     Function* Target() { return operands_[kFunctionOperandOffset]->As<ir::Function>(); }
 
+    /// @returns the called function
+    const Function* Target() const { return operands_[kFunctionOperandOffset]->As<ir::Function>(); }
+
     /// Sets called function
     /// @param target the new target of the call
     void SetTarget(Function* target) { SetOperand(kFunctionOperandOffset, target); }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "call"; }
+    std::string FriendlyName() const override { return "call"; }
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/user_call_test.cc b/src/tint/lang/core/ir/user_call_test.cc
index 1758bae..a0d65ee 100644
--- a/src/tint/lang/core/ir/user_call_test.cc
+++ b/src/tint/lang/core/ir/user_call_test.cc
@@ -53,10 +53,9 @@
     auto* arg2 = b.Constant(2_u);
     auto* e = b.Call(mod.Types().void_(), func, Vector{arg1, arg2});
 
-    EXPECT_TRUE(e->HasResults());
-    EXPECT_FALSE(e->HasMultiResults());
-    EXPECT_TRUE(e->Result()->Is<InstructionResult>());
-    EXPECT_EQ(e->Result()->Source(), e);
+    EXPECT_EQ(e->Results().Length(), 1u);
+    EXPECT_TRUE(e->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(e->Result(0)->Instruction(), e);
 }
 
 TEST_F(IR_UserCallTest, Fail_NullType) {
@@ -77,8 +76,8 @@
     auto* new_e = clone_ctx.Clone(e);
 
     EXPECT_NE(e, new_e);
-    EXPECT_NE(nullptr, new_e->Result());
-    EXPECT_NE(e->Result(), new_e->Result());
+    EXPECT_NE(nullptr, new_e->Result(0));
+    EXPECT_NE(e->Result(0), new_e->Result(0));
 
     EXPECT_EQ(new_func, new_e->Target());
 
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index 037610f..a3a83df 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -90,7 +90,7 @@
   public:
     /// Create a core validator
     /// @param mod the module to be validated
-    explicit Validator(Module& mod);
+    explicit Validator(const Module& mod);
 
     /// Destructor
     ~Validator();
@@ -103,47 +103,47 @@
     /// @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);
+    std::string InstError(const 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);
+    void AddError(const 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);
+    void AddError(const 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);
+    void AddResultError(const 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);
+    void AddError(const 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);
+    void AddNote(const 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);
+    void AddNote(const 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);
+    void AddNote(const Block* blk, std::string err);
 
     /// Adds an error to the diagnostics
     /// @param err the message to emit
@@ -157,160 +157,163 @@
 
     /// @param v the value to get the name for
     /// @returns the name for the given value
-    std::string Name(Value* v);
+    std::string Name(const Value* v);
 
     /// Checks the given operand is not null
-    /// @param inst the instruciton
+    /// @param inst the instruction
     /// @param operand the operand
     /// @param idx the operand index
-    void CheckOperandNotNull(ir::Instruction* inst, ir::Value* operand, size_t idx);
+    void CheckOperandNotNull(const ir::Instruction* inst, const 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);
+    void CheckOperandsNotNull(const ir::Instruction* inst,
+                              size_t start_operand,
+                              size_t end_operand);
 
     /// Validates the root block
     /// @param blk the block
-    void CheckRootBlock(Block* blk);
+    void CheckRootBlock(const Block* blk);
 
     /// Validates the given function
     /// @param func the function validate
-    void CheckFunction(Function* func);
+    void CheckFunction(const Function* func);
 
     /// Validates the given block
     /// @param blk the block to validate
-    void CheckBlock(Block* blk);
+    void CheckBlock(const Block* blk);
 
     /// Validates the given instruction
     /// @param inst the instruction to validate
-    void CheckInstruction(Instruction* inst);
+    void CheckInstruction(const Instruction* inst);
 
     /// Validates the given var
     /// @param var the var to validate
-    void CheckVar(Var* var);
+    void CheckVar(const Var* var);
 
     /// Validates the given let
     /// @param let the let to validate
-    void CheckLet(Let* let);
+    void CheckLet(const Let* let);
 
     /// Validates the given call
     /// @param call the call to validate
-    void CheckCall(Call* call);
+    void CheckCall(const Call* call);
 
     /// Validates the given builtin call
     /// @param call the call to validate
-    void CheckBuiltinCall(BuiltinCall* call);
+    void CheckBuiltinCall(const BuiltinCall* call);
 
     /// Validates the given user call
     /// @param call the call to validate
-    void CheckUserCall(UserCall* call);
+    void CheckUserCall(const UserCall* call);
 
     /// Validates the given access
     /// @param a the access to validate
-    void CheckAccess(ir::Access* a);
+    void CheckAccess(const Access* a);
 
     /// Validates the given binary
     /// @param b the binary to validate
-    void CheckBinary(ir::Binary* b);
+    void CheckBinary(const Binary* b);
 
     /// Validates the given unary
     /// @param u the unary to validate
-    void CheckUnary(ir::Unary* u);
+    void CheckUnary(const Unary* u);
 
     /// Validates the given if
     /// @param if_ the if to validate
-    void CheckIf(If* if_);
+    void CheckIf(const If* if_);
 
     /// Validates the given loop
     /// @param l the loop to validate
-    void CheckLoop(Loop* l);
+    void CheckLoop(const Loop* l);
 
     /// Validates the given switch
     /// @param s the switch to validate
-    void CheckSwitch(Switch* s);
+    void CheckSwitch(const Switch* s);
 
     /// Validates the given terminator
     /// @param b the terminator to validate
-    void CheckTerminator(ir::Terminator* b);
+    void CheckTerminator(const Terminator* b);
 
     /// Validates the given exit
     /// @param e the exit to validate
-    void CheckExit(ir::Exit* e);
+    void CheckExit(const Exit* e);
 
     /// Validates the given exit if
     /// @param e the exit if to validate
-    void CheckExitIf(ExitIf* e);
+    void CheckExitIf(const ExitIf* e);
 
     /// Validates the given return
     /// @param r the return to validate
-    void CheckReturn(Return* r);
+    void CheckReturn(const 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);
+    void CheckControlsAllowingIf(const Exit* exit, const Instruction* control);
 
     /// Validates the given exit switch
     /// @param s the exit switch to validate
-    void CheckExitSwitch(ExitSwitch* s);
+    void CheckExitSwitch(const ExitSwitch* s);
 
     /// Validates the given exit loop
     /// @param l the exit loop to validate
-    void CheckExitLoop(ExitLoop* l);
+    void CheckExitLoop(const ExitLoop* l);
 
     /// Validates the given store
     /// @param s the store to validate
-    void CheckStore(Store* s);
+    void CheckStore(const Store* s);
 
     /// Validates the given load vector element
     /// @param l the load vector element to validate
-    void CheckLoadVectorElement(LoadVectorElement* l);
+    void CheckLoadVectorElement(const LoadVectorElement* l);
 
     /// Validates the given store vector element
     /// @param s the store vector element to validate
-    void CheckStoreVectorElement(StoreVectorElement* s);
+    void CheckStoreVectorElement(const 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);
+    const core::type::Type* GetVectorPtrElementType(const Instruction* inst, size_t idx);
 
   private:
-    Module& mod_;
+    const Module& mod_;
+    std::shared_ptr<Source::File> disassembly_file;
     diag::List diagnostics_;
     Disassembler dis_{mod_};
-    Block* current_block_ = nullptr;
-    Hashset<Function*, 4> all_functions_;
-    Hashset<Instruction*, 4> visited_instructions_;
-    Vector<ControlInstruction*, 8> control_stack_;
+    const Block* current_block_ = nullptr;
+    Hashset<const Function*, 4> all_functions_;
+    Hashset<const Instruction*, 4> visited_instructions_;
+    Vector<const ControlInstruction*, 8> control_stack_;
 
     void DisassembleIfNeeded();
 };
 
-Validator::Validator(Module& mod) : mod_(mod) {}
+Validator::Validator(const Module& mod) : mod_(mod) {}
 
 Validator::~Validator() = default;
 
 void Validator::DisassembleIfNeeded() {
-    if (mod_.disassembly_file) {
+    if (disassembly_file) {
         return;
     }
-    mod_.disassembly_file = std::make_unique<Source::File>("", dis_.Disassemble());
+    disassembly_file = std::make_unique<Source::File>("", dis_.Disassemble());
 }
 
 Result<SuccessType> Validator::Run() {
     CheckRootBlock(mod_.root_block);
 
-    for (auto* func : mod_.functions) {
-        if (!all_functions_.Add(func)) {
-            AddError("function '" + Name(func) + "' added to module multiple times");
+    for (auto& func : mod_.functions) {
+        if (!all_functions_.Add(func.Get())) {
+            AddError("function '" + Name(func.Get()) + "' added to module multiple times");
         }
     }
 
-    for (auto* func : mod_.functions) {
+    for (auto& func : mod_.functions) {
         CheckFunction(func);
     }
 
@@ -326,20 +329,19 @@
     if (diagnostics_.contains_errors()) {
         DisassembleIfNeeded();
         diagnostics_.add_note(tint::diag::System::IR,
-                              "# Disassembly\n" + mod_.disassembly_file->content.data, {});
+                              "# Disassembly\n" + disassembly_file->content.data, {});
         return Failure{std::move(diagnostics_)};
     }
     return Success;
 }
 
-std::string Validator::InstError(Instruction* inst, std::string err) {
+std::string Validator::InstError(const Instruction* inst, std::string err) {
     return std::string(inst->FriendlyName()) + ": " + err;
 }
 
-void Validator::AddError(Instruction* inst, std::string err) {
+void Validator::AddError(const Instruction* inst, std::string err) {
     DisassembleIfNeeded();
     auto src = dis_.InstructionSource(inst);
-    src.file = mod_.disassembly_file.get();
     AddError(std::move(err), src);
 
     if (current_block_) {
@@ -347,10 +349,9 @@
     }
 }
 
-void Validator::AddError(Instruction* inst, size_t idx, std::string err) {
+void Validator::AddError(const Instruction* inst, size_t idx, std::string err) {
     DisassembleIfNeeded();
-    auto src = dis_.OperandSource(Usage{inst, static_cast<uint32_t>(idx)});
-    src.file = mod_.disassembly_file.get();
+    auto src = dis_.OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
     AddError(std::move(err), src);
 
     if (current_block_) {
@@ -358,10 +359,9 @@
     }
 }
 
-void Validator::AddResultError(Instruction* inst, size_t idx, std::string err) {
+void Validator::AddResultError(const Instruction* inst, size_t idx, std::string err) {
     DisassembleIfNeeded();
-    auto src = dis_.ResultSource(Usage{inst, static_cast<uint32_t>(idx)});
-    src.file = mod_.disassembly_file.get();
+    auto src = dis_.ResultSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
     AddError(std::move(err), src);
 
     if (current_block_) {
@@ -369,53 +369,57 @@
     }
 }
 
-void Validator::AddError(Block* blk, std::string err) {
+void Validator::AddError(const Block* blk, std::string err) {
     DisassembleIfNeeded();
     auto src = dis_.BlockSource(blk);
-    src.file = mod_.disassembly_file.get();
     AddError(std::move(err), src);
 }
 
-void Validator::AddNote(Instruction* inst, std::string err) {
+void Validator::AddNote(const Instruction* inst, std::string err) {
     DisassembleIfNeeded();
     auto src = dis_.InstructionSource(inst);
-    src.file = mod_.disassembly_file.get();
     AddNote(std::move(err), src);
 }
 
-void Validator::AddNote(Instruction* inst, size_t idx, std::string err) {
+void Validator::AddNote(const Instruction* inst, size_t idx, std::string err) {
     DisassembleIfNeeded();
-    auto src = dis_.OperandSource(Usage{inst, static_cast<uint32_t>(idx)});
-    src.file = mod_.disassembly_file.get();
+    auto src = dis_.OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
     AddNote(std::move(err), src);
 }
 
-void Validator::AddNote(Block* blk, std::string err) {
+void Validator::AddNote(const Block* blk, std::string err) {
     DisassembleIfNeeded();
     auto src = dis_.BlockSource(blk);
-    src.file = mod_.disassembly_file.get();
     AddNote(std::move(err), src);
 }
 
 void Validator::AddError(std::string err, Source src) {
-    diagnostics_.add_error(tint::diag::System::IR, std::move(err), src);
+    auto& diag = diagnostics_.add_error(tint::diag::System::IR, std::move(err), src);
+    if (src.range != Source::Range{{}}) {
+        diag.source.file = disassembly_file.get();
+        diag.owned_file = disassembly_file;
+    }
 }
 
 void Validator::AddNote(std::string note, Source src) {
-    diagnostics_.add_note(tint::diag::System::IR, std::move(note), src);
+    auto& diag = diagnostics_.add_note(tint::diag::System::IR, std::move(note), src);
+    if (src.range != Source::Range{{}}) {
+        diag.source.file = disassembly_file.get();
+        diag.owned_file = disassembly_file;
+    }
 }
 
-std::string Validator::Name(Value* v) {
+std::string Validator::Name(const Value* v) {
     return mod_.NameOf(v).Name();
 }
 
-void Validator::CheckOperandNotNull(ir::Instruction* inst, ir::Value* operand, size_t idx) {
+void Validator::CheckOperandNotNull(const Instruction* inst, const ir::Value* operand, size_t idx) {
     if (operand == nullptr) {
         AddError(inst, idx, InstError(inst, "operand is undefined"));
     }
 }
 
-void Validator::CheckOperandsNotNull(ir::Instruction* inst,
+void Validator::CheckOperandsNotNull(const Instruction* inst,
                                      size_t start_operand,
                                      size_t end_operand) {
     auto operands = inst->Operands();
@@ -424,7 +428,7 @@
     }
 }
 
-void Validator::CheckRootBlock(Block* blk) {
+void Validator::CheckRootBlock(const Block* blk) {
     TINT_SCOPED_ASSIGNMENT(current_block_, blk);
 
     for (auto* inst : *blk) {
@@ -444,14 +448,14 @@
     }
 }
 
-void Validator::CheckFunction(Function* func) {
+void Validator::CheckFunction(const Function* func) {
     CheckBlock(func->Block());
 }
 
-void Validator::CheckBlock(Block* blk) {
+void Validator::CheckBlock(const Block* blk) {
     TINT_SCOPED_ASSIGNMENT(current_block_, blk);
 
-    if (!blk->HasTerminator()) {
+    if (!blk->Terminator()) {
         AddError(blk, "block: does not end in a terminator instruction");
     }
 
@@ -470,27 +474,25 @@
     }
 }
 
-void Validator::CheckInstruction(Instruction* inst) {
+void Validator::CheckInstruction(const Instruction* inst) {
     visited_instructions_.Add(inst);
     if (!inst->Alive()) {
         AddError(inst, InstError(inst, "destroyed instruction found in instruction list"));
         return;
     }
-    if (inst->HasResults()) {
-        auto results = inst->Results();
-        for (size_t i = 0; i < results.Length(); ++i) {
-            auto* res = results[i];
-            if (!res) {
-                AddResultError(inst, i, InstError(inst, "instruction result is undefined"));
-                continue;
-            }
+    auto results = inst->Results();
+    for (size_t i = 0; i < results.Length(); ++i) {
+        auto* res = results[i];
+        if (!res) {
+            AddResultError(inst, i, InstError(inst, "instruction result is undefined"));
+            continue;
+        }
 
-            if (res->Source() == nullptr) {
-                AddResultError(inst, i, InstError(inst, "instruction result source is undefined"));
-            } else if (res->Source() != inst) {
-                AddResultError(inst, i,
-                               InstError(inst, "instruction result source has wrong instruction"));
-            }
+        if (res->Instruction() == nullptr) {
+            AddResultError(inst, i, InstError(inst, "instruction result source is undefined"));
+        } else if (res->Instruction() != inst) {
+            AddResultError(inst, i,
+                           InstError(inst, "instruction result source has wrong instruction"));
         }
     }
 
@@ -508,7 +510,7 @@
                      InstError(inst, "instruction operand " + std::to_string(i) + " is not alive"));
         }
 
-        if (!op->Usages().Contains({inst, i})) {
+        if (!op->HasUsage(inst, i)) {
             AddError(
                 inst, i,
                 InstError(inst, "instruction operand " + std::to_string(i) + " missing usage"));
@@ -516,78 +518,86 @@
     }
 
     tint::Switch(
-        inst,                                                        //
-        [&](Access* a) { CheckAccess(a); },                          //
-        [&](Binary* b) { CheckBinary(b); },                          //
-        [&](Call* c) { CheckCall(c); },                              //
-        [&](If* if_) { CheckIf(if_); },                              //
-        [&](Let* let) { CheckLet(let); },                            //
-        [&](Load*) {},                                               //
-        [&](LoadVectorElement* l) { CheckLoadVectorElement(l); },    //
-        [&](Loop* l) { CheckLoop(l); },                              //
-        [&](Store* s) { CheckStore(s); },                            //
-        [&](StoreVectorElement* s) { CheckStoreVectorElement(s); },  //
-        [&](Switch* s) { CheckSwitch(s); },                          //
-        [&](Swizzle*) {},                                            //
-        [&](Terminator* b) { CheckTerminator(b); },                  //
-        [&](Unary* u) { CheckUnary(u); },                            //
-        [&](Var* var) { CheckVar(var); },                            //
-        [&](Default) { AddError(inst, InstError(inst, "missing validation")); });
+        inst,                                                              //
+        [&](const Access* a) { CheckAccess(a); },                          //
+        [&](const Binary* b) { CheckBinary(b); },                          //
+        [&](const Call* c) { CheckCall(c); },                              //
+        [&](const If* if_) { CheckIf(if_); },                              //
+        [&](const Let* let) { CheckLet(let); },                            //
+        [&](const Load*) {},                                               //
+        [&](const LoadVectorElement* l) { CheckLoadVectorElement(l); },    //
+        [&](const Loop* l) { CheckLoop(l); },                              //
+        [&](const Store* s) { CheckStore(s); },                            //
+        [&](const StoreVectorElement* s) { CheckStoreVectorElement(s); },  //
+        [&](const Switch* s) { CheckSwitch(s); },                          //
+        [&](const Swizzle*) {},                                            //
+        [&](const Terminator* b) { CheckTerminator(b); },                  //
+        [&](const Unary* u) { CheckUnary(u); },                            //
+        [&](const Var* var) { CheckVar(var); },                            //
+        [&](const Default) { AddError(inst, InstError(inst, "missing validation")); });
 }
 
-void Validator::CheckVar(Var* var) {
-    if (var->Result() && var->Initializer()) {
-        if (var->Initializer()->Type() != var->Result()->Type()->UnwrapPtr()) {
+void Validator::CheckVar(const Var* var) {
+    if (var->Result(0) && var->Initializer()) {
+        if (var->Initializer()->Type() != var->Result(0)->Type()->UnwrapPtr()) {
             AddError(var, InstError(var, "initializer has incorrect type"));
         }
     }
 }
 
-void Validator::CheckLet(Let* let) {
+void Validator::CheckLet(const Let* let) {
     CheckOperandNotNull(let, let->Value(), Let::kValueOperandOffset);
 
-    if (let->Result() && let->Value()) {
-        if (let->Result()->Type() != let->Value()->Type()) {
+    if (let->Result(0) && let->Value()) {
+        if (let->Result(0)->Type() != let->Value()->Type()) {
             AddError(let, InstError(let, "result type does not match value type"));
         }
     }
 }
 
-void Validator::CheckCall(Call* call) {
+void Validator::CheckCall(const Call* call) {
     tint::Switch(
-        call,                                          //
-        [&](Bitcast*) {},                              //
-        [&](BuiltinCall* c) { CheckBuiltinCall(c); },  //
-        [&](Construct*) {},                            //
-        [&](Convert*) {},                              //
-        [&](Discard*) {},                              //
-        [&](UserCall* c) { CheckUserCall(c); },        //
+        call,                                                //
+        [&](const Bitcast*) {},                              //
+        [&](const BuiltinCall* c) { CheckBuiltinCall(c); },  //
+        [&](const Construct*) {},                            //
+        [&](const Convert*) {},                              //
+        [&](const Discard*) {},                              //
+        [&](const UserCall* c) { CheckUserCall(c); },        //
         [&](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_};
+void Validator::CheckBuiltinCall(const BuiltinCall* call) {
+    auto symbols = SymbolTable::Wrap(mod_.symbols);
+    auto type_mgr = type::Manager::Wrap(mod_.Types());
+
+    auto args = Transform<8>(call->Args(), [&](const ir::Value* v) { return v->Type(); });
+    intrinsic::Context context{
+        call->TableData(),
+        type_mgr,
+        symbols,
+        diagnostics_,
+    };
 
     auto result = core::intrinsic::LookupFn(context, call->FriendlyName().c_str(), call->FuncId(),
                                             args, core::EvaluationStage::kRuntime, Source{});
     if (result) {
-        if (result->return_type != call->Result()->Type()) {
+        if (result->return_type != call->Result(0)->Type()) {
             AddError(call, InstError(call, "call result type does not match builtin return type"));
         }
     }
 }
 
-void Validator::CheckUserCall(UserCall* call) {
+void Validator::CheckUserCall(const UserCall* call) {
     if (!all_functions_.Contains(call->Target())) {
         AddError(call, UserCall::kFunctionOperandOffset,
                  InstError(call, "call target is not part of the module"));
     }
 }
 
-void Validator::CheckAccess(ir::Access* a) {
+void Validator::CheckAccess(const Access* a) {
     bool is_ptr = a->Object()->Type()->Is<core::type::Pointer>();
     auto* ty = a->Object()->Type()->UnwrapPtr();
 
@@ -645,8 +655,8 @@
         }
     }
 
-    auto* want_ty = a->Result()->Type()->UnwrapPtr();
-    bool want_ptr = a->Result()->Type()->Is<core::type::Pointer>();
+    auto* want_ty = a->Result(0)->Type()->UnwrapPtr();
+    bool want_ptr = a->Result(0)->Type()->Is<core::type::Pointer>();
     if (TINT_UNLIKELY(ty != want_ty || is_ptr != want_ptr)) {
         std::string want =
             want_ptr ? "ptr<" + want_ty->FriendlyName() + ">" : want_ty->FriendlyName();
@@ -656,21 +666,21 @@
     }
 }
 
-void Validator::CheckBinary(ir::Binary* b) {
+void Validator::CheckBinary(const Binary* b) {
     CheckOperandsNotNull(b, Binary::kLhsOperandOffset, Binary::kRhsOperandOffset);
 }
 
-void Validator::CheckUnary(ir::Unary* u) {
+void Validator::CheckUnary(const Unary* u) {
     CheckOperandNotNull(u, u->Val(), Unary::kValueOperandOffset);
 
-    if (u->Result() && u->Val()) {
-        if (u->Result()->Type() != u->Val()->Type()) {
+    if (u->Result(0) && u->Val()) {
+        if (u->Result(0)->Type() != u->Val()->Type()) {
             AddError(u, InstError(u, "result type must match value type"));
         }
     }
 }
 
-void Validator::CheckIf(If* if_) {
+void Validator::CheckIf(const If* if_) {
     CheckOperandNotNull(if_, if_->Condition(), If::kConditionOperandOffset);
 
     if (if_->Condition() && !if_->Condition()->Type()->Is<core::type::Bool>()) {
@@ -687,7 +697,7 @@
     }
 }
 
-void Validator::CheckLoop(Loop* l) {
+void Validator::CheckLoop(const Loop* l) {
     control_stack_.Push(l);
     TINT_DEFER(control_stack_.Pop());
 
@@ -701,7 +711,7 @@
     }
 }
 
-void Validator::CheckSwitch(Switch* s) {
+void Validator::CheckSwitch(const Switch* s) {
     control_stack_.Push(s);
     TINT_DEFER(control_stack_.Pop());
 
@@ -710,23 +720,23 @@
     }
 }
 
-void Validator::CheckTerminator(ir::Terminator* b) {
+void Validator::CheckTerminator(const Terminator* b) {
     // Note, transforms create `undef` terminator arguments (this is done in MergeReturn and
     // DemoteToHelper) so we can't add validation.
 
     tint::Switch(
-        b,                                           //
-        [&](ir::BreakIf*) {},                        //
-        [&](ir::Continue*) {},                       //
-        [&](ir::Exit* e) { CheckExit(e); },          //
-        [&](ir::NextIteration*) {},                  //
-        [&](ir::Return* ret) { CheckReturn(ret); },  //
-        [&](ir::TerminateInvocation*) {},            //
-        [&](ir::Unreachable*) {},                    //
+        b,                                                 //
+        [&](const ir::BreakIf*) {},                        //
+        [&](const ir::Continue*) {},                       //
+        [&](const ir::Exit* e) { CheckExit(e); },          //
+        [&](const ir::NextIteration*) {},                  //
+        [&](const ir::Return* ret) { CheckReturn(ret); },  //
+        [&](const ir::TerminateInvocation*) {},            //
+        [&](const ir::Unreachable*) {},                    //
         [&](Default) { AddError(b, InstError(b, "missing validation")); });
 }
 
-void Validator::CheckExit(ir::Exit* e) {
+void Validator::CheckExit(const Exit* e) {
     if (e->ControlInstruction() == nullptr) {
         AddError(e, InstError(e, "has no parent control instruction"));
         return;
@@ -759,21 +769,21 @@
     }
 
     tint::Switch(
-        e,                                               //
-        [&](ir::ExitIf* i) { CheckExitIf(i); },          //
-        [&](ir::ExitLoop* l) { CheckExitLoop(l); },      //
-        [&](ir::ExitSwitch* s) { CheckExitSwitch(s); },  //
+        e,                                                     //
+        [&](const ir::ExitIf* i) { CheckExitIf(i); },          //
+        [&](const ir::ExitLoop* l) { CheckExitLoop(l); },      //
+        [&](const ir::ExitSwitch* s) { CheckExitSwitch(s); },  //
         [&](Default) { AddError(e, InstError(e, "missing validation")); });
 }
 
-void Validator::CheckExitIf(ExitIf* e) {
+void Validator::CheckExitIf(const ExitIf* e) {
     if (control_stack_.Back() != e->If()) {
         AddError(e, InstError(e, "if target jumps over other control instructions"));
         AddNote(control_stack_.Back(), "first control instruction jumped");
     }
 }
 
-void Validator::CheckReturn(Return* ret) {
+void Validator::CheckReturn(const Return* ret) {
     auto* func = ret->Func();
     if (func == nullptr) {
         AddError(ret, InstError(ret, "undefined function"));
@@ -792,7 +802,7 @@
     }
 }
 
-void Validator::CheckControlsAllowingIf(Exit* exit, Instruction* control) {
+void Validator::CheckControlsAllowingIf(const Exit* exit, const Instruction* control) {
     bool found = false;
     for (auto ctrl : tint::Reverse(control_stack_)) {
         if (ctrl == control) {
@@ -813,15 +823,15 @@
     }
 }
 
-void Validator::CheckExitSwitch(ExitSwitch* s) {
+void Validator::CheckExitSwitch(const ExitSwitch* s) {
     CheckControlsAllowingIf(s, s->ControlInstruction());
 }
 
-void Validator::CheckExitLoop(ExitLoop* l) {
+void Validator::CheckExitLoop(const ExitLoop* l) {
     CheckControlsAllowingIf(l, l->ControlInstruction());
 
-    Instruction* inst = l;
-    Loop* control = l->Loop();
+    const Instruction* inst = l;
+    const Loop* control = l->Loop();
     while (inst) {
         // Found parent loop
         if (inst->Block()->Parent() == control) {
@@ -842,7 +852,7 @@
     }
 }
 
-void Validator::CheckStore(Store* s) {
+void Validator::CheckStore(const Store* s) {
     CheckOperandsNotNull(s, Store::kToOperandOffset, Store::kFromOperandOffset);
 
     if (auto* from = s->From()) {
@@ -855,12 +865,12 @@
     }
 }
 
-void Validator::CheckLoadVectorElement(LoadVectorElement* l) {
+void Validator::CheckLoadVectorElement(const LoadVectorElement* l) {
     CheckOperandsNotNull(l,  //
                          LoadVectorElement::kFromOperandOffset,
                          LoadVectorElement::kIndexOperandOffset);
 
-    if (auto* res = l->Result()) {
+    if (auto* res = l->Result(0)) {
         if (auto* el_ty = GetVectorPtrElementType(l, LoadVectorElement::kFromOperandOffset)) {
             if (res->Type() != el_ty) {
                 AddResultError(l, 0, "result type does not match vector pointer element type");
@@ -869,7 +879,7 @@
     }
 }
 
-void Validator::CheckStoreVectorElement(StoreVectorElement* s) {
+void Validator::CheckStoreVectorElement(const StoreVectorElement* s) {
     CheckOperandsNotNull(s,  //
                          StoreVectorElement::kToOperandOffset,
                          StoreVectorElement::kValueOperandOffset);
@@ -884,7 +894,7 @@
     }
 }
 
-const core::type::Type* Validator::GetVectorPtrElementType(Instruction* inst, size_t idx) {
+const core::type::Type* Validator::GetVectorPtrElementType(const Instruction* inst, size_t idx) {
     auto* operand = inst->Operands()[idx];
     if (TINT_UNLIKELY(!operand)) {
         return nullptr;
@@ -909,12 +919,12 @@
 
 }  // namespace
 
-Result<SuccessType> Validate(Module& mod) {
+Result<SuccessType> Validate(const Module& mod) {
     Validator v(mod);
     return v.Run();
 }
 
-Result<SuccessType> ValidateAndDumpIfNeeded([[maybe_unused]] Module& ir,
+Result<SuccessType> ValidateAndDumpIfNeeded([[maybe_unused]] const Module& ir,
                                             [[maybe_unused]] const char* msg) {
 #if TINT_DUMP_IR_WHEN_VALIDATING
     std::cout << "=========================================================" << std::endl;
diff --git a/src/tint/lang/core/ir/validator.h b/src/tint/lang/core/ir/validator.h
index cfc6a81..d6b0843 100644
--- a/src/tint/lang/core/ir/validator.h
+++ b/src/tint/lang/core/ir/validator.h
@@ -42,13 +42,13 @@
 /// Validates that a given IR module is correctly formed
 /// @param mod the module to validate
 /// @returns success or failure
-Result<SuccessType> Validate(Module& mod);
+Result<SuccessType> Validate(const Module& mod);
 
 /// Validates the module @p ir and dumps its contents if required by the build configuration.
 /// @param ir the module to transform
 /// @param msg the msg to accompany the output
 /// @returns success or failure
-Result<SuccessType> ValidateAndDumpIfNeeded(Module& ir, const char* msg);
+Result<SuccessType> ValidateAndDumpIfNeeded(const Module& ir, const char* msg);
 
 }  // namespace tint::core::ir
 
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index d9f6abf..37cbbcc 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -1112,7 +1112,7 @@
     auto* v = sb.Var(ty.ptr<function, f32>());
     sb.Return(f);
 
-    v->Result()->SetSource(nullptr);
+    v->Result(0)->SetInstruction(nullptr);
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
@@ -1682,7 +1682,7 @@
         b.ExitIf(if_outer);
     });
 
-    auto* c = b.Case(switch_inner, {Switch::CaseSelector{b.Constant(1_i)}});
+    auto* c = b.Case(switch_inner, {b.Constant(1_i)});
     b.Append(c, [&] { b.ExitIf(if_outer); });
 
     b.Append(f->Block(), [&] {
@@ -1779,7 +1779,7 @@
 TEST_F(IR_ValidatorTest, ExitSwitch) {
     auto* switch_ = b.Switch(true);
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(b.ExitSwitch(switch_));
 
     auto* f = b.Function("my_func", ty.void_());
@@ -1794,7 +1794,7 @@
 TEST_F(IR_ValidatorTest, ExitSwitch_NullSwitch) {
     auto* switch_ = b.Switch(true);
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(mod.instructions.Create<ExitSwitch>(nullptr));
 
     auto* f = b.Function("my_func", ty.void_());
@@ -1834,7 +1834,7 @@
     auto* r2 = b.InstructionResult(ty.f32());
     switch_->SetResults(Vector{r1, r2});
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(b.ExitSwitch(switch_, 1_i));
 
     auto* f = b.Function("my_func", ty.void_());
@@ -1878,7 +1878,7 @@
     auto* r2 = b.InstructionResult(ty.f32());
     switch_->SetResults(Vector{r1, r2});
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(b.ExitSwitch(switch_, 1_i, 2_f, 3_i));
 
     auto* f = b.Function("my_func", ty.void_());
@@ -1922,7 +1922,7 @@
     auto* r2 = b.InstructionResult(ty.f32());
     switch_->SetResults(Vector{r1, r2});
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(b.ExitSwitch(switch_, 1_i, 2_f));
 
     auto* f = b.Function("my_func", ty.void_());
@@ -1940,7 +1940,7 @@
     auto* r2 = b.InstructionResult(ty.f32());
     switch_->SetResults(Vector{r1, r2});
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(b.ExitSwitch(switch_, 1_i, 2_i));
 
     auto* f = b.Function("my_func", ty.void_());
@@ -1983,7 +1983,7 @@
 
     auto* f = b.Function("my_func", ty.void_());
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     def->Append(b.Return(f));
 
     auto sb = b.Append(f->Block());
@@ -2037,7 +2037,7 @@
 
     auto* f = b.Function("my_func", ty.void_());
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     b.Append(def, [&] {
         auto* if_ = b.If(true);
         b.Append(if_->True(), [&] {
@@ -2059,12 +2059,12 @@
 TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverSwitch) {
     auto* switch_ = b.Switch(true);
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     b.Append(def, [&] {
         auto* inner = b.Switch(false);
         b.ExitSwitch(switch_);
 
-        auto* inner_def = b.Case(inner, {Switch::CaseSelector{}});
+        auto* inner_def = b.DefaultCase(inner);
         b.Append(inner_def, [&] { b.ExitSwitch(switch_); });
     });
 
@@ -2112,7 +2112,7 @@
 TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverLoop) {
     auto* switch_ = b.Switch(true);
 
-    auto* def = b.Case(switch_, {Switch::CaseSelector{}});
+    auto* def = b.DefaultCase(switch_);
     b.Append(def, [&] {
         auto* loop = b.Loop();
         b.Append(loop->Body(), [&] { b.ExitSwitch(switch_); });
@@ -2458,7 +2458,7 @@
         auto* inner = b.Switch(false);
         b.ExitLoop(loop);
 
-        auto* inner_def = b.Case(inner, {Switch::CaseSelector{}});
+        auto* inner_def = b.DefaultCase(inner);
         b.Append(inner_def, [&] { b.ExitLoop(loop); });
     });
 
@@ -2916,7 +2916,7 @@
 
     b.Append(f->Block(), [&] {
         auto* var = b.Var(ty.ptr<function, i32>());
-        b.Append(mod.instructions.Create<ir::Store>(var->Result(), nullptr));
+        b.Append(mod.instructions.Create<ir::Store>(var->Result(0), nullptr));
         b.Return(f);
     });
 
@@ -2946,7 +2946,7 @@
 
     b.Append(f->Block(), [&] {
         auto* var = b.Var(ty.ptr<function, i32>());
-        b.Append(mod.instructions.Create<ir::Store>(var->Result(), b.Constant(42_u)));
+        b.Append(mod.instructions.Create<ir::Store>(var->Result(0), b.Constant(42_u)));
         b.Return(f);
     });
 
@@ -2977,7 +2977,7 @@
 
     b.Append(f->Block(), [&] {
         auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(mod.instructions.Create<ir::LoadVectorElement>(nullptr, var->Result(),
+        b.Append(mod.instructions.Create<ir::LoadVectorElement>(nullptr, var->Result(0),
                                                                 b.Constant(1_i)));
         b.Return(f);
     });
@@ -3039,7 +3039,7 @@
     b.Append(f->Block(), [&] {
         auto* var = b.Var(ty.ptr<function, vec3<f32>>());
         b.Append(mod.instructions.Create<ir::LoadVectorElement>(b.InstructionResult(ty.f32()),
-                                                                var->Result(), nullptr));
+                                                                var->Result(0), nullptr));
         b.Return(f);
     });
 
@@ -3098,7 +3098,7 @@
 
     b.Append(f->Block(), [&] {
         auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(mod.instructions.Create<ir::StoreVectorElement>(var->Result(), nullptr,
+        b.Append(mod.instructions.Create<ir::StoreVectorElement>(var->Result(0), nullptr,
                                                                  b.Constant(2_i)));
         b.Return(f);
     });
@@ -3137,7 +3137,7 @@
 
     b.Append(f->Block(), [&] {
         auto* var = b.Var(ty.ptr<function, vec3<f32>>());
-        b.Append(mod.instructions.Create<ir::StoreVectorElement>(var->Result(), b.Constant(1_i),
+        b.Append(mod.instructions.Create<ir::StoreVectorElement>(var->Result(0), b.Constant(1_i),
                                                                  nullptr));
         b.Return(f);
     });
diff --git a/src/tint/lang/core/ir/value.h b/src/tint/lang/core/ir/value.h
index f309206..55f7463 100644
--- a/src/tint/lang/core/ir/value.h
+++ b/src/tint/lang/core/ir/value.h
@@ -47,14 +47,8 @@
     /// The index of the operand that is the value being used.
     size_t operand_index = 0u;
 
-    /// A specialization of Hasher for Usage.
-    struct Hasher {
-        /// @param u the usage to hash
-        /// @returns a hash of the usage
-        inline std::size_t operator()(const Usage& u) const {
-            return Hash(u.instruction, u.operand_index);
-        }
-    };
+    /// @returns the hash code of the Usage
+    size_t HashCode() const { return Hash(instruction, operand_index); }
 
     /// An equality helper for Usage.
     /// @param other the usage to compare against
@@ -71,7 +65,7 @@
     ~Value() override;
 
     /// @returns the type of the value
-    virtual const core::type::Type* Type() { return nullptr; }
+    virtual const core::type::Type* Type() const { return nullptr; }
 
     /// Destroys the Value. Once called, the Value must not be used again.
     /// The Value must not be in use by any instruction.
@@ -94,7 +88,20 @@
 
     /// @returns the set of usages of this value. An instruction may appear multiple times if it
     /// uses the value for multiple different operands.
-    const Hashset<Usage, 4, Usage::Hasher>& Usages() { return uses_; }
+    const Hashset<Usage, 4>& Usages() { return uses_; }
+
+    /// @returns true if this Value has any usages
+    bool IsUsed() const { return !uses_.IsEmpty(); }
+
+    /// @returns the number of usages of this Value
+    size_t NumUsages() const { return uses_.Count(); }
+
+    /// @returns true if the usages contains the instruction and operand index pair.
+    /// @param instruction the instruction
+    /// @param operand_index the in
+    bool HasUsage(const Instruction* instruction, size_t operand_index) const {
+        return uses_.Contains({const_cast<Instruction*>(instruction), operand_index});
+    }
 
     /// Apply a function to all uses of the value that exist prior to calling this method.
     /// @param func the function will be applied to each use
@@ -119,7 +126,7 @@
         kDead,
     };
 
-    Hashset<Usage, 4, Usage::Hasher> uses_;
+    Hashset<Usage, 4> uses_;
 
     /// Bitset of value flags
     tint::EnumSet<Flag> flags_;
diff --git a/src/tint/lang/core/ir/value_test.cc b/src/tint/lang/core/ir/value_test.cc
index 6ea04e7..a1adee2 100644
--- a/src/tint/lang/core/ir/value_test.cc
+++ b/src/tint/lang/core/ir/value_test.cc
@@ -71,7 +71,7 @@
         {
             Module mod;
             Builder b{mod};
-            auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result();
+            auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result(0);
             val->Destroy();
         },
         "");
diff --git a/src/tint/lang/core/ir/var.cc b/src/tint/lang/core/ir/var.cc
index 9c8cb74..b9a8f91 100644
--- a/src/tint/lang/core/ir/var.cc
+++ b/src/tint/lang/core/ir/var.cc
@@ -50,7 +50,7 @@
 Var::~Var() = default;
 
 Var* Var::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto* new_var = ctx.ir.instructions.Create<Var>(new_result);
 
     new_var->binding_point_ = binding_point_;
@@ -68,7 +68,7 @@
 }
 
 void Var::DestroyIfOnlyAssigned() {
-    auto* result = Result();
+    auto* result = Result(0);
     if (result->Usages().All([](const Usage& u) { return u.instruction->Is<ir::Store>(); })) {
         while (!result->Usages().IsEmpty()) {
             auto& usage = *result->Usages().begin();
diff --git a/src/tint/lang/core/ir/var.h b/src/tint/lang/core/ir/var.h
index 29d1ad4..cd72271 100644
--- a/src/tint/lang/core/ir/var.h
+++ b/src/tint/lang/core/ir/var.h
@@ -71,25 +71,27 @@
     void SetInitializer(Value* initializer);
     /// @returns the initializer
     Value* Initializer() { return operands_[kInitializerOperandOffset]; }
+    /// @returns the initializer
+    const Value* Initializer() const { return operands_[kInitializerOperandOffset]; }
 
     /// Sets the binding point
     /// @param group the group
     /// @param binding the binding
     void SetBindingPoint(uint32_t group, uint32_t binding) { binding_point_ = {group, binding}; }
     /// @returns the binding points if `Attributes` contains `kBindingPoint`
-    std::optional<struct BindingPoint> BindingPoint() { return binding_point_; }
+    std::optional<struct BindingPoint> BindingPoint() const { return binding_point_; }
 
     /// Sets the IO attributes
     /// @param attrs the attributes
     void SetAttributes(const IOAttributes& attrs) { attributes_ = attrs; }
     /// @returns the IO attributes
-    const IOAttributes& Attributes() { return attributes_; }
+    const IOAttributes& Attributes() const { return attributes_; }
 
     /// Destroys this instruction along with any assignment instructions, if the var is never read.
     void DestroyIfOnlyAssigned();
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return "var"; }
+    std::string FriendlyName() const override { return "var"; }
 
   private:
     std::optional<struct BindingPoint> binding_point_;
diff --git a/src/tint/lang/core/ir/var_test.cc b/src/tint/lang/core/ir/var_test.cc
index bd3d59a..f49c79b 100644
--- a/src/tint/lang/core/ir/var_test.cc
+++ b/src/tint/lang/core/ir/var_test.cc
@@ -53,10 +53,9 @@
 
 TEST_F(IR_VarTest, Results) {
     auto* var = b.Var(ty.ptr<function, f32>());
-    EXPECT_TRUE(var->HasResults());
-    EXPECT_FALSE(var->HasMultiResults());
-    EXPECT_TRUE(var->Result()->Is<InstructionResult>());
-    EXPECT_EQ(var->Result()->Source(), var);
+    EXPECT_EQ(var->Results().Length(), 1u);
+    EXPECT_TRUE(var->Result(0)->Is<InstructionResult>());
+    EXPECT_EQ(var->Result(0)->Instruction(), var);
 }
 
 TEST_F(IR_VarTest, Initializer_Usage) {
@@ -83,9 +82,9 @@
     auto* new_v = clone_ctx.Clone(v);
 
     EXPECT_NE(v, new_v);
-    ASSERT_NE(nullptr, new_v->Result());
-    EXPECT_NE(v->Result(), new_v->Result());
-    EXPECT_EQ(new_v->Result()->Type(),
+    ASSERT_NE(nullptr, new_v->Result(0));
+    EXPECT_NE(v->Result(0), new_v->Result(0));
+    EXPECT_EQ(new_v->Result(0)->Type(),
               mod.Types().ptr(core::AddressSpace::kFunction, mod.Types().f32()));
 
     auto new_val = v->Initializer()->As<Constant>()->Value();
diff --git a/src/tint/lang/core/texel_format_test.cc b/src/tint/lang/core/texel_format_test.cc
index e31030c..1500656 100644
--- a/src/tint/lang/core/texel_format_test.cc
+++ b/src/tint/lang/core/texel_format_test.cc
@@ -113,7 +113,7 @@
 TEST_P(TexelFormatPrintTest, Print) {
     TexelFormat value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, TexelFormatPrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/core/type/BUILD.bazel b/src/tint/lang/core/type/BUILD.bazel
index 0f0cbe4..f813fdc 100644
--- a/src/tint/lang/core/type/BUILD.bazel
+++ b/src/tint/lang/core/type/BUILD.bazel
@@ -166,6 +166,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/core/type/BUILD.cmake b/src/tint/lang/core/type/BUILD.cmake
index 2885687..273cb07 100644
--- a/src/tint/lang/core/type/BUILD.cmake
+++ b/src/tint/lang/core/type/BUILD.cmake
@@ -165,6 +165,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/core/type/BUILD.gn b/src/tint/lang/core/type/BUILD.gn
index 0abcd11..7da2908 100644
--- a/src/tint/lang/core/type/BUILD.gn
+++ b/src/tint/lang/core/type/BUILD.gn
@@ -167,6 +167,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/lang/core/type/manager.h b/src/tint/lang/core/type/manager.h
index 6d204c4..ada8c7e 100644
--- a/src/tint/lang/core/type/manager.h
+++ b/src/tint/lang/core/type/manager.h
@@ -84,8 +84,8 @@
     /// Wrap returns a new Manager created with the types of `inner`.
     /// The Manager returned by Wrap is intended to temporarily extend the types
     /// of an existing immutable Manager.
-    /// As the copied types are owned by `inner`, `inner` must not be destructed
-    /// or assigned while using the returned Manager.
+    /// @warning As the copied types are owned by `inner`, `inner` must not be destructed or
+    /// assigned while using the returned Manager.
     /// TODO(bclayton) - Evaluate whether there are safer alternatives to this
     /// function. See crbug.com/tint/460.
     /// @param inner the immutable Manager to extend
diff --git a/src/tint/lang/glsl/writer/BUILD.bazel b/src/tint/lang/glsl/writer/BUILD.bazel
index d5272f1..3dac23b 100644
--- a/src/tint/lang/glsl/writer/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/BUILD.bazel
@@ -51,13 +51,12 @@
     "//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/glsl/writer/raise",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader/lower",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -80,11 +79,6 @@
       "//src/tint/lang/glsl/writer/printer",
     ],
     "//conditions:default": [],
-  }) + select({
-    ":tint_build_wgsl_reader": [
-      "//src/tint/lang/wgsl/reader/program_to_ir",
-    ],
-    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -104,6 +98,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -136,8 +131,3 @@
   actual = "//src/tint:tint_build_glsl_writer_true",
 )
 
-alias(
-  name = "tint_build_wgsl_reader",
-  actual = "//src/tint:tint_build_wgsl_reader_true",
-)
-
diff --git a/src/tint/lang/glsl/writer/BUILD.cmake b/src/tint/lang/glsl/writer/BUILD.cmake
index f266bae..cb302d8 100644
--- a/src/tint/lang/glsl/writer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/BUILD.cmake
@@ -58,13 +58,12 @@
   tint_api_options
   tint_lang_core
   tint_lang_core_constant
-  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_glsl_writer_raise
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader_lower
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -90,12 +89,6 @@
   )
 endif(TINT_BUILD_GLSL_WRITER)
 
-if(TINT_BUILD_WGSL_READER)
-  tint_target_add_dependencies(tint_lang_glsl_writer lib
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_WGSL_READER)
-
 endif(TINT_BUILD_GLSL_WRITER)
 if(TINT_BUILD_GLSL_WRITER)
 ################################################################################
@@ -116,6 +109,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/glsl/writer/BUILD.gn b/src/tint/lang/glsl/writer/BUILD.gn
index 0ed5983..4d5bfda 100644
--- a/src/tint/lang/glsl/writer/BUILD.gn
+++ b/src/tint/lang/glsl/writer/BUILD.gn
@@ -54,13 +54,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/glsl/writer/raise",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader/lower",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -85,10 +84,6 @@
         "${tint_src_dir}/lang/glsl/writer/printer",
       ]
     }
-
-    if (tint_build_wgsl_reader) {
-      deps += [ "${tint_src_dir}/lang/wgsl/reader/program_to_ir" ]
-    }
   }
 }
 if (tint_build_benchmarks) {
@@ -105,6 +100,7 @@
         "${tint_src_dir}/lang/core/type",
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
index 29e2970..2841719 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.bazel
@@ -53,6 +53,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -128,6 +129,7 @@
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
index 0b6dd27..a8e36cc 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.cmake
@@ -54,6 +54,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -133,6 +134,7 @@
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/glsl/writer/ast_printer/BUILD.gn b/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
index 955ffa0..692ca9b 100644
--- a/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/glsl/writer/ast_printer/BUILD.gn
@@ -56,6 +56,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/helpers",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -132,6 +133,7 @@
         "${tint_src_dir}/lang/wgsl/ast:unittests",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
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 738844e..9694635 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -267,7 +267,6 @@
                 wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumInternalDualSourceBlending,
-                wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture,
                 wgsl::Extension::kChromiumExperimentalPushConstant,
                 wgsl::Extension::kF16,
             })) {
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
index 72bd733..8e05047 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
@@ -60,6 +60,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -100,6 +101,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
index 6617049..8fd03ab 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
@@ -61,6 +61,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -104,6 +105,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.gn b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
index 5a7980b..38e3359 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
@@ -63,6 +63,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -104,6 +105,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/glsl/writer/common/options.h b/src/tint/lang/glsl/writer/common/options.h
index e9de953..8068595 100644
--- a/src/tint/lang/glsl/writer/common/options.h
+++ b/src/tint/lang/glsl/writer/common/options.h
@@ -61,9 +61,6 @@
     /// Set to `true` to disable workgroup memory zero initialization
     bool disable_workgroup_init = false;
 
-    /// Set to `true` to generate GLSL via the Tint IR instead of from the AST.
-    bool use_tint_ir = false;
-
     /// The GLSL version to emit
     Version version;
 
diff --git a/src/tint/lang/glsl/writer/common/printer_support.cc b/src/tint/lang/glsl/writer/common/printer_support.cc
index d57f019..d4c62de 100644
--- a/src/tint/lang/glsl/writer/common/printer_support.cc
+++ b/src/tint/lang/glsl/writer/common/printer_support.cc
@@ -51,7 +51,7 @@
     } else if (std::isnan(value)) {
         out << "0.0f /* nan */";
     } else {
-        out << tint::writer::FloatToString(value) << "f";
+        out << tint::strconv::FloatToString(value) << "f";
     }
 }
 
@@ -61,7 +61,7 @@
     } else if (std::isnan(value)) {
         out << "0.0hf /* nan */";
     } else {
-        out << tint::writer::FloatToString(value) << "hf";
+        out << tint::strconv::FloatToString(value) << "hf";
     }
 }
 
diff --git a/src/tint/lang/glsl/writer/printer/printer.cc b/src/tint/lang/glsl/writer/printer/printer.cc
index f3dda8c..790931d 100644
--- a/src/tint/lang/glsl/writer/printer/printer.cc
+++ b/src/tint/lang/glsl/writer/printer/printer.cc
@@ -50,7 +50,7 @@
   public:
     /// Constructor
     /// @param module the Tint IR module to generate
-    explicit Printer(core::ir::Module& module) : ir_(module) {}
+    explicit Printer(const core::ir::Module& module) : ir_(module) {}
 
     /// @param version the GLSL version information
     /// @returns the generated GLSL shader
@@ -74,7 +74,7 @@
         EmitBlockInstructions(ir_.root_block);
 
         // Emit functions.
-        for (auto* func : ir_.functions) {
+        for (auto& func : ir_.functions) {
             EmitFunction(func);
         }
 
@@ -84,19 +84,19 @@
     }
 
   private:
-    core::ir::Module& ir_;
+    const core::ir::Module& ir_;
 
     /// The buffer holding preamble text
     TextBuffer preamble_buffer_;
 
     /// The current function being emitted
-    core::ir::Function* current_function_ = nullptr;
+    const core::ir::Function* current_function_ = nullptr;
     /// The current block being emitted
-    core::ir::Block* current_block_ = nullptr;
+    const core::ir::Block* current_block_ = nullptr;
 
     /// Emit the function
     /// @param func the function to emit
-    void EmitFunction(core::ir::Function* func) {
+    void EmitFunction(const core::ir::Function* func) {
         TINT_SCOPED_ASSIGNMENT(current_function_, func);
 
         {
@@ -120,7 +120,7 @@
 
     /// Emit a block
     /// @param block the block to emit
-    void EmitBlock(core::ir::Block* block) {
+    void EmitBlock(const core::ir::Block* block) {
         // TODO(dsinclair): Handle marking inline
         // MarkInlinable(block);
 
@@ -129,14 +129,14 @@
 
     /// Emit the instructions in a block
     /// @param block the block with the instructions to emit
-    void EmitBlockInstructions(core::ir::Block* block) {
+    void EmitBlockInstructions(const core::ir::Block* block) {
         TINT_SCOPED_ASSIGNMENT(current_block_, block);
 
         for (auto* inst : *block) {
             Switch(
-                inst,                                                //
-                [&](core::ir::Return* r) { EmitReturn(r); },         //
-                [&](core::ir::Unreachable*) { EmitUnreachable(); },  //
+                inst,                                                      //
+                [&](const core::ir::Return* r) { EmitReturn(r); },         //
+                [&](const core::ir::Unreachable*) { EmitUnreachable(); },  //
                 TINT_ICE_ON_NO_MATCH);
         }
     }
@@ -148,7 +148,7 @@
 
     /// Emit a return instruction
     /// @param r the return instruction
-    void EmitReturn(core::ir::Return* r) {
+    void EmitReturn(const core::ir::Return* r) {
         // If this return has no arguments and the current block is for the function which is
         // being returned, skip the return.
         if (current_block_ == current_function_->Block() && r->Args().IsEmpty()) {
@@ -169,7 +169,7 @@
 };
 }  // namespace
 
-Result<std::string> Print(core::ir::Module& module, const Version& version) {
+Result<std::string> Print(const core::ir::Module& module, const Version& version) {
     return Printer{module}.Generate(version);
 }
 
diff --git a/src/tint/lang/glsl/writer/printer/printer.h b/src/tint/lang/glsl/writer/printer/printer.h
index 49c6301..6f75b39 100644
--- a/src/tint/lang/glsl/writer/printer/printer.h
+++ b/src/tint/lang/glsl/writer/printer/printer.h
@@ -45,7 +45,7 @@
 /// @returns the generated GLSL shader on success, or failure
 /// @param module the Tint IR module to generate
 /// @param version the GLSL version information
-Result<std::string> Print(core::ir::Module& module, const Version& version);
+Result<std::string> Print(const core::ir::Module& module, const Version& version);
 
 }  // namespace tint::glsl::writer
 
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index 452bd4b..7283deb 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -34,13 +34,26 @@
 #include "src/tint/lang/glsl/writer/printer/printer.h"
 #include "src/tint/lang/glsl/writer/raise/raise.h"
 
-#if TINT_BUILD_WGSL_READER
-#include "src/tint/lang/wgsl/reader/lower/lower.h"
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
-#endif  // TINT_BUILD_WGSL_REAdDER
-
 namespace tint::glsl::writer {
 
+Result<Output> Generate(core::ir::Module& ir, const Options& options, const std::string&) {
+    Output output;
+
+    // Raise from core-dialect to GLSL-dialect.
+    if (auto res = raise::Raise(ir); !res) {
+        return res.Failure();
+    }
+
+    // Generate the GLSL code.
+    auto result = Print(ir, options.version);
+    if (!result) {
+        return result.Failure();
+    }
+    output.glsl = result.Get();
+
+    return output;
+}
+
 Result<Output> Generate(const Program& program,
                         const Options& options,
                         const std::string& entry_point) {
@@ -50,58 +63,27 @@
 
     Output output;
 
-    if (options.use_tint_ir) {
-#if TINT_BUILD_WGSL_READER
-        // Convert the AST program to an IR module.
-        auto converted = wgsl::reader::ProgramToIR(program);
-        if (!converted) {
-            return converted.Failure();
-        }
+    // Sanitize the program.
+    auto sanitized_result = Sanitize(program, options, entry_point);
+    if (!sanitized_result.program.IsValid()) {
+        return Failure{sanitized_result.program.Diagnostics()};
+    }
 
-        auto ir = converted.Move();
+    // Generate the GLSL code.
+    auto impl = std::make_unique<ASTPrinter>(sanitized_result.program, options.version);
+    if (!impl->Generate()) {
+        return Failure{impl->Diagnostics()};
+    }
 
-        // Lower from WGSL-dialect to core-dialect
-        if (auto res = wgsl::reader::Lower(ir); !res) {
-            return res.Failure();
-        }
+    output.glsl = impl->Result();
+    output.needs_internal_uniform_buffer = sanitized_result.needs_internal_uniform_buffer;
+    output.bindpoint_to_data = std::move(sanitized_result.bindpoint_to_data);
 
-        // Raise from core-dialect to GLSL-dialect.
-        if (auto res = raise::Raise(ir); !res) {
-            return res.Failure();
-        }
-
-        // Generate the GLSL code.
-        auto result = Print(ir, options.version);
-        if (!result) {
-            return result.Failure();
-        }
-        output.glsl = result.Get();
-#else
-        return Failure{"use_tint_ir requires building with TINT_BUILD_WGSL_READER"};
-#endif
-    } else {
-        // Sanitize the program.
-        auto sanitized_result = Sanitize(program, options, entry_point);
-        if (!sanitized_result.program.IsValid()) {
-            return Failure{sanitized_result.program.Diagnostics()};
-        }
-
-        // Generate the GLSL code.
-        auto impl = std::make_unique<ASTPrinter>(sanitized_result.program, options.version);
-        if (!impl->Generate()) {
-            return Failure{impl->Diagnostics()};
-        }
-
-        output.glsl = impl->Result();
-        output.needs_internal_uniform_buffer = sanitized_result.needs_internal_uniform_buffer;
-        output.bindpoint_to_data = std::move(sanitized_result.bindpoint_to_data);
-
-        // Collect the list of entry points in the sanitized program.
-        for (auto* func : sanitized_result.program.AST().Functions()) {
-            if (func->IsEntryPoint()) {
-                auto name = func->name->symbol.Name();
-                output.entry_points.push_back({name, func->PipelineStage()});
-            }
+    // Collect the list of entry points in the sanitized program.
+    for (auto* func : sanitized_result.program.AST().Functions()) {
+        if (func->IsEntryPoint()) {
+            auto name = func->name->symbol.Name();
+            output.entry_points.push_back({name, func->PipelineStage()});
         }
     }
 
diff --git a/src/tint/lang/glsl/writer/writer.h b/src/tint/lang/glsl/writer/writer.h
index a2aa79c..299ee08 100644
--- a/src/tint/lang/glsl/writer/writer.h
+++ b/src/tint/lang/glsl/writer/writer.h
@@ -39,12 +39,26 @@
 namespace tint {
 class Program;
 }  // namespace tint
+namespace tint::core::ir {
+class Module;
+}  // namespace tint::core::ir
 
 namespace tint::glsl::writer {
 
 /// Generate GLSL for a program, according to a set of configuration options.
 /// The result will contain the GLSL and supplementary information, or failure.
 /// information.
+/// @param ir the IR module to translate to GLSL
+/// @param options the configuration options to use when generating GLSL
+/// @param entry_point the entry point to generate GLSL for
+/// @returns the resulting GLSL and supplementary information, or failure
+Result<Output> Generate(core::ir::Module& ir,
+                        const Options& options,
+                        const std::string& entry_point);
+
+/// Generate GLSL for a program, according to a set of configuration options.
+/// The result will contain the GLSL and supplementary information, or failure.
+/// information.
 /// @param program the program to translate to GLSL
 /// @param options the configuration options to use when generating GLSL
 /// @param entry_point the entry point to generate GLSL for
diff --git a/src/tint/lang/hlsl/writer/BUILD.bazel b/src/tint/lang/hlsl/writer/BUILD.bazel
index 13ff49e..71c0cbd 100644
--- a/src/tint/lang/hlsl/writer/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/BUILD.bazel
@@ -56,6 +56,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/hlsl/writer/BUILD.cmake b/src/tint/lang/hlsl/writer/BUILD.cmake
index ad00144..b9d641d 100644
--- a/src/tint/lang/hlsl/writer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/BUILD.cmake
@@ -61,6 +61,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/hlsl/writer/BUILD.gn b/src/tint/lang/hlsl/writer/BUILD.gn
index f43dace..6b8a8fc 100644
--- a/src/tint/lang/hlsl/writer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/BUILD.gn
@@ -59,6 +59,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel
index 3bd3f35..f0031c3 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.bazel
@@ -54,6 +54,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -129,6 +130,7 @@
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake
index 8ce9a91..a700a8f 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.cmake
@@ -55,6 +55,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -134,6 +135,7 @@
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn b/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
index 3788b50..b61ed79 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/ast_printer/BUILD.gn
@@ -57,6 +57,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/helpers",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -131,6 +132,7 @@
         "${tint_src_dir}/lang/wgsl/ast:unittests",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
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 6f883e5..d66be8d 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -27,11 +27,9 @@
 
 #include "src/tint/lang/hlsl/writer/ast_printer/ast_printer.h"
 
-#include <algorithm>
 #include <cmath>
 #include <functional>
 #include <iomanip>
-#include <set>
 #include <utility>
 #include <vector>
 
@@ -41,7 +39,6 @@
 #include "src/tint/lang/core/type/array.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/core/type/depth_multisampled_texture.h"
-#include "src/tint/lang/core/type/depth_texture.h"
 #include "src/tint/lang/core/type/multisampled_texture.h"
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/core/type/storage_texture.h"
@@ -50,10 +47,10 @@
 #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/pixel_local.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"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h"
@@ -140,7 +137,7 @@
     } else if (std::isnan(value)) {
         out << "0.0f /* nan */";
     } else {
-        out << tint::writer::FloatToString(value) << "f";
+        out << tint::strconv::FloatToString(value) << "f";
     }
 }
 
@@ -150,7 +147,7 @@
     } else if (std::isnan(value)) {
         out << "0.0h /* nan */";
     } else {
-        out << tint::writer::FloatToString(value) << "h";
+        out << tint::strconv::FloatToString(value) << "h";
     }
 }
 
@@ -270,6 +267,34 @@
         manager.Add<ast::transform::ZeroInitWorkgroupMemory>();
     }
 
+    {
+        PixelLocal::Config cfg;
+        for (auto it : options.pixel_local_options.attachments) {
+            cfg.pls_member_to_rov_reg.Add(it.first, it.second);
+        }
+        for (auto it : options.pixel_local_options.attachment_formats) {
+            core::TexelFormat format = core::TexelFormat::kUndefined;
+            switch (it.second) {
+                case PixelLocalOptions::TexelFormat::kR32Sint:
+                    format = core::TexelFormat::kR32Sint;
+                    break;
+                case PixelLocalOptions::TexelFormat::kR32Uint:
+                    format = core::TexelFormat::kR32Uint;
+                    break;
+                case PixelLocalOptions::TexelFormat::kR32Float:
+                    format = core::TexelFormat::kR32Float;
+                    break;
+                default:
+                    TINT_ICE() << "missing texel format for pixel local storage attachment";
+                    return SanitizedResult();
+            }
+            cfg.pls_member_to_rov_format.Add(it.first, format);
+        }
+        cfg.rov_group_index = options.pixel_local_options.pixel_local_group_index;
+        data.Add<PixelLocal::Config>(cfg);
+        manager.Add<PixelLocal>();
+    }
+
     // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<ast::transform::CanonicalizeEntryPointIO>();
 
@@ -357,10 +382,10 @@
                 wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumExperimentalPushConstant,
-                wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
                 wgsl::Extension::kF16,
                 wgsl::Extension::kChromiumInternalDualSourceBlending,
+                wgsl::Extension::kChromiumExperimentalPixelLocal,
             })) {
         return false;
     }
@@ -3378,7 +3403,22 @@
 
     auto name = var->name->symbol.Name();
     auto* type = sem->Type()->UnwrapRef();
-    if (!EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(), name)) {
+    if (ast::HasAttribute<PixelLocal::RasterizerOrderedView>(var->attributes)) {
+        TINT_ASSERT(!type->Is<core::type::MultisampledTexture>());
+        auto* storage = type->As<core::type::StorageTexture>();
+        if (!storage) {
+            TINT_ICE() << "Rasterizer Ordered View type isn't storage texture";
+            return false;
+        }
+        out << "RasterizerOrderedTexture2D";
+        auto* component = image_format_to_rwtexture_type(storage->texel_format());
+        if (TINT_UNLIKELY(!component)) {
+            TINT_ICE() << "Unsupported StorageTexture TexelFormat: "
+                       << static_cast<int>(storage->texel_format());
+            return false;
+        }
+        out << "<" << component << "> " << name;
+    } else if (!EmitTypeAndName(out, type, sem->AddressSpace(), sem->Access(), name)) {
         return false;
     }
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
index 5ea675c..1af1d58 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.bazel
@@ -65,6 +65,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -107,6 +108,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
index a7a9437..ad2de08 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.cmake
@@ -66,6 +66,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -111,6 +112,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn b/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
index 3200039..c009305 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/ast_raise/BUILD.gn
@@ -68,6 +68,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -111,6 +112,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index e8d5cc0..1ff17bb 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -37,6 +37,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/lang/core/access.h"
 #include "src/tint/utils/reflection/reflection.h"
 
@@ -93,6 +94,9 @@
     /// AccessControls is a map of old binding point to new access control
     std::unordered_map<BindingPoint, core::Access> access_controls;
 
+    /// Options used to deal with pixel local storage variables
+    PixelLocalOptions pixel_local_options = {};
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
                  disable_workgroup_init,
@@ -104,7 +108,8 @@
                  external_texture_options,
                  binding_remapper_options,
                  binding_points_ignored_in_robustness_transform,
-                 access_controls);
+                 access_controls,
+                 pixel_local_options);
 };
 
 }  // namespace tint::hlsl::writer
diff --git a/src/tint/lang/msl/validate/BUILD.bazel b/src/tint/lang/msl/validate/BUILD.bazel
index 5ea0d1a..0606047 100644
--- a/src/tint/lang/msl/validate/BUILD.bazel
+++ b/src/tint/lang/msl/validate/BUILD.bazel
@@ -41,7 +41,7 @@
   srcs = [
     "validate.cc",
   ] + select({
-    ":is_mac": [
+    ":tint_build_is_mac": [
       "validate_metal.mm",
     ],
     "//conditions:default": [],
@@ -55,6 +55,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/command",
@@ -72,7 +73,7 @@
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ] + select({
-    ":is_mac": [
+    ":tint_build_is_mac": [
       
     ],
     "//conditions:default": [],
@@ -82,8 +83,8 @@
 )
 
 alias(
-  name = "is_mac",
-  actual = "//src/tint:is_mac_true",
+  name = "tint_build_is_mac",
+  actual = "//src/tint:tint_build_is_mac_true",
 )
 
 alias(
diff --git a/src/tint/lang/msl/validate/BUILD.cmake b/src/tint/lang/msl/validate/BUILD.cmake
index 14012fe..e51eab9 100644
--- a/src/tint/lang/msl/validate/BUILD.cmake
+++ b/src/tint/lang/msl/validate/BUILD.cmake
@@ -51,6 +51,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_command
@@ -69,13 +70,13 @@
   tint_utils_traits
 )
 
-if(IS_MAC)
+if(TINT_BUILD_IS_MAC)
   tint_target_add_sources(tint_lang_msl_validate lib
     "lang/msl/validate/validate_metal.mm"
   )
   tint_target_add_external_dependencies(tint_lang_msl_validate lib
     "metal"
   )
-endif(IS_MAC)
+endif(TINT_BUILD_IS_MAC)
 
 endif(TINT_BUILD_MSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/msl/validate/BUILD.gn b/src/tint/lang/msl/validate/BUILD.gn
index 5789793..2d587b1 100644
--- a/src/tint/lang/msl/validate/BUILD.gn
+++ b/src/tint/lang/msl/validate/BUILD.gn
@@ -49,6 +49,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/command",
@@ -67,7 +68,7 @@
       "${tint_src_dir}/utils/traits",
     ]
 
-    if (is_mac) {
+    if (tint_build_is_mac) {
       sources += [ "validate_metal.mm" ]
       deps += [ "${tint_src_dir}:metal" ]
     }
diff --git a/src/tint/lang/msl/validate/validate.h b/src/tint/lang/msl/validate/validate.h
index 89581d6..1a303d5 100644
--- a/src/tint/lang/msl/validate/validate.h
+++ b/src/tint/lang/msl/validate/validate.h
@@ -61,7 +61,7 @@
     std::string output;
 };
 
-/// Msl attempts to compile the shader with the Metal Shader Compiler,
+/// Validate attempts to compile the shader with the Metal Shader Compiler,
 /// verifying that the shader compiles successfully.
 /// @param xcrun_path path to xcrun
 /// @param source the generated MSL source
@@ -70,7 +70,7 @@
 Result Validate(const std::string& xcrun_path, const std::string& source, MslVersion version);
 
 #ifdef __APPLE__
-/// Msl attempts to compile the shader with the runtime Metal Shader Compiler
+/// ValidateUsingMetal attempts to compile the shader with the runtime Metal Shader Compiler
 /// API, verifying that the shader compiles successfully.
 /// @param source the generated MSL source
 /// @param version the version of MSL to validate against
diff --git a/src/tint/lang/msl/validate/validate_metal.mm b/src/tint/lang/msl/validate/validate_metal.mm
index 4669e6c..9a1b090 100644
--- a/src/tint/lang/msl/validate/validate_metal.mm
+++ b/src/tint/lang/msl/validate/validate_metal.mm
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_mac)
+// GEN_BUILD:CONDITION(tint_build_is_mac)
 
 #import <Metal/Metal.h>
 
diff --git a/src/tint/lang/msl/writer/BUILD.bazel b/src/tint/lang/msl/writer/BUILD.bazel
index f57979e..b2c8ad1 100644
--- a/src/tint/lang/msl/writer/BUILD.bazel
+++ b/src/tint/lang/msl/writer/BUILD.bazel
@@ -53,9 +53,9 @@
     "//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",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
     "//src/tint/lang/wgsl/sem",
@@ -78,6 +78,7 @@
       "//src/tint/lang/msl/writer/ast_printer",
       "//src/tint/lang/msl/writer/common",
       "//src/tint/lang/msl/writer/printer",
+      "//src/tint/lang/msl/writer/raise",
     ],
     "//conditions:default": [],
   }) + select({
@@ -104,6 +105,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/msl/writer/BUILD.cmake b/src/tint/lang/msl/writer/BUILD.cmake
index 243f93b..d1b3c1a 100644
--- a/src/tint/lang/msl/writer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/BUILD.cmake
@@ -61,9 +61,9 @@
   tint_lang_core_constant
   tint_lang_core_ir
   tint_lang_core_type
-  tint_lang_msl_writer_raise
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
   tint_lang_wgsl_sem
@@ -88,6 +88,7 @@
     tint_lang_msl_writer_ast_printer
     tint_lang_msl_writer_common
     tint_lang_msl_writer_printer
+    tint_lang_msl_writer_raise
   )
 endif(TINT_BUILD_MSL_WRITER)
 
@@ -117,6 +118,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/msl/writer/BUILD.gn b/src/tint/lang/msl/writer/BUILD.gn
index 2b379e5..e3f3066 100644
--- a/src/tint/lang/msl/writer/BUILD.gn
+++ b/src/tint/lang/msl/writer/BUILD.gn
@@ -56,9 +56,9 @@
       "${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",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/reader/lower",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -83,6 +83,7 @@
         "${tint_src_dir}/lang/msl/writer/ast_printer",
         "${tint_src_dir}/lang/msl/writer/common",
         "${tint_src_dir}/lang/msl/writer/printer",
+        "${tint_src_dir}/lang/msl/writer/raise",
       ]
     }
 
@@ -105,6 +106,7 @@
         "${tint_src_dir}/lang/core/type",
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.bazel b/src/tint/lang/msl/writer/ast_printer/BUILD.bazel
index 5ffc9c4..87734c8 100644
--- a/src/tint/lang/msl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.bazel
@@ -53,6 +53,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -125,6 +126,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.cmake b/src/tint/lang/msl/writer/ast_printer/BUILD.cmake
index c1c315b..e678ba3 100644
--- a/src/tint/lang/msl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.cmake
@@ -54,6 +54,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -130,6 +131,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/msl/writer/ast_printer/BUILD.gn b/src/tint/lang/msl/writer/ast_printer/BUILD.gn
index 9f46788..75dc19a 100644
--- a/src/tint/lang/msl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/ast_printer/BUILD.gn
@@ -56,6 +56,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/helpers",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -129,6 +130,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast:unittests",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
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 60f7f81..712eddb 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -278,7 +278,6 @@
                 wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumExperimentalPixelLocal,
-                wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
                 wgsl::Extension::kChromiumExperimentalFramebufferFetch,
                 wgsl::Extension::kChromiumInternalDualSourceBlending,
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_type_test.cc b/src/tint/lang/msl/writer/ast_printer/ast_type_test.cc
index f341c21..dcca0d6 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_type_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_type_test.cc
@@ -39,6 +39,10 @@
 #include "src/tint/lang/msl/writer/ast_printer/helper_test.h"
 #include "src/tint/utils/text/string_stream.h"
 
+// Done except for two questions on:
+// TEST_F(MslASTPrinterTest, EmitType_Struct_WithAttribute) {
+// TEST_F(MslASTPrinterTest, EmitType_Pointer) {
+
 namespace tint::msl::writer {
 namespace {
 
@@ -102,6 +106,7 @@
 
 using MslASTPrinterTest = TestHelper;
 
+// MslPrinterTest.EmitType_Array
 TEST_F(MslASTPrinterTest, EmitType_Array) {
     auto arr = ty.array<bool, 4>();
     ast::Type type = GlobalVar("G", arr, core::AddressSpace::kPrivate)->type;
@@ -113,6 +118,7 @@
     EXPECT_EQ(out.str(), "tint_array<bool, 4>");
 }
 
+// MslPrinterTest.EmitType_ArrayOfArray
 TEST_F(MslASTPrinterTest, EmitType_ArrayOfArray) {
     auto a = ty.array<bool, 4>();
     auto b = ty.array(a, 5_u);
@@ -125,6 +131,7 @@
     EXPECT_EQ(out.str(), "tint_array<tint_array<bool, 4>, 5>");
 }
 
+// MslPrinterTest.EmitType_ArrayOfArrayOfArray
 TEST_F(MslASTPrinterTest, EmitType_ArrayOfArrayOfArray) {
     auto a = ty.array<bool, 4>();
     auto b = ty.array(a, 5_u);
@@ -138,6 +145,7 @@
     EXPECT_EQ(out.str(), "tint_array<tint_array<tint_array<bool, 4>, 5>, 6>");
 }
 
+// TODO(dsinclair): Port? Not sure if this is relevant ...
 TEST_F(MslASTPrinterTest, EmitType_Array_WithoutName) {
     auto arr = ty.array<bool, 4>();
     ast::Type type = GlobalVar("G", arr, core::AddressSpace::kPrivate)->type;
@@ -149,6 +157,7 @@
     EXPECT_EQ(out.str(), "tint_array<bool, 4>");
 }
 
+// MslPrinterTest.EmitType_RuntimeArray
 TEST_F(MslASTPrinterTest, EmitType_RuntimeArray) {
     auto arr = ty.array<bool, 1>();
     ast::Type type = GlobalVar("G", arr, core::AddressSpace::kPrivate)->type;
@@ -160,6 +169,7 @@
     EXPECT_EQ(out.str(), "tint_array<bool, 1>");
 }
 
+// MSLPrinterTest.EmitType_Bool
 TEST_F(MslASTPrinterTest, EmitType_Bool) {
     auto* bool_ = create<core::type::Bool>();
 
@@ -170,6 +180,7 @@
     EXPECT_EQ(out.str(), "bool");
 }
 
+// MSLPrintertest.EmitType_F32
 TEST_F(MslASTPrinterTest, EmitType_F32) {
     auto* f32 = create<core::type::F32>();
 
@@ -180,6 +191,7 @@
     EXPECT_EQ(out.str(), "float");
 }
 
+// MSLPrinterTest.EmitType_F16
 TEST_F(MslASTPrinterTest, EmitType_F16) {
     auto* f16 = create<core::type::F16>();
 
@@ -190,6 +202,7 @@
     EXPECT_EQ(out.str(), "half");
 }
 
+// MSLPrinterTest.EmitType_I32
 TEST_F(MslASTPrinterTest, EmitType_I32) {
     auto* i32 = create<core::type::I32>();
 
@@ -200,6 +213,7 @@
     EXPECT_EQ(out.str(), "int");
 }
 
+// MSLPrinterTest.EmitType_Matrix_F32
 TEST_F(MslASTPrinterTest, EmitType_Matrix_F32) {
     auto* f32 = create<core::type::F32>();
     auto* vec3 = create<core::type::Vector>(f32, 3u);
@@ -212,6 +226,7 @@
     EXPECT_EQ(out.str(), "float2x3");
 }
 
+// MSLPrinterTest.EmitType_Matrix_F16
 TEST_F(MslASTPrinterTest, EmitType_Matrix_F16) {
     auto* f16 = create<core::type::F16>();
     auto* vec3 = create<core::type::Vector>(f16, 3u);
@@ -224,6 +239,7 @@
     EXPECT_EQ(out.str(), "half2x3");
 }
 
+// TODO(dsinclair): Not sure if this is relevant? MslPrinterTest.EmitType_Pointer_Workgroup
 TEST_F(MslASTPrinterTest, EmitType_Pointer) {
     auto* f32 = create<core::type::F32>();
     auto* p =
@@ -249,6 +265,7 @@
     EXPECT_EQ(out.str(), "S");
 }
 
+// MSLPrinterTest.EmitType_Struct
 TEST_F(MslASTPrinterTest, EmitType_StructDecl) {
     auto* s = Structure("S", Vector{
                                  Member("a", ty.i32()),
@@ -267,6 +284,7 @@
 )");
 }
 
+// MSLPrinterTest.EmitType_Struct_Layout_NonComposites
 TEST_F(MslASTPrinterTest, EmitType_Struct_Layout_NonComposites) {
     auto* s =
         Structure("S", Vector{
@@ -384,6 +402,7 @@
 #undef ALL_FIELDS
 }
 
+// MSLPrinterTest.EmitType_Struct_Layout_Structures
 TEST_F(MslASTPrinterTest, EmitType_Struct_Layout_Structures) {
     // inner_x: size(1024), align(512)
     auto* inner_x = Structure("inner_x", Vector{
@@ -472,6 +491,7 @@
 #undef ALL_FIELDS
 }
 
+// MSLPrinterTest.EmitType_Struct_Layout_ArrayDefaultStride
 TEST_F(MslASTPrinterTest, EmitType_Struct_Layout_ArrayDefaultStride) {
     // inner: size(1024), align(512)
     auto* inner = Structure("inner", Vector{
@@ -571,6 +591,7 @@
 #undef ALL_FIELDS
 }
 
+// MslPrinterTest.EmitType_Struct_Layout_ArrayVec3DefaultStride
 TEST_F(MslASTPrinterTest, EmitType_Struct_Layout_ArrayVec3DefaultStride) {
     // array: size(64), align(16)
     auto array = ty.array<vec3<f32>, 4>();
@@ -611,6 +632,7 @@
     EXPECT_EQ(buf.String(), expect.str());
 }
 
+// MslPrinterTest.AttemptTintPadSymbolCollision
 TEST_F(MslASTPrinterTest, AttemptTintPadSymbolCollision) {
     auto* s = Structure(
         "S", Vector{
@@ -696,6 +718,7 @@
 )");
 }
 
+// TODO(dsinclair): Missing from IR tests, but test also doesn't look right.
 TEST_F(MslASTPrinterTest, EmitType_Struct_WithAttribute) {
     auto* s = Structure("S", Vector{
                                  Member("a", ty.i32()),
@@ -718,6 +741,7 @@
 )");
 }
 
+// MSLPrinterTest.EmitType_U32
 TEST_F(MslASTPrinterTest, EmitType_U32) {
     auto* u32 = create<core::type::U32>();
 
@@ -728,6 +752,7 @@
     EXPECT_EQ(out.str(), "uint");
 }
 
+// MSLPrinterTest.EmitType_Vector
 TEST_F(MslASTPrinterTest, EmitType_Vector) {
     auto* f32 = create<core::type::F32>();
     auto* vec3 = create<core::type::Vector>(f32, 3u);
@@ -739,6 +764,7 @@
     EXPECT_EQ(out.str(), "float3");
 }
 
+// MSLPrinterTest.EmitType_Void
 TEST_F(MslASTPrinterTest, EmitType_Void) {
     auto* void_ = create<core::type::Void>();
 
@@ -749,6 +775,7 @@
     EXPECT_EQ(out.str(), "void");
 }
 
+// MSLPrinterTest.EmitType_Sampler
 TEST_F(MslASTPrinterTest, EmitType_Sampler) {
     auto* sampler = create<core::type::Sampler>(core::type::SamplerKind::kSampler);
 
@@ -759,6 +786,7 @@
     EXPECT_EQ(out.str(), "sampler");
 }
 
+// MSLPrinterTest.EmitType_SamplerComparison
 TEST_F(MslASTPrinterTest, EmitType_SamplerComparison) {
     auto* sampler = create<core::type::Sampler>(core::type::SamplerKind::kComparisonSampler);
 
@@ -780,6 +808,7 @@
     return out;
 }
 using MslDepthTexturesTest = TestParamHelper<MslDepthTextureData>;
+// MslPrinterDepthTexturesTest.Emit
 TEST_P(MslDepthTexturesTest, Emit) {
     auto params = GetParam();
 
@@ -804,6 +833,7 @@
                                         "depthcube_array<float, access::sample>"}));
 
 using MslDepthMultisampledTexturesTest = TestHelper;
+// MSLPrinterTest.EmitType_DepthMultisampledTexture
 TEST_F(MslDepthMultisampledTexturesTest, Emit) {
     core::type::DepthMultisampledTexture s(core::type::TextureDimension::k2d);
 
@@ -825,6 +855,7 @@
     return out;
 }
 using MslSampledtexturesTest = TestParamHelper<MslTextureData>;
+// MslPrinterSampledTexturesTest.Emit
 TEST_P(MslSampledtexturesTest, Emit) {
     auto params = GetParam();
 
@@ -850,6 +881,7 @@
         MslTextureData{core::type::TextureDimension::kCubeArray,
                        "texturecube_array<float, access::sample>"}));
 
+// MslPrinterTest.Emit_TypeMultisampledTexture
 TEST_F(MslASTPrinterTest, Emit_TypeMultisampledTexture) {
     auto* u32 = create<core::type::U32>();
     auto* ms = create<core::type::MultisampledTexture>(core::type::TextureDimension::k2d, u32);
@@ -871,6 +903,7 @@
     return out << str.str();
 }
 using MslStorageTexturesTest = TestParamHelper<MslStorageTextureData>;
+// MslPrinterStorageTexturesTest.Emit
 TEST_P(MslStorageTexturesTest, Emit) {
     auto params = GetParam();
 
diff --git a/src/tint/lang/msl/writer/ast_printer/binary_test.cc b/src/tint/lang/msl/writer/ast_printer/binary_test.cc
index e3bf09c..70601fc 100644
--- a/src/tint/lang/msl/writer/ast_printer/binary_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/binary_test.cc
@@ -29,6 +29,8 @@
 #include "src/tint/lang/msl/writer/ast_printer/helper_test.h"
 #include "src/tint/utils/text/string_stream.h"
 
+// All ported to IR.
+
 using namespace tint::core::fluent_types;  // NOLINT
 
 namespace tint::msl::writer {
@@ -44,6 +46,8 @@
     out << str.str();
     return out;
 }
+
+// MSLPrinterBinaryTest.Emit
 using MslBinaryTest = TestParamHelper<BinaryData>;
 TEST_P(MslBinaryTest, Emit) {
     auto params = GetParam();
@@ -89,6 +93,7 @@
                     BinaryData{"(left / right)", core::BinaryOp::kDivide},
                     BinaryData{"(left % right)", core::BinaryOp::kModulo}));
 
+// MSLPrinterBinaryTest_SignedOverflowDefinedBehaviour.Emit
 using MslBinaryTest_SignedOverflowDefinedBehaviour = TestParamHelper<BinaryData>;
 TEST_P(MslBinaryTest_SignedOverflowDefinedBehaviour, Emit) {
     auto params = GetParam();
@@ -122,6 +127,7 @@
                          MslBinaryTest_SignedOverflowDefinedBehaviour,
                          testing::ValuesIn(signed_overflow_defined_behaviour_cases));
 
+// MSLPrinterBinaryTest_ShiftSignedOverflowDefinedBehaviour_Chained.Emit
 using MslBinaryTest_SignedOverflowDefinedBehaviour_Chained = TestParamHelper<BinaryData>;
 TEST_P(MslBinaryTest_SignedOverflowDefinedBehaviour_Chained, Emit) {
     auto params = GetParam();
@@ -160,6 +166,7 @@
                          MslBinaryTest_SignedOverflowDefinedBehaviour_Chained,
                          testing::ValuesIn(signed_overflow_defined_behaviour_chained_cases));
 
+// MslPrinterTest.BinaryModF32
 TEST_F(MslBinaryTest, ModF32) {
     auto* left = Var("left", ty.f32());
     auto* right = Var("right", ty.f32());
@@ -173,6 +180,7 @@
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
 
+// MslPrinterTest.BinaryModF16
 TEST_F(MslBinaryTest, ModF16) {
     Enable(wgsl::Extension::kF16);
 
@@ -188,6 +196,7 @@
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
 
+// MslBinaryTest.BinaryModVecF32
 TEST_F(MslBinaryTest, ModVec3F32) {
     auto* left = Var("left", ty.vec3<f32>());
     auto* right = Var("right", ty.vec3<f32>());
@@ -201,6 +210,7 @@
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
 
+// MslPrinterTest.BinaryModVec3F16
 TEST_F(MslBinaryTest, ModVec3F16) {
     Enable(wgsl::Extension::kF16);
 
@@ -216,6 +226,7 @@
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
 
+// MslPrinterTest.BinaryBoolAnd
 TEST_F(MslBinaryTest, BoolAnd) {
     auto* left = Var("left", Expr(true));
     auto* right = Var("right", Expr(false));
@@ -229,6 +240,7 @@
     EXPECT_EQ(out.str(), "bool(left & right)");
 }
 
+// MslPrinterTest.BinaryBoolOr
 TEST_F(MslBinaryTest, BoolOr) {
     auto* left = Var("left", Expr(true));
     auto* right = Var("right", Expr(false));
diff --git a/src/tint/lang/msl/writer/ast_printer/discard_test.cc b/src/tint/lang/msl/writer/ast_printer/discard_test.cc
index f86373e..d1fdd53 100644
--- a/src/tint/lang/msl/writer/ast_printer/discard_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/discard_test.cc
@@ -32,6 +32,7 @@
 
 using MslASTPrinterTest = TestHelper;
 
+// MslPrinterTest.Discard
 TEST_F(MslASTPrinterTest, Emit_Discard) {
     auto* stmt = Discard();
 
diff --git a/src/tint/lang/msl/writer/ast_printer/if_test.cc b/src/tint/lang/msl/writer/ast_printer/if_test.cc
index cc074e1..1f4a8f0 100644
--- a/src/tint/lang/msl/writer/ast_printer/if_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/if_test.cc
@@ -32,6 +32,7 @@
 
 using MslASTPrinterTest = TestHelper;
 
+// MSLPrinterTest.If
 TEST_F(MslASTPrinterTest, Emit_If) {
     auto* cond = Var("cond", ty.bool_());
     auto* i = If(cond, Block(Return()));
@@ -48,6 +49,7 @@
 )");
 }
 
+// MSLPrinterTest.IfWithElseIf
 TEST_F(MslASTPrinterTest, Emit_IfWithElseIf) {
     auto* cond = Var("cond", ty.bool_());
     auto* else_cond = Var("else_cond", ty.bool_());
@@ -69,6 +71,7 @@
 )");
 }
 
+// MSLPrinterTest.IfWithElse
 TEST_F(MslASTPrinterTest, Emit_IfWithElse) {
     auto* cond = Var("cond", ty.bool_());
     auto* i = If(cond, Block(Return()), Else(Block(Return())));
@@ -87,6 +90,7 @@
 )");
 }
 
+// TODO(dj2): Port to MSLPrinterTest
 TEST_F(MslASTPrinterTest, Emit_IfWithMultiple) {
     auto* cond = Var("cond", ty.bool_());
     auto* else_cond = Var("else_cond", ty.bool_());
diff --git a/src/tint/lang/msl/writer/ast_printer/return_test.cc b/src/tint/lang/msl/writer/ast_printer/return_test.cc
index ac26018..4ea89a6 100644
--- a/src/tint/lang/msl/writer/ast_printer/return_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/return_test.cc
@@ -32,8 +32,11 @@
 namespace tint::msl::writer {
 namespace {
 
+// All ported to IR tests
+
 using MslASTPrinterTest = TestHelper;
 
+// MslPrinterTest.Return
 TEST_F(MslASTPrinterTest, Emit_Return) {
     auto* r = Return();
     WrapInFunction(r);
@@ -46,6 +49,7 @@
     EXPECT_EQ(gen.Result(), "  return;\n");
 }
 
+// MslPrinterTest.ReturnWithValue
 TEST_F(MslASTPrinterTest, Emit_ReturnWithValue) {
     auto* r = Return(123_i);
     Func("f", tint::Empty, ty.i32(), Vector{r});
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.bazel b/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
index a264fb7..8b2b905 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.bazel
@@ -59,6 +59,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -98,6 +99,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.cmake b/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
index f0a8790..93fb394 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.cmake
@@ -60,6 +60,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -102,6 +103,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/msl/writer/ast_raise/BUILD.gn b/src/tint/lang/msl/writer/ast_raise/BUILD.gn
index 6dff3c9..f5d025c 100644
--- a/src/tint/lang/msl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/msl/writer/ast_raise/BUILD.gn
@@ -62,6 +62,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -102,6 +103,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/lang/msl/writer/common/BUILD.bazel b/src/tint/lang/msl/writer/common/BUILD.bazel
index c8f2c0d..51256e7 100644
--- a/src/tint/lang/msl/writer/common/BUILD.bazel
+++ b/src/tint/lang/msl/writer/common/BUILD.bazel
@@ -61,6 +61,7 @@
     "//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",
diff --git a/src/tint/lang/msl/writer/common/BUILD.cmake b/src/tint/lang/msl/writer/common/BUILD.cmake
index 177f390..728427a 100644
--- a/src/tint/lang/msl/writer/common/BUILD.cmake
+++ b/src/tint/lang/msl/writer/common/BUILD.cmake
@@ -62,6 +62,7 @@
   tint_utils_math
   tint_utils_memory
   tint_utils_reflection
+  tint_utils_result
   tint_utils_rtti
   tint_utils_strconv
   tint_utils_symbol
diff --git a/src/tint/lang/msl/writer/common/BUILD.gn b/src/tint/lang/msl/writer/common/BUILD.gn
index 0ea4cc8..befabfe 100644
--- a/src/tint/lang/msl/writer/common/BUILD.gn
+++ b/src/tint/lang/msl/writer/common/BUILD.gn
@@ -64,6 +64,7 @@
       "${tint_src_dir}/utils/math",
       "${tint_src_dir}/utils/memory",
       "${tint_src_dir}/utils/reflection",
+      "${tint_src_dir}/utils/result",
       "${tint_src_dir}/utils/rtti",
       "${tint_src_dir}/utils/strconv",
       "${tint_src_dir}/utils/symbol",
diff --git a/src/tint/lang/msl/writer/common/option_helpers.cc b/src/tint/lang/msl/writer/common/option_helpers.cc
index cd44e5e..cda70c6 100644
--- a/src/tint/lang/msl/writer/common/option_helpers.cc
+++ b/src/tint/lang/msl/writer/common/option_helpers.cc
@@ -27,6 +27,8 @@
 
 #include "src/tint/lang/msl/writer/common/option_helpers.h"
 
+#include <utility>
+
 #include "src/tint/utils/containers/hashset.h"
 
 namespace tint::msl::writer {
@@ -34,7 +36,9 @@
 /// binding::BindingInfo to tint::BindingPoint map
 using InfoToPointMap = tint::Hashmap<binding::BindingInfo, tint::BindingPoint, 8>;
 
-bool ValidateBindingOptions(const Options& options, diag::List& diagnostics) {
+Result<SuccessType> ValidateBindingOptions(const Options& options) {
+    diag::List diagnostics;
+
     tint::Hashmap<tint::BindingPoint, binding::BindingInfo, 8> seen_wgsl_bindings{};
 
     InfoToPointMap seen_msl_buffer_bindings{};
@@ -94,27 +98,27 @@
     // Storage and uniform are both [[buffer()]]
     if (!valid(seen_msl_buffer_bindings, options.bindings.uniform)) {
         diagnostics.add_note(diag::System::Writer, "when processing uniform", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
     if (!valid(seen_msl_buffer_bindings, options.bindings.storage)) {
         diagnostics.add_note(diag::System::Writer, "when processing storage", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
 
     // Sampler is [[sampler()]]
     if (!valid(seen_msl_sampler_bindings, options.bindings.sampler)) {
         diagnostics.add_note(diag::System::Writer, "when processing sampler", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
 
     // Texture and storage texture are [[texture()]]
     if (!valid(seen_msl_texture_bindings, options.bindings.texture)) {
         diagnostics.add_note(diag::System::Writer, "when processing texture", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
     if (!valid(seen_msl_texture_bindings, options.bindings.storage_texture)) {
         diagnostics.add_note(diag::System::Writer, "when processing storage_texture", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
 
     for (const auto& it : options.bindings.external_texture) {
@@ -126,26 +130,26 @@
         // Validate with the actual source regardless of what the remapper will do
         if (wgsl_seen(src_binding, plane0)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
 
         // Plane0 & Plane1 are [[texture()]]
         if (msl_seen(seen_msl_texture_bindings, plane0, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
         if (msl_seen(seen_msl_texture_bindings, plane1, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
         // Metadata is [[buffer()]]
         if (msl_seen(seen_msl_buffer_bindings, metadata, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
     }
 
-    return true;
+    return Success;
 }
 
 // The remapped binding data and external texture data need to coordinate in order to put things in
diff --git a/src/tint/lang/msl/writer/common/option_helpers.h b/src/tint/lang/msl/writer/common/option_helpers.h
index 1abf984..6f21fbd 100644
--- a/src/tint/lang/msl/writer/common/option_helpers.h
+++ b/src/tint/lang/msl/writer/common/option_helpers.h
@@ -34,6 +34,7 @@
 #include "src/tint/api/options/external_texture.h"
 #include "src/tint/lang/msl/writer/common/options.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/result/result.h"
 
 namespace tint::msl::writer {
 
@@ -41,9 +42,8 @@
 using RemapperData = std::unordered_map<BindingPoint, BindingPoint>;
 
 /// @param options the options
-/// @param diagnostics the diagnostics
-/// @returns true if the binding points are valid
-bool ValidateBindingOptions(const Options& options, diag::List& diagnostics);
+/// @returns success or failure
+Result<SuccessType> ValidateBindingOptions(const Options& options);
 
 /// Populates data from the writer options for the remapper and external texture.
 /// @param options the writer options
diff --git a/src/tint/lang/msl/writer/common/options.h b/src/tint/lang/msl/writer/common/options.h
index d085a66..66623d9 100644
--- a/src/tint/lang/msl/writer/common/options.h
+++ b/src/tint/lang/msl/writer/common/options.h
@@ -130,9 +130,6 @@
     /// for all vertex shaders in the module.
     bool emit_vertex_point_size = false;
 
-    /// Set to `true` to generate MSL via the Tint IR instead of from the AST.
-    bool use_tint_ir = false;
-
     /// The index to use when generating a UBO to receive storage buffer sizes.
     /// Defaults to 30, which is the last valid buffer slot.
     uint32_t buffer_size_ubo_index = 30;
@@ -155,7 +152,6 @@
     TINT_REFLECT(disable_robustness,
                  disable_workgroup_init,
                  emit_vertex_point_size,
-                 use_tint_ir,
                  buffer_size_ubo_index,
                  fixed_sample_mask,
                  pixel_local_options,
diff --git a/src/tint/lang/msl/writer/common/printer_support.cc b/src/tint/lang/msl/writer/common/printer_support.cc
index d9e74c4..037e54e 100644
--- a/src/tint/lang/msl/writer/common/printer_support.cc
+++ b/src/tint/lang/msl/writer/common/printer_support.cc
@@ -235,7 +235,7 @@
     } else if (std::isnan(value)) {
         out << "NAN";
     } else {
-        out << tint::writer::FloatToString(value) << "f";
+        out << tint::strconv::FloatToString(value) << "f";
     }
 }
 
@@ -249,7 +249,7 @@
         // There is no NaN expr for half in MSL, "NAN" is of float type.
         out << "NAN";
     } else {
-        out << tint::writer::FloatToString(value) << "h";
+        out << tint::strconv::FloatToString(value) << "h";
     }
 }
 
diff --git a/src/tint/lang/msl/writer/common/printer_support.h b/src/tint/lang/msl/writer/common/printer_support.h
index 3cb135e..dde098f 100644
--- a/src/tint/lang/msl/writer/common/printer_support.h
+++ b/src/tint/lang/msl/writer/common/printer_support.h
@@ -32,7 +32,8 @@
 #include <string>
 
 #include "src/tint/lang/core/builtin_value.h"
-#include "src/tint/lang/core/interpolation.h"
+#include "src/tint/lang/core/interpolation_sampling.h"
+#include "src/tint/lang/core/interpolation_type.h"
 #include "src/tint/lang/core/type/type.h"
 #include "src/tint/utils/text/string_stream.h"
 
diff --git a/src/tint/lang/msl/writer/helpers/BUILD.bazel b/src/tint/lang/msl/writer/helpers/BUILD.bazel
index 095cd37..9d7a77b 100644
--- a/src/tint/lang/msl/writer/helpers/BUILD.bazel
+++ b/src/tint/lang/msl/writer/helpers/BUILD.bazel
@@ -52,6 +52,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/msl/writer/helpers/BUILD.cmake b/src/tint/lang/msl/writer/helpers/BUILD.cmake
index 1e27fed..9efd6c6 100644
--- a/src/tint/lang/msl/writer/helpers/BUILD.cmake
+++ b/src/tint/lang/msl/writer/helpers/BUILD.cmake
@@ -53,6 +53,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/msl/writer/helpers/BUILD.gn b/src/tint/lang/msl/writer/helpers/BUILD.gn
index 3b0713b..f1c9918 100644
--- a/src/tint/lang/msl/writer/helpers/BUILD.gn
+++ b/src/tint/lang/msl/writer/helpers/BUILD.gn
@@ -51,6 +51,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/msl/writer/output.cc b/src/tint/lang/msl/writer/output.cc
index ed184eb..cc2f3a0 100644
--- a/src/tint/lang/msl/writer/output.cc
+++ b/src/tint/lang/msl/writer/output.cc
@@ -35,4 +35,6 @@
 
 Output::Output(const Output&) = default;
 
+Output& Output::operator=(const Output&) = default;
+
 }  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/output.h b/src/tint/lang/msl/writer/output.h
index 6e873af..219e2a6 100644
--- a/src/tint/lang/msl/writer/output.h
+++ b/src/tint/lang/msl/writer/output.h
@@ -47,6 +47,10 @@
     /// Copy constructor
     Output(const Output&);
 
+    /// Copy assignment
+    /// @returns this
+    Output& operator=(const Output&);
+
     /// The generated MSL.
     std::string msl = "";
 
diff --git a/src/tint/lang/msl/writer/printer/BUILD.bazel b/src/tint/lang/msl/writer/printer/BUILD.bazel
index c72bc54..7de0377 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.bazel
+++ b/src/tint/lang/msl/writer/printer/BUILD.bazel
@@ -48,6 +48,7 @@
     "//src/tint/api/common",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/utils/containers",
@@ -79,6 +80,7 @@
   srcs = [
     "binary_test.cc",
     "constant_test.cc",
+    "discard_test.cc",
     "function_test.cc",
     "helper_test.h",
     "if_test.cc",
@@ -89,12 +91,12 @@
   ],
   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/ir",
     "//src/tint/lang/core/type",
-    "//src/tint/lang/msl/writer/raise",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -111,7 +113,9 @@
     "@gtest",
   ] + select({
     ":tint_build_msl_writer": [
+      "//src/tint/lang/msl/writer/common",
       "//src/tint/lang/msl/writer/printer",
+      "//src/tint/lang/msl/writer/raise",
     ],
     "//conditions:default": [],
   }),
diff --git a/src/tint/lang/msl/writer/printer/BUILD.cmake b/src/tint/lang/msl/writer/printer/BUILD.cmake
index 8992d6c..37d0fcb 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/printer/BUILD.cmake
@@ -49,6 +49,7 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
   tint_lang_core_ir
   tint_lang_core_type
   tint_utils_containers
@@ -83,6 +84,7 @@
 tint_add_target(tint_lang_msl_writer_printer_test test
   lang/msl/writer/printer/binary_test.cc
   lang/msl/writer/printer/constant_test.cc
+  lang/msl/writer/printer/discard_test.cc
   lang/msl/writer/printer/function_test.cc
   lang/msl/writer/printer/helper_test.h
   lang/msl/writer/printer/if_test.cc
@@ -94,12 +96,12 @@
 
 tint_target_add_dependencies(tint_lang_msl_writer_printer_test test
   tint_api_common
+  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_intrinsic
   tint_lang_core_ir
   tint_lang_core_type
-  tint_lang_msl_writer_raise
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -121,7 +123,9 @@
 
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_lang_msl_writer_printer_test test
+    tint_lang_msl_writer_common
     tint_lang_msl_writer_printer
+    tint_lang_msl_writer_raise
   )
 endif(TINT_BUILD_MSL_WRITER)
 
diff --git a/src/tint/lang/msl/writer/printer/BUILD.gn b/src/tint/lang/msl/writer/printer/BUILD.gn
index bfd0975..ce460cc 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/printer/BUILD.gn
@@ -51,6 +51,7 @@
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
       "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/utils/containers",
@@ -80,6 +81,7 @@
       sources = [
         "binary_test.cc",
         "constant_test.cc",
+        "discard_test.cc",
         "function_test.cc",
         "helper_test.h",
         "if_test.cc",
@@ -91,12 +93,12 @@
       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/ir",
         "${tint_src_dir}/lang/core/type",
-        "${tint_src_dir}/lang/msl/writer/raise",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
         "${tint_src_dir}/utils/ice",
@@ -113,7 +115,11 @@
       ]
 
       if (tint_build_msl_writer) {
-        deps += [ "${tint_src_dir}/lang/msl/writer/printer" ]
+        deps += [
+          "${tint_src_dir}/lang/msl/writer/common",
+          "${tint_src_dir}/lang/msl/writer/printer",
+          "${tint_src_dir}/lang/msl/writer/raise",
+        ]
       }
     }
   }
diff --git a/src/tint/lang/msl/writer/printer/binary_test.cc b/src/tint/lang/msl/writer/printer/binary_test.cc
index 8f87403..74808bf 100644
--- a/src/tint/lang/msl/writer/printer/binary_test.cc
+++ b/src/tint/lang/msl/writer/printer/binary_test.cc
@@ -76,13 +76,96 @@
     testing::Values(BinaryData{"(left + right)", core::ir::BinaryOp::kAdd},
                     BinaryData{"(left - right)", core::ir::BinaryOp::kSubtract},
                     BinaryData{"(left * right)", core::ir::BinaryOp::kMultiply},
-                    BinaryData{"(left / right)", core::ir::BinaryOp::kDivide},
-                    BinaryData{"(left % right)", core::ir::BinaryOp::kModulo},
                     BinaryData{"(left & right)", core::ir::BinaryOp::kAnd},
                     BinaryData{"(left | right)", core::ir::BinaryOp::kOr},
-                    BinaryData{"(left ^ right)", core::ir::BinaryOp::kXor},
-                    BinaryData{"(left << right)", core::ir::BinaryOp::kShiftLeft},
-                    BinaryData{"(left >> right)", core::ir::BinaryOp::kShiftRight}));
+                    BinaryData{"(left ^ right)", core::ir::BinaryOp::kXor}));
+
+TEST_F(MslPrinterTest, BinaryDivU32) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* l = b.Let("left", b.Constant(1_u));
+        auto* r = b.Let("right", b.Constant(2_u));
+        auto* bin = b.Binary(core::ir::BinaryOp::kDivide, ty.u32(), l, r);
+        b.Let("val", bin);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << err_ << output_;
+    EXPECT_EQ(output_, MetalHeader() + R"(
+void foo() {
+  uint const left = 1u;
+  uint const right = 2u;
+  uint const val = tint_div_u32(left, right);
+}
+uint tint_div_u32(uint lhs, uint rhs) {
+  return (lhs / select(rhs, 1u, (rhs == 0u)));
+}
+)");
+}
+
+TEST_F(MslPrinterTest, BinaryModU32) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* l = b.Let("left", b.Constant(1_u));
+        auto* r = b.Let("right", b.Constant(2_u));
+        auto* bin = b.Binary(core::ir::BinaryOp::kModulo, ty.u32(), l, r);
+        b.Let("val", bin);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << err_ << output_;
+    EXPECT_EQ(output_, MetalHeader() + R"(
+void foo() {
+  uint const left = 1u;
+  uint const right = 2u;
+  uint const val = tint_mod_u32(left, right);
+}
+uint tint_mod_u32(uint lhs, uint rhs) {
+  uint const v = select(rhs, 1u, (rhs == 0u));
+  return (lhs - ((lhs / v) * v));
+}
+)");
+}
+
+TEST_F(MslPrinterTest, BinaryShiftLeft) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* l = b.Let("left", b.Constant(1_u));
+        auto* r = b.Let("right", b.Constant(2_u));
+        auto* bin = b.Binary(core::ir::BinaryOp::kShiftLeft, ty.u32(), l, r);
+        b.Let("val", bin);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << err_ << output_;
+    EXPECT_EQ(output_, MetalHeader() + R"(
+void foo() {
+  uint const left = 1u;
+  uint const right = 2u;
+  uint const val = (left << (right & 31u));
+}
+)");
+}
+
+TEST_F(MslPrinterTest, BinaryShiftRight) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* l = b.Let("left", b.Constant(1_u));
+        auto* r = b.Let("right", b.Constant(2_u));
+        auto* bin = b.Binary(core::ir::BinaryOp::kShiftRight, ty.u32(), l, r);
+        b.Let("val", bin);
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << err_ << output_;
+    EXPECT_EQ(output_, MetalHeader() + R"(
+void foo() {
+  uint const left = 1u;
+  uint const right = 2u;
+  uint const val = (left >> (right & 31u));
+}
+)");
+}
 
 using MslPrinterBinaryBoolTest = MslPrinterTestWithParam<BinaryData>;
 TEST_P(MslPrinterBinaryBoolTest, Emit) {
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/msl/writer/printer/discard_test.cc
similarity index 61%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/msl/writer/printer/discard_test.cc
index 6f0f657..64451a3 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/msl/writer/printer/discard_test.cc
@@ -25,28 +25,47 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#include "src/tint/lang/msl/writer/printer/helper_test.h"
 
-#include <string>
+using namespace tint::core::number_suffixes;  // NOLINT
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
+namespace tint::msl::writer {
+namespace {
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
+TEST_F(MslPrinterTest, Discard) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&] {
+            b.Discard();
+            b.ExitIf(if_);
+        });
+        b.Return(func);
+    });
+
+    auto* ep = b.Function("main", ty.void_());
+    ep->SetStage(core::ir::Function::PipelineStage::kFragment);
+    b.Append(ep->Block(), [&] {
+        b.Call(func);
+        b.Return(ep);
+    });
+
+    ASSERT_TRUE(Generate()) << err_ << output_;
+    EXPECT_EQ(output_, MetalHeader() + R"(
+thread bool continue_execution = true;
+void foo() {
+  if (true) {
+    continue_execution = false;
+  }
+}
+fragment void main() {
+  foo();
+  if (!(continue_execution)) {
+    discard_fragment();
+  }
+}
+)");
 }
 
-namespace tint::wgsl::writer {
-
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
-
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+}  // namespace
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/printer/helper_test.h b/src/tint/lang/msl/writer/printer/helper_test.h
index f77774b..bf14aa6 100644
--- a/src/tint/lang/msl/writer/printer/helper_test.h
+++ b/src/tint/lang/msl/writer/printer/helper_test.h
@@ -80,7 +80,7 @@
     /// Run the writer on the IR module and validate the result.
     /// @returns true if generation and validation succeeded
     bool Generate() {
-        if (auto raised = raise::Raise(mod); !raised) {
+        if (auto raised = raise::Raise(mod, {}); !raised) {
             err_ = raised.Failure().reason.str();
             return false;
         }
diff --git a/src/tint/lang/msl/writer/printer/if_test.cc b/src/tint/lang/msl/writer/printer/if_test.cc
index 5892a86..2f1a583 100644
--- a/src/tint/lang/msl/writer/printer/if_test.cc
+++ b/src/tint/lang/msl/writer/printer/if_test.cc
@@ -116,7 +116,8 @@
 )");
 }
 
-TEST_F(MslPrinterTest, IfWithSinglePhi) {
+// Requires a transform to turn PHIs into lets
+TEST_F(MslPrinterTest, DISABLED_IfWithSinglePhi) {
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
         auto* i = b.If(true);
@@ -143,7 +144,8 @@
 )");
 }
 
-TEST_F(MslPrinterTest, IfWithMultiPhi) {
+// Requires a transform to turn PHIs into lets
+TEST_F(MslPrinterTest, DISABLED_IfWithMultiPhi) {
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
         auto* i = b.If(true);
@@ -173,7 +175,8 @@
 )");
 }
 
-TEST_F(MslPrinterTest, IfWithMultiPhiReturn1) {
+// Requires a transform to turn PHIs into lets
+TEST_F(MslPrinterTest, DISABLED_IfWithMultiPhiReturn1) {
     auto* func = b.Function("foo", ty.i32());
     b.Append(func->Block(), [&] {
         auto* i = b.If(true);
@@ -204,7 +207,8 @@
 )");
 }
 
-TEST_F(MslPrinterTest, IfWithMultiPhiReturn2) {
+// Requires a transform to turn PHIs into lets
+TEST_F(MslPrinterTest, DISABLED_IfWithMultiPhiReturn2) {
     auto* func = b.Function("foo", ty.bool_());
     b.Append(func->Block(), [&] {
         auto* i = b.If(true);
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index 02fb9c5..2ededba 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -34,16 +34,35 @@
 #include "src/tint/lang/core/constant/composite.h"
 #include "src/tint/lang/core/constant/splat.h"
 #include "src/tint/lang/core/fluent_types.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"
+#include "src/tint/lang/core/ir/break_if.h"
 #include "src/tint/lang/core/ir/constant.h"
+#include "src/tint/lang/core/ir/construct.h"
+#include "src/tint/lang/core/ir/continue.h"
+#include "src/tint/lang/core/ir/convert.h"
+#include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/discard.h"
 #include "src/tint/lang/core/ir/exit_if.h"
+#include "src/tint/lang/core/ir/exit_loop.h"
+#include "src/tint/lang/core/ir/exit_switch.h"
+#include "src/tint/lang/core/ir/ice.h"
 #include "src/tint/lang/core/ir/if.h"
 #include "src/tint/lang/core/ir/let.h"
 #include "src/tint/lang/core/ir/load.h"
+#include "src/tint/lang/core/ir/load_vector_element.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
+#include "src/tint/lang/core/ir/next_iteration.h"
 #include "src/tint/lang/core/ir/return.h"
+#include "src/tint/lang/core/ir/store.h"
+#include "src/tint/lang/core/ir/store_vector_element.h"
+#include "src/tint/lang/core/ir/switch.h"
+#include "src/tint/lang/core/ir/swizzle.h"
+#include "src/tint/lang/core/ir/terminate_invocation.h"
 #include "src/tint/lang/core/ir/unreachable.h"
+#include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/ir/var.h"
 #include "src/tint/lang/core/type/array.h"
@@ -100,7 +119,7 @@
         EmitBlockInstructions(ir_.root_block);
 
         // Emit functions.
-        for (auto* func : ir_.functions) {
+        for (auto& func : ir_.functions) {
             EmitFunction(func);
         }
 
@@ -115,6 +134,9 @@
 
     core::ir::Module& ir_;
 
+    /// A hashmap of value to name
+    Hashmap<const core::ir::Value*, std::string, 32> names_;
+
     /// The buffer holding preamble text
     TextBuffer preamble_buffer_;
 
@@ -164,6 +186,9 @@
     /// Values that can be inlined.
     Hashset<core::ir::Value*, 64> can_inline_;
 
+    /// Block to emit for a continuing
+    std::function<void()> emit_continuing_;
+
     /// @returns the name of the templated `tint_array` helper type, generating it if needed
     const std::string& ArrayTemplateName() {
         if (!array_template_name_.empty()) {
@@ -202,13 +227,83 @@
         {
             auto out = Line();
 
-            // TODO(dsinclair): Emit function stage if any
+            switch (func->Stage()) {
+                case core::ir::Function::PipelineStage::kCompute:
+                    out << "kernel ";
+                    break;
+                case core::ir::Function::PipelineStage::kFragment:
+                    out << "fragment ";
+                    break;
+                case core::ir::Function::PipelineStage::kVertex:
+                    out << "vertex ";
+                    break;
+                case core::ir::Function::PipelineStage::kUndefined:
+                    break;
+            }
+
             // TODO(dsinclair): Handle return type attributes
 
             EmitType(out, func->ReturnType());
-            out << " " << ir_.NameOf(func).Name() << "() {";
+            out << " " << NameOf(func) << "(";
 
-            // TODO(dsinclair): Emit Function parameters
+            size_t i = 0;
+            for (auto* param : func->Params()) {
+                if (i > 0) {
+                    out << ", ";
+                }
+                ++i;
+
+                // TODO(dsinclair): Handle parameter attributes
+                EmitType(out, param->Type());
+                out << " ";
+
+                // Non-entrypoint pointers are set to `const` for the value
+                if (func->Stage() == core::ir::Function::PipelineStage::kUndefined &&
+                    param->Type()->Is<core::type::Pointer>()) {
+                    out << "const ";
+                }
+
+                out << NameOf(param);
+
+                if (param->Builtin().has_value()) {
+                    out << " [[";
+                    switch (param->Builtin().value()) {
+                        case core::ir::FunctionParam::Builtin::kFrontFacing:
+                            out << "front_facing";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kGlobalInvocationId:
+                            out << "thread_position_in_grid";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kLocalInvocationId:
+                            out << "thread_position_in_threadgroup";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kLocalInvocationIndex:
+                            out << "thread_index_in_threadgroup";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kNumWorkgroups:
+                            out << "threadgroups_per_grid";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kPosition:
+                            out << "position";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kSampleIndex:
+                            out << "sample_id";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kSampleMask:
+                            out << "sample_mask";
+                            break;
+                        case core::ir::FunctionParam::Builtin::kWorkgroupId:
+                            out << "threadgroup_position_in_grid";
+                            break;
+
+                        default:
+                            break;
+                    }
+                    out << "]]";
+                }
+            }
+
+            out << ") {";
         }
         {
             ScopedIndent si(current_buffer_);
@@ -220,11 +315,7 @@
 
     /// Emit a block
     /// @param block the block to emit
-    void EmitBlock(core::ir::Block* block) {
-        MarkInlinable(block);
-
-        EmitBlockInstructions(block);
-    }
+    void EmitBlock(core::ir::Block* block) { EmitBlockInstructions(block); }
 
     /// Emit the instructions in a block
     /// @param block the block with the instructions to emit
@@ -234,27 +325,90 @@
         for (auto* inst : *block) {
             Switch(
                 inst,                                                //
-                [&](core::ir::Binary* b) { EmitBinary(b); },         //
-                [&](core::ir::ExitIf* e) { EmitExitIf(e); },         //
-                [&](core::ir::If* if_) { EmitIf(if_); },             //
-                [&](core::ir::Let* l) { EmitLet(l); },               //
-                [&](core::ir::Load* l) { EmitLoad(l); },             //
-                [&](core::ir::Return* r) { EmitReturn(r); },         //
+                [&](core::ir::BreakIf* i) { EmitBreakIf(i); },       //
+                [&](core::ir::Continue*) { EmitContinue(); },        //
+                [&](core::ir::Discard*) { EmitDiscard(); },          //
+                [&](core::ir::ExitIf* i) { EmitExitIf(i); },         //
+                [&](core::ir::ExitLoop*) { EmitExitLoop(); },        //
+                [&](core::ir::ExitSwitch*) { EmitExitSwitch(); },    //
+                [&](core::ir::If* i) { EmitIf(i); },                 //
+                [&](core::ir::Let* i) { EmitLet(i); },               //
+                [&](core::ir::Loop* i) { EmitLoop(i); },             //
+                [&](core::ir::NextIteration*) { /* do nothing */ },  //
+                [&](core::ir::Return* i) { EmitReturn(i); },         //
+                [&](core::ir::Store* i) { EmitStore(i); },           //
+                [&](core::ir::Switch* i) { EmitSwitch(i); },         //
                 [&](core::ir::Unreachable*) { EmitUnreachable(); },  //
-                [&](core::ir::Var* v) { EmitVar(v); },               //
+                [&](core::ir::Call* i) { EmitCallStmt(i); },         //
+                [&](core::ir::Var* i) { EmitVar(i); },               //
+                [&](core::ir::StoreVectorElement* e) { EmitStoreVectorElement(e); },
+                [&](core::ir::TerminateInvocation*) { EmitDiscard(); },  //
+
+                [&](core::ir::LoadVectorElement*) { /* inlined */ },  //
+                [&](core::ir::Swizzle*) { /* inlined */ },            //
+                [&](core::ir::Bitcast*) { /* inlined */ },            //
+                [&](core::ir::Unary*) { /* inlined */ },              //
+                [&](core::ir::Binary*) { /* inlined */ },             //
+                [&](core::ir::Load*) { /* inlined */ },               //
+                [&](core::ir::Construct*) { /* inlined */ },          //
+                [&](core::ir::Access*) { /* inlined */ },             //
                 TINT_ICE_ON_NO_MATCH);
         }
     }
 
+    void EmitValue(StringStream& out, const core::ir::Value* v) {
+        Switch(
+            v,                                                           //
+            [&](const core::ir::Constant* c) { EmitConstant(out, c); },  //
+            [&](const core::ir::InstructionResult* r) {
+                Switch(
+                    r->Instruction(),                                                          //
+                    [&](const core::ir::Unary* u) { EmitUnary(out, u); },                      //
+                    [&](const core::ir::Binary* b) { EmitBinary(out, b); },                    //
+                    [&](const core::ir::Convert* b) { EmitConvert(out, b); },                  //
+                    [&](const core::ir::Let* l) { out << NameOf(l->Result(0)); },              //
+                    [&](const core::ir::Load* l) { EmitValue(out, l->From()); },               //
+                    [&](const core::ir::Construct* c) { EmitConstruct(out, c); },              //
+                    [&](const core::ir::Var* var) { out << NameOf(var->Result(0)); },          //
+                    [&](const core::ir::Bitcast* b) { EmitBitcast(out, b); },                  //
+                    [&](const core::ir::Access* a) { EmitAccess(out, a); },                    //
+                    [&](const core::ir::CoreBuiltinCall* c) { EmitCoreBuiltinCall(out, c); },  //
+                    [&](const core::ir::UserCall* c) { EmitUserCall(out, c); },                //
+                    [&](const core::ir::LoadVectorElement* e) {
+                        EmitLoadVectorElement(out, e);
+                    },                                                         //
+                    [&](const core::ir::Swizzle* s) { EmitSwizzle(out, s); },  //
+                    TINT_ICE_ON_NO_MATCH);
+            },                                                            //
+            [&](const core::ir::FunctionParam* p) { out << NameOf(p); },  //
+            TINT_ICE_ON_NO_MATCH);
+    }
+
+    void EmitUnary(StringStream& out, const core::ir::Unary* u) {
+        switch (u->Op()) {
+            case core::ir::UnaryOp::kNegation:
+                out << "-";
+                break;
+            case core::ir::UnaryOp::kComplement:
+                out << "~";
+                break;
+        }
+        out << "(";
+        EmitValue(out, u->Val());
+        out << ")";
+    }
+
     /// Emit a binary instruction
     /// @param b the binary instruction
-    void EmitBinary(core::ir::Binary* b) {
+    void EmitBinary(StringStream& out, const core::ir::Binary* b) {
         if (b->Op() == core::ir::BinaryOp::kEqual) {
             auto* rhs = b->RHS()->As<core::ir::Constant>();
             if (rhs && rhs->Type()->Is<core::type::Bool>() &&
                 rhs->Value()->ValueAs<bool>() == false) {
                 // expr == false
-                Bind(b->Result(), "!(" + Expr(b->LHS()) + ")");
+                out << "!(";
+                EmitValue(out, b->LHS());
+                out << ")";
                 return;
             }
         }
@@ -297,17 +451,20 @@
             return "<error>";
         };
 
-        StringStream str;
-        str << "(" << Expr(b->LHS()) << " " << kind() << " " + Expr(b->RHS()) << ")";
-
-        Bind(b->Result(), str.str());
+        out << "(";
+        EmitValue(out, b->LHS());
+        out << " " << kind() << " ";
+        EmitValue(out, b->RHS());
+        out << ")";
     }
 
-    /// Emit a load instruction
-    /// @param l the load instruction
-    void EmitLoad(core::ir::Load* l) {
-        // Force loads to be bound as inlines
-        bindings_.Add(l->Result(), InlinedValue{Expr(l->From()), PtrKind::kRef});
+    /// Emit a convert instruction
+    /// @param c the convert instruction
+    void EmitConvert(StringStream& out, const core::ir::Convert* c) {
+        EmitType(out, c->Result(0)->Type());
+        out << "(";
+        EmitValue(out, c->Operand(0));
+        out << ")";
     }
 
     /// Emit a var instruction
@@ -315,7 +472,7 @@
     void EmitVar(core::ir::Var* v) {
         auto out = Line();
 
-        auto* ptr = v->Result()->Type()->As<core::type::Pointer>();
+        auto* ptr = v->Result(0)->Type()->As<core::type::Pointer>();
         TINT_ASSERT_OR_RETURN(ptr);
 
         auto space = ptr->AddressSpace();
@@ -330,17 +487,16 @@
                 out << "threadgroup ";
                 break;
             default:
-                TINT_ICE() << "unhandled variable address space";
+                TINT_IR_ICE(ir_) << "unhandled variable address space";
                 return;
         }
 
-        auto name = ir_.NameOf(v);
-
         EmitType(out, ptr->UnwrapPtr());
-        out << " " << name.Name();
+        out << " " << NameOf(v->Result(0));
 
         if (v->Initializer()) {
-            out << " = " << Expr(v->Initializer());
+            out << " = ";
+            EmitValue(out, v->Initializer());
         } else if (space == core::AddressSpace::kPrivate ||
                    space == core::AddressSpace::kFunction ||
                    space == core::AddressSpace::kUndefined) {
@@ -348,36 +504,147 @@
             EmitZeroValue(out, ptr->UnwrapPtr());
         }
         out << ";";
-
-        Bind(v->Result(), name, PtrKind::kRef);
     }
 
     /// Emit a let instruction
     /// @param l the let instruction
     void EmitLet(core::ir::Let* l) {
-        Bind(l->Result(), Expr(l->Value(), PtrKind::kPtr), PtrKind::kPtr);
+        auto out = Line();
+        EmitType(out, l->Result(0)->Type());
+        out << " const " << NameOf(l->Result(0)) << " = ";
+        EmitValue(out, l->Value());
+        out << ";";
+    }
+
+    void EmitExitLoop() { Line() << "break;"; }
+
+    void EmitBreakIf(core::ir::BreakIf* b) {
+        auto out = Line();
+        out << "if ";
+        EmitValue(out, b->Condition());
+        out << " { break; }";
+    }
+
+    void EmitContinue() {
+        if (emit_continuing_) {
+            emit_continuing_();
+        }
+        Line() << "continue;";
+    }
+
+    void EmitLoop(core::ir::Loop* l) {
+        // Note, we can't just emit the continuing inside a conditional at the top of the loop
+        // because any variable declared in the block must be visible to the continuing.
+        //
+        // loop {
+        //   var a = 3;
+        //   continue {
+        //     let y = a;
+        //   }
+        // }
+
+        auto emit_continuing = [&] { EmitBlock(l->Continuing()); };
+        TINT_SCOPED_ASSIGNMENT(emit_continuing_, emit_continuing);
+
+        Line() << "{";
+        {
+            ScopedIndent init(current_buffer_);
+            EmitBlock(l->Initializer());
+
+            Line() << "while(true) {";
+            {
+                ScopedIndent si(current_buffer_);
+                EmitBlock(l->Body());
+            }
+            Line() << "}";
+        }
+        Line() << "}";
+    }
+
+    void EmitExitSwitch() { Line() << "break;"; }
+
+    void EmitSwitch(core::ir::Switch* s) {
+        {
+            auto out = Line();
+            out << "switch(";
+            EmitValue(out, s->Condition());
+            out << ") {";
+        }
+        {
+            ScopedIndent blk(current_buffer_);
+            for (auto& case_ : s->Cases()) {
+                for (auto& sel : case_.selectors) {
+                    if (sel.IsDefault()) {
+                        Line() << "default:";
+                    } else {
+                        auto out = Line();
+                        out << "case ";
+                        EmitValue(out, sel.val);
+                        out << ":";
+                    }
+                }
+                Line() << "{";
+                {
+                    ScopedIndent ci(current_buffer_);
+                    EmitBlock(case_.block);
+                }
+                Line() << "}";
+            }
+        }
+        Line() << "}";
+    }
+
+    void EmitSwizzle(StringStream& out, const core::ir::Swizzle* swizzle) {
+        EmitValue(out, swizzle->Object());
+        out << ".";
+        for (const auto i : swizzle->Indices()) {
+            switch (i) {
+                case 0:
+                    out << "x";
+                    break;
+                case 1:
+                    out << "y";
+                    break;
+                case 2:
+                    out << "z";
+                    break;
+                case 3:
+                    out << "w";
+                    break;
+                default:
+                    TINT_UNREACHABLE();
+            }
+        }
+    }
+
+    void EmitStoreVectorElement(const core::ir::StoreVectorElement* l) {
+        auto out = Line();
+
+        EmitValue(out, l->To());
+        out << "[";
+        EmitValue(out, l->Index());
+        out << "] = ";
+        EmitValue(out, l->Value());
+        out << ";";
+    }
+
+    void EmitLoadVectorElement(StringStream& out, const core::ir::LoadVectorElement* l) {
+        EmitValue(out, l->From());
+        out << "[";
+        EmitValue(out, l->Index());
+        out << "]";
     }
 
     /// Emit an if instruction
     /// @param if_ the if instruction
     void EmitIf(core::ir::If* if_) {
-        // Emit any nodes that need to be used as PHI nodes
-        for (auto* phi : if_->Results()) {
-            if (!ir_.NameOf(phi).IsValid()) {
-                ir_.SetName(phi, ir_.symbols.New());
-            }
-
-            auto name = ir_.NameOf(phi);
-
+        {
             auto out = Line();
-            EmitType(out, phi->Type());
-            out << " " << name.Name() << ";";
-
-            Bind(phi, name);
+            out << "if (";
+            EmitValue(out, if_->Condition());
+            out << ") {";
         }
 
-        Line() << "if (" << Expr(if_->Condition()) << ") {";
-
         {
             ScopedIndent si(current_buffer_);
             EmitBlockInstructions(if_->True());
@@ -402,7 +669,10 @@
             auto* phi = results[i];
             auto* val = args[i];
 
-            Line() << ir_.NameOf(phi).Name() << " = " << Expr(val) << ";";
+            auto out = Line();
+            out << NameOf(phi) << " = ";
+            EmitValue(out, val);
+            out << ";";
         }
     }
 
@@ -418,7 +688,8 @@
         auto out = Line();
         out << "return";
         if (!r->Args().IsEmpty()) {
-            out << " " << Expr(r->Args().Front());
+            out << " ";
+            EmitValue(out, r->Args().Front());
         }
         out << ";";
     }
@@ -426,6 +697,257 @@
     /// Emit an unreachable instruction
     void EmitUnreachable() { Line() << "/* unreachable */"; }
 
+    /// Emit a discard instruction
+    void EmitDiscard() { Line() << "discard_fragment();"; }
+
+    /// Emit a store
+    void EmitStore(core::ir::Store* s) {
+        auto out = Line();
+
+        EmitValue(out, s->To());
+        out << " = ";
+        EmitValue(out, s->From());
+        out << ";";
+    }
+
+    /// Emit a bitcast instruction
+    void EmitBitcast(StringStream& out, const core::ir::Bitcast* b) {
+        out << "as_type<";
+        EmitType(out, b->Result(0)->Type());
+        out << ">(";
+        EmitValue(out, b->Val());
+        out << ")";
+    }
+
+    /// Emit an accessor
+    void EmitAccess(StringStream& out, const core::ir::Access* a) {
+        EmitValue(out, a->Object());
+
+        auto* current_type = a->Object()->Type();
+        for (auto* index : a->Indices()) {
+            TINT_ASSERT(current_type);
+
+            current_type = current_type->UnwrapPtr();
+            Switch(
+                current_type,  //
+                [&](const core::type::Struct* s) {
+                    auto* c = index->As<core::ir::Constant>();
+                    auto* member = s->Members()[c->Value()->ValueAs<uint32_t>()];
+                    out << "." << member->Name().Name();
+                    current_type = member->Type();
+                },
+                [&](Default) {
+                    out << "[";
+                    EmitValue(out, index);
+                    out << "]";
+                    current_type = current_type->Element(0);
+                });
+        }
+    }
+
+    void EmitCallStmt(const core::ir::Call* c) {
+        if (!c->Result(0)->IsUsed()) {
+            auto out = Line();
+            EmitValue(out, c->Result(0));
+            out << ";";
+        }
+    }
+
+    void EmitCoreBuiltinCall(StringStream& out, const core::ir::CoreBuiltinCall* c) {
+        EmitCoreBuiltinName(out, c->Func());
+        out << "(";
+
+        size_t i = 0;
+        for (const auto* arg : c->Args()) {
+            if (i > 0) {
+                out << ", ";
+            }
+            ++i;
+
+            EmitValue(out, arg);
+        }
+        out << ")";
+    }
+
+    void EmitCoreBuiltinName(StringStream& out, core::BuiltinFn func) {
+        switch (func) {
+            case core::BuiltinFn::kAcos:
+            case core::BuiltinFn::kAcosh:
+            case core::BuiltinFn::kAll:
+            case core::BuiltinFn::kAny:
+            case core::BuiltinFn::kAsin:
+            case core::BuiltinFn::kAsinh:
+            case core::BuiltinFn::kAtan2:
+            case core::BuiltinFn::kAtan:
+            case core::BuiltinFn::kAtanh:
+            case core::BuiltinFn::kCeil:
+            case core::BuiltinFn::kClamp:
+            case core::BuiltinFn::kCos:
+            case core::BuiltinFn::kCosh:
+            case core::BuiltinFn::kCross:
+            case core::BuiltinFn::kDeterminant:
+            case core::BuiltinFn::kExp2:
+            case core::BuiltinFn::kExp:
+            case core::BuiltinFn::kFloor:
+            case core::BuiltinFn::kFma:
+            case core::BuiltinFn::kFract:
+            case core::BuiltinFn::kLdexp:
+            case core::BuiltinFn::kLog2:
+            case core::BuiltinFn::kLog:
+            case core::BuiltinFn::kMix:
+            case core::BuiltinFn::kNormalize:
+            case core::BuiltinFn::kPow:
+            case core::BuiltinFn::kReflect:
+            case core::BuiltinFn::kRefract:
+            case core::BuiltinFn::kSaturate:
+            case core::BuiltinFn::kSelect:
+            case core::BuiltinFn::kSign:
+            case core::BuiltinFn::kSin:
+            case core::BuiltinFn::kSinh:
+            case core::BuiltinFn::kSqrt:
+            case core::BuiltinFn::kStep:
+            case core::BuiltinFn::kTan:
+            case core::BuiltinFn::kTanh:
+            case core::BuiltinFn::kTranspose:
+            case core::BuiltinFn::kTrunc:
+                out << func;
+                break;
+            case core::BuiltinFn::kCountLeadingZeros:
+                out << "clz";
+                break;
+            case core::BuiltinFn::kCountOneBits:
+                out << "popcount";
+                break;
+            case core::BuiltinFn::kCountTrailingZeros:
+                out << "ctz";
+                break;
+            case core::BuiltinFn::kDpdx:
+            case core::BuiltinFn::kDpdxCoarse:
+            case core::BuiltinFn::kDpdxFine:
+                out << "dfdx";
+                break;
+            case core::BuiltinFn::kDpdy:
+            case core::BuiltinFn::kDpdyCoarse:
+            case core::BuiltinFn::kDpdyFine:
+                out << "dfdy";
+                break;
+            case core::BuiltinFn::kExtractBits:
+                out << "extract_bits";
+                break;
+            case core::BuiltinFn::kInsertBits:
+                out << "insert_bits";
+                break;
+            case core::BuiltinFn::kFwidth:
+            case core::BuiltinFn::kFwidthCoarse:
+            case core::BuiltinFn::kFwidthFine:
+                out << "fwidth";
+                break;
+            case core::BuiltinFn::kFaceForward:
+                out << "faceforward";
+                break;
+            case core::BuiltinFn::kPack4X8Snorm:
+                out << "pack_float_to_snorm4x8";
+                break;
+            case core::BuiltinFn::kPack4X8Unorm:
+                out << "pack_float_to_unorm4x8";
+                break;
+            case core::BuiltinFn::kPack2X16Snorm:
+                out << "pack_float_to_snorm2x16";
+                break;
+            case core::BuiltinFn::kPack2X16Unorm:
+                out << "pack_float_to_unorm2x16";
+                break;
+            case core::BuiltinFn::kReverseBits:
+                out << "reverse_bits";
+                break;
+            case core::BuiltinFn::kRound:
+                out << "rint";
+                break;
+            case core::BuiltinFn::kSmoothstep:
+                out << "smoothstep";
+                break;
+            case core::BuiltinFn::kInverseSqrt:
+                out << "rsqrt";
+                break;
+            case core::BuiltinFn::kUnpack4X8Snorm:
+                out << "unpack_snorm4x8_to_float";
+                break;
+            case core::BuiltinFn::kUnpack4X8Unorm:
+                out << "unpack_unorm4x8_to_float";
+                break;
+            case core::BuiltinFn::kUnpack2X16Snorm:
+                out << "unpack_snorm2x16_to_float";
+                break;
+            case core::BuiltinFn::kUnpack2X16Unorm:
+                out << "unpack_unorm2x16_to_float";
+                break;
+            default:
+                TINT_UNREACHABLE() << "unhandled: " << func;
+        }
+    }
+
+    /// Emits a user call instruction
+    void EmitUserCall(StringStream& out, const core::ir::UserCall* c) {
+        out << NameOf(c->Target()) << "(";
+        size_t i = 0;
+        for (const auto* arg : c->Args()) {
+            if (i > 0) {
+                out << ", ";
+            }
+            ++i;
+
+            EmitValue(out, arg);
+        }
+        out << ")";
+    }
+
+    /// Emit a constructor
+    void EmitConstruct(StringStream& out, const core::ir::Construct* c) {
+        Switch(
+            c->Result(0)->Type(),
+            [&](const core::type::Array*) {
+                EmitType(out, c->Result(0)->Type());
+                out << "{";
+                size_t i = 0;
+                for (auto* arg : c->Args()) {
+                    if (i > 0) {
+                        out << ", ";
+                    }
+                    EmitValue(out, arg);
+                    i++;
+                }
+                out << "}";
+            },
+            [&](const core::type::Struct* struct_ty) {
+                out << "{";
+                size_t i = 0;
+                for (auto* arg : c->Args()) {
+                    if (i > 0) {
+                        out << ", ";
+                    }
+                    // Emit field designators for structures to account for padding members.
+                    auto name = struct_ty->Members()[i]->Name().Name();
+                    out << "." << name << "=";
+                    EmitValue(out, arg);
+                    i++;
+                }
+                out << "}";
+            },
+            [&](Default) {
+                EmitType(out, c->Result(0)->Type());
+                out << "(";
+                size_t i = 0;
+                for (auto* arg : c->Args()) {
+                    if (i > 0) {
+                        out << ", ";
+                    }
+                    EmitValue(out, arg);
+                    i++;
+                }
+                out << ")";
+            });
+    }
+
     /// Handles generating a address space
     /// @param out the output of the type stream
     /// @param sc the address space to generate
@@ -446,7 +968,7 @@
                 out << "constant";
                 break;
             default:
-                TINT_ICE() << "unhandled address space: " << sc;
+                TINT_IR_ICE(ir_) << "unhandled address space: " << sc;
                 break;
         }
     }
@@ -519,7 +1041,7 @@
         } else {
             auto count = arr->ConstantCount();
             if (!count) {
-                TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
+                TINT_IR_ICE(ir_) << core::type::Array::kErrExpectedConstantCount;
                 return;
             }
             out << count.value();
@@ -551,7 +1073,7 @@
     /// @param tex the texture to emit
     void EmitTextureType(StringStream& out, const core::type::Texture* tex) {
         if (TINT_UNLIKELY(tex->Is<core::type::ExternalTexture>())) {
-            TINT_ICE() << "Multiplanar external texture transform was not run.";
+            TINT_IR_ICE(ir_) << "Multiplanar external texture transform was not run.";
             return;
         }
 
@@ -581,7 +1103,7 @@
                 out << "cube_array";
                 break;
             default:
-                TINT_ICE() << "invalid texture dimensions";
+                TINT_IR_ICE(ir_) << "invalid texture dimensions";
                 return;
         }
         if (tex->IsAnyOf<core::type::MultisampledTexture, core::type::DepthMultisampledTexture>()) {
@@ -604,7 +1126,7 @@
                 } else if (storage->access() == core::Access::kWrite) {
                     out << "access::write";
                 } else {
-                    TINT_ICE() << "invalid access control for storage texture";
+                    TINT_IR_ICE(ir_) << "invalid access control for storage texture";
                     return;
                 }
             },
@@ -666,8 +1188,8 @@
             if (is_host_shareable) {
                 if (TINT_UNLIKELY(ir_offset < msl_offset)) {
                     // Unimplementable layout
-                    TINT_ICE() << "Structure member offset (" << ir_offset
-                               << ") is behind MSL offset (" << msl_offset << ")";
+                    TINT_IR_ICE(ir_) << "Structure member offset (" << ir_offset
+                                     << ") is behind MSL offset (" << msl_offset << ")";
                     return;
                 }
 
@@ -691,7 +1213,7 @@
             if (auto builtin = attributes.builtin) {
                 auto name = BuiltinToAttribute(builtin.value());
                 if (name.empty()) {
-                    TINT_ICE() << "unknown builtin";
+                    TINT_IR_ICE(ir_) << "unknown builtin";
                     return;
                 }
                 out << " [[" << name << "]]";
@@ -700,7 +1222,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_IR_ICE(ir_) << "invalid entry point IO struct uses";
                     return;
                 }
 
@@ -716,7 +1238,7 @@
                                core::type::PipelineStageUsage::kFragmentOutput))) {
                     out << " [[color(" + std::to_string(location.value()) + ")]]";
                 } else {
-                    TINT_ICE() << "invalid use of location decoration";
+                    TINT_IR_ICE(ir_) << "invalid use of location decoration";
                     return;
                 }
             }
@@ -724,7 +1246,7 @@
             if (auto interpolation = attributes.interpolation) {
                 auto name = InterpolationToAttribute(interpolation->type, interpolation->sampling);
                 if (name.empty()) {
-                    TINT_ICE() << "unknown interpolation attribute";
+                    TINT_IR_ICE(ir_) << "unknown interpolation attribute";
                     return;
                 }
                 out << " [[" << name << "]]";
@@ -741,9 +1263,9 @@
                 // Calculate new MSL offset
                 auto size_align = MslPackedTypeSizeAndAlign(ty);
                 if (TINT_UNLIKELY(msl_offset % size_align.align)) {
-                    TINT_ICE() << "Misaligned MSL structure member " << mem_name << " : "
-                               << ty->FriendlyName() << " offset: " << msl_offset
-                               << " align: " << size_align.align;
+                    TINT_IR_ICE(ir_) << "Misaligned MSL structure member " << mem_name << " : "
+                                     << ty->FriendlyName() << " offset: " << msl_offset
+                                     << " align: " << size_align.align;
                     return;
                 }
                 msl_offset += size_align.size;
@@ -763,7 +1285,9 @@
     /// Handles core::ir::Constant values
     /// @param out the stream to write the constant too
     /// @param c the constant to emit
-    void EmitConstant(StringStream& out, core::ir::Constant* c) { EmitConstant(out, c->Value()); }
+    void EmitConstant(StringStream& out, const core::ir::Constant* c) {
+        EmitConstant(out, c->Value());
+    }
 
     /// Handles core::constant::Value values
     /// @param out the stream to write the constant too
@@ -811,7 +1335,7 @@
 
                 auto count = a->ConstantCount();
                 if (!count) {
-                    TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
+                    TINT_IR_ICE(ir_) << core::type::Array::kErrExpectedConstantCount;
                     return;
                 }
                 emit_values(*count);
@@ -872,177 +1396,26 @@
         return name;
     }
 
+    /// @param value the value to get the name of
+    /// @returns the name of the given value, creating a new unique name if the value is unnamed in
+    /// the module.
+    std::string NameOf(const core::ir::Value* value) {
+        return names_.GetOrCreate(value, [&] {
+            if (auto sym = ir_.NameOf(value); sym.IsValid()) {
+                return sym.Name();
+            }
+            return UniqueIdentifier("v");
+        });
+    }
+
     /// @return a new, unique identifier with the given prefix.
-    /// @param prefix optional prefix to apply to the generated identifier. If empty "tint_symbol"
-    /// will be used.
+    /// @param prefix optional prefix to apply to the generated identifier. If empty
+    /// "tint_symbol" will be used.
     std::string UniqueIdentifier(const std::string& prefix /* = "" */) {
         return ir_.symbols.New(prefix).Name();
     }
-
-    TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
-
-    /// Returns the expression for the given value
-    /// @param value the value to lookup
-    /// @param want_ptr_kind the pointer information for the return
-    /// @returns the string expression
-    std::string Expr(core::ir::Value* value, PtrKind want_ptr_kind = PtrKind::kRef) {
-        using ExprAndPtrKind = std::pair<std::string, PtrKind>;
-
-        auto [expr, got_ptr_kind] = tint::Switch(
-            value,
-            [&](core::ir::Constant* c) -> ExprAndPtrKind {
-                StringStream str;
-                EmitConstant(str, c);
-                return {str.str(), PtrKind::kRef};
-            },
-            [&](Default) -> ExprAndPtrKind {
-                auto lookup = bindings_.Find(value);
-                if (TINT_UNLIKELY(!lookup)) {
-                    TINT_ICE() << "Expr(" << (value ? value->TypeInfo().name : "null")
-                               << ") value has no expression";
-                    return {};
-                }
-
-                return std::visit(
-                    [&](auto&& got) -> ExprAndPtrKind {
-                        using T = std::decay_t<decltype(got)>;
-
-                        if constexpr (std::is_same_v<T, VariableValue>) {
-                            return {got.name.Name(), got.ptr_kind};
-                        }
-
-                        if constexpr (std::is_same_v<T, InlinedValue>) {
-                            auto result = ExprAndPtrKind{got.expr, got.ptr_kind};
-
-                            // Single use (inlined) expression.
-                            // Mark the bindings_ map entry as consumed.
-                            *lookup = ConsumedValue{};
-                            return result;
-                        }
-
-                        if constexpr (std::is_same_v<T, ConsumedValue>) {
-                            TINT_ICE() << "Expr(" << value->TypeInfo().name
-                                       << ") called twice on the same value";
-                        } else {
-                            TINT_ICE()
-                                << "Expr(" << value->TypeInfo().name << ") has unhandled value";
-                        }
-                        return {};
-                    },
-                    *lookup);
-            });
-        if (expr.empty()) {
-            return "<error>";
-        }
-
-        if (value->Type()->Is<core::type::Pointer>()) {
-            return ToPtrKind(expr, got_ptr_kind, want_ptr_kind);
-        }
-
-        return expr;
-    }
-
-    TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
-
-    /// Returns the given expression converted to the given pointer kind
-    /// @param in the input expression
-    /// @param got the pointer kind we have
-    /// @param want the pointer kind we want
-    std::string ToPtrKind(const std::string& in, PtrKind got, PtrKind want) {
-        if (want == PtrKind::kRef && got == PtrKind::kPtr) {
-            return "*(" + in + ")";
-        }
-        if (want == PtrKind::kPtr && got == PtrKind::kRef) {
-            return "&(" + in + ")";
-        }
-        return in;
-    }
-
-    /// Associates an IR value with a result expression
-    /// @param value the IR value
-    /// @param expr the result expression
-    /// @param ptr_kind defines how pointer values are represented by the expression
-    void Bind(core::ir::Value* value, const std::string& expr, PtrKind ptr_kind = PtrKind::kRef) {
-        TINT_ASSERT(value);
-
-        if (can_inline_.Remove(value)) {
-            // Value will be inlined at its place of usage.
-            if (TINT_LIKELY(bindings_.Add(value, InlinedValue{expr, ptr_kind}))) {
-                return;
-            }
-        } else {
-            auto mod_name = ir_.NameOf(value);
-            if (value->Usages().IsEmpty() && !mod_name.IsValid()) {
-                // Drop phonies.
-            } else {
-                if (mod_name.Name().empty()) {
-                    mod_name = ir_.symbols.New("v");
-                }
-
-                auto out = Line();
-                EmitType(out, value->Type());
-                out << " const " << mod_name.Name() << " = ";
-                if (value->Type()->Is<core::type::Pointer>()) {
-                    out << ToPtrKind(expr, ptr_kind, PtrKind::kPtr);
-                } else {
-                    out << expr;
-                }
-                out << ";";
-
-                Bind(value, mod_name, PtrKind::kPtr);
-            }
-            return;
-        }
-
-        TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
-    }
-
-    /// Associates an IR value the 'var', 'let' or parameter of the given name
-    /// @param value the IR value
-    /// @param name the name for the value
-    /// @param ptr_kind defines how pointer values are represented by @p expr.
-    void Bind(core::ir::Value* value, Symbol name, PtrKind ptr_kind = PtrKind::kRef) {
-        TINT_ASSERT(value);
-
-        bool added = bindings_.Add(value, VariableValue{name, ptr_kind});
-        if (TINT_UNLIKELY(!added)) {
-            TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
-        }
-    }
-
-    /// Marks instructions in a block for inlineability
-    /// @param block the block
-    void MarkInlinable(core::ir::Block* block) {
-        // An ordered list of possibly-inlinable values returned by sequenced instructions that have
-        // not yet been marked-for or ruled-out-for inlining.
-        UniqueVector<core::ir::Value*, 32> pending_resolution;
-
-        // Walk the instructions of the block starting with the first.
-        for (auto* inst : *block) {
-            // Is the instruction sequenced?
-            bool sequenced = inst->Sequenced();
-
-            if (inst->Results().Length() != 1) {
-                continue;
-            }
-
-            // Instruction has a single result value.
-            // Check to see if the result of this instruction is a candidate for inlining.
-            auto* result = inst->Result();
-            // Only values with a single usage can be inlined.
-            // Named values are not inlined, as we want to emit the name for a let.
-            if (result->Usages().Count() == 1 && !ir_.NameOf(result).IsValid()) {
-                if (sequenced) {
-                    // The value comes from a sequenced instruction.  Don't inline.
-                } else {
-                    // The value comes from an unsequenced instruction. Just inline.
-                    can_inline_.Add(result);
-                }
-                continue;
-            }
-        }
-    }
 };
+
 }  // namespace
 
 Result<std::string> Print(core::ir::Module& module) {
diff --git a/src/tint/lang/msl/writer/printer/var_test.cc b/src/tint/lang/msl/writer/printer/var_test.cc
index ab15f44..04f9358 100644
--- a/src/tint/lang/msl/writer/printer/var_test.cc
+++ b/src/tint/lang/msl/writer/printer/var_test.cc
@@ -254,9 +254,9 @@
 
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
-        auto* ld = b.Load(v->Result());
+        auto* ld = b.Load(v->Result(0));
         auto* a = b.Var("a", ty.ptr<core::AddressSpace::kFunction, f32>());
-        a->SetInitializer(ld->Result());
+        a->SetInitializer(ld->Result(0));
         b.Return(func);
     });
 
@@ -281,9 +281,9 @@
 
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
-        auto* ld = b.Load(v->Result());
+        auto* ld = b.Load(v->Result(0));
         auto* a = b.Var("a", ty.ptr<core::AddressSpace::kFunction, f32>());
-        a->SetInitializer(ld->Result());
+        a->SetInitializer(ld->Result(0));
         b.Return(func);
     });
 
diff --git a/src/tint/lang/msl/writer/raise/BUILD.bazel b/src/tint/lang/msl/writer/raise/BUILD.bazel
index ab458ab..a192956 100644
--- a/src/tint/lang/msl/writer/raise/BUILD.bazel
+++ b/src/tint/lang/msl/writer/raise/BUILD.bazel
@@ -45,18 +45,32 @@
     "raise.h",
   ],
   deps = [
+    "//src/tint/api/common",
+    "//src/tint/api/options",
+    "//src/tint/lang/core/ir/transform",
     "//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/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//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"],
 )
 
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
diff --git a/src/tint/lang/msl/writer/raise/BUILD.cfg b/src/tint/lang/msl/writer/raise/BUILD.cfg
new file mode 100644
index 0000000..70e4a45
--- /dev/null
+++ b/src/tint/lang/msl/writer/raise/BUILD.cfg
@@ -0,0 +1,4 @@
+{
+    "condition": "tint_build_msl_writer"
+}
+
diff --git a/src/tint/lang/msl/writer/raise/BUILD.cmake b/src/tint/lang/msl/writer/raise/BUILD.cmake
index cb64e93..0e41a3c 100644
--- a/src/tint/lang/msl/writer/raise/BUILD.cmake
+++ b/src/tint/lang/msl/writer/raise/BUILD.cmake
@@ -34,9 +34,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
+if(TINT_BUILD_MSL_WRITER)
 ################################################################################
 # Target:    tint_lang_msl_writer_raise
 # Kind:      lib
+# Condition: TINT_BUILD_MSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_msl_writer_raise lib
   lang/msl/writer/raise/raise.cc
@@ -44,14 +46,26 @@
 )
 
 tint_target_add_dependencies(tint_lang_msl_writer_raise lib
+  tint_api_common
+  tint_api_options
+  tint_lang_core_ir_transform
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
+
+if(TINT_BUILD_MSL_WRITER)
+  tint_target_add_dependencies(tint_lang_msl_writer_raise lib
+    tint_lang_msl_writer_common
+  )
+endif(TINT_BUILD_MSL_WRITER)
+
+endif(TINT_BUILD_MSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/msl/writer/raise/BUILD.gn b/src/tint/lang/msl/writer/raise/BUILD.gn
index bd20a51..74cc477 100644
--- a/src/tint/lang/msl/writer/raise/BUILD.gn
+++ b/src/tint/lang/msl/writer/raise/BUILD.gn
@@ -37,22 +37,31 @@
 import("../../../../../../scripts/tint_overrides_with_defaults.gni")
 
 import("${tint_src_dir}/tint.gni")
+if (tint_build_msl_writer) {
+  libtint_source_set("raise") {
+    sources = [
+      "raise.cc",
+      "raise.h",
+    ]
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/api/options",
+      "${tint_src_dir}/lang/core/ir/transform",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
 
-libtint_source_set("raise") {
-  sources = [
-    "raise.cc",
-    "raise.h",
-  ]
-  deps = [
-    "${tint_src_dir}/utils/containers",
-    "${tint_src_dir}/utils/diagnostic",
-    "${tint_src_dir}/utils/ice",
-    "${tint_src_dir}/utils/macros",
-    "${tint_src_dir}/utils/math",
-    "${tint_src_dir}/utils/memory",
-    "${tint_src_dir}/utils/result",
-    "${tint_src_dir}/utils/rtti",
-    "${tint_src_dir}/utils/text",
-    "${tint_src_dir}/utils/traits",
-  ]
+    if (tint_build_msl_writer) {
+      deps += [ "${tint_src_dir}/lang/msl/writer/common" ]
+    }
+  }
 }
diff --git a/src/tint/lang/msl/writer/raise/raise.cc b/src/tint/lang/msl/writer/raise/raise.cc
index 6a7cd81..da70216 100644
--- a/src/tint/lang/msl/writer/raise/raise.cc
+++ b/src/tint/lang/msl/writer/raise/raise.cc
@@ -29,18 +29,82 @@
 
 #include <utility>
 
-namespace tint::msl::raise {
+#include "src/tint/lang/core/ir/transform/binary_polyfill.h"
+#include "src/tint/lang/core/ir/transform/binding_remapper.h"
+#include "src/tint/lang/core/ir/transform/builtin_polyfill.h"
+#include "src/tint/lang/core/ir/transform/conversion_polyfill.h"
+#include "src/tint/lang/core/ir/transform/demote_to_helper.h"
+#include "src/tint/lang/core/ir/transform/multiplanar_external_texture.h"
+#include "src/tint/lang/core/ir/transform/preserve_padding.h"
+#include "src/tint/lang/core/ir/transform/robustness.h"
+#include "src/tint/lang/core/ir/transform/value_to_let.h"
+#include "src/tint/lang/core/ir/transform/vectorize_scalar_matrix_constructors.h"
+#include "src/tint/lang/core/ir/transform/zero_init_workgroup_memory.h"
+#include "src/tint/lang/msl/writer/common/option_helpers.h"
 
-Result<SuccessType> Raise(core::ir::Module&) {
-    // #define RUN_TRANSFORM(name)
-    //     do {
-    //         auto result = core::ir::transform::name(module);
-    //         if (!result) {
-    //             return result;
-    //         }
-    //     } while (false)
+namespace tint::msl::writer::raise {
+
+Result<SuccessType> Raise(core::ir::Module& module, const Options& options) {
+#define RUN_TRANSFORM(name, ...)                   \
+    do {                                           \
+        auto result = name(module, ##__VA_ARGS__); \
+        if (!result) {                             \
+            return result;                         \
+        }                                          \
+    } while (false)
+
+    ExternalTextureOptions external_texture_options{};
+    RemapperData remapper_data{};
+    PopulateRemapperAndMultiplanarOptions(options, remapper_data, external_texture_options);
+    RUN_TRANSFORM(core::ir::transform::BindingRemapper, remapper_data);
+
+    {
+        core::ir::transform::BinaryPolyfillConfig binary_polyfills{};
+        binary_polyfills.int_div_mod = true;
+        binary_polyfills.bitshift_modulo = true;  // crbug.com/tint/1543
+        RUN_TRANSFORM(core::ir::transform::BinaryPolyfill, binary_polyfills);
+    }
+
+    {
+        core::ir::transform::BuiltinPolyfillConfig core_polyfills{};
+        core_polyfills.clamp_int = true;
+        core_polyfills.extract_bits = core::ir::transform::BuiltinPolyfillLevel::kClampOrRangeCheck;
+        core_polyfills.first_leading_bit = true;
+        core_polyfills.first_trailing_bit = true;
+        core_polyfills.insert_bits = core::ir::transform::BuiltinPolyfillLevel::kClampOrRangeCheck;
+        core_polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
+        RUN_TRANSFORM(core::ir::transform::BuiltinPolyfill, core_polyfills);
+    }
+    // polyfills.sign_int = true;
+
+    {
+        core::ir::transform::ConversionPolyfillConfig conversion_polyfills;
+        conversion_polyfills.ftoi = true;
+        RUN_TRANSFORM(core::ir::transform::ConversionPolyfill, conversion_polyfills);
+    }
+
+    if (!options.disable_robustness) {
+        core::ir::transform::RobustnessConfig config{};
+        RUN_TRANSFORM(core::ir::transform::Robustness, config);
+    }
+
+    RUN_TRANSFORM(core::ir::transform::MultiplanarExternalTexture, external_texture_options);
+
+    if (!options.disable_workgroup_init) {
+        RUN_TRANSFORM(core::ir::transform::ZeroInitWorkgroupMemory);
+    }
+
+    // PreservePadding must come before DirectVariableAccess.
+    RUN_TRANSFORM(core::ir::transform::PreservePadding);
+
+    RUN_TRANSFORM(core::ir::transform::VectorizeScalarMatrixConstructors);
+
+    // DemoteToHelper must come before any transform that introduces non-core instructions.
+    RUN_TRANSFORM(core::ir::transform::DemoteToHelper);
+
+    RUN_TRANSFORM(core::ir::transform::ValueToLet);
 
     return Success;
 }
 
-}  // namespace tint::msl::raise
+}  // namespace tint::msl::writer::raise
diff --git a/src/tint/lang/msl/writer/raise/raise.h b/src/tint/lang/msl/writer/raise/raise.h
index 979d713..256925d 100644
--- a/src/tint/lang/msl/writer/raise/raise.h
+++ b/src/tint/lang/msl/writer/raise/raise.h
@@ -30,6 +30,7 @@
 
 #include <string>
 
+#include "src/tint/lang/msl/writer/common/options.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
@@ -38,13 +39,14 @@
 class Module;
 }  // namespace tint::core::ir
 
-namespace tint::msl::raise {
+namespace tint::msl::writer::raise {
 
 /// Raise a core IR module to the MSL dialect of the IR.
-/// @param mod the core IR module to raise to MSL dialect
+/// @param module the core IR module to raise to MSL dialect
+/// @param options the printer options
 /// @returns success or failure
-Result<SuccessType> Raise(core::ir::Module& mod);
+Result<SuccessType> Raise(core::ir::Module& module, const Options& options);
 
-}  // namespace tint::msl::raise
+}  // namespace tint::msl::writer::raise
 
 #endif  // SRC_TINT_LANG_MSL_WRITER_RAISE_RAISE_H_
diff --git a/src/tint/lang/msl/writer/writer.cc b/src/tint/lang/msl/writer/writer.cc
index cb5281d..bc1a5e0 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -42,68 +42,61 @@
 
 namespace tint::msl::writer {
 
+Result<Output> Generate(core::ir::Module& ir, const Options& options) {
+    {
+        auto res = ValidateBindingOptions(options);
+        if (!res) {
+            return res.Failure();
+        }
+    }
+
+    Output output;
+
+    // Raise from core-dialect to MSL-dialect.
+    if (auto res = raise::Raise(ir, options); !res) {
+        return res.Failure();
+    }
+
+    // Generate the MSL code.
+    auto result = Print(ir);
+    if (!result) {
+        return result.Failure();
+    }
+    output.msl = result.Get();
+    return output;
+}
+
 Result<Output> Generate(const Program& program, const Options& options) {
     if (!program.IsValid()) {
         return Failure{program.Diagnostics()};
     }
 
     {
-        diag::List validation_diagnostics;
-        if (!ValidateBindingOptions(options, validation_diagnostics)) {
-            return Failure{validation_diagnostics};
+        auto res = ValidateBindingOptions(options);
+        if (!res) {
+            return res.Failure();
         }
     }
 
     Output output;
 
-    if (options.use_tint_ir) {
-#if TINT_BUILD_WGSL_READER
-        // Convert the AST program to an IR module.
-        auto converted = wgsl::reader::ProgramToIR(program);
-        if (!converted) {
-            return converted.Failure();
-        }
-
-        auto ir = converted.Move();
-
-        // Lower from WGSL-dialect to core-dialect
-        if (auto res = wgsl::reader::Lower(ir); !res) {
-            return res.Failure();
-        }
-
-        // Raise from core-dialect to MSL-dialect.
-        if (auto res = raise::Raise(ir); !res) {
-            return res.Failure();
-        }
-
-        // Generate the MSL code.
-        auto result = Print(ir);
-        if (!result) {
-            return result.Failure();
-        }
-        output.msl = result.Get();
-#else
-        return Failure{"use_tint_ir requires building with TINT_BUILD_WGSL_READER"};
-#endif
-    } else {
-        // Sanitize the program.
-        auto sanitized_result = Sanitize(program, options);
-        if (!sanitized_result.program.IsValid()) {
-            return Failure{sanitized_result.program.Diagnostics()};
-        }
-        output.needs_storage_buffer_sizes = sanitized_result.needs_storage_buffer_sizes;
-        output.used_array_length_from_uniform_indices =
-            std::move(sanitized_result.used_array_length_from_uniform_indices);
-
-        // Generate the MSL code.
-        auto impl = std::make_unique<ASTPrinter>(sanitized_result.program);
-        if (!impl->Generate()) {
-            return Failure{impl->Diagnostics()};
-        }
-        output.msl = impl->Result();
-        output.has_invariant_attribute = impl->HasInvariant();
-        output.workgroup_allocations = impl->DynamicWorkgroupAllocations();
+    // Sanitize the program.
+    auto sanitized_result = Sanitize(program, options);
+    if (!sanitized_result.program.IsValid()) {
+        return Failure{sanitized_result.program.Diagnostics()};
     }
+    output.needs_storage_buffer_sizes = sanitized_result.needs_storage_buffer_sizes;
+    output.used_array_length_from_uniform_indices =
+        std::move(sanitized_result.used_array_length_from_uniform_indices);
+
+    // Generate the MSL code.
+    auto impl = std::make_unique<ASTPrinter>(sanitized_result.program);
+    if (!impl->Generate()) {
+        return Failure{impl->Diagnostics()};
+    }
+    output.msl = impl->Result();
+    output.has_invariant_attribute = impl->HasInvariant();
+    output.workgroup_allocations = impl->DynamicWorkgroupAllocations();
 
     return output;
 }
diff --git a/src/tint/lang/msl/writer/writer.h b/src/tint/lang/msl/writer/writer.h
index 7d34ddd..e27c318 100644
--- a/src/tint/lang/msl/writer/writer.h
+++ b/src/tint/lang/msl/writer/writer.h
@@ -39,11 +39,21 @@
 namespace tint {
 class Program;
 }  // namespace tint
+namespace tint::core::ir {
+class Module;
+}  // namespace tint::core::ir
 
 namespace tint::msl::writer {
 
 /// Generate MSL for a program, according to a set of configuration options.
 /// The result will contain the MSL and supplementary information, or failure.
+/// @param ir the IR module to translate to MSL
+/// @param options the configuration options to use when generating MSL
+/// @returns the resulting MSL and supplementary information, or failure
+Result<Output> Generate(core::ir::Module& ir, const Options& options);
+
+/// Generate MSL for a program, according to a set of configuration options.
+/// The result will contain the MSL and supplementary information, or failure.
 /// @param program the program to translate to MSL
 /// @param options the configuration options to use when generating MSL
 /// @returns the resulting MSL and supplementary information, or failure
diff --git a/src/tint/lang/spirv/BUILD.cmake b/src/tint/lang/spirv/BUILD.cmake
index 30c3163..06f7cfd 100644
--- a/src/tint/lang/spirv/BUILD.cmake
+++ b/src/tint/lang/spirv/BUILD.cmake
@@ -38,6 +38,7 @@
 include(lang/spirv/ir/BUILD.cmake)
 include(lang/spirv/reader/BUILD.cmake)
 include(lang/spirv/type/BUILD.cmake)
+include(lang/spirv/validate/BUILD.cmake)
 include(lang/spirv/writer/BUILD.cmake)
 
 ################################################################################
diff --git a/src/tint/lang/spirv/ir/builtin_call.cc b/src/tint/lang/spirv/ir/builtin_call.cc
index dd01293..5503e55 100644
--- a/src/tint/lang/spirv/ir/builtin_call.cc
+++ b/src/tint/lang/spirv/ir/builtin_call.cc
@@ -48,7 +48,7 @@
 BuiltinCall::~BuiltinCall() = default;
 
 BuiltinCall* BuiltinCall::Clone(core::ir::CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto new_args = ctx.Clone<BuiltinCall::kDefaultNumOperands>(Args());
     return ctx.ir.instructions.Create<BuiltinCall>(new_result, func_, new_args);
 }
diff --git a/src/tint/lang/spirv/ir/builtin_call.h b/src/tint/lang/spirv/ir/builtin_call.h
index fbc958b..54e357f 100644
--- a/src/tint/lang/spirv/ir/builtin_call.h
+++ b/src/tint/lang/spirv/ir/builtin_call.h
@@ -54,16 +54,16 @@
     BuiltinCall* Clone(core::ir::CloneContext& ctx) override;
 
     /// @returns the builtin function
-    BuiltinFn Func() { return func_; }
+    BuiltinFn Func() const { return func_; }
 
     /// @returns the identifier for the function
-    size_t FuncId() override { return static_cast<size_t>(func_); }
+    size_t FuncId() const override { return static_cast<size_t>(func_); }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return std::string("spirv.") + str(func_); }
+    std::string FriendlyName() const override { return std::string("spirv.") + str(func_); }
 
     /// @returns the table data to validate this builtin
-    const core::intrinsic::TableData& TableData() override {
+    const core::intrinsic::TableData& TableData() const override {
         return spirv::intrinsic::Dialect::kData;
     }
 
diff --git a/src/tint/lang/spirv/ir/builtin_call_test.cc b/src/tint/lang/spirv/ir/builtin_call_test.cc
index a48c8fb..4256f26 100644
--- a/src/tint/lang/spirv/ir/builtin_call_test.cc
+++ b/src/tint/lang/spirv/ir/builtin_call_test.cc
@@ -43,8 +43,8 @@
     auto* new_b = clone_ctx.Clone(builtin);
 
     EXPECT_NE(builtin, new_b);
-    EXPECT_NE(builtin->Result(), new_b->Result());
-    EXPECT_EQ(mod.Types().f32(), new_b->Result()->Type());
+    EXPECT_NE(builtin->Result(0), new_b->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_b->Result(0)->Type());
 
     EXPECT_EQ(BuiltinFn::kArrayLength, new_b->Func());
 
@@ -62,8 +62,8 @@
     auto* builtin = b.Call<BuiltinCall>(mod.Types().f32(), BuiltinFn::kArrayLength);
 
     auto* new_b = clone_ctx.Clone(builtin);
-    EXPECT_NE(builtin->Result(), new_b->Result());
-    EXPECT_EQ(mod.Types().f32(), new_b->Result()->Type());
+    EXPECT_NE(builtin->Result(0), new_b->Result(0));
+    EXPECT_EQ(mod.Types().f32(), new_b->Result(0)->Type());
 
     EXPECT_EQ(BuiltinFn::kArrayLength, new_b->Func());
 
diff --git a/src/tint/lang/spirv/reader/BUILD.bazel b/src/tint/lang/spirv/reader/BUILD.bazel
index 7d88550..599d625 100644
--- a/src/tint/lang/spirv/reader/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/BUILD.bazel
@@ -52,6 +52,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/spirv/reader/BUILD.cmake b/src/tint/lang/spirv/reader/BUILD.cmake
index 540b7b4..46c4639 100644
--- a/src/tint/lang/spirv/reader/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/BUILD.cmake
@@ -57,6 +57,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/spirv/reader/BUILD.gn b/src/tint/lang/spirv/reader/BUILD.gn
index 2b415fb..e6eb783 100644
--- a/src/tint/lang/spirv/reader/BUILD.gn
+++ b/src/tint/lang/spirv/reader/BUILD.gn
@@ -51,6 +51,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
index 5f667a1..4bb7e6a 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
@@ -59,6 +59,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -98,6 +99,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
index f73503c..6d3870b 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
@@ -60,6 +60,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -102,6 +103,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.gn b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
index d904f52..c2ded34 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
@@ -62,6 +62,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -102,6 +103,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
index 3afb571..421ec97 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.bazel
@@ -72,6 +72,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -151,6 +152,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
index 6316fb3..cf9191f 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.cmake
@@ -73,6 +73,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -157,6 +158,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
index 85be154..f82d2fb 100644
--- a/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_parser/BUILD.gn
@@ -75,6 +75,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -157,6 +158,7 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/spirv/reader/common/BUILD.bazel b/src/tint/lang/spirv/reader/common/BUILD.bazel
index 0700a25..b74c742 100644
--- a/src/tint/lang/spirv/reader/common/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/common/BUILD.bazel
@@ -47,6 +47,7 @@
   deps = [
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/utils/containers",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
diff --git a/src/tint/lang/spirv/reader/common/BUILD.cmake b/src/tint/lang/spirv/reader/common/BUILD.cmake
index 871983e..23c0ff1 100644
--- a/src/tint/lang/spirv/reader/common/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/common/BUILD.cmake
@@ -46,6 +46,7 @@
 tint_target_add_dependencies(tint_lang_spirv_reader_common lib
   tint_lang_wgsl
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_utils_containers
   tint_utils_ice
   tint_utils_macros
diff --git a/src/tint/lang/spirv/reader/common/BUILD.gn b/src/tint/lang/spirv/reader/common/BUILD.gn
index 391f597..9e75c83 100644
--- a/src/tint/lang/spirv/reader/common/BUILD.gn
+++ b/src/tint/lang/spirv/reader/common/BUILD.gn
@@ -46,6 +46,7 @@
   deps = [
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
diff --git a/src/tint/lang/spirv/validate/BUILD.bazel b/src/tint/lang/spirv/validate/BUILD.bazel
new file mode 100644
index 0000000..bc1d952
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.bazel
@@ -0,0 +1,112 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "validate",
+  srcs = [
+    "validate.cc",
+  ],
+  hdrs = [
+    "validate.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "validate_test.cc",
+  ],
+  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",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "//src/tint/lang/spirv/validate",
+    ],
+    "//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/validate/BUILD.cfg b/src/tint/lang/spirv/validate/BUILD.cfg
new file mode 100644
index 0000000..3b3caf4
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_spv_reader || tint_build_spv_writer"
+}
diff --git a/src/tint/lang/spirv/validate/BUILD.cmake b/src/tint/lang/spirv/validate/BUILD.cmake
new file mode 100644
index 0000000..e50d0e9
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.cmake
@@ -0,0 +1,101 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_validate
+# Kind:      lib
+# Condition: TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_validate lib
+  lang/spirv/validate/validate.cc
+  lang/spirv/validate/validate.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_validate lib
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_external_dependencies(tint_lang_spirv_validate lib
+    "spirv-tools"
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_validate_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_validate_test test
+  lang/spirv/validate/validate_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_validate_test test
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_validate_test test
+  "gtest"
+)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_validate_test test
+    tint_lang_spirv_validate
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/validate/BUILD.gn b/src/tint/lang/spirv/validate/BUILD.gn
new file mode 100644
index 0000000..99890f7
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.gn
@@ -0,0 +1,94 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_spv_reader || tint_build_spv_writer) {
+  libtint_source_set("validate") {
+    sources = [
+      "validate.cc",
+      "validate.h",
+    ]
+    deps = [
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_spv_reader || tint_build_spv_writer) {
+      deps += [
+        "${tint_spirv_tools_dir}:spvtools_headers",
+        "${tint_spirv_tools_dir}:spvtools_val",
+      ]
+    }
+  }
+}
+if (tint_build_unittests) {
+  if (tint_build_spv_reader || tint_build_spv_writer) {
+    tint_unittests_source_set("unittests") {
+      sources = [ "validate_test.cc" ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_spv_reader || tint_build_spv_writer) {
+        deps += [ "${tint_src_dir}/lang/spirv/validate" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/spirv/validate/validate.cc b/src/tint/lang/spirv/validate/validate.cc
new file mode 100644
index 0000000..13344b7
--- /dev/null
+++ b/src/tint/lang/spirv/validate/validate.cc
@@ -0,0 +1,91 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/validate/validate.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "spirv-tools/libspirv.hpp"
+
+namespace tint::spirv::validate {
+
+Result<SuccessType> Validate(Slice<const uint32_t> spirv) {
+    Vector<diag::Diagnostic, 4> diags;
+    diags.Push(diag::Diagnostic{});  // Filled in on error
+
+    spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_3);
+    tools.SetMessageConsumer(
+        [&](spv_message_level_t level, const char*, const spv_position_t& pos, const char* msg) {
+            diag::Diagnostic diag;
+            diag.message = msg;
+            diag.source.range.begin.line = pos.line + 1;
+            diag.source.range.begin.column = pos.column + 1;
+            diag.source.range.end = diag.source.range.begin;
+            switch (level) {
+                case SPV_MSG_FATAL:
+                case SPV_MSG_INTERNAL_ERROR:
+                case SPV_MSG_ERROR:
+                    diag.severity = diag::Severity::Error;
+                    break;
+                case SPV_MSG_WARNING:
+                    diag.severity = diag::Severity::Warning;
+                    break;
+                case SPV_MSG_INFO:
+                case SPV_MSG_DEBUG:
+                    diag.severity = diag::Severity::Note;
+                    break;
+            }
+            diags.Push(std::move(diag));
+        });
+
+    if (tools.Validate(spirv.data, spirv.len)) {
+        return Success;
+    }
+
+    std::string disassembly;
+    if (tools.Disassemble(
+            spirv.data, spirv.len, &disassembly,
+            SPV_BINARY_TO_TEXT_OPTION_INDENT | SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) {
+        diag::Diagnostic& err = diags.Front();
+        err.message = "SPIR-V failed validation.\n\nDisassembly:\n" + std::move(disassembly);
+        err.severity = diag::Severity::Error;
+    } else {
+        diag::Diagnostic& err = diags.Front();
+        err.message = "SPIR-V failed validation and disassembly\n";
+        err.severity = diag::Severity::Error;
+    }
+    auto file = std::make_shared<Source::File>("spirv", disassembly);
+    for (auto& diag : diags) {
+        diag.source.file = file.get();
+        diag.owned_file = file;
+    }
+    return Failure{diag::List{std::move(diags)}};
+}
+
+}  // namespace tint::spirv::validate
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/spirv/validate/validate.h
similarity index 67%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/spirv/validate/validate.h
index 6f0f657..840d90f 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/spirv/validate/validate.h
@@ -25,28 +25,22 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_LANG_SPIRV_VALIDATE_VALIDATE_H_
+#define SRC_TINT_LANG_SPIRV_VALIDATE_VALIDATE_H_
 
-#include <string>
-
-#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
-}
+// Forward declarations
+namespace tint {
+class Program;
+}  // namespace tint
 
-namespace tint::wgsl::writer {
+namespace tint::spirv::validate {
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
+/// Validate checks that the provided SPIR-V passes validation.
+/// @return success or failure(s)
+Result<SuccessType> Validate(Slice<const uint32_t> spirv);
 
-}  // namespace tint::wgsl::writer
+}  // namespace tint::spirv::validate
 
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_LANG_SPIRV_VALIDATE_VALIDATE_H_
diff --git a/src/tint/lang/spirv/validate/validate_test.cc b/src/tint/lang/spirv/validate/validate_test.cc
new file mode 100644
index 0000000..96ba63e
--- /dev/null
+++ b/src/tint/lang/spirv/validate/validate_test.cc
@@ -0,0 +1,113 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/spirv/validate/validate.h"
+
+namespace tint::spirv::validate {
+namespace {
+
+TEST(SpirvValidateTest, Valid) {
+    uint32_t spirv[] = {
+        0x07230203, 0x00010600, 0x00070000, 0x00000011, 0x00000000, 0x00020011, 0x00000001,
+        0x0003000e, 0x00000000, 0x00000001, 0x0005000f, 0x00000005, 0x00000001, 0x6e69616d,
+        0x00000000, 0x00060010, 0x00000001, 0x00000011, 0x00000001, 0x00000001, 0x00000001,
+        0x00040005, 0x00000001, 0x6e69616d, 0x00000000, 0x00030005, 0x00000002, 0x0000006d,
+        0x00020013, 0x00000003, 0x00030021, 0x00000004, 0x00000003, 0x00030016, 0x00000005,
+        0x00000020, 0x00040017, 0x00000006, 0x00000005, 0x00000003, 0x00040018, 0x00000007,
+        0x00000006, 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x0003002e,
+        0x00000007, 0x00000009, 0x00040015, 0x0000000a, 0x00000020, 0x00000001, 0x0004002b,
+        0x0000000a, 0x0000000b, 0x00000001, 0x00040020, 0x0000000c, 0x00000007, 0x00000006,
+        0x00050036, 0x00000003, 0x00000001, 0x00000000, 0x00000004, 0x000200f8, 0x0000000d,
+        0x0005003b, 0x00000008, 0x00000002, 0x00000007, 0x00000009, 0x00050041, 0x0000000c,
+        0x0000000e, 0x00000002, 0x0000000b, 0x0004003d, 0x00000006, 0x0000000f, 0x0000000e,
+        0x00050051, 0x00000005, 0x00000010, 0x0000000f, 0x00000001, 0x000100fd, 0x00010038,
+    };
+    auto res = Validate(spirv);
+    EXPECT_TRUE(res) << res;
+}
+
+TEST(SpirvValidateTest, Invalid) {
+    uint32_t spirv[] = {
+        0x07230203, 0x00010600, 0x00070000, 0x00000011, 0x00000000, 0x00020011, 0x00000001,
+        0x0003000e, 0x00000000, 0x00000001, 0x0005000f, 0x00000005, 0x00000001, 0x6e69616d,
+        0x00000000, 0x00060010, 0x00000001, 0x00000011, 0x00000001, 0x00000001, 0x00000001,
+        0x00040005, 0x00000001, 0x6e69616d, 0x00000000, 0x00030005, 0x00000002, 0x0000006d,
+        0x00020013, 0x00000003, 0x00030021, 0x00000004, 0x00000003, 0x00030016, 0x00000005,
+        0x00000020, 0x00040017, 0x00000006, 0x00000005, 0x00000003, 0x00040018, 0x00000007,
+        0x00000006, 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x0003002e,
+        0x00000006, 0x00000009, 0x00040015, 0x0000000a, 0x00000020, 0x00000001, 0x0004002b,
+        0x0000000a, 0x0000000b, 0x00000001, 0x00040020, 0x0000000c, 0x00000007, 0x00000006,
+        0x00050036, 0x00000003, 0x00000001, 0x00000000, 0x00000004, 0x000200f8, 0x0000000d,
+        0x0005003b, 0x00000008, 0x00000002, 0x00000007, 0x00000009, 0x00050041, 0x0000000c,
+        0x0000000e, 0x00000002, 0x0000000b, 0x0004003d, 0x00000006, 0x0000000f, 0x0000000e,
+        0x00050051, 0x00000005, 0x00000010, 0x0000000f, 0x00000001, 0x000100fd, 0x00010038,
+    };
+    auto res = Validate(spirv);
+    ASSERT_FALSE(res);
+    EXPECT_EQ(res.Failure().reason.str(), R"(spirv error: SPIR-V failed validation.
+
+Disassembly:
+; SPIR-V
+; Version: 1.6
+; Generator: Khronos SPIR-V Tools Assembler; 0
+; Bound: 17
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %main "main"
+               OpName %m "m"
+       %void = OpTypeVoid
+          %4 = OpTypeFunction %void
+      %float = OpTypeFloat 32
+    %v3float = OpTypeVector %float 3
+%mat3v3float = OpTypeMatrix %v3float 3
+%_ptr_Function_mat3v3float = OpTypePointer Function %mat3v3float
+          %9 = OpConstantNull %v3float
+        %int = OpTypeInt 32 1
+      %int_1 = OpConstant %int 1
+%_ptr_Function_v3float = OpTypePointer Function %v3float
+       %main = OpFunction %void None %4
+         %13 = OpLabel
+          %m = OpVariable %_ptr_Function_mat3v3float Function %9
+         %14 = OpAccessChain %_ptr_Function_v3float %m %int_1
+         %15 = OpLoad %v3float %14
+         %16 = OpCompositeExtract %float %15 1
+               OpReturn
+               OpFunctionEnd
+
+spirv:1:1 error: Initializer type must match the type pointed to by the Result Type
+  %m = OpVariable %_ptr_Function_mat3v3float Function %9
+
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::validate
diff --git a/src/tint/lang/spirv/writer/BUILD.bazel b/src/tint/lang/spirv/writer/BUILD.bazel
index 1525211..9cfb1ed 100644
--- a/src/tint/lang/spirv/writer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/BUILD.bazel
@@ -39,6 +39,7 @@
 cc_library(
   name = "writer",
   srcs = [
+    "output.cc",
     "writer.cc",
   ],
   hdrs = [
@@ -54,8 +55,8 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
-    "//src/tint/lang/wgsl/reader/lower",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -83,11 +84,6 @@
       "//src/tint/lang/spirv/writer/raise",
     ],
     "//conditions:default": [],
-  }) + select({
-    ":tint_build_wgsl_reader": [
-      "//src/tint/lang/wgsl/reader/program_to_ir",
-    ],
-    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
@@ -167,9 +163,12 @@
     "//src/tint/cmd/bench:bench",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -192,6 +191,11 @@
       "//src/tint/lang/spirv/writer/common",
     ],
     "//conditions:default": [],
+  }) + select({
+    ":tint_build_wgsl_reader": [
+      "//src/tint/lang/wgsl/reader",
+    ],
+    "//conditions:default": [],
   }),
   copts = COPTS,
   visibility = ["//visibility:public"],
diff --git a/src/tint/lang/spirv/writer/BUILD.cmake b/src/tint/lang/spirv/writer/BUILD.cmake
index 767a497..0720114 100644
--- a/src/tint/lang/spirv/writer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/BUILD.cmake
@@ -48,6 +48,7 @@
 # Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer lib
+  lang/spirv/writer/output.cc
   lang/spirv/writer/output.h
   lang/spirv/writer/writer.cc
   lang/spirv/writer/writer.h
@@ -62,8 +63,8 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
-  tint_lang_wgsl_reader_lower
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -95,12 +96,6 @@
   )
 endif(TINT_BUILD_SPV_WRITER)
 
-if(TINT_BUILD_WGSL_READER)
-  tint_target_add_dependencies(tint_lang_spirv_writer lib
-    tint_lang_wgsl_reader_program_to_ir
-  )
-endif(TINT_BUILD_WGSL_READER)
-
 endif(TINT_BUILD_SPV_WRITER)
 if(TINT_BUILD_SPV_WRITER)
 ################################################################################
@@ -189,9 +184,12 @@
   tint_cmd_bench_bench
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -220,4 +218,72 @@
   )
 endif(TINT_BUILD_SPV_WRITER)
 
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_bench bench
+    tint_lang_wgsl_reader
+  )
+endif(TINT_BUILD_WGSL_READER)
+
+endif(TINT_BUILD_SPV_WRITER)
+if(TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_writer_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_writer_fuzz fuzz
+  lang/spirv/writer/writer_fuzz.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_writer_fuzz fuzz
+  tint_api_common
+  tint_cmd_fuzz_ir_fuzz
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_helpers
+  tint_lang_wgsl_program
+  tint_lang_wgsl_sem
+  tint_utils_bytes
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_fuzz fuzz
+    tint_lang_spirv_validate
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+if(TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_writer_fuzz fuzz
+    tint_lang_spirv_writer
+    tint_lang_spirv_writer_common
+    tint_lang_spirv_writer_helpers
+  )
+endif(TINT_BUILD_SPV_WRITER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_spirv_writer_fuzz fuzz
+    "lang/spirv/writer/ast_writer_fuzz.cc"
+  )
+  tint_target_add_dependencies(tint_lang_spirv_writer_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 endif(TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/writer/BUILD.gn b/src/tint/lang/spirv/writer/BUILD.gn
index d9cefee..8045c04 100644
--- a/src/tint/lang/spirv/writer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/BUILD.gn
@@ -44,6 +44,7 @@
 if (tint_build_spv_writer) {
   libtint_source_set("writer") {
     sources = [
+      "output.cc",
       "output.h",
       "writer.cc",
       "writer.h",
@@ -57,8 +58,8 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
-      "${tint_src_dir}/lang/wgsl/reader/lower",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
@@ -87,10 +88,6 @@
         "${tint_src_dir}/lang/spirv/writer/raise",
       ]
     }
-
-    if (tint_build_wgsl_reader) {
-      deps += [ "${tint_src_dir}/lang/wgsl/reader/program_to_ir" ]
-    }
   }
 }
 if (tint_build_unittests) {
@@ -170,9 +167,12 @@
         "${tint_src_dir}/cmd/bench:bench",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/ir",
         "${tint_src_dir}/lang/core/type",
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
+        "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
@@ -196,6 +196,59 @@
           "${tint_src_dir}/lang/spirv/writer/common",
         ]
       }
+
+      if (tint_build_wgsl_reader) {
+        deps += [ "${tint_src_dir}/lang/wgsl/reader" ]
+      }
+    }
+  }
+}
+if (tint_build_spv_writer) {
+  tint_fuzz_source_set("fuzz") {
+    sources = [ "writer_fuzz.cc" ]
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/cmd/fuzz/ir:fuzz",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/helpers",
+      "${tint_src_dir}/lang/wgsl/program",
+      "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/bytes",
+      "${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 || tint_build_spv_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/validate" ]
+    }
+
+    if (tint_build_spv_writer) {
+      deps += [
+        "${tint_src_dir}/lang/spirv/writer",
+        "${tint_src_dir}/lang/spirv/writer/common",
+        "${tint_src_dir}/lang/spirv/writer/helpers",
+      ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "ast_writer_fuzz.cc" ]
+      deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
     }
   }
 }
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
index 40919cf..1b8df51 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.bazel
@@ -56,6 +56,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -124,11 +125,13 @@
     "//src/tint/api/common",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
index e1f6e05..1acdba0 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.cmake
@@ -57,6 +57,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -130,11 +131,13 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
index 12b2c96..1ee1bb6 100644
--- a/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_printer/BUILD.gn
@@ -59,6 +59,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/helpers",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -127,11 +128,13 @@
         "${tint_src_dir}/api/common",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/ir",
         "${tint_src_dir}/lang/core/type",
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast:unittests",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index a75d703..2e1271e 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -280,7 +280,6 @@
                 wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumExperimentalPushConstant,
-                wgsl::Extension::kChromiumExperimentalReadWriteStorageTexture,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
                 wgsl::Extension::kF16,
                 wgsl::Extension::kChromiumInternalDualSourceBlending,
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
index 4dd9ee8..ac25dcc 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
@@ -63,6 +63,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -104,6 +105,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
index a3ac9dd..b79c8bb 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
@@ -64,6 +64,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -108,6 +109,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.gn b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
index b54c6fc..ba17c58 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
@@ -66,6 +66,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -108,6 +109,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/spirv/writer/ast_writer_fuzz.cc b/src/tint/lang/spirv/writer/ast_writer_fuzz.cc
new file mode 100644
index 0000000..7ce83db
--- /dev/null
+++ b/src/tint/lang/spirv/writer/ast_writer_fuzz.cc
@@ -0,0 +1,59 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
+
+#include "src/tint/lang/spirv/writer/writer.h"
+
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
+#include "src/tint/lang/spirv/validate/validate.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
+
+namespace tint::spirv::writer {
+namespace {
+
+void ASTPrinterFuzzer(const tint::Program& program, const Options& options) {
+    auto transformed = tint::wgsl::ApplySubstituteOverrides(program);
+    auto& no_overrides = transformed ? transformed.value() : program;
+    if (!no_overrides.IsValid()) {
+        return;
+    }
+    auto output = Generate(no_overrides, options);
+    if (!output) {
+        return;
+    }
+    auto& spirv = output->spirv;
+    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size())); !res) {
+        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
+                   << res.Failure();
+    }
+}
+
+}  // namespace
+}  // namespace tint::spirv::writer
+
+TINT_WGSL_PROGRAM_FUZZER(tint::spirv::writer::ASTPrinterFuzzer);
diff --git a/src/tint/lang/spirv/writer/common/BUILD.bazel b/src/tint/lang/spirv/writer/common/BUILD.bazel
index 7c07fad..87e23c6 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/common/BUILD.bazel
@@ -65,6 +65,7 @@
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
     "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
diff --git a/src/tint/lang/spirv/writer/common/BUILD.cmake b/src/tint/lang/spirv/writer/common/BUILD.cmake
index 969c5e7..4d4db56 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/common/BUILD.cmake
@@ -66,6 +66,7 @@
   tint_utils_math
   tint_utils_memory
   tint_utils_reflection
+  tint_utils_result
   tint_utils_rtti
   tint_utils_text
   tint_utils_traits
diff --git a/src/tint/lang/spirv/writer/common/BUILD.gn b/src/tint/lang/spirv/writer/common/BUILD.gn
index 0c9830d..c0aa3c5 100644
--- a/src/tint/lang/spirv/writer/common/BUILD.gn
+++ b/src/tint/lang/spirv/writer/common/BUILD.gn
@@ -68,6 +68,7 @@
       "${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/text",
       "${tint_src_dir}/utils/traits",
diff --git a/src/tint/lang/spirv/writer/common/option_helper.cc b/src/tint/lang/spirv/writer/common/option_helper.cc
index dc3670b..aec904a 100644
--- a/src/tint/lang/spirv/writer/common/option_helper.cc
+++ b/src/tint/lang/spirv/writer/common/option_helper.cc
@@ -27,11 +27,15 @@
 
 #include "src/tint/lang/spirv/writer/common/option_helpers.h"
 
+#include <utility>
+
 #include "src/tint/utils/containers/hashset.h"
 
 namespace tint::spirv::writer {
 
-bool ValidateBindingOptions(const Options& options, diag::List& diagnostics) {
+Result<SuccessType> ValidateBindingOptions(const Options& options) {
+    diag::List diagnostics;
+
     tint::Hashmap<tint::BindingPoint, binding::BindingInfo, 8> seen_wgsl_bindings{};
     tint::Hashmap<binding::BindingInfo, tint::BindingPoint, 8> seen_spirv_bindings{};
 
@@ -88,23 +92,23 @@
 
     if (!valid(options.bindings.uniform)) {
         diagnostics.add_note(diag::System::Writer, "when processing uniform", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.storage)) {
         diagnostics.add_note(diag::System::Writer, "when processing storage", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.texture)) {
         diagnostics.add_note(diag::System::Writer, "when processing texture", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.storage_texture)) {
         diagnostics.add_note(diag::System::Writer, "when processing storage_texture", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.sampler)) {
         diagnostics.add_note(diag::System::Writer, "when processing sampler", {});
-        return false;
+        return Failure{std::move(diagnostics)};
     }
 
     for (const auto& it : options.bindings.external_texture) {
@@ -116,24 +120,24 @@
         // Validate with the actual source regardless of what the remapper will do
         if (wgsl_seen(src_binding, plane0)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
 
         if (spirv_seen(plane0, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
         if (spirv_seen(plane1, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
         if (spirv_seen(metadata, src_binding)) {
             diagnostics.add_note(diag::System::Writer, "when processing external_texture", {});
-            return false;
+            return Failure{std::move(diagnostics)};
         }
     }
 
-    return true;
+    return Success;
 }
 
 // The remapped binding data and external texture data need to coordinate in order to put things in
diff --git a/src/tint/lang/spirv/writer/common/option_helpers.h b/src/tint/lang/spirv/writer/common/option_helpers.h
index 0be6d35..3f51355 100644
--- a/src/tint/lang/spirv/writer/common/option_helpers.h
+++ b/src/tint/lang/spirv/writer/common/option_helpers.h
@@ -34,14 +34,15 @@
 #include "src/tint/api/options/external_texture.h"
 #include "src/tint/lang/spirv/writer/common/options.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/result/result.h"
 
 namespace tint::spirv::writer {
 
 using RemapperData = std::unordered_map<BindingPoint, BindingPoint>;
 
 /// @param options the options
-/// @returns true if the binding points are valid
-bool ValidateBindingOptions(const Options& options, diag::List& diagnostics);
+/// @return success or failure
+Result<SuccessType> ValidateBindingOptions(const Options& options);
 
 /// Populates data from the writer options for the remapper and external texture.
 /// @param options the writer options
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index 46718d2..f5c7897 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -138,9 +138,6 @@
     /// Set to `true` to always pass matrices to user functions by pointer instead of by value.
     bool pass_matrix_by_pointer = false;
 
-    /// Set to `true` to generate SPIR-V via the Tint IR instead of from the AST.
-    bool use_tint_ir = false;
-
     /// 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
@@ -157,7 +154,6 @@
                  use_zero_initialize_workgroup_memory_extension,
                  emit_vertex_point_size,
                  clamp_frag_depth,
-                 use_tint_ir,
                  experimental_require_subgroup_uniform_control_flow,
                  bindings);
 };
diff --git a/src/tint/lang/spirv/writer/discard_test.cc b/src/tint/lang/spirv/writer/discard_test.cc
index 5398726..5e9ccf9 100644
--- a/src/tint/lang/spirv/writer/discard_test.cc
+++ b/src/tint/lang/spirv/writer/discard_test.cc
@@ -85,5 +85,61 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, DiscardBeforeAtomic) {
+    auto* buffer = b.Var("buffer", ty.ptr(storage, ty.atomic<i32>()));
+    buffer->SetBindingPoint(0, 0);
+    mod.root_block->Append(buffer);
+
+    auto* front_facing = b.FunctionParam("front_facing", ty.bool_());
+    front_facing->SetBuiltin(core::ir::FunctionParam::Builtin::kFrontFacing);
+    auto* ep = b.Function("ep", ty.f32(), core::ir::Function::PipelineStage::kFragment);
+    ep->SetParams({front_facing});
+    ep->SetReturnLocation(0_u, {});
+
+    b.Append(ep->Block(), [&] {
+        auto* ifelse = b.If(front_facing);
+        b.Append(ifelse->True(), [&] {  //
+            b.Discard();
+            b.ExitIf(ifelse);
+        });
+        b.Call(ty.i32(), core::BuiltinFn::kAtomicAdd, buffer, 1_i);
+        b.Return(ep, 0.5_f);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+               ; Function ep_inner
+   %ep_inner = OpFunction %float None %16
+%front_facing = OpFunctionParameter %bool
+         %17 = OpLabel
+               OpSelectionMerge %18 None
+               OpBranchConditional %front_facing %19 %18
+         %19 = OpLabel
+               OpStore %continue_execution %false
+               OpBranch %18
+         %18 = OpLabel
+         %21 = OpAccessChain %_ptr_StorageBuffer_int %1 %uint_0
+         %25 = OpLoad %bool %continue_execution
+               OpSelectionMerge %26 None
+               OpBranchConditional %25 %27 %28
+         %27 = OpLabel
+         %29 = OpAtomicIAdd %int %21 %uint_1 %uint_0 %int_1
+               OpBranch %26
+         %28 = OpLabel
+               OpBranch %26
+         %26 = OpLabel
+         %32 = OpPhi %int %29 %27 %33 %28
+         %34 = OpLoad %bool %continue_execution
+         %35 = OpLogicalEqual %bool %34 %false
+               OpSelectionMerge %36 None
+               OpBranchConditional %35 %37 %36
+         %37 = OpLabel
+               OpKill
+         %36 = OpLabel
+               OpReturnValue %float_0_5
+               OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.bazel b/src/tint/lang/spirv/writer/helpers/BUILD.bazel
index 50b7a44..e8b7913 100644
--- a/src/tint/lang/spirv/writer/helpers/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.bazel
@@ -39,18 +39,22 @@
 cc_library(
   name = "helpers",
   srcs = [
+    "ast_generate_bindings.cc",
     "generate_bindings.cc",
   ],
   hdrs = [
+    "ast_generate_bindings.h",
     "generate_bindings.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/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.cmake b/src/tint/lang/spirv/writer/helpers/BUILD.cmake
index d6b94dd..08da907 100644
--- a/src/tint/lang/spirv/writer/helpers/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.cmake
@@ -41,6 +41,8 @@
 # Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_helpers lib
+  lang/spirv/writer/helpers/ast_generate_bindings.cc
+  lang/spirv/writer/helpers/ast_generate_bindings.h
   lang/spirv/writer/helpers/generate_bindings.cc
   lang/spirv/writer/helpers/generate_bindings.h
 )
@@ -49,9 +51,11 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/spirv/writer/helpers/BUILD.gn b/src/tint/lang/spirv/writer/helpers/BUILD.gn
index a1f5bb3..d0fc799 100644
--- a/src/tint/lang/spirv/writer/helpers/BUILD.gn
+++ b/src/tint/lang/spirv/writer/helpers/BUILD.gn
@@ -40,6 +40,8 @@
 if (tint_build_spv_writer) {
   libtint_source_set("helpers") {
     sources = [
+      "ast_generate_bindings.cc",
+      "ast_generate_bindings.h",
       "generate_bindings.cc",
       "generate_bindings.h",
     ]
@@ -47,9 +49,11 @@
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/spirv/writer/helpers/ast_generate_bindings.cc b/src/tint/lang/spirv/writer/helpers/ast_generate_bindings.cc
new file mode 100644
index 0000000..cd40f27
--- /dev/null
+++ b/src/tint/lang/spirv/writer/helpers/ast_generate_bindings.cc
@@ -0,0 +1,123 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h"
+
+#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/lang/core/type/external_texture.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+#include "src/tint/utils/containers/hashmap.h"
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::spirv::writer {
+
+Bindings GenerateBindings(const Program& program) {
+    // TODO(tint:1491): Use Inspector once we can get binding info for all
+    // variables, not just those referenced by entry points.
+
+    Bindings bindings{};
+
+    // Collect next valid binding number per group
+    Hashmap<uint32_t, uint32_t, 4> group_to_next_binding_number;
+    Vector<tint::BindingPoint, 4> ext_tex_bps;
+    for (auto* var : program.AST().GlobalVariables()) {
+        if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
+            if (auto bp = sem_var->Attributes().binding_point) {
+                if (auto val = group_to_next_binding_number.Find(bp->group)) {
+                    *val = std::max(*val, bp->binding + 1);
+                } else {
+                    group_to_next_binding_number.Add(bp->group, bp->binding + 1);
+                }
+
+                // Store up the external textures, we'll add them in the next step
+                if (sem_var->Type()->UnwrapRef()->Is<core::type::ExternalTexture>()) {
+                    ext_tex_bps.Push(*bp);
+                    continue;
+                }
+
+                binding::BindingInfo info{bp->group, bp->binding};
+                switch (sem_var->AddressSpace()) {
+                    case core::AddressSpace::kHandle:
+                        Switch(
+                            sem_var->Type()->UnwrapRef(),  //
+                            [&](const core::type::Sampler*) {
+                                bindings.sampler.emplace(*bp, info);
+                            },
+                            [&](const core::type::StorageTexture*) {
+                                bindings.storage_texture.emplace(*bp, info);
+                            },
+                            [&](const core::type::Texture*) {
+                                bindings.texture.emplace(*bp, info);
+                            });
+                        break;
+                    case core::AddressSpace::kStorage:
+                        bindings.storage.emplace(*bp, info);
+                        break;
+                    case core::AddressSpace::kUniform:
+                        bindings.uniform.emplace(*bp, info);
+                        break;
+
+                    case core::AddressSpace::kUndefined:
+                    case core::AddressSpace::kPixelLocal:
+                    case core::AddressSpace::kPrivate:
+                    case core::AddressSpace::kPushConstant:
+                    case core::AddressSpace::kIn:
+                    case core::AddressSpace::kOut:
+                    case core::AddressSpace::kFunction:
+                    case core::AddressSpace::kWorkgroup:
+                        break;
+                }
+            }
+        }
+    }
+
+    for (auto bp : ext_tex_bps) {
+        uint32_t g = bp.group;
+        uint32_t next_num = *(group_to_next_binding_number.GetOrZero(g));
+
+        binding::BindingInfo plane0{bp.group, bp.binding};
+        binding::BindingInfo plane1{g, next_num++};
+        binding::BindingInfo metadata{g, next_num++};
+
+        group_to_next_binding_number.Replace(g, next_num);
+
+        bindings.external_texture.emplace(bp, binding::ExternalTexture{metadata, plane0, plane1});
+    }
+
+    return bindings;
+}
+
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h
similarity index 66%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h
index 6f0f657..ae4c1b6 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h
@@ -25,28 +25,20 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_HELPERS_AST_GENERATE_BINDINGS_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_HELPERS_AST_GENERATE_BINDINGS_H_
 
-#include <string>
+#include "src/tint/lang/spirv/writer/common/options.h"
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
-
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
+// Forward declarations
+namespace tint {
+class Program;
 }
 
-namespace tint::wgsl::writer {
+namespace tint::spirv::writer {
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
+Bindings GenerateBindings(const Program& program);
 
-}  // namespace tint::wgsl::writer
+}  // namespace tint::spirv::writer
 
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_HELPERS_AST_GENERATE_BINDINGS_H_
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
index 076a594..1cf3b6a 100644
--- a/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.cc
@@ -28,78 +28,76 @@
 #include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
 
 #include <algorithm>
-#include <unordered_map>
-#include <unordered_set>
-#include <vector>
 
 #include "src/tint/api/common/binding_point.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/var.h"
 #include "src/tint/lang/core/type/external_texture.h"
+#include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/core/type/storage_texture.h"
-#include "src/tint/lang/wgsl/ast/module.h"
-#include "src/tint/lang/wgsl/program/program.h"
-#include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/rtti/switch.h"
 
 namespace tint::spirv::writer {
 
-Bindings GenerateBindings(const Program& program) {
-    // TODO(tint:1491): Use Inspector once we can get binding info for all
-    // variables, not just those referenced by entry points.
-
+Bindings GenerateBindings(const core::ir::Module& module) {
     Bindings bindings{};
+    if (!module.root_block) {
+        return bindings;
+    }
 
     // Collect next valid binding number per group
     Hashmap<uint32_t, uint32_t, 4> group_to_next_binding_number;
     Vector<tint::BindingPoint, 4> ext_tex_bps;
-    for (auto* var : program.AST().GlobalVariables()) {
-        if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
-            if (auto bp = sem_var->Attributes().binding_point) {
-                if (auto val = group_to_next_binding_number.Find(bp->group)) {
-                    *val = std::max(*val, bp->binding + 1);
-                } else {
-                    group_to_next_binding_number.Add(bp->group, bp->binding + 1);
-                }
+    for (auto* inst : *module.root_block) {
+        if (!inst->Alive()) {
+            continue;
+        }
+        auto* var = inst->As<core::ir::Var>();
+        if (auto bp = var->BindingPoint()) {
+            if (auto val = group_to_next_binding_number.Find(bp->group)) {
+                *val = std::max(*val, bp->binding + 1);
+            } else {
+                group_to_next_binding_number.Add(bp->group, bp->binding + 1);
+            }
 
-                // Store up the external textures, we'll add them in the next step
-                if (sem_var->Type()->UnwrapRef()->Is<core::type::ExternalTexture>()) {
-                    ext_tex_bps.Push(*bp);
-                    continue;
-                }
+            auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
+            auto* ty = ptr->UnwrapPtr();
 
-                binding::BindingInfo info{bp->group, bp->binding};
-                switch (sem_var->AddressSpace()) {
-                    case core::AddressSpace::kHandle:
-                        Switch(
-                            sem_var->Type()->UnwrapRef(),  //
-                            [&](const core::type::Sampler*) {
-                                bindings.sampler.emplace(*bp, info);
-                            },
-                            [&](const core::type::StorageTexture*) {
-                                bindings.storage_texture.emplace(*bp, info);
-                            },
-                            [&](const core::type::Texture*) {
-                                bindings.texture.emplace(*bp, info);
-                            });
-                        break;
-                    case core::AddressSpace::kStorage:
-                        bindings.storage.emplace(*bp, info);
-                        break;
-                    case core::AddressSpace::kUniform:
-                        bindings.uniform.emplace(*bp, info);
-                        break;
+            // Store up the external textures, we'll add them in the next step
+            if (ty->Is<core::type::ExternalTexture>()) {
+                ext_tex_bps.Push(*bp);
+                continue;
+            }
 
-                    case core::AddressSpace::kUndefined:
-                    case core::AddressSpace::kPixelLocal:
-                    case core::AddressSpace::kPrivate:
-                    case core::AddressSpace::kPushConstant:
-                    case core::AddressSpace::kIn:
-                    case core::AddressSpace::kOut:
-                    case core::AddressSpace::kFunction:
-                    case core::AddressSpace::kWorkgroup:
-                        break;
-                }
+            binding::BindingInfo info{bp->group, bp->binding};
+            switch (ptr->AddressSpace()) {
+                case core::AddressSpace::kHandle:
+                    Switch(
+                        ptr->UnwrapPtr(),  //
+                        [&](const core::type::Sampler*) { bindings.sampler.emplace(*bp, info); },
+                        [&](const core::type::StorageTexture*) {
+                            bindings.storage_texture.emplace(*bp, info);
+                        },
+                        [&](const core::type::Texture*) { bindings.texture.emplace(*bp, info); });
+                    break;
+                case core::AddressSpace::kStorage:
+                    bindings.storage.emplace(*bp, info);
+                    break;
+                case core::AddressSpace::kUniform:
+                    bindings.uniform.emplace(*bp, info);
+                    break;
+
+                case core::AddressSpace::kUndefined:
+                case core::AddressSpace::kPixelLocal:
+                case core::AddressSpace::kPrivate:
+                case core::AddressSpace::kPushConstant:
+                case core::AddressSpace::kIn:
+                case core::AddressSpace::kOut:
+                case core::AddressSpace::kFunction:
+                case core::AddressSpace::kWorkgroup:
+                    break;
             }
         }
     }
diff --git a/src/tint/lang/spirv/writer/helpers/generate_bindings.h b/src/tint/lang/spirv/writer/helpers/generate_bindings.h
index 8b3c3c7..b79d3af 100644
--- a/src/tint/lang/spirv/writer/helpers/generate_bindings.h
+++ b/src/tint/lang/spirv/writer/helpers/generate_bindings.h
@@ -31,13 +31,13 @@
 #include "src/tint/lang/spirv/writer/common/options.h"
 
 // Forward declarations
-namespace tint {
-class Program;
+namespace tint::core::ir {
+class Module;
 }
 
 namespace tint::spirv::writer {
 
-Bindings GenerateBindings(const Program& program);
+Bindings GenerateBindings(const core::ir::Module& module);
 
 }  // 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 a91ab22..321ed89 100644
--- a/src/tint/lang/spirv/writer/if_test.cc
+++ b/src/tint/lang/spirv/writer/if_test.cc
@@ -241,6 +241,34 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, If_Phi_SingleValue_ImplicitFalse) {
+    auto* func = b.Function("foo", ty.i32());
+    b.Append(func->Block(), [&] {
+        auto* i = b.If(true);
+        i->SetResults(b.InstructionResult(ty.i32()));
+        b.Append(i->True(), [&] {  //
+            b.ExitIf(i, 10_i);
+        });
+        b.Return(func, i);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%12 = OpUndef %int");
+    EXPECT_INST(R"(
+          %4 = OpLabel
+               OpSelectionMerge %5 None
+               OpBranchConditional %true %6 %7
+          %6 = OpLabel
+               OpBranch %5
+          %7 = OpLabel
+               OpBranch %5
+          %5 = OpLabel
+         %10 = OpPhi %int %int_10 %6 %12 %7
+               OpReturnValue %10
+               OpFunctionEnd
+)");
+}
+
 TEST_F(SpirvWriterTest, If_Phi_MultipleValue_0) {
     auto* func = b.Function("foo", ty.i32());
     b.Append(func->Block(), [&] {
@@ -315,7 +343,7 @@
             b.Append(inner->False(), [&] {  //
                 b.ExitIf(inner, 20_i);
             });
-            b.ExitIf(outer, inner->Result());
+            b.ExitIf(outer, inner->Result(0));
         });
         b.Append(outer->False(), [&] {  //
             b.ExitIf(outer, 30_i);
diff --git a/src/tint/lang/spirv/writer/loop_test.cc b/src/tint/lang/spirv/writer/loop_test.cc
index 332c52a..633eb9b 100644
--- a/src/tint/lang/spirv/writer/loop_test.cc
+++ b/src/tint/lang/spirv/writer/loop_test.cc
@@ -460,7 +460,7 @@
             b.Append(inner->False(), [&] {  //
                 b.ExitIf(inner, 20_i);
             });
-            b.Continue(loop, inner->Result());
+            b.Continue(loop, inner->Result(0));
         });
 
         auto* cont_param = b.BlockParam(ty.i32());
@@ -572,5 +572,54 @@
 )");
 }
 
+TEST_F(SpirvWriterTest, Loop_Phi_NestedIfWithResultAndImplicitFalse_InContinuing) {
+    auto* func = b.Function("foo", ty.void_());
+
+    b.Append(func->Block(), [&] {
+        auto* loop = b.Loop();
+
+        b.Append(loop->Body(), [&] {  //
+            b.Continue(loop);
+        });
+
+        b.Append(loop->Continuing(), [&] {
+            auto* if_ = b.If(true);
+            auto* cond = b.InstructionResult(ty.bool_());
+            if_->SetResults(Vector{cond});
+            b.Append(if_->True(), [&] {  //
+                b.ExitIf(if_, true);
+            });
+            b.BreakIf(loop, cond);
+        });
+
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST("%15 = OpUndef %bool");
+    EXPECT_INST(R"(
+          %4 = OpLabel
+               OpBranch %7
+          %7 = OpLabel
+               OpLoopMerge %8 %6 None
+               OpBranch %5
+          %5 = OpLabel
+               OpBranch %6
+          %6 = OpLabel
+               OpSelectionMerge %9 None
+               OpBranchConditional %true %10 %11
+         %10 = OpLabel
+               OpBranch %9
+         %11 = OpLabel
+               OpBranch %9
+          %9 = OpLabel
+         %14 = OpPhi %bool %true %10 %15 %11
+               OpBranchConditional %14 %8 %7
+          %8 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/spirv/writer/output.cc
similarity index 65%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/spirv/writer/output.cc
index 6f0f657..ec197e5 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/spirv/writer/output.cc
@@ -25,28 +25,16 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#include "src/tint/lang/spirv/writer/output.h"
 
-#include <string>
+namespace tint::spirv::writer {
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
+Output::Output() = default;
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
-}
+Output::~Output() = default;
 
-namespace tint::wgsl::writer {
+Output::Output(const Output&) = default;
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
+Output& Output::operator=(const Output&) = default;
 
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/output.h b/src/tint/lang/spirv/writer/output.h
index e14f7b1..c6b6e3c 100644
--- a/src/tint/lang/spirv/writer/output.h
+++ b/src/tint/lang/spirv/writer/output.h
@@ -28,6 +28,7 @@
 #ifndef SRC_TINT_LANG_SPIRV_WRITER_OUTPUT_H_
 #define SRC_TINT_LANG_SPIRV_WRITER_OUTPUT_H_
 
+#include <cstdint>
 #include <string>
 #include <vector>
 
@@ -44,6 +45,10 @@
     /// Copy constructor
     Output(const Output&);
 
+    /// Copy assignment
+    /// @returns this
+    Output& operator=(const Output&);
+
     /// The generated SPIR-V.
     std::vector<uint32_t> spirv;
 };
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.bazel b/src/tint/lang/spirv/writer/printer/BUILD.bazel
index 1488b1e..1c64224 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/printer/BUILD.bazel
@@ -57,6 +57,7 @@
     "//src/tint/lang/spirv/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.cmake b/src/tint/lang/spirv/writer/printer/BUILD.cmake
index 1118e66..6002e0c 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/printer/BUILD.cmake
@@ -58,6 +58,7 @@
   tint_lang_spirv_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/spirv/writer/printer/BUILD.gn b/src/tint/lang/spirv/writer/printer/BUILD.gn
index ab8d4f1..58b49ef 100644
--- a/src/tint/lang/spirv/writer/printer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/printer/BUILD.gn
@@ -56,6 +56,7 @@
       "${tint_src_dir}/lang/spirv/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index a7c86b8..add607e 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -261,13 +261,13 @@
     Hashmap<const core::type::Type*, uint32_t, 4> undef_values_;
 
     /// The map of non-constant values to their result IDs.
-    Hashmap<core::ir::Value*, uint32_t, 8> values_;
+    Hashmap<const core::ir::Value*, uint32_t, 8> values_;
 
     /// The map of blocks to the IDs of their label instructions.
-    Hashmap<core::ir::Block*, uint32_t, 8> block_labels_;
+    Hashmap<const core::ir::Block*, uint32_t, 8> block_labels_;
 
     /// The map of control instructions to the IDs of the label of their SPIR-V merge blocks.
-    Hashmap<core::ir::ControlInstruction*, uint32_t, 8> merge_block_labels_;
+    Hashmap<const core::ir::ControlInstruction*, uint32_t, 8> merge_block_labels_;
 
     /// The map of extended instruction set names to their result IDs.
     Hashmap<std::string_view, uint32_t, 2> imports_;
@@ -304,7 +304,7 @@
         EmitRootBlock(ir_.root_block);
 
         // Emit functions.
-        for (auto* func : ir_.functions) {
+        for (core::ir::Function* func : ir_.functions) {
             EmitFunction(func);
         }
 
@@ -538,7 +538,7 @@
     /// Get the result ID of the instruction result `value`, emitting its instruction if necessary.
     /// @param inst the instruction to get the ID for
     /// @returns the result ID of the instruction
-    uint32_t Value(core::ir::Instruction* inst) { return Value(inst->Result()); }
+    uint32_t Value(core::ir::Instruction* inst) { return Value(inst->Result(0)); }
 
     /// Get the result ID of the value `value`, emitting its instruction if necessary.
     /// @param value the value to get the ID for
@@ -555,7 +555,7 @@
     /// Get the ID of the label for `block`.
     /// @param block the block to get the label ID for
     /// @returns the ID of the block's label
-    uint32_t Label(core::ir::Block* block) {
+    uint32_t Label(const core::ir::Block* block) {
         return block_labels_.GetOrCreate(block, [&] { return module_.NextId(); });
     }
 
@@ -781,7 +781,7 @@
                 continue;
             }
 
-            auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+            auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
             if (!(ptr->AddressSpace() == core::AddressSpace::kIn ||
                   ptr->AddressSpace() == core::AddressSpace::kOut)) {
                 continue;
@@ -789,7 +789,7 @@
 
             // Determine if this IO variable is used by the entry point.
             bool used = false;
-            for (const auto& use : var->Result()->Usages()) {
+            for (const auto& use : var->Result(0)->Usages()) {
                 auto* block = use.instruction->Block();
                 while (block->Parent()) {
                     block = block->Parent()->Block();
@@ -837,7 +837,11 @@
         // If there are no instructions in the block, it's a dead end, so we shouldn't be able to
         // get here to begin with.
         if (block->IsEmpty()) {
-            current_function_.push_inst(spv::Op::OpUnreachable, {});
+            if (!block->Parent()->Results().IsEmpty()) {
+                current_function_.push_inst(spv::Op::OpBranch, {GetMergeLabel(block->Parent())});
+            } else {
+                current_function_.push_inst(spv::Op::OpUnreachable, {});
+            }
             return;
         }
 
@@ -897,7 +901,7 @@
                 TINT_ICE_ON_NO_MATCH);
 
             // Set the name for the SPIR-V result ID if provided in the module.
-            if (inst->Result() && !inst->Is<core::ir::Var>()) {
+            if (inst->Result(0) && !inst->Is<core::ir::Var>()) {
                 if (auto name = ir_.NameOf(inst)) {
                     module_.PushDebug(spv::Op::OpName, {Value(inst), Operand(name.Name())});
                 }
@@ -974,12 +978,12 @@
 
         uint32_t true_label = merge_label;
         uint32_t false_label = merge_label;
-        if (true_block->Length() > 1 || i->HasResults() ||
-            (true_block->HasTerminator() && !true_block->Terminator()->Is<core::ir::ExitIf>())) {
+        if (true_block->Length() > 1 || !i->Results().IsEmpty() ||
+            (true_block->Terminator() && !true_block->Terminator()->Is<core::ir::ExitIf>())) {
             true_label = Label(true_block);
         }
-        if (false_block->Length() > 1 || i->HasResults() ||
-            (false_block->HasTerminator() && !false_block->Terminator()->Is<core::ir::ExitIf>())) {
+        if (false_block->Length() > 1 || !i->Results().IsEmpty() ||
+            (false_block->Terminator() && !false_block->Terminator()->Is<core::ir::ExitIf>())) {
             false_label = Label(false_block);
         }
 
@@ -1006,7 +1010,7 @@
     /// Emit an access instruction
     /// @param access the access instruction to emit
     void EmitAccess(core::ir::Access* access) {
-        auto* ty = access->Result()->Type();
+        auto* ty = access->Result(0)->Type();
 
         auto id = Value(access);
         OperandList operands = {Type(ty), id, Value(access->Object())};
@@ -1060,7 +1064,7 @@
         auto id = Value(binary);
         auto lhs = Value(binary->LHS());
         auto rhs = Value(binary->RHS());
-        auto* ty = binary->Result()->Type();
+        auto* ty = binary->Result(0)->Type();
         auto* lhs_ty = binary->LHS()->Type();
 
         // Determine the opcode.
@@ -1206,9 +1210,9 @@
     /// Emit a bitcast instruction.
     /// @param bitcast the bitcast instruction to emit
     void EmitBitcast(core::ir::Bitcast* bitcast) {
-        auto* ty = bitcast->Result()->Type();
+        auto* ty = bitcast->Result(0)->Type();
         if (ty == bitcast->Val()->Type()) {
-            values_.Add(bitcast->Result(), Value(bitcast->Val()));
+            values_.Add(bitcast->Result(0), Value(bitcast->Val()));
             return;
         }
         current_function_.push_inst(spv::Op::OpBitcast,
@@ -1341,8 +1345,8 @@
         }
 
         OperandList operands;
-        if (!builtin->Result()->Type()->Is<core::type::Void>()) {
-            operands = {Type(builtin->Result()->Type()), id};
+        if (!builtin->Result(0)->Type()->Is<core::type::Void>()) {
+            operands = {Type(builtin->Result(0)->Type()), id};
         }
         for (auto* arg : builtin->Args()) {
             operands.push_back(Value(arg));
@@ -1353,19 +1357,19 @@
     /// Emit a builtin function call instruction.
     /// @param builtin the builtin call instruction to emit
     void EmitCoreBuiltinCall(core::ir::CoreBuiltinCall* builtin) {
-        auto* result_ty = builtin->Result()->Type();
+        auto* result_ty = builtin->Result(0)->Type();
 
         if (builtin->Func() == core::BuiltinFn::kAbs &&
             result_ty->is_unsigned_integer_scalar_or_vector()) {
             // abs() is a no-op for unsigned integers.
-            values_.Add(builtin->Result(), Value(builtin->Args()[0]));
+            values_.Add(builtin->Result(0), Value(builtin->Args()[0]));
             return;
         }
         if ((builtin->Func() == core::BuiltinFn::kAll ||
              builtin->Func() == core::BuiltinFn::kAny) &&
             builtin->Args()[0]->Type()->Is<core::type::Bool>()) {
             // all() and any() are passthroughs for scalar arguments.
-            values_.Add(builtin->Result(), Value(builtin->Args()[0]));
+            values_.Add(builtin->Result(0), Value(builtin->Args()[0]));
             return;
         }
 
@@ -1710,12 +1714,12 @@
         // If there is just a single argument with the same type as the result, this is an identity
         // constructor and we can just pass through the ID of the argument.
         if (construct->Args().Length() == 1 &&
-            construct->Result()->Type() == construct->Args()[0]->Type()) {
-            values_.Add(construct->Result(), Value(construct->Args()[0]));
+            construct->Result(0)->Type() == construct->Args()[0]->Type()) {
+            values_.Add(construct->Result(0), Value(construct->Args()[0]));
             return;
         }
 
-        OperandList operands = {Type(construct->Result()->Type()), Value(construct)};
+        OperandList operands = {Type(construct->Result(0)->Type()), Value(construct)};
         for (auto* arg : construct->Args()) {
             operands.push_back(Value(arg));
         }
@@ -1725,10 +1729,10 @@
     /// Emit a convert instruction.
     /// @param convert the convert instruction to emit
     void EmitConvert(core::ir::Convert* convert) {
-        auto* res_ty = convert->Result()->Type();
+        auto* res_ty = convert->Result(0)->Type();
         auto* arg_ty = convert->Args()[0]->Type();
 
-        OperandList operands = {Type(convert->Result()->Type()), Value(convert)};
+        OperandList operands = {Type(convert->Result(0)->Type()), Value(convert)};
         for (auto* arg : convert->Args()) {
             operands.push_back(Value(arg));
         }
@@ -1810,21 +1814,21 @@
     /// @param load the load instruction to emit
     void EmitLoad(core::ir::Load* load) {
         current_function_.push_inst(
-            spv::Op::OpLoad, {Type(load->Result()->Type()), Value(load), Value(load->From())});
+            spv::Op::OpLoad, {Type(load->Result(0)->Type()), Value(load), Value(load->From())});
     }
 
     /// Emit a load vector element instruction.
     /// @param load the load vector element instruction to emit
     void EmitLoadVectorElement(core::ir::LoadVectorElement* load) {
         auto* vec_ptr_ty = load->From()->Type()->As<core::type::Pointer>();
-        auto* el_ty = load->Result()->Type();
+        auto* el_ty = load->Result(0)->Type();
         auto* el_ptr_ty = ir_.Types().ptr(vec_ptr_ty->AddressSpace(), el_ty, vec_ptr_ty->Access());
         auto el_ptr_id = module_.NextId();
         current_function_.push_inst(
             spv::Op::OpAccessChain,
             {Type(el_ptr_ty), el_ptr_id, Value(load->From()), Value(load->Index())});
         current_function_.push_inst(spv::Op::OpLoad,
-                                    {Type(load->Result()->Type()), Value(load), el_ptr_id});
+                                    {Type(load->Result(0)->Type()), Value(load), el_ptr_id});
     }
 
     /// Emit a loop instruction.
@@ -1862,7 +1866,7 @@
         EmitBlockInstructions(loop->Body());
 
         // Emit the loop continuing block.
-        if (loop->Continuing()->HasTerminator()) {
+        if (loop->Continuing()->Terminator()) {
             EmitBlock(loop->Continuing());
         } else {
             // We still need to emit a continuing block with a back-edge, even if it is unreachable.
@@ -1885,7 +1889,7 @@
         for (auto& c : swtch->Cases()) {
             for (auto& sel : c.selectors) {
                 if (sel.IsDefault()) {
-                    default_label = Label(c.Block());
+                    default_label = Label(c.block);
                 }
             }
         }
@@ -1894,7 +1898,7 @@
         // Build the operands to the OpSwitch instruction.
         OperandList switch_operands = {Value(swtch->Condition()), default_label};
         for (auto& c : swtch->Cases()) {
-            auto label = Label(c.Block());
+            auto label = Label(c.block);
             for (auto& sel : c.selectors) {
                 if (sel.IsDefault()) {
                     continue;
@@ -1914,7 +1918,7 @@
 
         // Emit the cases.
         for (auto& c : swtch->Cases()) {
-            EmitBlock(c.Block());
+            EmitBlock(c.block);
         }
 
         // Emit the switch merge block.
@@ -1929,7 +1933,7 @@
     void EmitSwizzle(core::ir::Swizzle* swizzle) {
         auto id = Value(swizzle);
         auto obj = Value(swizzle->Object());
-        OperandList operands = {Type(swizzle->Result()->Type()), id, obj, obj};
+        OperandList operands = {Type(swizzle->Result(0)->Type()), id, obj, obj};
         for (auto idx : swizzle->Indices()) {
             operands.push_back(idx);
         }
@@ -1959,7 +1963,7 @@
     /// @param unary the unary instruction to emit
     void EmitUnary(core::ir::Unary* unary) {
         auto id = Value(unary);
-        auto* ty = unary->Result()->Type();
+        auto* ty = unary->Result(0)->Type();
         spv::Op op = spv::Op::Max;
         switch (unary->Op()) {
             case core::ir::UnaryOp::kComplement:
@@ -1980,7 +1984,7 @@
     /// @param call the user call instruction to emit
     void EmitUserCall(core::ir::UserCall* call) {
         auto id = Value(call);
-        OperandList operands = {Type(call->Result()->Type()), id, Value(call->Target())};
+        OperandList operands = {Type(call->Result(0)->Type()), id, Value(call->Target())};
         for (auto* arg : call->Args()) {
             operands.push_back(Value(arg));
         }
@@ -2041,7 +2045,7 @@
     /// @param var the var instruction to emit
     void EmitVar(core::ir::Var* var) {
         auto id = Value(var);
-        auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+        auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
         auto* store_ty = ptr->StoreType();
         auto ty = Type(ptr);
 
@@ -2139,7 +2143,7 @@
     /// @param let the let instruction to emit
     void EmitLet(core::ir::Let* let) {
         auto id = Value(let->Value());
-        values_.Add(let->Result(), id);
+        values_.Add(let->Result(0), id);
     }
 
     /// Emit the OpPhis for the given flow control instruction.
@@ -2163,6 +2167,13 @@
             }
             branches.Sort();  // Sort the branches by label to ensure deterministic output
 
+            // Also add phi nodes from implicit exit blocks.
+            inst->ForeachBlock([&](core::ir::Block* block) {
+                if (block->IsEmpty()) {
+                    branches.Push(Branch{Label(block), nullptr});
+                }
+            });
+
             OperandList ops{Type(ty), Value(result)};
             for (auto& branch : branches) {
                 if (branch.value == nullptr) {
@@ -2188,9 +2199,9 @@
     /// @returns the label ID
     uint32_t GetTerminatorBlockLabel(core::ir::Terminator* t) {
         // Walk backwards from `t` until we find a control instruction.
-        auto* inst = t->prev;
+        auto* inst = t->prev.Get();
         while (inst) {
-            auto* prev = inst->prev;
+            auto* prev = inst->prev.Get();
             if (auto* ci = inst->As<core::ir::ControlInstruction>()) {
                 // This is the last control instruction before `t`, so use its merge block label.
                 return GetMergeLabel(ci);
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
index ddce778..923a526 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
@@ -105,7 +105,7 @@
                         worklist.Push(builtin);
                         break;
                     case core::BuiltinFn::kQuantizeToF16:
-                        if (builtin->Result()->Type()->Is<core::type::Vector>()) {
+                        if (builtin->Result(0)->Type()->Is<core::type::Vector>()) {
                             worklist.Push(builtin);
                         }
                         break;
@@ -178,10 +178,10 @@
             TINT_ASSERT_OR_RETURN(replacement);
 
             // Replace the old builtin result with the new value.
-            if (auto name = ir.NameOf(builtin->Result())) {
+            if (auto name = ir.NameOf(builtin->Result(0))) {
                 ir.SetName(replacement, name);
             }
-            builtin->Result()->ReplaceAllUsesWith(replacement);
+            builtin->Result(0)->ReplaceAllUsesWith(replacement);
             builtin->Destroy();
         }
     }
@@ -199,12 +199,12 @@
     core::ir::Value* ArrayLength(core::ir::CoreBuiltinCall* builtin) {
         // Strip away any let instructions to get to the original struct member access instruction.
         auto* ptr = builtin->Args()[0]->As<core::ir::InstructionResult>();
-        while (auto* let = tint::As<core::ir::Let>(ptr->Source())) {
+        while (auto* let = tint::As<core::ir::Let>(ptr->Instruction())) {
             ptr = let->Value()->As<core::ir::InstructionResult>();
         }
         TINT_ASSERT_OR_RETURN_VALUE(ptr, nullptr);
 
-        auto* access = ptr->Source()->As<core::ir::Access>();
+        auto* access = ptr->Instruction()->As<core::ir::Access>();
         TINT_ASSERT_OR_RETURN_VALUE(access, nullptr);
         TINT_ASSERT_OR_RETURN_VALUE(access->Indices().Length() == 1u, nullptr);
         TINT_ASSERT_OR_RETURN_VALUE(access->Object()->Type()->UnwrapPtr()->Is<core::type::Struct>(),
@@ -213,17 +213,17 @@
 
         // Replace the builtin call with a call to the spirv.array_length intrinsic.
         auto* call = b.Call<spirv::ir::BuiltinCall>(
-            builtin->Result()->Type(), spirv::BuiltinFn::kArrayLength,
+            builtin->Result(0)->Type(), spirv::BuiltinFn::kArrayLength,
             Vector{access->Object(), Literal(u32(const_idx->Value()->ValueAs<uint32_t>()))});
         call->InsertBefore(builtin);
-        return call->Result();
+        return call->Result(0);
     }
 
     /// Handle an atomic*() builtin.
     /// @param builtin the builtin call instruction
     /// @returns the replacement value
     core::ir::Value* Atomic(core::ir::CoreBuiltinCall* builtin) {
-        auto* result_ty = builtin->Result()->Type();
+        auto* result_ty = builtin->Result(0)->Type();
 
         auto* pointer = builtin->Args()[0];
         auto* memory = [&]() -> core::ir::Value* {
@@ -267,14 +267,14 @@
                 call->InsertBefore(builtin);
 
                 // Compare the original value to the comparator to see if an exchange happened.
-                auto* original = call->Result();
+                auto* original = call->Result(0);
                 auto* compare = b.Equal(ty.bool_(), original, cmp);
                 compare->InsertBefore(builtin);
 
                 // Construct the atomicCompareExchange result structure.
                 call = b.Construct(
                     core::type::CreateAtomicCompareExchangeResult(ty, ir.symbols, int_ty),
-                    Vector{original, compare->Result()});
+                    Vector{original, compare->Result(0)});
                 break;
             }
             case core::BuiltinFn::kAtomicExchange:
@@ -321,7 +321,7 @@
         }
 
         call->InsertBefore(builtin);
-        return call->Result();
+        return call->Result(0);
     }
 
     /// Handle a `dot()` builtin.
@@ -330,7 +330,7 @@
     core::ir::Value* Dot(core::ir::CoreBuiltinCall* builtin) {
         // OpDot only supports floating point operands, so we need to polyfill the integer case.
         // TODO(crbug.com/tint/1267): If SPV_KHR_integer_dot_product is supported, use that instead.
-        if (builtin->Result()->Type()->is_integer_scalar()) {
+        if (builtin->Result(0)->Type()->is_integer_scalar()) {
             core::ir::Instruction* sum = nullptr;
 
             auto* v1 = builtin->Args()[0];
@@ -349,15 +349,15 @@
                     }
                 });
             }
-            return sum->Result();
+            return sum->Result(0);
         }
 
         // 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<spirv::ir::BuiltinCall>(builtin->Result()->Type(),
+        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result(0)->Type(),
                                                     spirv::BuiltinFn::kDot, std::move(args));
         call->InsertBefore(builtin);
-        return call->Result();
+        return call->Result(0);
     }
 
     /// Handle a `dot4{I,U}8Packed()` builtin.
@@ -365,7 +365,7 @@
     /// @returns the replacement value
     core::ir::Value* DotPacked4x8(core::ir::CoreBuiltinCall* builtin) {
         // Replace the builtin call with a call to the spirv.{s,u}dot intrinsic.
-        auto* type = builtin->Result()->Type();
+        auto* type = builtin->Result(0)->Type();
         auto is_signed = builtin->Func() == core::BuiltinFn::kDot4I8Packed;
         auto inst = is_signed ? spirv::BuiltinFn::kSdot : spirv::BuiltinFn::kUdot;
 
@@ -374,7 +374,7 @@
 
         auto* call = b.Call<spirv::ir::BuiltinCall>(type, inst, std::move(args));
         call->InsertBefore(builtin);
-        return call->Result();
+        return call->Result(0);
     }
 
     /// Handle a `select()` builtin.
@@ -391,21 +391,21 @@
         // If the condition is scalar and the objects are vectors, we need to splat the condition
         // into a vector of the same size.
         // TODO(jrprice): We don't need to do this if we're targeting SPIR-V 1.4 or newer.
-        auto* vec = builtin->Result()->Type()->As<core::type::Vector>();
+        auto* vec = builtin->Result(0)->Type()->As<core::type::Vector>();
         if (vec && args[0]->Type()->Is<core::type::Scalar>()) {
             Vector<core::ir::Value*, 4> elements;
             elements.Resize(vec->Width(), args[0]);
 
             auto* construct = b.Construct(ty.vec(ty.bool_(), vec->Width()), std::move(elements));
             construct->InsertBefore(builtin);
-            args[0] = construct->Result();
+            args[0] = construct->Result(0);
         }
 
         // Replace the builtin call with a call to the spirv.select intrinsic.
-        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result()->Type(),
+        auto* call = b.Call<spirv::ir::BuiltinCall>(builtin->Result(0)->Type(),
                                                     spirv::BuiltinFn::kSelect, std::move(args));
         call->InsertBefore(builtin);
-        return call->Result();
+        return call->Result(0);
     }
 
     /// ImageOperands represents the optional image operands for an image instruction.
@@ -449,7 +449,7 @@
             if (requires_float_lod && operands.lod->Type()->is_integer_scalar()) {
                 auto* convert = b.Convert(ty.f32(), operands.lod);
                 convert->InsertBefore(insertion_point);
-                operands.lod = convert->Result();
+                operands.lod = convert->Result(0);
             }
             args.Push(operands.lod);
         }
@@ -486,7 +486,7 @@
         if (array_idx->Type() != element_ty) {
             auto* array_idx_converted = b.Convert(element_ty, array_idx);
             array_idx_converted->InsertBefore(insertion_point);
-            array_idx = array_idx_converted->Result();
+            array_idx = array_idx_converted->Result(0);
         }
 
         // Construct a new coordinate vector.
@@ -494,7 +494,7 @@
         auto* coord_ty = ty.vec(element_ty, num_coords + 1);
         auto* construct = b.Construct(coord_ty, Vector{coords, array_idx});
         construct->InsertBefore(insertion_point);
-        return construct->Result();
+        return construct->Result(0);
     }
 
     /// Handle a textureSample*() builtin.
@@ -568,7 +568,7 @@
         // The first two operands are always the sampled image and then the coordinates, followed by
         // the depth reference if used.
         Vector<core::ir::Value*, 8> function_args;
-        function_args.Push(sampled_image->Result());
+        function_args.Push(sampled_image->Result(0));
         function_args.Push(coords);
         if (depth) {
             function_args.Push(depth);
@@ -584,7 +584,7 @@
             b.Call<spirv::ir::BuiltinCall>(result_ty, function, std::move(function_args));
         texture_call->InsertBefore(builtin);
 
-        auto* result = texture_call->Result();
+        auto* result = texture_call->Result(0);
 
         // If this is not a depth comparison but we are sampling a depth texture, extract the first
         // component to get the scalar f32 that SPIR-V expects.
@@ -592,7 +592,7 @@
             texture_ty->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) {
             auto* extract = b.Access(ty.f32(), result, 0_u);
             extract->InsertBefore(builtin);
-            result = extract->Result();
+            result = extract->Result(0);
         }
 
         return result;
@@ -654,7 +654,7 @@
         // The first two operands are always the sampled image and then the coordinates, followed by
         // either the depth reference or the component.
         Vector<core::ir::Value*, 8> function_args;
-        function_args.Push(sampled_image->Result());
+        function_args.Push(sampled_image->Result(0));
         function_args.Push(coords);
         if (depth) {
             function_args.Push(depth);
@@ -666,11 +666,11 @@
         AppendImageOperands(operands, function_args, builtin, /* requires_float_lod */ true);
 
         // Call the function.
-        auto* result_ty = builtin->Result()->Type();
+        auto* result_ty = builtin->Result(0)->Type();
         auto* texture_call =
             b.Call<spirv::ir::BuiltinCall>(result_ty, function, std::move(function_args));
         texture_call->InsertBefore(builtin);
-        return texture_call->Result();
+        return texture_call->Result(0);
     }
 
     /// Handle a textureLoad() builtin.
@@ -711,7 +711,7 @@
 
         // Call the builtin.
         // The result is always a vec4 in SPIR-V.
-        auto* result_ty = builtin->Result()->Type();
+        auto* result_ty = builtin->Result(0)->Type();
         bool expects_scalar_result = result_ty->Is<core::type::Scalar>();
         if (expects_scalar_result) {
             result_ty = ty.vec4(result_ty);
@@ -721,13 +721,13 @@
         auto* texture_call =
             b.Call<spirv::ir::BuiltinCall>(result_ty, kind, std::move(builtin_args));
         texture_call->InsertBefore(builtin);
-        auto* result = texture_call->Result();
+        auto* result = texture_call->Result(0);
 
         // If we are expecting a scalar result, extract the first component.
         if (expects_scalar_result) {
             auto* extract = b.Access(ty.f32(), result, 0_u);
             extract->InsertBefore(builtin);
-            result = extract->Result();
+            result = extract->Result(0);
         }
 
         return result;
@@ -769,7 +769,7 @@
         auto* texture_call = b.Call<spirv::ir::BuiltinCall>(
             ty.void_(), spirv::BuiltinFn::kImageWrite, std::move(function_args));
         texture_call->InsertBefore(builtin);
-        return texture_call->Result();
+        return texture_call->Result(0);
     }
 
     /// Handle a textureDimensions() builtin.
@@ -805,7 +805,7 @@
         }
 
         // Add an extra component to the result vector for arrayed textures.
-        auto* result_ty = builtin->Result()->Type();
+        auto* result_ty = builtin->Result(0)->Type();
         if (core::type::IsTextureArray(texture_ty->dim())) {
             auto* vec = result_ty->As<core::type::Vector>();
             result_ty = ty.vec(vec->type(), vec->Width() + 1);
@@ -816,13 +816,13 @@
             b.Call<spirv::ir::BuiltinCall>(result_ty, function, std::move(function_args));
         texture_call->InsertBefore(builtin);
 
-        auto* result = texture_call->Result();
+        auto* result = texture_call->Result(0);
 
         // Swizzle the first two components from the result for arrayed textures.
         if (core::type::IsTextureArray(texture_ty->dim())) {
-            auto* swizzle = b.Swizzle(builtin->Result()->Type(), result, {0, 1});
+            auto* swizzle = b.Swizzle(builtin->Result(0)->Type(), result, {0, 1});
             swizzle->InsertBefore(builtin);
-            result = swizzle->Result();
+            result = swizzle->Result(0);
         }
 
         return result;
@@ -855,9 +855,9 @@
         texture_call->InsertBefore(builtin);
 
         // Extract the third component to get the number of array layers.
-        auto* extract = b.Access(ty.u32(), texture_call->Result(), 2_u);
+        auto* extract = b.Access(ty.u32(), texture_call->Result(0), 2_u);
         extract->InsertBefore(builtin);
-        return extract->Result();
+        return extract->Result(0);
     }
 
     /// Scalarize the vector form of a `quantizeToF16()` builtin.
@@ -874,13 +874,13 @@
         for (uint32_t i = 0; i < vec->Width(); i++) {
             auto* el = b.Access(ty.f32(), arg, u32(i));
             auto* scalar_call = b.Call(ty.f32(), core::BuiltinFn::kQuantizeToF16, el);
-            args.Push(scalar_call->Result());
+            args.Push(scalar_call->Result(0));
             el->InsertBefore(builtin);
             scalar_call->InsertBefore(builtin);
         }
         auto* construct = b.Construct(vec, std::move(args));
         construct->InsertBefore(builtin);
-        return construct->Result();
+        return construct->Result(0);
     }
 };
 
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 7df2b91..033cb2a 100644
--- a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
+++ b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
@@ -55,7 +55,7 @@
         if (auto* construct = inst->As<core::ir::Construct>()) {
             // A vector constructor with a single scalar argument needs to be modified to replicate
             // the argument N times.
-            auto* vec = construct->Result()->Type()->As<core::type::Vector>();
+            auto* vec = construct->Result(0)->Type()->As<core::type::Vector>();
             if (vec &&  //
                 construct->Args().Length() == 1 &&
                 construct->Args()[0]->Type()->Is<core::type::Scalar>()) {
@@ -66,7 +66,7 @@
         } else if (auto* binary = inst->As<core::ir::Binary>()) {
             // A binary instruction that mixes vector and scalar operands needs to have the scalar
             // operand replaced with an explicit vector constructor.
-            if (binary->Result()->Type()->Is<core::type::Vector>()) {
+            if (binary->Result(0)->Type()->Is<core::type::Vector>()) {
                 if (binary->LHS()->Type()->Is<core::type::Scalar>() ||
                     binary->RHS()->Type()->Is<core::type::Scalar>()) {
                     binary_worklist.Push(binary);
@@ -76,7 +76,7 @@
             // A mix builtin call that mixes vector and scalar operands needs to have the scalar
             // operand replaced with an explicit vector constructor.
             if (builtin->Func() == core::BuiltinFn::kMix) {
-                if (builtin->Result()->Type()->Is<core::type::Vector>()) {
+                if (builtin->Result(0)->Type()->Is<core::type::Vector>()) {
                     if (builtin->Args()[2]->Type()->Is<core::type::Scalar>()) {
                         builtin_worklist.Push(builtin);
                     }
@@ -88,19 +88,19 @@
     // Helper to expand a scalar operand of an instruction by replacing it with an explicitly
     // constructed vector that matches the result type.
     auto expand_operand = [&](core::ir::Instruction* inst, size_t operand_idx) {
-        auto* vec = inst->Result()->Type()->As<core::type::Vector>();
+        auto* vec = inst->Result(0)->Type()->As<core::type::Vector>();
 
         Vector<core::ir::Value*, 4> args;
         args.Resize(vec->Width(), inst->Operands()[operand_idx]);
 
         auto* construct = b.Construct(vec, std::move(args));
         construct->InsertBefore(inst);
-        inst->SetOperand(operand_idx, construct->Result());
+        inst->SetOperand(operand_idx, construct->Result(0));
     };
 
     // Replace scalar operands to binary instructions that produce vectors.
     for (auto* binary : binary_worklist) {
-        auto* result_ty = binary->Result()->Type();
+        auto* result_ty = binary->Result(0)->Type();
         if (result_ty->is_float_vector() && binary->Op() == core::ir::BinaryOp::kMultiply) {
             // Use OpVectorTimesScalar for floating point multiply.
             auto* vts =
@@ -113,9 +113,9 @@
                 vts->AppendArg(binary->RHS());
             }
             if (auto name = ir.NameOf(binary)) {
-                ir.SetName(vts->Result(), name);
+                ir.SetName(vts->Result(0), name);
             }
-            binary->Result()->ReplaceAllUsesWith(vts->Result());
+            binary->Result(0)->ReplaceAllUsesWith(vts->Result(0));
             binary->ReplaceWith(vts);
             binary->Destroy();
         } else {
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 bfd212f..43ede7c 100644
--- a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
+++ b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
@@ -61,7 +61,7 @@
                 binary_worklist.Push(binary);
             }
         } else if (auto* convert = inst->As<core::ir::Convert>()) {
-            if (convert->Result()->Type()->Is<core::type::Matrix>()) {
+            if (convert->Result(0)->Type()->Is<core::type::Matrix>()) {
                 convert_worklist.Push(convert);
             }
         }
@@ -73,14 +73,14 @@
         auto* rhs = binary->RHS();
         auto* lhs_ty = lhs->Type();
         auto* rhs_ty = rhs->Type();
-        auto* ty = binary->Result()->Type();
+        auto* ty = binary->Result(0)->Type();
 
         // Helper to replace the instruction with a new one.
         auto replace = [&](core::ir::Instruction* inst) {
             if (auto name = ir.NameOf(binary)) {
-                ir.SetName(inst->Result(), name);
+                ir.SetName(inst->Result(0), name);
             }
-            binary->Result()->ReplaceAllUsesWith(inst->Result());
+            binary->Result(0)->ReplaceAllUsesWith(inst->Result(0));
             binary->ReplaceWith(inst);
             binary->Destroy();
         };
@@ -94,7 +94,7 @@
                     auto* lhs_col = b.Access(mat->ColumnType(), lhs, u32(col));
                     auto* rhs_col = b.Access(mat->ColumnType(), rhs, u32(col));
                     auto* add = b.Binary(op, mat->ColumnType(), lhs_col, rhs_col);
-                    args.Push(add->Result());
+                    args.Push(add->Result(0));
                 });
             }
             replace(b.Construct(ty, std::move(args)));
@@ -141,7 +141,7 @@
     for (auto* convert : convert_worklist) {
         auto* arg = convert->Args()[core::ir::Convert::kValueOperandOffset];
         auto* in_mat = arg->Type()->As<core::type::Matrix>();
-        auto* out_mat = convert->Result()->Type()->As<core::type::Matrix>();
+        auto* out_mat = convert->Result(0)->Type()->As<core::type::Matrix>();
 
         // Extract and convert each column separately.
         Vector<core::ir::Value*, 4> args;
@@ -149,16 +149,16 @@
             b.InsertBefore(convert, [&] {
                 auto* col = b.Access(in_mat->ColumnType(), arg, u32(c));
                 auto* new_col = b.Convert(out_mat->ColumnType(), col);
-                args.Push(new_col->Result());
+                args.Push(new_col->Result(0));
             });
         }
 
         // Reconstruct the result matrix from the converted columns.
         auto* construct = b.Construct(out_mat, std::move(args));
         if (auto name = ir.NameOf(convert)) {
-            ir.SetName(construct->Result(), name);
+            ir.SetName(construct->Result(0), name);
         }
-        convert->Result()->ReplaceAllUsesWith(construct->Result());
+        convert->Result(0)->ReplaceAllUsesWith(construct->Result(0));
         convert->ReplaceWith(construct);
         convert->Destroy();
     }
diff --git a/src/tint/lang/spirv/writer/raise/merge_return.cc b/src/tint/lang/spirv/writer/raise/merge_return.cc
index dbd06d7..cab47ce 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return.cc
+++ b/src/tint/lang/spirv/writer/raise/merge_return.cc
@@ -132,7 +132,7 @@
         for (auto* inst = *block->begin(); inst;) {  // For each instruction in 'block'
             // As we're modifying the block that we're iterating over, grab the pointer to the next
             // instruction before (potentially) moving 'inst' to another block.
-            auto* next = inst->next;
+            auto* next = inst->next.Get();
             TINT_DEFER(inst = next);
 
             if (auto* ret = inst->As<core::ir::Return>()) {
@@ -178,8 +178,8 @@
                 return b.InstructionResult(v->Type());
             };
 
-            if (inner_if->True()->HasTerminator()) {
-                if (auto* exit_if = inner_if->True()->Terminator()->As<core::ir::ExitIf>()) {
+            if (auto* terminator = inner_if->True()->Terminator()) {
+                if (auto* exit_if = terminator->As<core::ir::ExitIf>()) {
                     // Ensure the associated 'if' is updated.
                     exit_if->SetIf(inner_if);
 
@@ -198,10 +198,10 @@
             // Loop over the 'if' instructions, starting with the inner-most, and add any missing
             // terminating instructions to the blocks holding the 'if'.
             for (auto* i = inner_if; i; i = tint::As<core::ir::If>(i->Block()->Parent())) {
-                if (!i->Block()->HasTerminator() && i->Block()->Parent()) {
+                if (!i->Block()->Terminator() && i->Block()->Parent()) {
                     // Append the exit instruction to the block holding the 'if'.
                     Vector<core::ir::InstructionResult*, 8> exit_args = i->Results();
-                    if (!i->HasResults()) {
+                    if (i->Results().IsEmpty()) {
                         i->SetResults(tint::Transform(exit_args, new_value_with_type));
                     }
                     auto* exit = b.Exit(i->Block()->Parent(), std::move(exit_args));
@@ -244,7 +244,7 @@
         // Change the function return to unconditionally load 'return_val' and return it
         auto* load = b.Load(return_val);
         load->InsertBefore(ret);
-        ret->SetValue(load->Result());
+        ret->SetValue(load->Result(0));
     }
 
     /// Transforms the return instruction that is found in a control instruction.
@@ -306,7 +306,7 @@
     }
 
     // Process each function.
-    for (auto* fn : ir.functions) {
+    for (auto& fn : ir.functions) {
         State{ir}.Process(fn);
     }
 
diff --git a/src/tint/lang/spirv/writer/raise/merge_return_test.cc b/src/tint/lang/spirv/writer/raise/merge_return_test.cc
index 29a5b3f..425f1c0 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return_test.cc
+++ b/src/tint/lang/spirv/writer/raise/merge_return_test.cc
@@ -111,7 +111,7 @@
 
     b.Append(func->Block(), [&] {
         auto* swtch = b.Switch(in);
-        b.Append(b.Case(swtch, {core::ir::Switch::CaseSelector{}}), [&] { b.ExitSwitch(swtch); });
+        b.Append(b.DefaultCase(swtch), [&] { b.ExitSwitch(swtch); });
 
         auto* l = b.Loop();
         b.Append(l->Body(), [&] { b.ExitLoop(l); });
@@ -1649,9 +1649,8 @@
 
     b.Append(func->Block(), [&] {
         auto* sw = b.Switch(cond);
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{b.Constant(1_i)}}),
-                 [&] { b.Return(func, 42_i); });
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{}}), [&] { b.ExitSwitch(sw); });
+        b.Append(b.Case(sw, {b.Constant(1_i)}), [&] { b.Return(func, 42_i); });
+        b.Append(b.DefaultCase(sw), [&] { b.ExitSwitch(sw); });
 
         b.Return(func, 0_i);
     });
@@ -1716,7 +1715,7 @@
 
     b.Append(func->Block(), [&] {
         auto* sw = b.Switch(cond);
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{b.Constant(1_i)}}), [&] {
+        b.Append(b.Case(sw, {b.Constant(1_i)}), [&] {
             auto* ifcond = b.Equal(ty.bool_(), cond, 1_i);
             auto* ifelse = b.If(ifcond);
             b.Append(ifelse->True(), [&] { b.Return(func, 42_i); });
@@ -1726,7 +1725,7 @@
             b.ExitSwitch(sw);
         });
 
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{}}), [&] { b.ExitSwitch(sw); });
+        b.Append(b.DefaultCase(sw), [&] { b.ExitSwitch(sw); });
 
         b.Return(func, 0_i);
     });
@@ -1823,13 +1822,10 @@
     b.Append(func->Block(), [&] {
         auto* sw = b.Switch(cond);
         sw->SetResults(b.InstructionResult(ty.i32()));  // NOLINT: false detection of std::tuple
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{b.Constant(1_i)}}),
-                 [&] { b.Return(func, 42_i); });
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{b.Constant(2_i)}}),
-                 [&] { b.Return(func, 99_i); });
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{b.Constant(3_i)}}),
-                 [&] { b.ExitSwitch(sw, 1_i); });
-        b.Append(b.Case(sw, {core::ir::Switch::CaseSelector{}}), [&] { b.ExitSwitch(sw, 0_i); });
+        b.Append(b.Case(sw, {b.Constant(1_i)}), [&] { b.Return(func, 42_i); });
+        b.Append(b.Case(sw, {b.Constant(2_i)}), [&] { b.Return(func, 99_i); });
+        b.Append(b.Case(sw, {b.Constant(3_i)}), [&] { b.ExitSwitch(sw, 1_i); });
+        b.Append(b.DefaultCase(sw), [&] { b.ExitSwitch(sw, 0_i); });
 
         b.Return(func, sw->Result(0));
     });
diff --git a/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc
index 62bfb78..2fe2e74 100644
--- a/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc
+++ b/src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.cc
@@ -54,7 +54,7 @@
     /// Process the module.
     void Process() {
         // Find user-declared functions that have value arguments containing matrices.
-        for (auto* func : ir.functions) {
+        for (auto& func : ir.functions) {
             for (auto* param : func->Params()) {
                 if (ContainsMatrix(param->Type())) {
                     TransformFunction(func);
@@ -95,7 +95,7 @@
                 // Load from the pointer to get the value.
                 auto* load = b.Load(new_param);
                 func->Block()->Prepend(load);
-                param->ReplaceAllUsesWith(load->Result());
+                param->ReplaceAllUsesWith(load->Result(0));
 
                 // Modify all of the callsites.
                 func->ForEachUse([&](core::ir::Usage use) {
@@ -121,7 +121,7 @@
         local_var->SetInitializer(arg);
         local_var->InsertBefore(call);
 
-        call->SetOperand(core::ir::UserCall::kArgsOperandOffset + arg_index, local_var->Result());
+        call->SetOperand(core::ir::UserCall::kArgsOperandOffset + arg_index, local_var->Result(0));
     }
 };
 
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 8160fe6..fad09b4 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -133,8 +133,10 @@
     // produce pointers to matrices.
     RUN_TRANSFORM(core::ir::transform::CombineAccessInstructions, module);
 
-    RUN_TRANSFORM(BuiltinPolyfill, module);
+    // DemoteToHelper must come before any transform that introduces non-core instructions.
     RUN_TRANSFORM(core::ir::transform::DemoteToHelper, module);
+
+    RUN_TRANSFORM(BuiltinPolyfill, module);
     RUN_TRANSFORM(ExpandImplicitSplats, module);
     RUN_TRANSFORM(HandleMatrixArithmetic, module);
     RUN_TRANSFORM(MergeReturn, module);
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.cc b/src/tint/lang/spirv/writer/raise/shader_io.cc
index c44fdee..5ec25d1 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -143,25 +143,25 @@
     core::ir::Value* GetInput(core::ir::Builder& builder, uint32_t idx) override {
         // Load the input from the global variable declared earlier.
         auto* ptr = ty.ptr(core::AddressSpace::kIn, inputs[idx].type, core::Access::kRead);
-        auto* from = input_vars[idx]->Result();
+        auto* from = input_vars[idx]->Result(0);
         if (inputs[idx].attributes.builtin) {
             if (inputs[idx].attributes.builtin.value() == core::BuiltinValue::kSampleMask) {
                 // SampleMask becomes an array for SPIR-V, so load from the first element.
-                from = builder.Access(ptr, input_vars[idx], 0_u)->Result();
+                from = builder.Access(ptr, input_vars[idx], 0_u)->Result(0);
             }
         }
-        return builder.Load(from)->Result();
+        return builder.Load(from)->Result(0);
     }
 
     /// @copydoc ShaderIO::BackendState::SetOutput
     void SetOutput(core::ir::Builder& builder, uint32_t idx, core::ir::Value* value) override {
         // Store the output to the global variable declared earlier.
         auto* ptr = ty.ptr(core::AddressSpace::kOut, outputs[idx].type, core::Access::kWrite);
-        auto* to = output_vars[idx]->Result();
+        auto* to = output_vars[idx]->Result(0);
         if (outputs[idx].attributes.builtin) {
             if (outputs[idx].attributes.builtin.value() == core::BuiltinValue::kSampleMask) {
                 // SampleMask becomes an array for SPIR-V, so store to the first element.
-                to = builder.Access(ptr, to, 0_u)->Result();
+                to = builder.Access(ptr, to, 0_u)->Result(0);
             }
 
             // Clamp frag_depth values if necessary.
@@ -186,7 +186,7 @@
             // Check that there are no push constants in the module already.
             for (auto* inst : *ir.root_block) {
                 if (auto* var = inst->As<core::ir::Var>()) {
-                    auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
+                    auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
                     if (ptr->AddressSpace() == core::AddressSpace::kPushConstant) {
                         TINT_ICE() << "cannot clamp frag_depth with pre-existing push constants";
                     }
@@ -204,7 +204,7 @@
             // Declare the variable.
             auto* var = b.Var("tint_frag_depth_clamp_args", ty.ptr(push_constant, str));
             ir.root_block->Append(var);
-            module_state.frag_depth_clamp_args = var->Result();
+            module_state.frag_depth_clamp_args = var->Result(0);
         }
 
         // Clamp the value.
@@ -213,7 +213,7 @@
         auto* frag_depth_max = builder.Access(ty.f32(), args, 1_u);
         return builder
             .Call(ty.f32(), core::BuiltinFn::kClamp, frag_depth, frag_depth_min, frag_depth_max)
-            ->Result();
+            ->Result(0);
     }
 
     /// @copydoc ShaderIO::BackendState::NeedsVertexPointSize
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
index e58d05d..855ba42 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -94,7 +94,7 @@
 }
 
 std::optional<AccessToReplace> ShouldReplace(core::ir::Access* access) {
-    if (access->Result()->Type()->Is<core::type::Pointer>()) {
+    if (access->Result(0)->Type()->Is<core::type::Pointer>()) {
         // No need to modify accesses into pointer types.
         return {};
     }
@@ -153,7 +153,7 @@
                 auto* intermediate_source = builder.Access(to_replace.dynamic_index_source_type,
                                                            source_object, partial_access.indices);
                 intermediate_source->InsertBefore(access);
-                return intermediate_source->Result();
+                return intermediate_source->Result(0);
             });
         }
 
@@ -163,13 +163,13 @@
                 core::AddressSpace::kFunction, source_object->Type(), core::Access::kReadWrite));
             decl->SetInitializer(source_object);
             decl->InsertBefore(access);
-            return decl->Result();
+            return decl->Result(0);
         });
 
         // Create a new access instruction using the local variable as the source.
         Vector<core::ir::Value*, 4> indices{
             access->Indices().Offset(to_replace.first_dynamic_index)};
-        const core::type::Type* access_type = access->Result()->Type();
+        const core::type::Type* access_type = access->Result(0)->Type();
         core::ir::Value* vector_index = nullptr;
         if (to_replace.vector_access_type) {
             // The old access indexed the element of a vector.
@@ -189,13 +189,13 @@
 
         core::ir::Instruction* load = nullptr;
         if (to_replace.vector_access_type) {
-            load = builder.LoadVectorElement(new_access->Result(), vector_index);
+            load = builder.LoadVectorElement(new_access->Result(0), vector_index);
         } else {
             load = builder.Load(new_access);
         }
 
         // Replace all uses of the old access instruction with the loaded result.
-        access->Result()->ReplaceAllUsesWith(load->Result());
+        access->Result(0)->ReplaceAllUsesWith(load->Result(0));
         access->ReplaceWith(load);
         access->Destroy();
     }
diff --git a/src/tint/lang/spirv/writer/switch_test.cc b/src/tint/lang/spirv/writer/switch_test.cc
index d00d32e..957d552 100644
--- a/src/tint/lang/spirv/writer/switch_test.cc
+++ b/src/tint/lang/spirv/writer/switch_test.cc
@@ -37,7 +37,7 @@
     b.Append(func->Block(), [&] {
         auto* swtch = b.Switch(42_i);
 
-        auto* def_case = b.Case(swtch, Vector{core::ir::Switch::CaseSelector()});
+        auto* def_case = b.DefaultCase(swtch);
         b.Append(def_case, [&] {  //
             b.ExitSwitch(swtch);
         });
@@ -63,17 +63,17 @@
     b.Append(func->Block(), [&] {
         auto* swtch = b.Switch(42_i);
 
-        auto* case_a = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)}});
+        auto* case_a = b.Case(swtch, Vector{b.Constant(1_i)});
         b.Append(case_a, [&] {  //
             b.ExitSwitch(swtch);
         });
 
-        auto* case_b = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(swtch, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(swtch);
         });
 
-        auto* def_case = b.Case(swtch, Vector{core::ir::Switch::CaseSelector()});
+        auto* def_case = b.DefaultCase(swtch);
         b.Append(def_case, [&] {  //
             b.ExitSwitch(swtch);
         });
@@ -103,20 +103,17 @@
     b.Append(func->Block(), [&] {
         auto* swtch = b.Switch(42_i);
 
-        auto* case_a = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                            core::ir::Switch::CaseSelector{b.Constant(3_i)}});
+        auto* case_a = b.Case(swtch, Vector{b.Constant(1_i), b.Constant(3_i)});
         b.Append(case_a, [&] {  //
             b.ExitSwitch(swtch);
         });
 
-        auto* case_b = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)},
-                                            core::ir::Switch::CaseSelector{b.Constant(4_i)}});
+        auto* case_b = b.Case(swtch, Vector{b.Constant(2_i), b.Constant(4_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(swtch);
         });
 
-        auto* def_case = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(5_i)},
-                                              core::ir::Switch::CaseSelector()});
+        auto* def_case = b.Case(swtch, Vector{b.Constant(5_i), nullptr});
         b.Append(def_case, [&] {  //
             b.ExitSwitch(swtch);
         });
@@ -146,17 +143,17 @@
     b.Append(func->Block(), [&] {
         auto* swtch = b.Switch(42_i);
 
-        auto* case_a = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)}});
+        auto* case_a = b.Case(swtch, Vector{b.Constant(1_i)});
         b.Append(case_a, [&] {  //
             b.Return(func);
         });
 
-        auto* case_b = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(swtch, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.Return(func);
         });
 
-        auto* def_case = b.Case(swtch, Vector{core::ir::Switch::CaseSelector()});
+        auto* def_case = b.DefaultCase(swtch);
         b.Append(def_case, [&] {  //
             b.Return(func);
         });
@@ -186,7 +183,7 @@
     b.Append(func->Block(), [&] {
         auto* swtch = b.Switch(42_i);
 
-        auto* case_a = b.Case(swtch, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)}});
+        auto* case_a = b.Case(swtch, Vector{b.Constant(1_i)});
         b.Append(case_a, [&] {
             auto* cond_break = b.If(true);
             b.Append(cond_break->True(), [&] {  //
@@ -199,7 +196,7 @@
             b.Return(func);
         });
 
-        auto* def_case = b.Case(swtch, Vector{core::ir::Switch::CaseSelector()});
+        auto* def_case = b.DefaultCase(swtch);
         b.Append(def_case, [&] {  //
             b.ExitSwitch(swtch);
         });
@@ -232,13 +229,12 @@
     b.Append(func->Block(), [&] {
         auto* s = b.Switch(42_i);
         s->SetResults(b.InstructionResult(ty.i32()));
-        auto* case_a = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                        core::ir::Switch::CaseSelector{nullptr}});
+        auto* case_a = b.Case(s, Vector{b.Constant(1_i), nullptr});
         b.Append(case_a, [&] {  //
             b.ExitSwitch(s, 10_i);
         });
 
-        auto* case_b = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(s, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(s, 20_i);
         });
@@ -267,13 +263,12 @@
     b.Append(func->Block(), [&] {
         auto* s = b.Switch(42_i);
         s->SetResults(b.InstructionResult(ty.i32()));
-        auto* case_a = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                        core::ir::Switch::CaseSelector{nullptr}});
+        auto* case_a = b.Case(s, Vector{b.Constant(1_i), nullptr});
         b.Append(case_a, [&] {  //
             b.Return(func, 10_i);
         });
 
-        auto* case_b = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(s, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(s, 20_i);
         });
@@ -315,13 +310,12 @@
     b.Append(func->Block(), [&] {
         auto* s = b.Switch(42_i);
         s->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.bool_()));
-        auto* case_a = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                        core::ir::Switch::CaseSelector{nullptr}});
+        auto* case_a = b.Case(s, Vector{b.Constant(1_i), nullptr});
         b.Append(case_a, [&] {  //
             b.ExitSwitch(s, 10_i, true);
         });
 
-        auto* case_b = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(s, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(s, 20_i, false);
         });
@@ -351,13 +345,12 @@
     b.Append(func->Block(), [&] {
         auto* s = b.Switch(b.Constant(42_i));
         s->SetResults(b.InstructionResult(ty.i32()), b.InstructionResult(ty.bool_()));
-        auto* case_a = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                        core::ir::Switch::CaseSelector{nullptr}});
+        auto* case_a = b.Case(s, Vector{b.Constant(1_i), nullptr});
         b.Append(case_a, [&] {  //
             b.ExitSwitch(s, 10_i, true);
         });
 
-        auto* case_b = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(s, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(s, 20_i, false);
         });
@@ -387,8 +380,7 @@
     b.Append(func->Block(), [&] {
         auto* s = b.Switch(42_i);
         s->SetResults(b.InstructionResult(ty.i32()));
-        auto* case_a = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                        core::ir::Switch::CaseSelector{nullptr}});
+        auto* case_a = b.Case(s, Vector{b.Constant(1_i), nullptr});
         b.Append(case_a, [&] {  //
             auto* inner = b.If(true);
             inner->SetResults(b.InstructionResult(ty.i32()));
@@ -399,10 +391,10 @@
                 b.ExitIf(inner, 20_i);
             });
 
-            b.ExitSwitch(s, inner->Result());
+            b.ExitSwitch(s, inner->Result(0));
         });
 
-        auto* case_b = b.Case(s, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(s, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(s, 20_i);
         });
@@ -439,12 +431,10 @@
     b.Append(func->Block(), [&] {
         auto* outer = b.Switch(42_i);
         outer->SetResults(b.InstructionResult(ty.i32()));
-        auto* case_a = b.Case(outer, Vector{core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                            core::ir::Switch::CaseSelector{nullptr}});
+        auto* case_a = b.Case(outer, Vector{b.Constant(1_i), nullptr});
         b.Append(case_a, [&] {  //
             auto* inner = b.Switch(42_i);
-            auto* case_inner = b.Case(inner, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)},
-                                                    core::ir::Switch::CaseSelector{nullptr}});
+            auto* case_inner = b.Case(inner, Vector{b.Constant(2_i), nullptr});
             b.Append(case_inner, [&] {  //
                 b.ExitSwitch(inner);
             });
@@ -452,7 +442,7 @@
             b.ExitSwitch(outer, 10_i);
         });
 
-        auto* case_b = b.Case(outer, Vector{core::ir::Switch::CaseSelector{b.Constant(2_i)}});
+        auto* case_b = b.Case(outer, Vector{b.Constant(2_i)});
         b.Append(case_b, [&] {  //
             b.ExitSwitch(outer, 20_i);
         });
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index e6f3098..1ee7d51 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -34,20 +34,39 @@
 #include "src/tint/lang/spirv/writer/common/option_helpers.h"
 #include "src/tint/lang/spirv/writer/printer/printer.h"
 #include "src/tint/lang/spirv/writer/raise/raise.h"
-#include "src/tint/lang/wgsl/reader/lower/lower.h"
-
-#if TINT_BUILD_WGSL_READER
-#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
-#endif
 
 // Included by 'ast_printer.h', included again here for './tools/run gen' track the dependency.
 #include "spirv/unified1/spirv.h"
 
 namespace tint::spirv::writer {
 
-Output::Output() = default;
-Output::~Output() = default;
-Output::Output(const Output&) = default;
+Result<Output> Generate(core::ir::Module& ir, const Options& options) {
+    bool zero_initialize_workgroup_memory =
+        !options.disable_workgroup_init && options.use_zero_initialize_workgroup_memory_extension;
+
+    {
+        auto res = ValidateBindingOptions(options);
+        if (!res) {
+            return res.Failure();
+        }
+    }
+
+    Output output;
+
+    // Raise from core-dialect to SPIR-V-dialect.
+    if (auto res = raise::Raise(ir, options); !res) {
+        return std::move(res.Failure());
+    }
+
+    // Generate the SPIR-V code.
+    auto spirv = Print(ir, zero_initialize_workgroup_memory);
+    if (!spirv) {
+        return std::move(spirv.Failure());
+    }
+    output.spirv = std::move(spirv.Get());
+
+    return output;
+}
 
 Result<Output> Generate(const Program& program, const Options& options) {
     if (!program.IsValid()) {
@@ -58,60 +77,29 @@
         !options.disable_workgroup_init && options.use_zero_initialize_workgroup_memory_extension;
 
     {
-        diag::List validation_diagnostics;
-        if (!ValidateBindingOptions(options, validation_diagnostics)) {
-            return Failure{validation_diagnostics};
+        auto res = ValidateBindingOptions(options);
+        if (!res) {
+            return res.Failure();
         }
     }
 
     Output output;
 
-    if (options.use_tint_ir) {
-#if TINT_BUILD_WGSL_READER
-        // Convert the AST program to an IR module.
-        auto converted = wgsl::reader::ProgramToIR(program);
-        if (!converted) {
-            return converted.Failure();
-        }
-
-        auto ir = converted.Move();
-
-        // Lower from WGSL-dialect to core-dialect
-        if (auto res = wgsl::reader::Lower(ir); !res) {
-            return res.Failure();
-        }
-
-        // Raise from core-dialect to SPIR-V-dialect.
-        if (auto res = raise::Raise(ir, options); !res) {
-            return std::move(res.Failure());
-        }
-
-        // Generate the SPIR-V code.
-        auto spirv = Print(ir, zero_initialize_workgroup_memory);
-        if (!spirv) {
-            return std::move(spirv.Failure());
-        }
-        output.spirv = std::move(spirv.Get());
-#else
-        return Failure{"use_tint_ir requires building with TINT_BUILD_WGSL_READER"};
-#endif
-    } else {
-        // Sanitize the program.
-        auto sanitized_result = Sanitize(program, options);
-        if (!sanitized_result.program.IsValid()) {
-            return Failure{sanitized_result.program.Diagnostics()};
-        }
-
-        // Generate the SPIR-V code.
-        auto impl = std::make_unique<ASTPrinter>(
-            sanitized_result.program, zero_initialize_workgroup_memory,
-            options.experimental_require_subgroup_uniform_control_flow);
-        if (!impl->Generate()) {
-            return Failure{impl->Diagnostics()};
-        }
-        output.spirv = std::move(impl->Result());
+    // Sanitize the program.
+    auto sanitized_result = Sanitize(program, options);
+    if (!sanitized_result.program.IsValid()) {
+        return Failure{sanitized_result.program.Diagnostics()};
     }
 
+    // Generate the SPIR-V code.
+    auto impl =
+        std::make_unique<ASTPrinter>(sanitized_result.program, zero_initialize_workgroup_memory,
+                                     options.experimental_require_subgroup_uniform_control_flow);
+    if (!impl->Generate()) {
+        return Failure{impl->Diagnostics()};
+    }
+    output.spirv = std::move(impl->Result());
+
     return output;
 }
 
diff --git a/src/tint/lang/spirv/writer/writer.h b/src/tint/lang/spirv/writer/writer.h
index b050065..807aa9f 100644
--- a/src/tint/lang/spirv/writer/writer.h
+++ b/src/tint/lang/spirv/writer/writer.h
@@ -30,6 +30,7 @@
 
 #include <string>
 
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/spirv/writer/common/options.h"
 #include "src/tint/lang/spirv/writer/output.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
@@ -44,6 +45,13 @@
 
 /// Generate SPIR-V for a program, according to a set of configuration options.
 /// The result will contain the SPIR-V or failure.
+/// @param ir the IR module to translate to SPIR-V
+/// @param options the configuration options to use when generating SPIR-V
+/// @returns the resulting SPIR-V and supplementary information, or failure.
+Result<Output> Generate(core::ir::Module& ir, const Options& options);
+
+/// Generate SPIR-V for a program, according to a set of configuration options.
+/// The result will contain the SPIR-V or failure.
 /// @param program the program to translate to SPIR-V
 /// @param options the configuration options to use when generating SPIR-V
 /// @returns the resulting SPIR-V and supplementary information, or failure.
diff --git a/src/tint/lang/spirv/writer/writer_bench.cc b/src/tint/lang/spirv/writer/writer_bench.cc
index 5d8766d..2112631 100644
--- a/src/tint/lang/spirv/writer/writer_bench.cc
+++ b/src/tint/lang/spirv/writer/writer_bench.cc
@@ -30,31 +30,50 @@
 #include "src/tint/cmd/bench/bench.h"
 #include "src/tint/lang/spirv/writer/writer.h"
 
+#if TINT_BUILD_WGSL_READER
+#include "src/tint/lang/wgsl/reader/reader.h"
+#endif  // TINT_BUILD_WGSL_READER
+
 namespace tint::spirv::writer {
 namespace {
 
-void RunBenchmark(benchmark::State& state, std::string input_name, Options options) {
+void GenerateSPIRV(benchmark::State& state, std::string input_name) {
     auto res = bench::LoadProgram(input_name);
     if (!res) {
         state.SkipWithError(res.Failure().reason.str());
         return;
     }
     for (auto _ : state) {
-        auto gen_res = Generate(res->program, options);
+        auto gen_res = Generate(res->program, {});
         if (!gen_res) {
             state.SkipWithError(gen_res.Failure().reason.str());
         }
     }
 }
 
-void GenerateSPIRV(benchmark::State& state, std::string input_name) {
-    RunBenchmark(state, input_name, {});
-}
-
 void GenerateSPIRV_UseIR(benchmark::State& state, std::string input_name) {
-    Options options;
-    options.use_tint_ir = true;
-    RunBenchmark(state, input_name, std::move(options));
+#if TINT_BUILD_WGSL_READER
+    auto res = bench::LoadProgram(input_name);
+    if (!res) {
+        state.SkipWithError(res.Failure().reason.str());
+        return;
+    }
+    for (auto _ : state) {
+        // Convert the AST program to an IR module.
+        auto ir = tint::wgsl::reader::ProgramToLoweredIR(res->program);
+        if (!ir) {
+            state.SkipWithError(ir.Failure().reason.str());
+            return;
+        }
+
+        auto gen_res = Generate(ir.Get(), {});
+        if (!gen_res) {
+            state.SkipWithError(gen_res.Failure().reason.str());
+        }
+    }
+#else
+#error "WGSL Reader is required to build IR generator"
+#endif  // TINT_BUILD_WGSL_READER
 }
 
 TINT_BENCHMARK_PROGRAMS(GenerateSPIRV);
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/spirv/writer/writer_fuzz.cc
similarity index 65%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/lang/spirv/writer/writer_fuzz.cc
index 6f0f657..ff37ab7 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/spirv/writer/writer_fuzz.cc
@@ -25,28 +25,29 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#include "src/tint/lang/spirv/writer/writer.h"
 
-#include <string>
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+#include "src/tint/lang/spirv/validate/validate.h"
+#include "src/tint/lang/spirv/writer/helpers/generate_bindings.h"
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
+namespace tint::spirv::writer {
+namespace {
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
+void IRPrinterFuzzer(core::ir::Module& module, Options options) {
+    options.bindings = GenerateBindings(module);
+    auto output = Generate(module, options);
+    if (!output) {
+        return;
+    }
+    auto& spirv = output->spirv;
+    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size())); !res) {
+        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
+                   << res.Failure();
+    }
 }
 
-namespace tint::wgsl::writer {
+}  // namespace
+}  // namespace tint::spirv::writer
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
-
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
diff --git a/src/tint/lang/wgsl/BUILD.bazel b/src/tint/lang/wgsl/BUILD.bazel
index 72a4cc6..6541b22 100644
--- a/src/tint/lang/wgsl/BUILD.bazel
+++ b/src/tint/lang/wgsl/BUILD.bazel
@@ -43,14 +43,12 @@
     "diagnostic_rule.cc",
     "diagnostic_severity.cc",
     "extension.cc",
-    "language_feature.cc",
   ],
   hdrs = [
     "builtin_fn.h",
     "diagnostic_rule.h",
     "diagnostic_severity.h",
     "extension.h",
-    "language_feature.h",
   ],
   deps = [
     "//src/tint/utils/containers",
@@ -73,7 +71,6 @@
     "diagnostic_rule_test.cc",
     "diagnostic_severity_test.cc",
     "extension_test.cc",
-    "language_feature_test.cc",
     "wgsl_test.cc",
   ] + select({
     "//conditions:default": [],
@@ -94,6 +91,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers:test",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
diff --git a/src/tint/lang/wgsl/BUILD.cmake b/src/tint/lang/wgsl/BUILD.cmake
index 7e09df7..4e0a08b 100644
--- a/src/tint/lang/wgsl/BUILD.cmake
+++ b/src/tint/lang/wgsl/BUILD.cmake
@@ -36,6 +36,7 @@
 
 include(lang/wgsl/ast/BUILD.cmake)
 include(lang/wgsl/common/BUILD.cmake)
+include(lang/wgsl/features/BUILD.cmake)
 include(lang/wgsl/helpers/BUILD.cmake)
 include(lang/wgsl/inspector/BUILD.cmake)
 include(lang/wgsl/intrinsic/BUILD.cmake)
@@ -59,8 +60,6 @@
   lang/wgsl/diagnostic_severity.h
   lang/wgsl/extension.cc
   lang/wgsl/extension.h
-  lang/wgsl/language_feature.cc
-  lang/wgsl/language_feature.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl lib
@@ -83,7 +82,6 @@
   lang/wgsl/diagnostic_rule_test.cc
   lang/wgsl/diagnostic_severity_test.cc
   lang/wgsl/extension_test.cc
-  lang/wgsl/language_feature_test.cc
   lang/wgsl/wgsl_test.cc
 )
 
@@ -96,6 +94,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
@@ -176,6 +175,7 @@
 
 tint_target_add_dependencies(tint_lang_wgsl_fuzz fuzz
   tint_api_common
+  tint_cmd_fuzz_ir_fuzz
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_ir
@@ -183,13 +183,14 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
-  tint_lang_wgsl_helpers
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_lang_wgsl_writer_ir_to_program
   tint_lang_wgsl_writer_raise
+  tint_utils_bytes
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -207,7 +208,6 @@
 
 if(TINT_BUILD_WGSL_READER)
   tint_target_add_dependencies(tint_lang_wgsl_fuzz fuzz
-    tint_cmd_fuzz_wgsl_fuzz
     tint_lang_wgsl_reader_parser
     tint_lang_wgsl_reader_program_to_ir
   )
diff --git a/src/tint/lang/wgsl/BUILD.gn b/src/tint/lang/wgsl/BUILD.gn
index e27e459..0b9801f 100644
--- a/src/tint/lang/wgsl/BUILD.gn
+++ b/src/tint/lang/wgsl/BUILD.gn
@@ -52,8 +52,6 @@
     "diagnostic_severity.h",
     "extension.cc",
     "extension.h",
-    "language_feature.cc",
-    "language_feature.h",
   ]
   deps = [
     "${tint_src_dir}/utils/containers",
@@ -73,7 +71,6 @@
       "diagnostic_rule_test.cc",
       "diagnostic_severity_test.cc",
       "extension_test.cc",
-      "language_feature_test.cc",
       "wgsl_test.cc",
     ]
     deps = [
@@ -86,6 +83,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/helpers:unittests",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/reader/lower",
@@ -150,6 +148,7 @@
   sources = []
   deps = [
     "${tint_src_dir}/api/common",
+    "${tint_src_dir}/cmd/fuzz/ir:fuzz",
     "${tint_src_dir}/lang/core",
     "${tint_src_dir}/lang/core/constant",
     "${tint_src_dir}/lang/core/ir",
@@ -157,13 +156,14 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
-    "${tint_src_dir}/lang/wgsl/helpers",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/reader/lower",
     "${tint_src_dir}/lang/wgsl/resolver",
     "${tint_src_dir}/lang/wgsl/sem",
     "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
     "${tint_src_dir}/lang/wgsl/writer/raise",
+    "${tint_src_dir}/utils/bytes",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
@@ -181,7 +181,6 @@
 
   if (tint_build_wgsl_reader) {
     deps += [
-      "${tint_src_dir}/cmd/fuzz/wgsl:fuzz",
       "${tint_src_dir}/lang/wgsl/reader/parser",
       "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
     ]
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
index f7514c9..c44c746 100644
--- a/src/tint/lang/wgsl/ast/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -208,6 +208,7 @@
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -313,6 +314,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/ast/BUILD.cmake b/src/tint/lang/wgsl/ast/BUILD.cmake
index fe4eef9..3664cd7 100644
--- a/src/tint/lang/wgsl/ast/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/BUILD.cmake
@@ -209,6 +209,7 @@
   tint_lang_core_constant
   tint_lang_core_type
   tint_lang_wgsl
+  tint_lang_wgsl_features
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -305,6 +306,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/wgsl/ast/BUILD.gn b/src/tint/lang/wgsl/ast/BUILD.gn
index 6cc263d..fe2138f 100644
--- a/src/tint/lang/wgsl/ast/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/BUILD.gn
@@ -211,6 +211,7 @@
     "${tint_src_dir}/lang/core/constant",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/wgsl",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
@@ -305,6 +306,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index 1b80ec9..7bdaf1e 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -1623,7 +1623,7 @@
     /// @param feature the feature to require
     /// @return a `ast::Requires` requiring the given language feature.
     const ast::Requires* Require(wgsl::LanguageFeature feature) {
-        auto* req = create<ast::Requires>(wgsl::LanguageFeatures({feature}));
+        auto* req = create<ast::Requires>(Requires::LanguageFeatures({feature}));
         AST().AddRequires(req);
         return req;
     }
@@ -1633,7 +1633,7 @@
     /// @param feature the feature to require
     /// @return a `ast::Requires` requiring the given language feature.
     const ast::Requires* Require(const Source& source, wgsl::LanguageFeature feature) {
-        auto* req = create<ast::Requires>(source, wgsl::LanguageFeatures({feature}));
+        auto* req = create<ast::Requires>(source, Requires::LanguageFeatures({feature}));
         AST().AddRequires(req);
         return req;
     }
diff --git a/src/tint/lang/wgsl/ast/requires.cc b/src/tint/lang/wgsl/ast/requires.cc
index 059c35f..0ea77e0 100644
--- a/src/tint/lang/wgsl/ast/requires.cc
+++ b/src/tint/lang/wgsl/ast/requires.cc
@@ -34,7 +34,7 @@
 
 namespace tint::ast {
 
-Requires::Requires(GenerationID pid, NodeID nid, const Source& src, wgsl::LanguageFeatures feats)
+Requires::Requires(GenerationID pid, NodeID nid, const Source& src, LanguageFeatures feats)
     : Base(pid, nid, src), features(std::move(feats)) {}
 
 Requires::~Requires() = default;
diff --git a/src/tint/lang/wgsl/ast/requires.h b/src/tint/lang/wgsl/ast/requires.h
index c94d6b0..eb1f5b8 100644
--- a/src/tint/lang/wgsl/ast/requires.h
+++ b/src/tint/lang/wgsl/ast/requires.h
@@ -33,7 +33,8 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/node.h"
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
+#include "src/tint/utils/containers/unique_vector.h"
 
 namespace tint::ast {
 
@@ -44,12 +45,15 @@
 /// ```
 class Requires final : public Castable<Requires, Node> {
   public:
+    /// A unique list of WGSL language features
+    using LanguageFeatures = UniqueVector<wgsl::LanguageFeature, 4>;
+
     /// Create a requires directive
     /// @param pid the identifier of the program that owns this node
     /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param feats the language features being required by this directive
-    Requires(GenerationID pid, NodeID nid, const Source& src, wgsl::LanguageFeatures feats);
+    Requires(GenerationID pid, NodeID nid, const Source& src, LanguageFeatures feats);
 
     /// Destructor
     ~Requires() override;
@@ -60,7 +64,7 @@
     const Requires* Clone(CloneContext& ctx) const override;
 
     /// The features being required by this directive.
-    const wgsl::LanguageFeatures features;
+    const LanguageFeatures features;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.bazel b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
index 6410632..c8ff399 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
@@ -115,6 +115,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -187,6 +188,7 @@
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index 9100dc7..35b0dcc 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -114,6 +114,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -188,6 +189,7 @@
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -240,8 +242,10 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
+  tint_utils_bytes
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -249,6 +253,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_symbol
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 0a1bc92..e0af19e 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -118,6 +118,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/resolver",
     "${tint_src_dir}/lang/wgsl/sem",
@@ -189,6 +190,7 @@
         "${tint_src_dir}/lang/wgsl/ast:unittests",
         "${tint_src_dir}/lang/wgsl/ast/transform",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
@@ -227,8 +229,10 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast/transform",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/bytes",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
@@ -236,6 +240,7 @@
       "${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",
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 8bc328b..e2d0dde 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
@@ -456,8 +456,6 @@
 
 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> {
@@ -466,8 +464,6 @@
 )";
 
     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> {
@@ -554,8 +550,6 @@
 
 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>) {
@@ -564,8 +558,6 @@
 )";
 
     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>) {
diff --git a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory_fuzz.cc b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory_fuzz.cc
index 246fa9b..7fe5685 100644
--- a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory_fuzz.cc
+++ b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory_fuzz.cc
@@ -27,7 +27,7 @@
 
 #include "src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h"
 
-#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
 #include "src/tint/lang/wgsl/ast/module.h"
 
 namespace tint::ast::transform {
diff --git a/src/tint/lang/wgsl/common/BUILD.bazel b/src/tint/lang/wgsl/common/BUILD.bazel
index d587bc9..6607f76 100644
--- a/src/tint/lang/wgsl/common/BUILD.bazel
+++ b/src/tint/lang/wgsl/common/BUILD.bazel
@@ -46,6 +46,7 @@
   ],
   deps = [
     "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/utils/containers",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
diff --git a/src/tint/lang/wgsl/common/BUILD.cmake b/src/tint/lang/wgsl/common/BUILD.cmake
index e99cd84..308e43f 100644
--- a/src/tint/lang/wgsl/common/BUILD.cmake
+++ b/src/tint/lang/wgsl/common/BUILD.cmake
@@ -45,6 +45,7 @@
 
 tint_target_add_dependencies(tint_lang_wgsl_common lib
   tint_lang_wgsl
+  tint_lang_wgsl_features
   tint_utils_containers
   tint_utils_ice
   tint_utils_macros
diff --git a/src/tint/lang/wgsl/common/BUILD.gn b/src/tint/lang/wgsl/common/BUILD.gn
index 7b21348..6751319 100644
--- a/src/tint/lang/wgsl/common/BUILD.gn
+++ b/src/tint/lang/wgsl/common/BUILD.gn
@@ -45,6 +45,7 @@
   ]
   deps = [
     "${tint_src_dir}/lang/wgsl",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
diff --git a/src/tint/lang/wgsl/common/allowed_features.h b/src/tint/lang/wgsl/common/allowed_features.h
index d494352..7549fc7 100644
--- a/src/tint/lang/wgsl/common/allowed_features.h
+++ b/src/tint/lang/wgsl/common/allowed_features.h
@@ -31,7 +31,7 @@
 #include <unordered_set>
 
 #include "src/tint/lang/wgsl/extension.h"
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::wgsl {
diff --git a/src/tint/lang/wgsl/diagnostic_rule_test.cc b/src/tint/lang/wgsl/diagnostic_rule_test.cc
index 2aa78b2..85eccfe 100644
--- a/src/tint/lang/wgsl/diagnostic_rule_test.cc
+++ b/src/tint/lang/wgsl/diagnostic_rule_test.cc
@@ -84,7 +84,7 @@
 TEST_P(CoreDiagnosticRulePrintTest, Print) {
     CoreDiagnosticRule value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, CoreDiagnosticRulePrintTest, testing::ValuesIn(kValidCases));
@@ -136,7 +136,7 @@
 TEST_P(ChromiumDiagnosticRulePrintTest, Print) {
     ChromiumDiagnosticRule value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases,
diff --git a/src/tint/lang/wgsl/diagnostic_severity_test.cc b/src/tint/lang/wgsl/diagnostic_severity_test.cc
index 5b09668..88e2b7a 100644
--- a/src/tint/lang/wgsl/diagnostic_severity_test.cc
+++ b/src/tint/lang/wgsl/diagnostic_severity_test.cc
@@ -90,7 +90,7 @@
 TEST_P(DiagnosticSeverityPrintTest, Print) {
     DiagnosticSeverity value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, DiagnosticSeverityPrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/wgsl/extension.cc b/src/tint/lang/wgsl/extension.cc
index e30d7b2..6b001ef 100644
--- a/src/tint/lang/wgsl/extension.cc
+++ b/src/tint/lang/wgsl/extension.cc
@@ -60,9 +60,6 @@
     if (str == "chromium_experimental_push_constant") {
         return Extension::kChromiumExperimentalPushConstant;
     }
-    if (str == "chromium_experimental_read_write_storage_texture") {
-        return Extension::kChromiumExperimentalReadWriteStorageTexture;
-    }
     if (str == "chromium_experimental_subgroups") {
         return Extension::kChromiumExperimentalSubgroups;
     }
@@ -94,8 +91,6 @@
             return "chromium_experimental_pixel_local";
         case Extension::kChromiumExperimentalPushConstant:
             return "chromium_experimental_push_constant";
-        case Extension::kChromiumExperimentalReadWriteStorageTexture:
-            return "chromium_experimental_read_write_storage_texture";
         case Extension::kChromiumExperimentalSubgroups:
             return "chromium_experimental_subgroups";
         case Extension::kChromiumInternalDualSourceBlending:
diff --git a/src/tint/lang/wgsl/extension.h b/src/tint/lang/wgsl/extension.h
index 0e259f1..259c925 100644
--- a/src/tint/lang/wgsl/extension.h
+++ b/src/tint/lang/wgsl/extension.h
@@ -52,7 +52,6 @@
     kChromiumExperimentalFullPtrParameters,
     kChromiumExperimentalPixelLocal,
     kChromiumExperimentalPushConstant,
-    kChromiumExperimentalReadWriteStorageTexture,
     kChromiumExperimentalSubgroups,
     kChromiumInternalDualSourceBlending,
     kChromiumInternalRelaxedUniformLayout,
@@ -77,17 +76,11 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr std::string_view kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",
-    "chromium_experimental_dp4a",
-    "chromium_experimental_framebuffer_fetch",
-    "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",
+    "chromium_disable_uniformity_analysis",     "chromium_experimental_dp4a",
+    "chromium_experimental_framebuffer_fetch",  "chromium_experimental_full_ptr_parameters",
+    "chromium_experimental_pixel_local",        "chromium_experimental_push_constant",
+    "chromium_experimental_subgroups",          "chromium_internal_dual_source_blending",
+    "chromium_internal_relaxed_uniform_layout", "f16",
 };
 
 /// All extensions
@@ -98,7 +91,6 @@
     Extension::kChromiumExperimentalFullPtrParameters,
     Extension::kChromiumExperimentalPixelLocal,
     Extension::kChromiumExperimentalPushConstant,
-    Extension::kChromiumExperimentalReadWriteStorageTexture,
     Extension::kChromiumExperimentalSubgroups,
     Extension::kChromiumInternalDualSourceBlending,
     Extension::kChromiumInternalRelaxedUniformLayout,
diff --git a/src/tint/lang/wgsl/extension_bench.cc b/src/tint/lang/wgsl/extension_bench.cc
index 06b7f67..63c8636 100644
--- a/src/tint/lang/wgsl/extension_bench.cc
+++ b/src/tint/lang/wgsl/extension_bench.cc
@@ -87,41 +87,34 @@
         "chromium_epHrimentkk_psh_constant",
         "chromium_expegimenja_puRRh_costant",
         "chromium_bxperimental_push_contan",
-        "chromium_experimental_read_wjite_storage_texture",
-        "chromium_expeimental_read_write_storage_texture",
-        "chqomium_experimentl_rad_write_storage_texture",
-        "chromium_experimental_read_write_storage_texture",
-        "chromium_experimental_read_writNN_storage_texure",
-        "chromiumexperimental_read_write_stovvage_texure",
-        "chromium_xperimental_read_write_storage_textQQre",
-        "cffromium_experimenralsubgrous",
-        "chromium_experimentjl_subgroups",
-        "chromiu2ww8NNperimental_subgoups",
+        "chromium_experimental_sjbgroups",
+        "chromium_experimental_sbgroups",
+        "cromum_experimentalqsubgroups",
         "chromium_experimental_subgroups",
-        "chromium_experimental_subgroup",
-        "chromiurr_experimental_subgroups",
-        "cGromium_experimental_subgroups",
-        "chromium_internFFl_dual_source_blending",
-        "cEromum_internl_dual_source_bleding",
-        "chromium_internal_dual_source_brrendin",
+        "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",
-        "chromium_internal_dual_surce_blendin",
-        "chromium_iXterJJa_dual_souDce_blending",
-        "chromi8m_ineral_dual_source_blendin",
-        "chromu11_internal_relaed_uniform_kayou",
-        "chromium_internal_relaxd_uniform_layout",
-        "chromium_internal_elaxed_uJiform_layout",
+        "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_intcrnal_relaxed_uniform_layout",
-        "chromiOm_internal_relaxed_uniform_layout",
-        "chromiutt_intervva_KK_relaxed_uniform_layout",
-        "xx8",
-        "__F",
-        "f1q",
+        "chromiuminternal_relaxed_uniform_layut",
+        "cXroDium_internal_rJJlaed_uniform_layout",
+        "chromium_int8nal_relaed_uniform_layut",
+        "k",
+        "16",
+        "J1",
         "f16",
-        "331O",
-        "ftt6QQ",
-        "666",
+        "c16",
+        "fO6",
+        "_KKttvv",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/wgsl/extension_test.cc b/src/tint/lang/wgsl/extension_test.cc
index 029deb2..c40bacf 100644
--- a/src/tint/lang/wgsl/extension_test.cc
+++ b/src/tint/lang/wgsl/extension_test.cc
@@ -64,8 +64,6 @@
      Extension::kChromiumExperimentalFullPtrParameters},
     {"chromium_experimental_pixel_local", Extension::kChromiumExperimentalPixelLocal},
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
-    {"chromium_experimental_read_write_storage_texture",
-     Extension::kChromiumExperimentalReadWriteStorageTexture},
     {"chromium_experimental_subgroups", Extension::kChromiumExperimentalSubgroups},
     {"chromium_internal_dual_source_blending", Extension::kChromiumInternalDualSourceBlending},
     {"chromium_internal_relaxed_uniform_layout", Extension::kChromiumInternalRelaxedUniformLayout},
@@ -91,21 +89,18 @@
     {"chromium_experEmental_push_constant", Extension::kUndefined},
     {"chPPomiumexperimental_push_conTTtant", Extension::kUndefined},
     {"chromixxm_experimentddl_push_constnt", Extension::kUndefined},
-    {"44hromium_experimental_read_write_storage_texture", Extension::kUndefined},
-    {"chromium_experimental_reaSS_wriVVe_storage_texture", Extension::kUndefined},
-    {"chro22ium_eRperimental_read_Rrite_storag_texture", Extension::kUndefined},
-    {"chromium_experimental_sbgroup9", Extension::kUndefined},
-    {"cromium_experimental_subgroups", Extension::kUndefined},
-    {"VhrHium_experimental_subOOrouRRs", Extension::kUndefined},
-    {"chromium_internay_dual_sorce_blending", Extension::kUndefined},
-    {"chrnnmium_internal_duGrr_source_bllend77ng", Extension::kUndefined},
-    {"chromiu4_inter00al_dual_source_blending", Extension::kUndefined},
-    {"chrmoom_internal_relaxed_uniform_lyout", Extension::kUndefined},
-    {"chroium_internal_rlaxed_uniform_layzzut", Extension::kUndefined},
-    {"chromium_internaii_r11axed_uppifor_layout", Extension::kUndefined},
-    {"f1XX", Extension::kUndefined},
-    {"55199II", Extension::kUndefined},
-    {"frSSHHa", 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>;
@@ -124,7 +119,7 @@
 TEST_P(ExtensionPrintTest, Print) {
     Extension value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, ExtensionPrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/wgsl/features/BUILD.bazel b/src/tint/lang/wgsl/features/BUILD.bazel
new file mode 100644
index 0000000..cec9088
--- /dev/null
+++ b/src/tint/lang/wgsl/features/BUILD.bazel
@@ -0,0 +1,73 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "features",
+  srcs = [
+    "language_feature.cc",
+  ],
+  hdrs = [
+    "language_feature.h",
+  ],
+  deps = [
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "language_feature_test.cc",
+  ],
+  deps = [
+    "//src/tint/lang/wgsl/features",
+    "//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/lang/wgsl/features/BUILD.cmake b/src/tint/lang/wgsl/features/BUILD.cmake
new file mode 100644
index 0000000..9bbfe60
--- /dev/null
+++ b/src/tint/lang/wgsl/features/BUILD.cmake
@@ -0,0 +1,68 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_lang_wgsl_features
+# Kind:      lib
+################################################################################
+tint_add_target(tint_lang_wgsl_features lib
+  lang/wgsl/features/language_feature.cc
+  lang/wgsl/features/language_feature.h
+)
+
+################################################################################
+# Target:    tint_lang_wgsl_features_test
+# Kind:      test
+################################################################################
+tint_add_target(tint_lang_wgsl_features_test test
+  lang/wgsl/features/language_feature_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_wgsl_features_test test
+  tint_lang_wgsl_features
+  tint_utils_containers
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_wgsl_features_test test
+  "gtest"
+)
diff --git a/src/tint/lang/wgsl/features/BUILD.gn b/src/tint/lang/wgsl/features/BUILD.gn
new file mode 100644
index 0000000..db3557a
--- /dev/null
+++ b/src/tint/lang/wgsl/features/BUILD.gn
@@ -0,0 +1,50 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# NOTE: This file is intentionally *not* generated by 'tools/src/cmd/gen' as
+# this GN target is to be used outside of Dawn and must be entirely dependency
+# free.
+#
+# GEN_BUILD:DO_NOT_GENERATE
+#
+################################################################################
+
+source_set("features") {
+  sources = [
+    "language_feature.cc",
+    "language_feature.h",
+  ]
+  deps = []
+
+  include_dirs = [ "../../../../../" ]
+}
+
+source_set("unittests") {
+  testonly = true
+  sources = []  # empty as we don't want to depend on gtest, etc.
+}
diff --git a/src/tint/lang/wgsl/language_feature.cc b/src/tint/lang/wgsl/features/language_feature.cc
similarity index 95%
rename from src/tint/lang/wgsl/language_feature.cc
rename to src/tint/lang/wgsl/features/language_feature.cc
index a5e0430..df6a24c 100644
--- a/src/tint/lang/wgsl/language_feature.cc
+++ b/src/tint/lang/wgsl/features/language_feature.cc
@@ -27,14 +27,14 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 // File generated by 'tools/src/cmd/gen' using the template:
-//   src/tint/lang/wgsl/language_feature.cc.tmpl
+//   src/tint/lang/wgsl/features/language_feature.cc.tmpl
 //
 // To regenerate run: './tools/run gen'
 //
 //                       Do not modify this file directly
 ////////////////////////////////////////////////////////////////////////////////
 
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
 
 namespace tint::wgsl {
 
diff --git a/src/tint/lang/wgsl/language_feature.cc.tmpl b/src/tint/lang/wgsl/features/language_feature.cc.tmpl
similarity index 91%
rename from src/tint/lang/wgsl/language_feature.cc.tmpl
rename to src/tint/lang/wgsl/features/language_feature.cc.tmpl
index 782b515..3e0b174 100644
--- a/src/tint/lang/wgsl/language_feature.cc.tmpl
+++ b/src/tint/lang/wgsl/features/language_feature.cc.tmpl
@@ -12,7 +12,7 @@
 {{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
 {{- $enum := ($I.Sem.Enum "language_feature") -}}
 
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
 
 namespace tint::wgsl {
 
diff --git a/src/tint/lang/wgsl/language_feature.h b/src/tint/lang/wgsl/features/language_feature.h
similarity index 80%
rename from src/tint/lang/wgsl/language_feature.h
rename to src/tint/lang/wgsl/features/language_feature.h
index afdbdba..1187bfa 100644
--- a/src/tint/lang/wgsl/language_feature.h
+++ b/src/tint/lang/wgsl/features/language_feature.h
@@ -27,18 +27,18 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 // File generated by 'tools/src/cmd/gen' using the template:
-//   src/tint/lang/wgsl/language_feature.h.tmpl
+//   src/tint/lang/wgsl/features/language_feature.h.tmpl
 //
 // To regenerate run: './tools/run gen'
 //
 //                       Do not modify this file directly
 ////////////////////////////////////////////////////////////////////////////////
 
-#ifndef SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
-#define SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+#ifndef SRC_TINT_LANG_WGSL_FEATURES_LANGUAGE_FEATURE_H_
+#define SRC_TINT_LANG_WGSL_FEATURES_LANGUAGE_FEATURE_H_
 
-#include "src/tint/utils/containers/unique_vector.h"
-#include "src/tint/utils/traits/traits.h"
+#include <cstdint>
+#include <string>
 
 namespace tint::wgsl {
 
@@ -53,14 +53,6 @@
 /// @returns the string for the given enum value
 std::string_view ToString(LanguageFeature value);
 
-/// @param out the stream to write to
-/// @param value the LanguageFeature
-/// @returns @p out so calls can be chained
-template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, LanguageFeature value) {
-    return out << ToString(value);
-}
-
 /// ParseLanguageFeature parses a LanguageFeature from a string.
 /// @param str the string to parse
 /// @returns the parsed enum, or LanguageFeature::kUndefined if the string could not be parsed.
@@ -75,9 +67,6 @@
     LanguageFeature::kReadonlyAndReadwriteStorageTextures,
 };
 
-/// A unique vector of language features
-using LanguageFeatures = UniqueVector<LanguageFeature, 4>;
-
 }  // namespace tint::wgsl
 
-#endif  // SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+#endif  // SRC_TINT_LANG_WGSL_FEATURES_LANGUAGE_FEATURE_H_
diff --git a/src/tint/lang/wgsl/language_feature.h.tmpl b/src/tint/lang/wgsl/features/language_feature.h.tmpl
similarity index 71%
rename from src/tint/lang/wgsl/language_feature.h.tmpl
rename to src/tint/lang/wgsl/features/language_feature.h.tmpl
index 7c88975..da42959 100644
--- a/src/tint/lang/wgsl/language_feature.h.tmpl
+++ b/src/tint/lang/wgsl/features/language_feature.h.tmpl
@@ -12,17 +12,17 @@
 {{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
 {{- $enum := ($I.Sem.Enum "language_feature") -}}
 
-#ifndef SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
-#define SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+#ifndef SRC_TINT_LANG_WGSL_FEATURES_LANGUAGE_FEATURE_H_
+#define SRC_TINT_LANG_WGSL_FEATURES_LANGUAGE_FEATURE_H_
 
-#include "src/tint/utils/traits/traits.h"
-#include "src/tint/utils/containers/unique_vector.h"
+#include <cstdint>
+#include <string>
 
 namespace tint::wgsl {
 
 /// An enumerator of WGSL language features
 /// @see src/tint/lang/wgsl/intrinsics.def for language feature descriptions
-{{ Eval "DeclareEnum" $enum}}
+{{ Eval "DeclareEnum" "Enum" $enum "EmitOStream" false}}
 
 /// All features
 static constexpr LanguageFeature kAllLanguageFeatures[] = {
@@ -31,9 +31,6 @@
 {{-   end }}
 };
 
-/// A unique vector of language features
-using LanguageFeatures = UniqueVector<LanguageFeature, 4>;
-
 }  // namespace tint::wgsl
 
-#endif  // SRC_TINT_LANG_WGSL_LANGUAGE_FEATURE_H_
+#endif  // SRC_TINT_LANG_WGSL_FEATURES_LANGUAGE_FEATURE_H_
diff --git a/src/tint/lang/wgsl/language_feature_test.cc b/src/tint/lang/wgsl/features/language_feature_test.cc
similarity index 95%
rename from src/tint/lang/wgsl/language_feature_test.cc
rename to src/tint/lang/wgsl/features/language_feature_test.cc
index 4597ce9..295e3a5 100644
--- a/src/tint/lang/wgsl/language_feature_test.cc
+++ b/src/tint/lang/wgsl/features/language_feature_test.cc
@@ -27,14 +27,14 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 // File generated by 'tools/src/cmd/gen' using the template:
-//   src/tint/lang/wgsl/language_feature_test.cc.tmpl
+//   src/tint/lang/wgsl/features/language_feature_test.cc.tmpl
 //
 // To regenerate run: './tools/run gen'
 //
 //                       Do not modify this file directly
 ////////////////////////////////////////////////////////////////////////////////
 
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
 
 #include <gtest/gtest.h>
 
@@ -83,7 +83,7 @@
 TEST_P(LanguageFeaturePrintTest, Print) {
     LanguageFeature value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, LanguageFeaturePrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/lang/wgsl/language_feature_test.cc.tmpl b/src/tint/lang/wgsl/features/language_feature_test.cc.tmpl
similarity index 92%
rename from src/tint/lang/wgsl/language_feature_test.cc.tmpl
rename to src/tint/lang/wgsl/features/language_feature_test.cc.tmpl
index fe98798..1a3c4cb 100644
--- a/src/tint/lang/wgsl/language_feature_test.cc.tmpl
+++ b/src/tint/lang/wgsl/features/language_feature_test.cc.tmpl
@@ -12,7 +12,7 @@
 {{- Import "src/tint/utils/templates/enums.tmpl.inc" -}}
 {{- $enum := ($I.Sem.Enum "language_feature") -}}
 
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
 
 #include <gtest/gtest.h>
 
diff --git a/src/tint/lang/wgsl/helpers/BUILD.bazel b/src/tint/lang/wgsl/helpers/BUILD.bazel
index acc0183..c43af56 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.bazel
+++ b/src/tint/lang/wgsl/helpers/BUILD.bazel
@@ -58,6 +58,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast/transform",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -102,6 +103,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
diff --git a/src/tint/lang/wgsl/helpers/BUILD.cmake b/src/tint/lang/wgsl/helpers/BUILD.cmake
index dbf2a9e..f76d258 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.cmake
+++ b/src/tint/lang/wgsl/helpers/BUILD.cmake
@@ -57,6 +57,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_transform
+  tint_lang_wgsl_features
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -96,6 +97,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
diff --git a/src/tint/lang/wgsl/helpers/BUILD.gn b/src/tint/lang/wgsl/helpers/BUILD.gn
index 4ae3943..91edc4b 100644
--- a/src/tint/lang/wgsl/helpers/BUILD.gn
+++ b/src/tint/lang/wgsl/helpers/BUILD.gn
@@ -61,6 +61,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/ast/transform",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/inspector",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
@@ -98,6 +99,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/helpers",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
diff --git a/src/tint/lang/wgsl/inspector/BUILD.bazel b/src/tint/lang/wgsl/inspector/BUILD.bazel
index a85b5c8..84c7690 100644
--- a/src/tint/lang/wgsl/inspector/BUILD.bazel
+++ b/src/tint/lang/wgsl/inspector/BUILD.bazel
@@ -57,6 +57,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -95,6 +96,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/inspector",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
diff --git a/src/tint/lang/wgsl/inspector/BUILD.cmake b/src/tint/lang/wgsl/inspector/BUILD.cmake
index a36fd2c..71cddb5 100644
--- a/src/tint/lang/wgsl/inspector/BUILD.cmake
+++ b/src/tint/lang/wgsl/inspector/BUILD.cmake
@@ -56,6 +56,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -96,6 +97,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_inspector
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
diff --git a/src/tint/lang/wgsl/inspector/BUILD.gn b/src/tint/lang/wgsl/inspector/BUILD.gn
index a01205f..5b75094 100644
--- a/src/tint/lang/wgsl/inspector/BUILD.gn
+++ b/src/tint/lang/wgsl/inspector/BUILD.gn
@@ -60,6 +60,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
     "${tint_src_dir}/utils/containers",
@@ -97,6 +98,7 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/inspector",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
diff --git a/src/tint/lang/wgsl/ir/builtin_call.cc b/src/tint/lang/wgsl/ir/builtin_call.cc
index 45cc898..b841a6d 100644
--- a/src/tint/lang/wgsl/ir/builtin_call.cc
+++ b/src/tint/lang/wgsl/ir/builtin_call.cc
@@ -48,7 +48,7 @@
 BuiltinCall::~BuiltinCall() = default;
 
 BuiltinCall* BuiltinCall::Clone(core::ir::CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result());
+    auto* new_result = ctx.Clone(Result(0));
     auto new_args = ctx.Clone<BuiltinCall::kDefaultNumOperands>(Args());
     return ctx.ir.instructions.Create<BuiltinCall>(new_result, fn_, new_args);
 }
diff --git a/src/tint/lang/wgsl/ir/builtin_call.h b/src/tint/lang/wgsl/ir/builtin_call.h
index 331c558..043129e 100644
--- a/src/tint/lang/wgsl/ir/builtin_call.h
+++ b/src/tint/lang/wgsl/ir/builtin_call.h
@@ -54,16 +54,18 @@
     BuiltinCall* Clone(core::ir::CloneContext& ctx) override;
 
     /// @returns the builtin function
-    BuiltinFn Func() { return fn_; }
+    BuiltinFn Func() const { return fn_; }
 
     /// @returns the identifier for the function
-    size_t FuncId() override { return static_cast<size_t>(fn_); }
+    size_t FuncId() const override { return static_cast<size_t>(fn_); }
 
     /// @returns the friendly name for the instruction
-    std::string FriendlyName() override { return std::string("wgsl.") + str(fn_); }
+    std::string FriendlyName() const override { return std::string("wgsl.") + str(fn_); }
 
     /// @returns the table data to validate this builtin
-    const core::intrinsic::TableData& TableData() override { return intrinsic::Dialect::kData; }
+    const core::intrinsic::TableData& TableData() const override {
+        return intrinsic::Dialect::kData;
+    }
 
   private:
     BuiltinFn fn_;
diff --git a/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc b/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc
index 8671ac9..12d97c1 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc
@@ -29,9 +29,8 @@
 
 #include <iostream>
 
-#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
 #include "src/tint/lang/core/ir/disassembler.h"
-#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
 #include "src/tint/lang/wgsl/reader/parser/parser.h"
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
@@ -40,57 +39,16 @@
 #include "src/tint/lang/wgsl/writer/writer.h"
 
 namespace tint::wgsl {
-namespace {
 
-bool IsUnsupported(const tint::ast::Enable* enable) {
-    for (auto ext : enable->extensions) {
-        switch (ext->name) {
-            case tint::wgsl::Extension::kChromiumExperimentalDp4A:
-            case tint::wgsl::Extension::kChromiumExperimentalFullPtrParameters:
-            case tint::wgsl::Extension::kChromiumExperimentalPixelLocal:
-            case tint::wgsl::Extension::kChromiumExperimentalPushConstant:
-            case tint::wgsl::Extension::kChromiumInternalDualSourceBlending:
-            case tint::wgsl::Extension::kChromiumInternalRelaxedUniformLayout:
-                return true;
-            default:
-                break;
-        }
-    }
-    return false;
-}
-
-}  // namespace
-
-void IRRoundtripFuzzer(const tint::Program& program) {
-    if (program.AST().Enables().Any(IsUnsupported)) {
-        return;
-    }
-
-    auto transformed = tint::wgsl::ApplySubstituteOverrides(program);
-    auto& src = transformed ? transformed.value() : program;
-    if (!src.IsValid()) {
-        return;
-    }
-
-    auto ir = tint::wgsl::reader::ProgramToIR(src);
-    if (!ir) {
-        TINT_ICE() << ir.Failure();
-        return;
-    }
-
-    if (auto res = tint::wgsl::reader::Lower(ir.Get()); !res) {
+void IRRoundtripFuzzer(core::ir::Module& ir) {
+    if (auto res = tint::wgsl::writer::Raise(ir); !res) {
         TINT_ICE() << res.Failure();
         return;
     }
 
-    if (auto res = tint::wgsl::writer::Raise(ir.Get()); !res) {
-        TINT_ICE() << res.Failure();
-        return;
-    }
-
-    auto dst = tint::wgsl::writer::IRToProgram(ir.Get());
+    auto dst = tint::wgsl::writer::IRToProgram(ir);
     if (!dst.IsValid()) {
-        std::cerr << "IR:\n" << core::ir::Disassemble(ir.Get()) << std::endl;
+        std::cerr << "IR:\n" << core::ir::Disassemble(ir) << std::endl;
         if (auto result = tint::wgsl::writer::Generate(dst, {}); result) {
             std::cerr << "WGSL:\n" << result->wgsl << std::endl << std::endl;
         }
@@ -103,4 +61,4 @@
 
 }  // namespace tint::wgsl
 
-TINT_WGSL_PROGRAM_FUZZER(tint::wgsl::IRRoundtripFuzzer);
+TINT_IR_MODULE_FUZZER(tint::wgsl::IRRoundtripFuzzer);
diff --git a/src/tint/lang/wgsl/program/BUILD.bazel b/src/tint/lang/wgsl/program/BUILD.bazel
index 3950041..96c0879 100644
--- a/src/tint/lang/wgsl/program/BUILD.bazel
+++ b/src/tint/lang/wgsl/program/BUILD.bazel
@@ -55,6 +55,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
@@ -90,6 +91,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/program/BUILD.cmake b/src/tint/lang/wgsl/program/BUILD.cmake
index 2e04315..c55fc5a 100644
--- a/src/tint/lang/wgsl/program/BUILD.cmake
+++ b/src/tint/lang/wgsl/program/BUILD.cmake
@@ -54,6 +54,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_sem
   tint_utils_containers
   tint_utils_diagnostic
@@ -89,6 +90,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -126,9 +128,11 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
+  tint_utils_bytes
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
diff --git a/src/tint/lang/wgsl/program/BUILD.gn b/src/tint/lang/wgsl/program/BUILD.gn
index 03d0b77..f3e03fd 100644
--- a/src/tint/lang/wgsl/program/BUILD.gn
+++ b/src/tint/lang/wgsl/program/BUILD.gn
@@ -58,6 +58,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/sem",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
@@ -91,6 +92,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/ast:unittests",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -121,9 +123,11 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/resolver",
     "${tint_src_dir}/lang/wgsl/sem",
+    "${tint_src_dir}/utils/bytes",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
diff --git a/src/tint/lang/wgsl/program/clone_context_fuzz.cc b/src/tint/lang/wgsl/program/clone_context_fuzz.cc
index e4cfc2f..17d802a 100644
--- a/src/tint/lang/wgsl/program/clone_context_fuzz.cc
+++ b/src/tint/lang/wgsl/program/clone_context_fuzz.cc
@@ -30,7 +30,7 @@
 #include <string>
 #include <unordered_set>
 
-#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
 #include "src/tint/lang/wgsl/reader/parser/parser.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
 
diff --git a/src/tint/lang/wgsl/program/program_builder.cc b/src/tint/lang/wgsl/program/program_builder.cc
index 596aa84..7f68f22 100644
--- a/src/tint/lang/wgsl/program/program_builder.cc
+++ b/src/tint/lang/wgsl/program/program_builder.cc
@@ -67,7 +67,7 @@
     builder.ast_ =
         builder.create<ast::Module>(program.AST().source, program.AST().GlobalDeclarations());
     builder.sem_ = sem::Info::Wrap(program.Sem());
-    builder.symbols_.Wrap(program.Symbols());
+    builder.symbols_ = SymbolTable::Wrap(program.Symbols());
     builder.diagnostics_ = program.Diagnostics();
     return builder;
 }
diff --git a/src/tint/lang/wgsl/reader/BUILD.bazel b/src/tint/lang/wgsl/reader/BUILD.bazel
index 07fb326..bba5e4a 100644
--- a/src/tint/lang/wgsl/reader/BUILD.bazel
+++ b/src/tint/lang/wgsl/reader/BUILD.bazel
@@ -54,6 +54,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
     "//src/tint/lang/wgsl/resolver",
@@ -97,6 +98,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/wgsl/reader/BUILD.cmake b/src/tint/lang/wgsl/reader/BUILD.cmake
index a213212..f30cb80 100644
--- a/src/tint/lang/wgsl/reader/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/BUILD.cmake
@@ -59,6 +59,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
   tint_lang_wgsl_resolver
@@ -106,6 +107,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/wgsl/reader/BUILD.gn b/src/tint/lang/wgsl/reader/BUILD.gn
index 8f4b35c..eb9cbc1 100644
--- a/src/tint/lang/wgsl/reader/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/BUILD.gn
@@ -57,6 +57,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/reader/lower",
       "${tint_src_dir}/lang/wgsl/resolver",
@@ -99,6 +100,7 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/sem",
         "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/wgsl/reader/lower/lower.cc b/src/tint/lang/wgsl/reader/lower/lower.cc
index dabab5d..381fad1 100644
--- a/src/tint/lang/wgsl/reader/lower/lower.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower.cc
@@ -176,6 +176,9 @@
     core::ir::Builder b{mod};
     core::type::Manager& ty{mod.Types()};
     for (auto* inst : mod.instructions.Objects()) {
+        if (!inst->Alive()) {
+            continue;
+        }
         if (auto* call = inst->As<wgsl::ir::BuiltinCall>()) {
             switch (call->Func()) {
                 case BuiltinFn::kWorkgroupUniformLoad: {
@@ -188,7 +191,7 @@
                     b.InsertBefore(call, [&] {
                         b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
                         auto* load = b.Load(call->Args()[0]);
-                        call->Result()->ReplaceAllUsesWith(load->Result());
+                        call->Result(0)->ReplaceAllUsesWith(load->Result(0));
                         b.Call(ty.void_(), core::BuiltinFn::kWorkgroupBarrier);
                     });
                     break;
@@ -196,7 +199,7 @@
                 default: {
                     Vector<core::ir::Value*, 8> args(call->Args());
                     auto* replacement = mod.instructions.Create<core::ir::CoreBuiltinCall>(
-                        call->Result(), Convert(call->Func()), std::move(args));
+                        call->Result(0), Convert(call->Func()), std::move(args));
                     call->ReplaceWith(replacement);
                     call->ClearResults();
                     break;
diff --git a/src/tint/lang/wgsl/reader/lower/lower_test.cc b/src/tint/lang/wgsl/reader/lower/lower_test.cc
index 6244192..dd05a5d 100644
--- a/src/tint/lang/wgsl/reader/lower/lower_test.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower_test.cc
@@ -85,7 +85,7 @@
     b.Append(f->Block(), [&] {  //
         auto* result = b.InstructionResult(ty.i32());
         b.Append(b.ir.instructions.Create<wgsl::ir::BuiltinCall>(
-            result, wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{wgvar->Result()}));
+            result, wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{wgvar->Result(0)}));
         b.Return(f, result);
     });
 
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.bazel b/src/tint/lang/wgsl/reader/parser/BUILD.bazel
index ce2d480..724f05d 100644
--- a/src/tint/lang/wgsl/reader/parser/BUILD.bazel
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.bazel
@@ -59,6 +59,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
@@ -161,6 +162,7 @@
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.cmake b/src/tint/lang/wgsl/reader/parser/BUILD.cmake
index 427ec60..d2e9c49 100644
--- a/src/tint/lang/wgsl/reader/parser/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.cmake
@@ -60,6 +60,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -165,6 +166,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/wgsl/reader/parser/BUILD.gn b/src/tint/lang/wgsl/reader/parser/BUILD.gn
index 9aca136..f68c92c 100644
--- a/src/tint/lang/wgsl/reader/parser/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/parser/BUILD.gn
@@ -62,6 +62,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -164,6 +165,7 @@
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/ast:unittests",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
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 893573c..6536271 100644
--- a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
@@ -205,7 +205,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_framebuffer_fetch', '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')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_framebuffer_fetch', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', '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/parser/lexer.cc b/src/tint/lang/wgsl/reader/parser/lexer.cc
index 7b19d4c..102dffa 100644
--- a/src/tint/lang/wgsl/reader/parser/lexer.cc
+++ b/src/tint/lang/wgsl/reader/parser/lexer.cc
@@ -31,11 +31,10 @@
 #include <charconv>
 #include <cmath>
 #include <cstring>
-#include <functional>
 #include <limits>
 #include <optional>
+#include <string>
 #include <tuple>
-#include <type_traits>
 #include <utility>
 
 #include "src/tint/lang/core/fluent_types.h"
@@ -430,9 +429,9 @@
         end_ptr = &at(length() - 1) + 1;
     }
 
-    auto ret = tint::ParseDouble(std::string_view(&at(start), end - start));
+    auto ret = tint::strconv::ParseDouble(std::string_view(&at(start), end - start));
     double value = ret ? ret.Get() : 0.0;
-    bool overflow = !ret && ret.Failure() == tint::ParseNumberError::kResultOutOfRange;
+    bool overflow = !ret && ret.Failure() == tint::strconv::ParseNumberError::kResultOutOfRange;
 
     // If the value didn't fit in a double, check for underflow as that is 0.0 in WGSL and not an
     // error.
diff --git a/src/tint/lang/wgsl/reader/parser/lexer.h b/src/tint/lang/wgsl/reader/parser/lexer.h
index 7d10dd9..6e97551 100644
--- a/src/tint/lang/wgsl/reader/parser/lexer.h
+++ b/src/tint/lang/wgsl/reader/parser/lexer.h
@@ -29,7 +29,6 @@
 #define SRC_TINT_LANG_WGSL_READER_PARSER_LEXER_H_
 
 #include <optional>
-#include <string>
 #include <vector>
 
 #include "src/tint/lang/wgsl/reader/parser/token.h"
diff --git a/src/tint/lang/wgsl/reader/parser/lexer_test.cc b/src/tint/lang/wgsl/reader/parser/lexer_test.cc
index 105c9f6..1464396 100644
--- a/src/tint/lang/wgsl/reader/parser/lexer_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/lexer_test.cc
@@ -28,6 +28,7 @@
 #include "src/tint/lang/wgsl/reader/parser/lexer.h"
 
 #include <limits>
+#include <string>
 #include <tuple>
 #include <vector>
 
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index 8cc0bba..e3698f9 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -474,7 +474,7 @@
             return add_error(t.source(), "requires directives don't take parenthesis");
         }
 
-        wgsl::LanguageFeatures features;
+        ast::Requires::LanguageFeatures features;
         while (continue_parsing()) {
             auto& t2 = next();
             if (handle_error(t2)) {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
index 619764a..551faa8 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.bazel
@@ -53,6 +53,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/ir",
     "//src/tint/lang/wgsl/program",
@@ -101,6 +102,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/helpers:test",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/reader/lower",
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 c869ca2..1c43bfe 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.cmake
@@ -54,6 +54,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_ir
   tint_lang_wgsl_program
@@ -105,6 +106,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers_test
   tint_lang_wgsl_program
   tint_lang_wgsl_reader_lower
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 9948752..41ff6f8 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
+++ b/src/tint/lang/wgsl/reader/program_to_ir/BUILD.gn
@@ -56,6 +56,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/ir",
       "${tint_src_dir}/lang/wgsl/program",
@@ -104,6 +105,7 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/helpers:unittests",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/reader/lower",
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc
index 3f939fa..9e666f9 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc
@@ -58,7 +58,8 @@
   %b1 = block {
     %a:ptr<function, array<u32, 3>, read_write> = var
     %3:ptr<function, u32, read_write> = access %a, 2u
-    %b:u32 = load %3
+    %4:u32 = load %3
+    %b:u32 = let %4
     ret
   }
 }
@@ -83,8 +84,10 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:vec3<u32> = let vec3<u32>(0u)
-    %b:u32 = access %a, 2u
-    %c:u32 = access %a, 1u
+    %3:u32 = access %a, 2u
+    %b:u32 = let %3
+    %5:u32 = access %a, 1u
+    %c:u32 = let %5
     ret
   }
 }
@@ -106,7 +109,8 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:ptr<function, vec3<u32>, read_write> = var
-    %b:u32 = load_vector_element %a, 2u
+    %3:u32 = load_vector_element %a, 2u
+    %b:u32 = let %3
     ret
   }
 }
@@ -129,7 +133,8 @@
   %b1 = block {
     %a:ptr<function, array<array<f32, 4>, 3>, read_write> = var
     %3:ptr<function, f32, read_write> = access %a, 2u, 3u
-    %b:f32 = load %3
+    %4:f32 = load %3
+    %b:f32 = let %4
     ret
   }
 }
@@ -152,7 +157,8 @@
   %b1 = block {
     %a:ptr<function, mat3x4<f32>, read_write> = var
     %3:ptr<function, vec4<f32>, read_write> = access %a, 2u
-    %b:f32 = load_vector_element %3, 3u
+    %4:f32 = load_vector_element %3, 3u
+    %b:f32 = let %4
     ret
   }
 }
@@ -183,7 +189,8 @@
   %b1 = block {
     %a:ptr<function, MyStruct, read_write> = var
     %3:ptr<function, i32, read_write> = access %a, 0u
-    %b:i32 = load %3
+    %4:i32 = load %3
+    %b:i32 = let %4
     ret
   }
 }
@@ -224,7 +231,8 @@
   %b1 = block {
     %a:ptr<function, Outer, read_write> = var
     %3:ptr<function, f32, read_write> = access %a, 1u, 0u
-    %b:f32 = load %3
+    %4:f32 = load %3
+    %b:f32 = let %4
     ret
   }
 }
@@ -271,7 +279,8 @@
   %b1 = block {
     %a:ptr<function, array<Outer, 4>, read_write> = var
     %3:ptr<function, vec4<f32>, read_write> = access %a, 0u, 1u, 1u, 2u
-    %b:vec4<f32> = load %3
+    %4:vec4<f32> = load %3
+    %b:vec4<f32> = let %4
     ret
   }
 }
@@ -316,7 +325,8 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:ptr<function, vec2<f32>, read_write> = var
-    %b:f32 = load_vector_element %a, 1u
+    %3:f32 = load_vector_element %a, 1u
+    %b:f32 = let %3
     ret
   }
 }
@@ -339,7 +349,8 @@
   %b1 = block {
     %a:ptr<function, vec3<f32>, read_write> = var
     %3:vec3<f32> = load %a
-    %b:vec4<f32> = swizzle %3, zyxz
+    %4:vec4<f32> = swizzle %3, zyxz
+    %b:vec4<f32> = let %4
     ret
   }
 }
@@ -363,7 +374,8 @@
     %a:ptr<function, vec3<f32>, read_write> = var
     %3:vec3<f32> = load %a
     %4:vec3<f32> = swizzle %3, zyx
-    %b:vec2<f32> = swizzle %4, yy
+    %5:vec2<f32> = swizzle %4, yy
+    %b:vec2<f32> = let %5
     ret
   }
 }
@@ -401,7 +413,8 @@
     %4:vec4<f32> = load %3
     %5:vec3<f32> = swizzle %4, zyx
     %6:vec2<f32> = swizzle %5, yx
-    %b:f32 = access %6, 0u
+    %7:f32 = access %6, 0u
+    %b:f32 = let %7
     ret
   }
 }
@@ -422,7 +435,8 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:vec3<u32> = let vec3<u32>(0u)
-    %b:u32 = access %a, 2u
+    %3:u32 = access %a, 2u
+    %b:u32 = let %3
     ret
   }
 }
@@ -444,7 +458,8 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:mat3x4<f32> = let mat3x4<f32>(vec4<f32>(0.0f))
-    %b:f32 = access %a, 2u, 3u
+    %3:f32 = access %a, 2u, 3u
+    %b:f32 = let %3
     ret
   }
 }
@@ -474,7 +489,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:MyStruct = let MyStruct(0i)
-    %b:i32 = access %a, 0u
+    %3:i32 = access %a, 0u
+    %b:i32 = let %3
     ret
   }
 }
@@ -514,7 +530,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:Outer = let Outer(0i, Inner(0.0f))
-    %b:f32 = access %a, 1u, 0u
+    %3:f32 = access %a, 1u, 0u
+    %b:f32 = let %3
     ret
   }
 }
@@ -560,7 +577,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:array<Outer, 4> = let array<Outer, 4>(Outer(0i, array<Inner, 4>(Inner(0i, 0.0f, vec4<f32>(0.0f)))))
-    %b:vec4<f32> = access %a, 0u, 1u, 1u, 2u
+    %3:vec4<f32> = access %a, 0u, 1u, 1u, 2u
+    %b:vec4<f32> = let %3
     ret
   }
 }
@@ -582,7 +600,8 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:vec2<f32> = let vec2<f32>(0.0f)
-    %b:f32 = access %a, 1u
+    %3:f32 = access %a, 1u
+    %b:f32 = let %3
     ret
   }
 }
@@ -604,7 +623,8 @@
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
   %b1 = block {
     %a:vec3<f32> = let vec3<f32>(0.0f)
-    %b:vec4<f32> = swizzle %a, zyxz
+    %3:vec4<f32> = swizzle %a, zyxz
+    %b:vec4<f32> = let %3
     ret
   }
 }
@@ -627,7 +647,8 @@
   %b1 = block {
     %a:vec3<f32> = let vec3<f32>(0.0f)
     %3:vec3<f32> = swizzle %a, zyx
-    %b:vec2<f32> = swizzle %3, yy
+    %4:vec2<f32> = swizzle %3, yy
+    %b:vec2<f32> = let %4
     ret
   }
 }
@@ -664,7 +685,8 @@
     %3:vec4<f32> = access %a, 1u
     %4:vec3<f32> = swizzle %3, zyx
     %5:vec2<f32> = swizzle %4, yx
-    %b:f32 = access %5, 0u
+    %6:f32 = access %5, 0u
+    %b:f32 = let %6
     ret
   }
 }
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc
index 31d31ef..11df6fa 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc
@@ -54,7 +54,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = add %3, 4u
+    %4:u32 = add %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -123,7 +124,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = sub %3, 4u
+    %4:u32 = sub %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -192,7 +194,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = mul %3, 4u
+    %4:u32 = mul %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -238,7 +241,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = div %3, 4u
+    %4:u32 = div %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -284,7 +288,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = mod %3, 4u
+    %4:u32 = mod %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -330,7 +335,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = and %3, 4u
+    %4:u32 = and %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -376,7 +382,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = or %3, 4u
+    %4:u32 = or %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -422,7 +429,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = xor %3, 4u
+    %4:u32 = xor %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -469,7 +477,7 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:bool = call %my_func
-    %logical_and:bool = if %3 [t: %b3, f: %b4] {  # if_1
+    %4:bool = if %3 [t: %b3, f: %b4] {  # if_1
       %b3 = block {  # true
         exit_if false  # if_1
       }
@@ -477,6 +485,7 @@
         exit_if false  # if_1
       }
     }
+    %logical_and:bool = let %4
     if %logical_and [t: %b5] {  # if_2
       %b5 = block {  # true
         exit_if  # if_2
@@ -505,7 +514,7 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:bool = call %my_func
-    %logical_or:bool = if %3 [t: %b3, f: %b4] {  # if_1
+    %4:bool = if %3 [t: %b3, f: %b4] {  # if_1
       %b3 = block {  # true
         exit_if true  # if_1
       }
@@ -513,6 +522,7 @@
         exit_if true  # if_1
       }
     }
+    %logical_or:bool = let %4
     if %logical_or [t: %b5] {  # if_2
       %b5 = block {  # true
         exit_if  # if_2
@@ -540,7 +550,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:bool = eq %3, 4u
+    %4:bool = eq %3, 4u
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -563,7 +574,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:bool = neq %3, 4u
+    %4:bool = neq %3, 4u
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -586,7 +598,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:bool = lt %3, 4u
+    %4:bool = lt %3, 4u
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -609,7 +622,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:bool = gt %3, 4u
+    %4:bool = gt %3, 4u
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -632,7 +646,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:bool = lte %3, 4u
+    %4:bool = lte %3, 4u
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -655,7 +670,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:bool = gte %3, 4u
+    %4:bool = gte %3, 4u
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -678,7 +694,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = shiftl %3, 4u
+    %4:u32 = shiftl %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -724,7 +741,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = shiftr %3, 4u
+    %4:u32 = shiftr %3, 4u
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -773,7 +791,7 @@
   %b2 = block {
     %3:f32 = call %my_func
     %4:bool = lt %3, 2.0f
-    %tint_symbol:bool = if %4 [t: %b3, f: %b4] {  # if_1
+    %5:bool = if %4 [t: %b3, f: %b4] {  # if_1
       %b3 = block {  # true
         %6:f32 = call %my_func
         %7:f32 = call %my_func
@@ -786,6 +804,7 @@
         exit_if false  # if_1
       }
     }
+    %tint_symbol:bool = let %5
     ret
   }
 }
@@ -808,7 +827,8 @@
 }
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
-    %tint_symbol:bool = call %my_func, false
+    %4:bool = call %my_func, false
+    %tint_symbol:bool = let %4
     ret
   }
 }
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc
index fd6f77e..b92e960 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc
@@ -53,7 +53,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:f32 = load %i
-    %tint_symbol:f32 = asin %3
+    %4:f32 = asin %3
+    %tint_symbol:f32 = let %4
     ret
   }
 }
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc
index 4c1dbfc..52cd00c 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc
@@ -56,7 +56,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:f32 = call %my_func
-    %tint_symbol:f32 = bitcast %3
+    %4:f32 = bitcast %3
+    %tint_symbol:f32 = let %4
     ret
   }
 }
@@ -119,7 +120,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:i32 = load %i
-    %tint_symbol:f32 = convert %3
+    %4:f32 = convert %3
+    %tint_symbol:f32 = let %4
     ret
   }
 }
@@ -155,7 +157,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:f32 = load %i
-    %tint_symbol:vec3<f32> = construct 2.0f, 3.0f, %3
+    %4:vec3<f32> = construct 2.0f, 3.0f, %3
+    %tint_symbol:vec3<f32> = let %4
     ret
   }
 }
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index e2c1c43..4644964 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -205,11 +205,11 @@
         diagnostics_.add_error(tint::diag::System::IR, err, s);
     }
 
-    bool NeedTerminator() { return current_block_ && !current_block_->HasTerminator(); }
+    bool NeedTerminator() { return current_block_ && !current_block_->Terminator(); }
 
     void SetTerminator(core::ir::Terminator* terminator) {
         TINT_ASSERT(current_block_);
-        TINT_ASSERT(!current_block_->HasTerminator());
+        TINT_ASSERT(!current_block_->Terminator());
 
         current_block_->Append(terminator);
         current_block_ = nullptr;
@@ -574,13 +574,13 @@
         auto b = builder_.Append(current_block_);
         if (auto* v = std::get_if<core::ir::Value*>(&lhs)) {
             auto* load = b.Load(*v);
-            auto* ty = load->Result()->Type();
-            auto* inst = current_block_->Append(BinaryOp(ty, load->Result(), rhs, op));
+            auto* ty = load->Result(0)->Type();
+            auto* inst = current_block_->Append(BinaryOp(ty, load->Result(0), rhs, op));
             b.Store(*v, inst);
         } else if (auto ref = std::get_if<VectorRefElementAccess>(&lhs)) {
             auto* load = b.LoadVectorElement(ref->vector, ref->index);
-            auto* ty = load->Result()->Type();
-            auto* inst = b.Append(BinaryOp(ty, load->Result(), rhs, op));
+            auto* ty = load->Result(0)->Type();
+            auto* inst = b.Append(BinaryOp(ty, load->Result(0), rhs, op));
             b.StoreVectorElement(ref->vector, ref->index, inst);
         }
     }
@@ -771,12 +771,12 @@
 
         const auto* sem = program_.Sem().Get(stmt);
         for (const auto* c : sem->Cases()) {
-            Vector<core::ir::Switch::CaseSelector, 4> selectors;
+            Vector<core::ir::Constant*, 4> selectors;
             for (const auto* selector : c->Selectors()) {
                 if (selector->IsDefault()) {
-                    selectors.Push({nullptr});
+                    selectors.Push(nullptr);
                 } else {
-                    selectors.Push({builder_.Constant(selector->Value()->Clone(clone_ctx_))});
+                    selectors.Push(builder_.Constant(selector->Value()->Clone(clone_ctx_)));
                 }
             }
 
@@ -878,7 +878,7 @@
                 if (impl.program_.Sem().Get<sem::Load>(expr)) {
                     auto* load = impl.builder_.Load(value);
                     impl.current_block_->Append(load);
-                    value = load->Result();
+                    value = load->Result(0);
                 }
                 bindings_.Add(expr, value);
             }
@@ -888,7 +888,7 @@
                 if (impl.program_.Sem().Get<sem::Load>(expr)) {
                     auto* load = impl.builder_.LoadVectorElement(access.vector, access.index);
                     impl.current_block_->Append(load);
-                    bindings_.Add(expr, load->Result());
+                    bindings_.Add(expr, load->Result(0));
                 } else {
                     bindings_.Add(expr, access);
                 }
@@ -978,7 +978,7 @@
                         }
                         auto* val = impl.builder_.Swizzle(ty, obj, std::move(indices));
                         impl.current_block_->Append(val);
-                        Bind(expr, val->Result());
+                        Bind(expr, val->Result(0));
                         return nullptr;
                     },  //
                     TINT_ICE_ON_NO_MATCH);
@@ -991,16 +991,16 @@
                 // of another access, then we can just append the index to that access.
                 if (!impl.mod.NameOf(obj).IsValid()) {
                     if (auto* inst_res = obj->As<core::ir::InstructionResult>()) {
-                        if (auto* access = inst_res->Source()->As<core::ir::Access>()) {
+                        if (auto* access = inst_res->Instruction()->As<core::ir::Access>()) {
                             access->AddIndex(index);
-                            access->Result()->SetType(ty);
+                            access->Result(0)->SetType(ty);
                             bindings_.Remove(expr->object);
                             // Move the access after the index expression.
                             if (impl.current_block_->Back() != access) {
                                 impl.current_block_->Remove(access);
                                 impl.current_block_->Append(access);
                             }
-                            Bind(expr, access->Result());
+                            Bind(expr, access->Result(0));
                             return;
                         }
                     }
@@ -1009,7 +1009,7 @@
                 // Create a new access
                 auto* access = impl.builder_.Access(ty, obj, index);
                 impl.current_block_->Append(access);
-                Bind(expr, access->Result());
+                Bind(expr, access->Result(0));
             }
 
             void EmitBinary(const ast::BinaryExpression* b) {
@@ -1028,7 +1028,7 @@
                     return;
                 }
                 impl.current_block_->Append(inst);
-                Bind(b, inst->Result());
+                Bind(b, inst->Result(0));
             }
 
             void EmitUnary(const ast::UnaryOpExpression* expr) {
@@ -1057,7 +1057,7 @@
                         break;
                 }
                 impl.current_block_->Append(inst);
-                Bind(expr, inst->Result());
+                Bind(expr, inst->Result(0));
             }
 
             void EmitBitcast(const ast::BitcastExpression* b) {
@@ -1069,7 +1069,7 @@
                 auto* ty = sem->Type()->Clone(impl.clone_ctx_.type_ctx);
                 auto* inst = impl.builder_.Bitcast(ty, val);
                 impl.current_block_->Append(inst);
-                Bind(b, inst->Result());
+                Bind(b, inst->Result(0));
             }
 
             void EmitCall(const ast::CallExpression* expr) {
@@ -1128,7 +1128,7 @@
                     return;
                 }
                 impl.current_block_->Append(inst);
-                Bind(expr, inst->Result());
+                Bind(expr, inst->Result(0));
             }
 
             void EmitIdentifier(const ast::IdentifierExpression* i) {
@@ -1225,7 +1225,7 @@
 
             void EndShortCircuit(const ast::BinaryExpression* b) {
                 auto res = GetValue(b);
-                auto* src = res->As<core::ir::InstructionResult>()->Source();
+                auto* src = res->As<core::ir::InstructionResult>()->Instruction();
                 auto* if_ = src->As<core::ir::If>();
                 TINT_ASSERT_OR_RETURN(if_);
                 auto rhs = GetValue(b->rhs);
@@ -1328,34 +1328,21 @@
                 }
 
                 // Store the declaration so we can get the instruction to store too
-                scopes_.Set(v->name->symbol, val->Result());
+                scopes_.Set(v->name->symbol, val->Result(0));
 
                 // Record the original name of the var
                 builder_.ir.SetName(val, v->name->symbol.Name());
             },
             [&](const ast::Let* l) {
-                auto* last_stmt = current_block_->Back();
                 auto init = EmitValueExpression(l->initializer);
                 if (!init) {
                     return;
                 }
 
-                auto* value = init;
-                if (current_block_->Back() == last_stmt) {
-                    // Emitting the let's initializer didn't create an instruction.
-                    // Create an core::ir::Let to give the let an instruction. This gives the let a
-                    // place of declaration and name, which preserves runtime semantics of the
-                    // let, and can be used by consumers of the IR to produce a variable or
-                    // debug info.
-                    auto* let = current_block_->Append(builder_.Let(l->name->symbol.Name(), value));
-                    value = let->Result();
-                } else {
-                    // Record the original name of the let
-                    builder_.ir.SetName(value, l->name->symbol.Name());
-                }
+                auto* let = current_block_->Append(builder_.Let(l->name->symbol.Name(), init));
 
                 // Store the results of the initialization
-                scopes_.Set(l->name->symbol, value);
+                scopes_.Set(l->name->symbol, let->Result(0));
             },
             [&](const ast::Override*) {
                 add_error(var->source,
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
index f704fa5..bd204c0 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
@@ -77,7 +77,7 @@
 
     ASSERT_EQ(1u, m->functions.Length());
 
-    auto* f = m->functions[0];
+    core::ir::Function* f = m->functions[0];
     ASSERT_NE(f->Block(), nullptr);
 
     EXPECT_EQ(m->functions[0]->Stage(), core::ir::Function::PipelineStage::kUndefined);
@@ -98,7 +98,7 @@
 
     ASSERT_EQ(1u, m->functions.Length());
 
-    auto* f = m->functions[0];
+    core::ir::Function* f = m->functions[0];
     ASSERT_NE(f->Block(), nullptr);
 
     EXPECT_EQ(m->functions[0]->Stage(), core::ir::Function::PipelineStage::kUndefined);
@@ -120,7 +120,7 @@
 
     ASSERT_EQ(1u, m->functions.Length());
 
-    auto* f = m->functions[0];
+    core::ir::Function* f = m->functions[0];
     ASSERT_NE(f->Block(), nullptr);
 
     EXPECT_EQ(m->functions[0]->Stage(), core::ir::Function::PipelineStage::kUndefined);
@@ -850,7 +850,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    auto cases = swtch->Cases();
+    auto& cases = swtch->Cases();
     ASSERT_EQ(3u, cases.Length());
 
     ASSERT_EQ(1u, cases[0].selectors.Length());
@@ -901,7 +901,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    auto cases = swtch->Cases();
+    auto& cases = swtch->Cases();
     ASSERT_EQ(1u, cases.Length());
     ASSERT_EQ(3u, cases[0].selectors.Length());
     ASSERT_TRUE(cases[0].selectors[0].val->Value()->Is<core::constant::Scalar<i32>>());
@@ -940,7 +940,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    auto cases = swtch->Cases();
+    auto& cases = swtch->Cases();
     ASSERT_EQ(1u, cases.Length());
     ASSERT_EQ(1u, cases[0].selectors.Length());
     EXPECT_TRUE(cases[0].selectors[0].IsDefault());
@@ -973,7 +973,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    auto cases = swtch->Cases();
+    auto& cases = swtch->Cases();
     ASSERT_EQ(2u, cases.Length());
     ASSERT_EQ(1u, cases[0].selectors.Length());
     ASSERT_TRUE(cases[0].selectors[0].val->Value()->Is<core::constant::Scalar<i32>>());
@@ -1017,7 +1017,7 @@
 
     ASSERT_EQ(1u, m.functions.Length());
 
-    auto cases = swtch->Cases();
+    auto& cases = swtch->Cases();
     ASSERT_EQ(2u, cases.Length());
     ASSERT_EQ(1u, cases[0].selectors.Length());
     ASSERT_TRUE(cases[0].selectors[0].val->Value()->Is<core::constant::Scalar<i32>>());
@@ -1151,7 +1151,7 @@
 
     ASSERT_EQ(1u, m->functions.Length());
 
-    auto* f = m->functions[0];
+    core::ir::Function* f = m->functions[0];
     ASSERT_NE(f->Block(), nullptr);
 
     EXPECT_EQ(m->functions[0]->Stage(), core::ir::Function::PipelineStage::kUndefined);
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc
index 1536188..09af944 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc
@@ -59,8 +59,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-S = struct @align(4) {
+    EXPECT_EQ(Disassemble(m.Get()), R"(S = struct @align(4) {
   i:i32 @offset(0)
 }
 
@@ -88,8 +87,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-S = struct @align(4) {
+    EXPECT_EQ(Disassemble(m.Get()), R"(S = struct @align(4) {
   i:i32 @offset(0)
 }
 
@@ -115,8 +113,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%b1 = block {  # root
+    EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %i:ptr<private, i32, read_write> = var, 1i
 }
 
@@ -148,8 +145,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%b1 = block {  # root
+    EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %i:ptr<private, i32, read_write> = var, 1i
 }
 
@@ -159,7 +155,8 @@
     %4:i32 = add %3, 1i
     store %i, %4
     %5:i32 = load %i
-    %i_1:i32 = add %5, 1i  # %i_1: 'i'
+    %6:i32 = add %5, 1i
+    %i_1:i32 = let %6  # %i_1: 'i'
     ret %i_1
   }
 }
@@ -181,8 +178,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     if true [t: %b2] {  # if_1
@@ -221,8 +217,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     if true [t: %b2] {  # if_1
@@ -231,12 +226,13 @@
         %4:i32 = add %3, 1i
         store %i, %4
         %5:i32 = load %i
-        %i_1:i32 = add %5, 1i  # %i_1: 'i'
+        %6:i32 = add %5, 1i
+        %i_1:i32 = let %6  # %i_1: 'i'
         ret %i_1
       }
     }
-    %7:i32 = load %i
-    ret %7
+    %8:i32 = load %i
+    ret %8
   }
 }
 )");
@@ -256,8 +252,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [b: %b2, c: %b3] {  # loop_1
@@ -303,8 +298,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [b: %b2, c: %b3] {  # loop_1
@@ -320,15 +314,16 @@
           }
         }
         %5:i32 = load %i
-        %i_1:i32 = add %5, 1i  # %i_1: 'i'
+        %6:i32 = add %5, 1i
+        %i_1:i32 = let %6  # %i_1: 'i'
         ret %i_1
       }
       %b3 = block {  # continuing
         next_iteration %b2
       }
     }
-    %7:i32 = load %i
-    ret %7
+    %8:i32 = load %i
+    ret %8
   }
 }
 )");
@@ -347,8 +342,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [i: %b2, b: %b3] {  # loop_1
@@ -367,12 +361,13 @@
             exit_loop  # loop_1
           }
         }
-        %j:f32 = load %i_1
+        %6:f32 = load %i_1
+        %j:f32 = let %6
         continue %b6
       }
     }
-    %7:i32 = load %i
-    ret %7
+    %8:i32 = load %i
+    ret %8
   }
 }
 )");
@@ -391,8 +386,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [i: %b2, b: %b3] {  # loop_1
@@ -435,8 +429,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [i: %b2, b: %b3] {  # loop_1
@@ -483,8 +476,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [i: %b2, b: %b3] {  # loop_1
@@ -504,12 +496,13 @@
           }
         }
         %6:i32 = load %i
-        %i_1:i32 = add %6, 1i  # %i_1: 'i'
+        %7:i32 = add %6, 1i
+        %i_1:i32 = let %7  # %i_1: 'i'
         ret %i_1
       }
     }
-    %8:i32 = load %i
-    ret %8
+    %9:i32 = load %i
+    ret %9
   }
 }
 )");
@@ -534,8 +527,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [b: %b2, c: %b3] {  # loop_1
@@ -589,8 +581,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [b: %b2, c: %b3] {  # loop_1
@@ -603,9 +594,10 @@
           }
         }
         %5:i32 = load %i
-        %i_1:i32 = add %5, 1i  # %i_1: 'i'
-        %7:bool = eq %i_1, 3i
-        if %7 [t: %b5] {  # if_2
+        %6:i32 = add %5, 1i
+        %i_1:i32 = let %6  # %i_1: 'i'
+        %8:bool = eq %i_1, 3i
+        if %8 [t: %b5] {  # if_2
           %b5 = block {  # true
             exit_loop  # loop_1
           }
@@ -616,8 +608,8 @@
         next_iteration %b2
       }
     }
-    %8:i32 = load %i
-    ret %8
+    %9:i32 = load %i
+    ret %9
   }
 }
 )");
@@ -643,8 +635,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [b: %b2, c: %b3] {  # loop_1
@@ -694,8 +685,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     loop [b: %b2, c: %b3] {  # loop_1
@@ -711,13 +701,14 @@
       }
       %b3 = block {  # continuing
         %5:i32 = load %i
-        %i_1:i32 = add %5, 1i  # %i_1: 'i'
-        %7:bool = gt %i_1, 2i
-        break_if %7 %b2
+        %6:i32 = add %5, 1i
+        %i_1:i32 = let %6  # %i_1: 'i'
+        %8:bool = gt %i_1, 2i
+        break_if %8 %b2
       }
     }
-    %8:i32 = load %i
-    ret %8
+    %9:i32 = load %i
+    ret %9
   }
 }
 )");
@@ -744,8 +735,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     %3:i32 = load %i
@@ -793,8 +783,7 @@
 
     ASSERT_TRUE(m) << m;
 
-    EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
-%f = func():i32 -> %b1 {
+    EXPECT_EQ(Disassemble(m.Get()), R"(%f = func():i32 -> %b1 {
   %b1 = block {
     %i:ptr<function, i32, read_write> = var
     %3:i32 = load %i
@@ -805,12 +794,13 @@
       }
       %b3 = block {  # case
         %5:i32 = load %i
-        %i_1:i32 = add %5, 1i  # %i_1: 'i'
+        %6:i32 = add %5, 1i
+        %i_1:i32 = let %6  # %i_1: 'i'
         ret %i_1
       }
       %b4 = block {  # case
-        %7:i32 = load %i
-        ret %7
+        %8:i32 = load %i
+        ret %8
       }
     }
     unreachable
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc
index 0db5a17..98ff492 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc
@@ -54,7 +54,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:bool = call %my_func
-    %tint_symbol:bool = eq %3, false
+    %4:bool = eq %3, false
+    %tint_symbol:bool = let %4
     ret
   }
 }
@@ -77,7 +78,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:vec4<bool> = call %my_func
-    %tint_symbol:vec4<bool> = eq %3, vec4<bool>(false)
+    %4:vec4<bool> = eq %3, vec4<bool>(false)
+    %tint_symbol:vec4<bool> = let %4
     ret
   }
 }
@@ -100,7 +102,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:u32 = call %my_func
-    %tint_symbol:u32 = complement %3
+    %4:u32 = complement %3
+    %tint_symbol:u32 = let %4
     ret
   }
 }
@@ -123,7 +126,8 @@
 %test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
   %b2 = block {
     %3:i32 = call %my_func
-    %tint_symbol:i32 = negation %3
+    %4:i32 = negation %3
+    %tint_symbol:i32 = let %4
     ret
   }
 }
diff --git a/src/tint/lang/wgsl/reader/reader.cc b/src/tint/lang/wgsl/reader/reader.cc
index df1dc7b..f74d837 100644
--- a/src/tint/lang/wgsl/reader/reader.cc
+++ b/src/tint/lang/wgsl/reader/reader.cc
@@ -55,4 +55,19 @@
     return module;
 }
 
+tint::Result<core::ir::Module> ProgramToLoweredIR(const Program& program) {
+    auto ir = tint::wgsl::reader::ProgramToIR(program);
+    if (!ir) {
+        return ir.Failure();
+    }
+
+    // Lower from WGSL-dialect to core-dialect
+    auto res = tint::wgsl::reader::Lower(ir.Get());
+    if (!res) {
+        return res.Failure();
+    }
+
+    return ir;
+}
+
 }  // namespace tint::wgsl::reader
diff --git a/src/tint/lang/wgsl/reader/reader.h b/src/tint/lang/wgsl/reader/reader.h
index 40c973a..8594250 100644
--- a/src/tint/lang/wgsl/reader/reader.h
+++ b/src/tint/lang/wgsl/reader/reader.h
@@ -49,6 +49,15 @@
 /// @returns the resulting IR module, or failure
 Result<core::ir::Module> WgslToIR(const Source::File* file, const Options& options = {});
 
+/// Builds a core-dialect core::ir::Module from the given Program
+/// @param program the Program to use.
+/// @returns the core-dialect IR module.
+///
+/// @note this assumes the `program.IsValid()`, and has had const-eval done so
+/// any abstract values have been calculated and converted into the relevant
+/// concrete types.
+tint::Result<core::ir::Module> ProgramToLoweredIR(const Program& program);
+
 }  // namespace tint::wgsl::reader
 
 #endif  // SRC_TINT_LANG_WGSL_READER_READER_H_
diff --git a/src/tint/lang/wgsl/resolver/BUILD.bazel b/src/tint/lang/wgsl/resolver/BUILD.bazel
index 4187995..51904b9 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.bazel
+++ b/src/tint/lang/wgsl/resolver/BUILD.bazel
@@ -67,6 +67,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
@@ -168,6 +169,7 @@
     "//src/tint/lang/wgsl/ast/transform",
     "//src/tint/lang/wgsl/ast:test",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
diff --git a/src/tint/lang/wgsl/resolver/BUILD.cmake b/src/tint/lang/wgsl/resolver/BUILD.cmake
index 745d508..6cb53ed 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.cmake
+++ b/src/tint/lang/wgsl/resolver/BUILD.cmake
@@ -66,6 +66,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -162,6 +163,7 @@
   tint_lang_wgsl_ast_transform
   tint_lang_wgsl_ast_test
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
diff --git a/src/tint/lang/wgsl/resolver/BUILD.gn b/src/tint/lang/wgsl/resolver/BUILD.gn
index f5e4097..18cab8b 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.gn
+++ b/src/tint/lang/wgsl/resolver/BUILD.gn
@@ -70,6 +70,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/intrinsic",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
@@ -164,6 +165,7 @@
       "${tint_src_dir}/lang/wgsl/ast:unittests",
       "${tint_src_dir}/lang/wgsl/ast/transform",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index aa54ccf..6e60fa8 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -4017,7 +4017,8 @@
     for (auto feature : req->features) {
         if (!allowed_features_.features.count(feature)) {
             StringStream ss;
-            ss << "language feature '" << feature << "' is not allowed in the current environment";
+            ss << "language feature '" << wgsl::ToString(feature)
+               << "' is not allowed in the current environment";
             AddError(ss.str(), req->source);
             return false;
         }
diff --git a/src/tint/lang/wgsl/resolver/uniformity_test.cc b/src/tint/lang/wgsl/resolver/uniformity_test.cc
index 671d6f2..769ab82 100644
--- a/src/tint/lang/wgsl/resolver/uniformity_test.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity_test.cc
@@ -305,8 +305,6 @@
     auto condition = static_cast<Condition>(std::get<0>(GetParam()));
     auto function = static_cast<Function>(std::get<1>(GetParam()));
     std::string src = R"(
-enable chromium_experimental_read_write_storage_texture;
-
 var<private> p : i32;
 var<workgroup> w : i32;
 @group(0) @binding(0) var<uniform> u : i32;
@@ -8247,8 +8245,6 @@
 
 TEST_F(UniformityAnalysisTest, StorageTextureLoad_ReadOnly) {
     std::string src = R"(
-enable chromium_experimental_read_write_storage_texture;
-
 @group(0) @binding(0) var t : texture_storage_2d<r32sint, read>;
 
 fn foo() {
@@ -8263,8 +8259,6 @@
 
 TEST_F(UniformityAnalysisTest, StorageTextureLoad_ReadWrite) {
     std::string src = R"(
-enable chromium_experimental_read_write_storage_texture;
-
 @group(0) @binding(0) var t : texture_storage_2d<r32sint, read_write>;
 
 fn foo() {
@@ -8276,15 +8270,15 @@
 
     RunTest(src, false);
     EXPECT_EQ(error_,
-              R"(test:8:5 error: 'storageBarrier' must only be called from uniform control flow
+              R"(test:6:5 error: 'storageBarrier' must only be called from uniform control flow
     storageBarrier();
     ^^^^^^^^^^^^^^
 
-test:7:3 note: control flow depends on possibly non-uniform value
+test:5:3 note: control flow depends on possibly non-uniform value
   if (textureLoad(t, vec2()).r == 0) {
   ^^
 
-test:7:7 note: return value of 'textureLoad' may be non-uniform
+test:5:7 note: return value of 'textureLoad' may be non-uniform
   if (textureLoad(t, vec2()).r == 0) {
       ^^^^^^^^^^^^^^^^^^^^^^
 )");
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 05b2a44..106d739 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -1850,7 +1850,7 @@
     if (feature != wgsl::LanguageFeature::kUndefined) {
         if (!allowed_features_.features.count(feature)) {
             AddError("built-in function '" + std::string(builtin->str()) + "' requires the " +
-                         tint::ToString(feature) +
+                         std::string(wgsl::ToString(feature)) +
                          " language feature, which is not allowed in the current environment",
                      call->Declaration()->source);
             return false;
diff --git a/src/tint/lang/wgsl/sem/BUILD.bazel b/src/tint/lang/wgsl/sem/BUILD.bazel
index 22b1c0e..b8fc112 100644
--- a/src/tint/lang/wgsl/sem/BUILD.bazel
+++ b/src/tint/lang/wgsl/sem/BUILD.bazel
@@ -116,6 +116,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -151,6 +152,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/sem/BUILD.cmake b/src/tint/lang/wgsl/sem/BUILD.cmake
index 119cde7..732d1e9 100644
--- a/src/tint/lang/wgsl/sem/BUILD.cmake
+++ b/src/tint/lang/wgsl/sem/BUILD.cmake
@@ -115,6 +115,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -150,6 +151,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
diff --git a/src/tint/lang/wgsl/sem/BUILD.gn b/src/tint/lang/wgsl/sem/BUILD.gn
index 6d7dfe9..0f29ddc 100644
--- a/src/tint/lang/wgsl/sem/BUILD.gn
+++ b/src/tint/lang/wgsl/sem/BUILD.gn
@@ -119,6 +119,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/utils/containers",
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
@@ -152,6 +153,7 @@
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/resolver",
       "${tint_src_dir}/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/sem/builtin_fn.h b/src/tint/lang/wgsl/sem/builtin_fn.h
index 44e3138..83d62bf 100644
--- a/src/tint/lang/wgsl/sem/builtin_fn.h
+++ b/src/tint/lang/wgsl/sem/builtin_fn.h
@@ -33,7 +33,7 @@
 
 #include "src/tint/lang/wgsl/builtin_fn.h"
 #include "src/tint/lang/wgsl/extension.h"
-#include "src/tint/lang/wgsl/language_feature.h"
+#include "src/tint/lang/wgsl/features/language_feature.h"
 #include "src/tint/lang/wgsl/sem/call_target.h"
 #include "src/tint/lang/wgsl/sem/pipeline_stage_set.h"
 #include "src/tint/utils/math/hash.h"
diff --git a/src/tint/lang/wgsl/wgsl.def b/src/tint/lang/wgsl/wgsl.def
index 8c545c9..23ba104 100644
--- a/src/tint/lang/wgsl/wgsl.def
+++ b/src/tint/lang/wgsl/wgsl.def
@@ -78,8 +78,6 @@
   // A Chromium-specific extension that enables passing of uniform, storage and workgroup
   // address-spaced pointers as parameters, as well as pointers into sub-objects.
   chromium_experimental_full_ptr_parameters
-  // A Chromium-specific extension that adds support for read-write storage textures.
-  chromium_experimental_read_write_storage_texture
   // A Chromium-specific extension that adds basic subgroup functionality.
   chromium_experimental_subgroups
   // A Chromium-specific extension that relaxes memory layout requirements for uniform storage.
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
index bd87e10..fa89968 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
@@ -50,6 +50,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -115,6 +116,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
index 94c711f..11af11b 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
@@ -51,6 +51,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -119,6 +120,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
@@ -166,6 +168,7 @@
   tint_lang_wgsl_ast
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
+  tint_utils_bytes
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_generator
@@ -174,6 +177,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_symbol
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
index 88734f2..dd5524f 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
@@ -53,6 +53,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
@@ -118,6 +119,7 @@
         "${tint_src_dir}/lang/wgsl",
         "${tint_src_dir}/lang/wgsl/ast",
         "${tint_src_dir}/lang/wgsl/common",
+        "${tint_src_dir}/lang/wgsl/features",
         "${tint_src_dir}/lang/wgsl/program",
         "${tint_src_dir}/lang/wgsl/resolver",
         "${tint_src_dir}/lang/wgsl/sem",
@@ -154,6 +156,7 @@
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/utils/bytes",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/generator",
@@ -162,6 +165,7 @@
       "${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",
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index 12688b4..2f51a4c 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -27,7 +27,7 @@
 
 #include "src/tint/lang/wgsl/writer/ast_printer/ast_printer.h"
 
-#include <algorithm>
+#include <string>
 
 #include "src/tint/lang/core/texel_format.h"
 #include "src/tint/lang/wgsl/ast/accessor_expression.h"
@@ -161,7 +161,7 @@
         if (!first) {
             out << ", ";
         }
-        out << feature;
+        out << wgsl::ToString(feature);
         first = false;
     }
     out << ";";
@@ -263,9 +263,9 @@
             // and Inf are not allowed to be spelled in literal, it should be fine to emit f16
             // literals in this way.
             if (l->suffix == ast::FloatLiteralExpression::Suffix::kNone) {
-                out << tint::writer::DoubleToBitPreservingString(l->value);
+                out << tint::strconv::DoubleToBitPreservingString(l->value);
             } else {
-                out << tint::writer::FloatToBitPreservingString(static_cast<float>(l->value))
+                out << tint::strconv::FloatToBitPreservingString(static_cast<float>(l->value))
                     << l->suffix;
             }
         },
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
index 71bb09a..4fa1655 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
@@ -28,10 +28,7 @@
 #ifndef SRC_TINT_LANG_WGSL_WRITER_AST_PRINTER_AST_PRINTER_H_
 #define SRC_TINT_LANG_WGSL_WRITER_AST_PRINTER_AST_PRINTER_H_
 
-#include <string>
-
 #include "src/tint/lang/core/binary_op.h"
-#include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/utils/generator/text_generator.h"
 #include "src/tint/utils/text/string_stream.h"
 
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc
index 77022be..230ab20 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer_fuzz.cc
@@ -29,7 +29,7 @@
 
 #include "src/tint/lang/wgsl/writer/ast_printer/ast_printer.h"
 
-#include "src/tint/cmd/fuzz/wgsl/wgsl_fuzz.h"
+#include "src/tint/cmd/fuzz/wgsl/fuzz.h"
 
 namespace tint::wgsl::writer {
 namespace {
diff --git a/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc b/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc
index 1526fbb..d3a2684 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/requires_test.cc
@@ -47,9 +47,9 @@
 
 // TODO(jrprice): Enable this once we have multiple language features.
 TEST_F(WgslASTPrinterTest, DISABLED_Emit_Requires_Multiple) {
-    auto* req = create<ast::Requires>(
-        wgsl::LanguageFeatures({wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures,
-                                wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures}));
+    auto* req = create<ast::Requires>(ast::Requires::LanguageFeatures(
+        {wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures,
+         wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures}));
     AST().AddRequires(req);
 
     ASTPrinter& gen = Build();
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
index 9eae515..812a463 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.bazel
@@ -40,11 +40,9 @@
   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",
@@ -56,6 +54,7 @@
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/ir",
     "//src/tint/lang/wgsl/program",
@@ -83,7 +82,6 @@
   alwayslink = True,
   srcs = [
     "ir_to_program_test.h",
-    "rename_conflicts_test.cc",
   ] + select({
     ":tint_build_wgsl_writer": [
       "inlining_test.cc",
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 d86a7dc..6fd9f71 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.cmake
@@ -41,8 +41,6 @@
 tint_add_target(tint_lang_wgsl_writer_ir_to_program lib
   lang/wgsl/writer/ir_to_program/ir_to_program.cc
   lang/wgsl/writer/ir_to_program/ir_to_program.h
-  lang/wgsl/writer/ir_to_program/rename_conflicts.cc
-  lang/wgsl/writer/ir_to_program/rename_conflicts.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_writer_ir_to_program lib
@@ -55,6 +53,7 @@
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
+  tint_lang_wgsl_features
   tint_lang_wgsl_intrinsic
   tint_lang_wgsl_ir
   tint_lang_wgsl_program
@@ -81,7 +80,6 @@
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_ir_to_program_test test
   lang/wgsl/writer/ir_to_program/ir_to_program_test.h
-  lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_writer_ir_to_program_test test
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 e2957b0..ed032db 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ir_to_program/BUILD.gn
@@ -46,8 +46,6 @@
   sources = [
     "ir_to_program.cc",
     "ir_to_program.h",
-    "rename_conflicts.cc",
-    "rename_conflicts.h",
   ]
   deps = [
     "${tint_src_dir}/api/common",
@@ -59,6 +57,7 @@
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
     "${tint_src_dir}/lang/wgsl/common",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/intrinsic",
     "${tint_src_dir}/lang/wgsl/ir",
     "${tint_src_dir}/lang/wgsl/program",
@@ -81,10 +80,7 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    sources = [
-      "ir_to_program_test.h",
-      "rename_conflicts_test.cc",
-    ]
+    sources = [ "ir_to_program_test.h" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
       "${tint_src_dir}/api/common",
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc
index d08dbbf..e69347d 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/inlining_test.cc
@@ -934,7 +934,7 @@
     b.Append(fn->Block(), [&] {
         auto* v = b.Add(ty.i32(), 1_i, 2_i);
         auto* switch_ = b.Switch(3_i);
-        auto* case_ = b.Case(switch_, {core::ir::Switch::CaseSelector{}});
+        auto* case_ = b.DefaultCase(switch_);
         b.Append(case_, [&] { b.Return(fn, v); });
         b.Return(fn, 0_i);
     });
@@ -959,7 +959,7 @@
         auto* v_1 = b.Load(var);
         auto* v_2 = b.Add(ty.i32(), v_1, 2_i);
         auto* switch_ = b.Switch(3_i);
-        auto* case_ = b.Case(switch_, {core::ir::Switch::CaseSelector{}});
+        auto* case_ = b.DefaultCase(switch_);
         b.Append(case_, [&] { b.Return(fn, v_2); });
         b.Return(fn, 0_i);
     });
@@ -983,7 +983,7 @@
     b.Append(fn->Block(), [&] {
         auto* v = b.Add(ty.i32(), 1_i, 2_i);
         auto* switch_ = b.Switch(v);
-        auto* case_ = b.Case(switch_, {core::ir::Switch::CaseSelector{}});
+        auto* case_ = b.DefaultCase(switch_);
         b.Append(case_, [&] { b.Return(fn, 3_i); });
         b.Return(fn, 0_i);
     });
@@ -1007,7 +1007,7 @@
         var->SetInitializer(b.Constant(1_i));
         auto* v_1 = b.Load(var);
         auto* switch_ = b.Switch(v_1);
-        auto* case_ = b.Case(switch_, {core::ir::Switch::CaseSelector{}});
+        auto* case_ = b.DefaultCase(switch_);
         b.Append(case_, [&] { b.Return(fn, 3_i); });
         b.Return(fn, 0_i);
     });
@@ -1032,7 +1032,7 @@
         b.Store(var, 1_i);
         auto* load = b.Load(var);
         auto* switch_ = b.Switch(1_i);
-        auto* case_ = b.Case(switch_, {core::ir::Switch::CaseSelector{}});
+        auto* case_ = b.DefaultCase(switch_);
         b.Append(case_, [&] {
             b.Store(var, 2_i);
             b.ExitSwitch(switch_);
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index e3650e6..9f83536 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -79,7 +79,6 @@
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
-#include "src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h"
 #include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/containers/predicates.h"
 #include "src/tint/utils/containers/reverse.h"
@@ -102,18 +101,9 @@
 
 class State {
   public:
-    explicit State(core::ir::Module& m) : mod(m) {}
+    explicit State(const core::ir::Module& m) : mod(m) {}
 
     Program Run() {
-        // Run transforms need to sanitize for WGSL.
-        {
-            auto result = RenameConflicts(&mod);
-            if (!result) {
-                b.Diagnostics().add(result.Failure().reason);
-                return Program(std::move(b));
-            }
-        }
-
         if (auto res = core::ir::Validate(mod); !res) {
             // IR module failed validation.
             b.Diagnostics() = res.Failure().reason;
@@ -123,7 +113,7 @@
         RootBlock(mod.root_block);
 
         // TODO(crbug.com/tint/1902): Emit user-declared types
-        for (auto* fn : mod.functions) {
+        for (auto& fn : mod.functions) {
             Fn(fn);
         }
         return Program{resolver::Resolve(b)};
@@ -137,7 +127,7 @@
     };
 
     /// The source IR module
-    core::ir::Module& mod;
+    const core::ir::Module& mod;
 
     /// The target ProgramBuilder
     ProgramBuilder b;
@@ -162,10 +152,10 @@
     using ValueBinding = std::variant<VariableValue, InlinedValue, ConsumedValue>;
 
     /// IR values to their representation
-    Hashmap<core::ir::Value*, ValueBinding, 32> bindings_;
+    Hashmap<const core::ir::Value*, ValueBinding, 32> bindings_;
 
     /// Names for values
-    Hashmap<core::ir::Value*, Symbol, 32> names_;
+    Hashmap<const core::ir::Value*, Symbol, 32> names_;
 
     /// The nesting depth of the currently generated AST
     /// 0  is module scope
@@ -178,10 +168,10 @@
     StatementList* statements_ = nullptr;
 
     /// The current switch case block
-    core::ir::Block* current_switch_case_ = nullptr;
+    const core::ir::Block* current_switch_case_ = nullptr;
 
     /// Values that can be inlined.
-    Hashset<core::ir::Value*, 64> can_inline_;
+    Hashset<const core::ir::Value*, 64> can_inline_;
 
     /// Set of enable directives emitted.
     Hashset<wgsl::Extension, 4> enables_;
@@ -192,20 +182,20 @@
     /// True if 'diagnostic(off, derivative_uniformity)' has been emitted
     bool disabled_derivative_uniformity_ = false;
 
-    void RootBlock(core::ir::Block* root) {
+    void RootBlock(const core::ir::Block* root) {
         for (auto* inst : *root) {
             tint::Switch(
-                inst,                                   //
-                [&](core::ir::Var* var) { Var(var); },  //
+                inst,                                         //
+                [&](const core::ir::Var* var) { Var(var); },  //
                 TINT_ICE_ON_NO_MATCH);
         }
     }
-    const ast::Function* Fn(core::ir::Function* fn) {
+    const ast::Function* Fn(const core::ir::Function* fn) {
         SCOPED_NESTING();
 
         // TODO(crbug.com/tint/1915): Properly implement this when we've fleshed out Function
         static constexpr size_t N = decltype(ast::Function::params)::static_length;
-        auto params = tint::Transform<N>(fn->Params(), [&](core::ir::FunctionParam* param) {
+        auto params = tint::Transform<N>(fn->Params(), [&](const core::ir::FunctionParam* param) {
             auto ty = Type(param->Type());
             auto name = NameFor(param);
             Bind(param, name, PtrKind::kPtr);
@@ -225,12 +215,12 @@
                       std::move(ret_attrs));
     }
 
-    const ast::BlockStatement* Block(core::ir::Block* block) {
+    const ast::BlockStatement* Block(const core::ir::Block* block) {
         // TODO(crbug.com/tint/1902): Handle block arguments.
         return b.Block(Statements(block));
     }
 
-    StatementList Statements(core::ir::Block* block) {
+    StatementList Statements(const core::ir::Block* block) {
         StatementList stmts;
         if (block) {
             MarkInlinable(block);
@@ -242,10 +232,10 @@
         return stmts;
     }
 
-    void MarkInlinable(core::ir::Block* block) {
+    void MarkInlinable(const core::ir::Block* block) {
         // An ordered list of possibly-inlinable values returned by sequenced instructions that have
         // not yet been marked-for or ruled-out-for inlining.
-        UniqueVector<core::ir::Value*, 32> pending_resolution;
+        UniqueVector<const core::ir::Value*, 32> pending_resolution;
 
         // Walk the instructions of the block starting with the first.
         for (auto* inst : *block) {
@@ -283,10 +273,10 @@
             if (inst->Results().Length() == 1) {
                 // Instruction has a single result value.
                 // Check to see if the result of this instruction is a candidate for inlining.
-                auto* result = inst->Result();
+                auto* result = inst->Result(0);
                 // Only values with a single usage can be inlined.
                 // Named values are not inlined, as we want to emit the name for a let.
-                if (result->Usages().Count() == 1 && !mod.NameOf(result).IsValid()) {
+                if (result->NumUsages() == 1 && !mod.NameOf(result).IsValid()) {
                     if (sequenced) {
                         // The value comes from a sequenced instruction. We need to ensure
                         // instruction ordering so add it to 'pending_resolution'.
@@ -311,35 +301,35 @@
 
     void Append(const ast::Statement* inst) { statements_->Push(inst); }
 
-    void Instruction(core::ir::Instruction* inst) {
+    void Instruction(const core::ir::Instruction* inst) {
         tint::Switch(
-            inst,                                                             //
-            [&](core::ir::Access* i) { Access(i); },                          //
-            [&](core::ir::Binary* i) { Binary(i); },                          //
-            [&](core::ir::BreakIf* i) { BreakIf(i); },                        //
-            [&](core::ir::Call* i) { Call(i); },                              //
-            [&](core::ir::Continue*) {},                                      //
-            [&](core::ir::ExitIf*) {},                                        //
-            [&](core::ir::ExitLoop* i) { ExitLoop(i); },                      //
-            [&](core::ir::ExitSwitch* i) { ExitSwitch(i); },                  //
-            [&](core::ir::If* i) { If(i); },                                  //
-            [&](core::ir::Let* i) { Let(i); },                                //
-            [&](core::ir::Load* l) { Load(l); },                              //
-            [&](core::ir::LoadVectorElement* i) { LoadVectorElement(i); },    //
-            [&](core::ir::Loop* l) { Loop(l); },                              //
-            [&](core::ir::NextIteration*) {},                                 //
-            [&](core::ir::Return* i) { Return(i); },                          //
-            [&](core::ir::Store* i) { Store(i); },                            //
-            [&](core::ir::StoreVectorElement* i) { StoreVectorElement(i); },  //
-            [&](core::ir::Switch* i) { Switch(i); },                          //
-            [&](core::ir::Swizzle* i) { Swizzle(i); },                        //
-            [&](core::ir::Unary* i) { Unary(i); },                            //
-            [&](core::ir::Unreachable*) {},                                   //
-            [&](core::ir::Var* i) { Var(i); },                                //
+            inst,                                                                   //
+            [&](const core::ir::Access* i) { Access(i); },                          //
+            [&](const core::ir::Binary* i) { Binary(i); },                          //
+            [&](const core::ir::BreakIf* i) { BreakIf(i); },                        //
+            [&](const core::ir::Call* i) { Call(i); },                              //
+            [&](const core::ir::Continue*) {},                                      //
+            [&](const core::ir::ExitIf*) {},                                        //
+            [&](const core::ir::ExitLoop* i) { ExitLoop(i); },                      //
+            [&](const core::ir::ExitSwitch* i) { ExitSwitch(i); },                  //
+            [&](const core::ir::If* i) { If(i); },                                  //
+            [&](const core::ir::Let* i) { Let(i); },                                //
+            [&](const core::ir::Load* l) { Load(l); },                              //
+            [&](const core::ir::LoadVectorElement* i) { LoadVectorElement(i); },    //
+            [&](const core::ir::Loop* l) { Loop(l); },                              //
+            [&](const core::ir::NextIteration*) {},                                 //
+            [&](const core::ir::Return* i) { Return(i); },                          //
+            [&](const core::ir::Store* i) { Store(i); },                            //
+            [&](const core::ir::StoreVectorElement* i) { StoreVectorElement(i); },  //
+            [&](const core::ir::Switch* i) { Switch(i); },                          //
+            [&](const core::ir::Swizzle* i) { Swizzle(i); },                        //
+            [&](const core::ir::Unary* i) { Unary(i); },                            //
+            [&](const core::ir::Unreachable*) {},                                   //
+            [&](const core::ir::Var* i) { Var(i); },                                //
             TINT_ICE_ON_NO_MATCH);
     }
 
-    void If(core::ir::If* if_) {
+    void If(const core::ir::If* if_) {
         SCOPED_NESTING();
 
         auto true_stmts = Statements(if_->True());
@@ -367,7 +357,7 @@
         Append(b.If(cond, true_block, b.Else(false_block)));
     }
 
-    void Loop(core::ir::Loop* l) {
+    void Loop(const core::ir::Loop* l) {
         SCOPED_NESTING();
 
         // Build all the initializer statements
@@ -398,7 +388,7 @@
             for (auto* inst : *l->Body()) {
                 if (body_stmts.IsEmpty()) {
                     if (auto* if_ = inst->As<core::ir::If>()) {
-                        if (!if_->HasResults() &&                                //
+                        if (if_->Results().IsEmpty() &&                          //
                             if_->True()->Length() == 1 &&                        //
                             if_->False()->Length() == 1 &&                       //
                             tint::Is<core::ir::ExitIf>(if_->True()->Front()) &&  //
@@ -464,24 +454,24 @@
         statements_->Push(loop);
     }
 
-    void Switch(core::ir::Switch* s) {
+    void Switch(const core::ir::Switch* s) {
         SCOPED_NESTING();
 
         auto* cond = Expr(s->Condition());
 
-        auto cases = tint::Transform(
+        auto cases = tint::Transform<4>(
             s->Cases(),  //
-            [&](core::ir::Switch::Case c) -> const tint::ast::CaseStatement* {
+            [&](const core::ir::Switch::Case& c) -> const tint::ast::CaseStatement* {
                 SCOPED_NESTING();
 
                 const ast::BlockStatement* body = nullptr;
                 {
-                    TINT_SCOPED_ASSIGNMENT(current_switch_case_, c.Block());
-                    body = Block(c.Block());
+                    TINT_SCOPED_ASSIGNMENT(current_switch_case_, c.block);
+                    body = Block(c.block);
                 }
 
                 auto selectors = tint::Transform(c.selectors,  //
-                                                 [&](core::ir::Switch::CaseSelector cs) {
+                                                 [&](const core::ir::Switch::CaseSelector& cs) {
                                                      return cs.IsDefault()
                                                                 ? b.DefaultCaseSelector()
                                                                 : b.CaseSelector(Expr(cs.val));
@@ -501,9 +491,9 @@
 
     void ExitLoop(const core::ir::ExitLoop*) { Append(b.Break()); }
 
-    void BreakIf(core::ir::BreakIf* i) { Append(b.BreakIf(Expr(i->Condition()))); }
+    void BreakIf(const core::ir::BreakIf* i) { Append(b.BreakIf(Expr(i->Condition()))); }
 
-    void Return(core::ir::Return* ret) {
+    void Return(const core::ir::Return* ret) {
         if (ret->Args().IsEmpty()) {
             // Return has no arguments.
             // If this block is nested withing some control flow, then we must
@@ -524,12 +514,12 @@
         Append(b.Return(Expr(ret->Args().Front())));
     }
 
-    void Var(core::ir::Var* var) {
-        auto* val = var->Result();
+    void Var(const core::ir::Var* var) {
+        auto* val = var->Result(0);
         auto* ptr = As<core::type::Pointer>(val->Type());
         auto ty = Type(ptr->StoreType());
-        Symbol name = NameFor(var->Result());
-        Bind(var->Result(), name, PtrKind::kRef);
+        Symbol name = NameFor(var->Result(0));
+        Bind(var->Result(0), name, PtrKind::kRef);
 
         Vector<const ast::Attribute*, 4> attrs;
         if (auto bp = var->BindingPoint()) {
@@ -557,32 +547,32 @@
         }
     }
 
-    void Let(core::ir::Let* let) {
-        Symbol name = NameFor(let->Result());
+    void Let(const core::ir::Let* let) {
+        Symbol name = NameFor(let->Result(0));
         Append(b.Decl(b.Let(name, Expr(let->Value(), PtrKind::kPtr))));
-        Bind(let->Result(), name, PtrKind::kPtr);
+        Bind(let->Result(0), name, PtrKind::kPtr);
     }
 
-    void Store(core::ir::Store* store) {
+    void Store(const core::ir::Store* store) {
         auto* dst = Expr(store->To());
         auto* src = Expr(store->From());
         Append(b.Assign(dst, src));
     }
 
-    void StoreVectorElement(core::ir::StoreVectorElement* store) {
+    void StoreVectorElement(const core::ir::StoreVectorElement* store) {
         auto* ptr = Expr(store->To());
         auto* val = Expr(store->Value());
         Append(b.Assign(VectorMemberAccess(ptr, store->Index()), val));
     }
 
-    void Call(core::ir::Call* call) {
-        auto args = tint::Transform<4>(call->Args(), [&](core::ir::Value* arg) {
+    void Call(const core::ir::Call* call) {
+        auto args = tint::Transform<4>(call->Args(), [&](const core::ir::Value* arg) {
             // Pointer-like arguments are passed by pointer, never reference.
             return Expr(arg, PtrKind::kPtr);
         });
         tint::Switch(
             call,  //
-            [&](core::ir::UserCall* c) {
+            [&](const core::ir::UserCall* c) {
                 for (auto* arg : call->Args()) {
                     if (ArgRequiresFullPtrParameters(arg)) {
                         Enable(wgsl::Extension::kChromiumExperimentalFullPtrParameters);
@@ -590,13 +580,13 @@
                     }
                 }
                 auto* expr = b.Call(NameFor(c->Target()), std::move(args));
-                if (!call->HasResults() || call->Result()->Usages().IsEmpty()) {
+                if (call->Results().IsEmpty() || !call->Result(0)->IsUsed()) {
                     Append(b.CallStmt(expr));
                     return;
                 }
-                Bind(c->Result(), expr, PtrKind::kPtr);
+                Bind(c->Result(0), expr, PtrKind::kPtr);
             },
-            [&](wgsl::ir::BuiltinCall* c) {
+            [&](const wgsl::ir::BuiltinCall* c) {
                 if (!disabled_derivative_uniformity_ && RequiresDerivativeUniformity(c->Func())) {
                     // TODO(crbug.com/tint/1985): Be smarter about disabling derivative uniformity.
                     b.DiagnosticDirective(wgsl::DiagnosticSeverity::kOff,
@@ -614,36 +604,36 @@
                 }
 
                 auto* expr = b.Call(c->Func(), std::move(args));
-                if (!call->HasResults() || call->Result()->Type()->Is<core::type::Void>()) {
+                if (call->Results().IsEmpty() || call->Result(0)->Type()->Is<core::type::Void>()) {
                     Append(b.CallStmt(expr));
                     return;
                 }
-                Bind(c->Result(), expr, PtrKind::kPtr);
+                Bind(c->Result(0), expr, PtrKind::kPtr);
             },
-            [&](core::ir::Construct* c) {
-                auto ty = Type(c->Result()->Type());
-                Bind(c->Result(), b.Call(ty, std::move(args)), PtrKind::kPtr);
+            [&](const core::ir::Construct* c) {
+                auto ty = Type(c->Result(0)->Type());
+                Bind(c->Result(0), b.Call(ty, std::move(args)), PtrKind::kPtr);
             },
-            [&](core::ir::Convert* c) {
-                auto ty = Type(c->Result()->Type());
-                Bind(c->Result(), b.Call(ty, std::move(args)), PtrKind::kPtr);
+            [&](const core::ir::Convert* c) {
+                auto ty = Type(c->Result(0)->Type());
+                Bind(c->Result(0), b.Call(ty, std::move(args)), PtrKind::kPtr);
             },
-            [&](core::ir::Bitcast* c) {
-                auto ty = Type(c->Result()->Type());
-                Bind(c->Result(), b.Bitcast(ty, args[0]), PtrKind::kPtr);
+            [&](const core::ir::Bitcast* c) {
+                auto ty = Type(c->Result(0)->Type());
+                Bind(c->Result(0), b.Bitcast(ty, args[0]), PtrKind::kPtr);
             },
-            [&](core::ir::Discard*) { Append(b.Discard()); },  //
+            [&](const core::ir::Discard*) { Append(b.Discard()); },  //
             TINT_ICE_ON_NO_MATCH);
     }
 
-    void Load(core::ir::Load* l) { Bind(l->Result(), Expr(l->From())); }
+    void Load(const core::ir::Load* l) { Bind(l->Result(0), Expr(l->From())); }
 
-    void LoadVectorElement(core::ir::LoadVectorElement* load) {
+    void LoadVectorElement(const core::ir::LoadVectorElement* load) {
         auto* ptr = Expr(load->From());
-        Bind(load->Result(), VectorMemberAccess(ptr, load->Index()));
+        Bind(load->Result(0), VectorMemberAccess(ptr, load->Index()));
     }
 
-    void Unary(core::ir::Unary* u) {
+    void Unary(const core::ir::Unary* u) {
         const ast::Expression* expr = nullptr;
         switch (u->Op()) {
             case core::ir::UnaryOp::kComplement:
@@ -653,10 +643,10 @@
                 expr = b.Negation(Expr(u->Val()));
                 break;
         }
-        Bind(u->Result(), expr);
+        Bind(u->Result(0), expr);
     }
 
-    void Access(core::ir::Access* a) {
+    void Access(const core::ir::Access* a) {
         auto* expr = Expr(a->Object());
         auto* obj_ty = a->Object()->Type()->UnwrapPtr();
         for (auto* index : a->Indices()) {
@@ -687,10 +677,10 @@
                 },  //
                 TINT_ICE_ON_NO_MATCH);
         }
-        Bind(a->Result(), expr);
+        Bind(a->Result(0), expr);
     }
 
-    void Swizzle(core::ir::Swizzle* s) {
+    void Swizzle(const core::ir::Swizzle* s) {
         auto* vec = Expr(s->Object());
         Vector<char, 4> components;
         for (uint32_t i : s->Indices()) {
@@ -702,16 +692,16 @@
         }
         auto* swizzle =
             b.MemberAccessor(vec, std::string_view(components.begin(), components.Length()));
-        Bind(s->Result(), swizzle);
+        Bind(s->Result(0), swizzle);
     }
 
-    void Binary(core::ir::Binary* e) {
+    void Binary(const core::ir::Binary* e) {
         if (e->Op() == core::ir::BinaryOp::kEqual) {
             auto* rhs = e->RHS()->As<core::ir::Constant>();
             if (rhs && rhs->Type()->Is<core::type::Bool>() &&
                 rhs->Value()->ValueAs<bool>() == false) {
                 // expr == false
-                Bind(e->Result(), b.Not(Expr(e->LHS())));
+                Bind(e->Result(0), b.Not(Expr(e->LHS())));
                 return;
             }
         }
@@ -768,17 +758,18 @@
                 expr = b.Shr(lhs, rhs);
                 break;
         }
-        Bind(e->Result(), expr);
+        Bind(e->Result(0), expr);
     }
 
     TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
 
-    const ast::Expression* Expr(core::ir::Value* value, PtrKind want_ptr_kind = PtrKind::kRef) {
+    const ast::Expression* Expr(const core::ir::Value* value,
+                                PtrKind want_ptr_kind = PtrKind::kRef) {
         using ExprAndPtrKind = std::pair<const ast::Expression*, PtrKind>;
 
         auto [expr, got_ptr_kind] = tint::Switch(
             value,
-            [&](core::ir::Constant* c) -> ExprAndPtrKind {
+            [&](const core::ir::Constant* c) -> ExprAndPtrKind {
                 return {Constant(c), PtrKind::kRef};
             },
             [&](Default) -> ExprAndPtrKind {
@@ -829,7 +820,7 @@
 
     TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
 
-    const ast::Expression* Constant(core::ir::Constant* c) { return Constant(c->Value()); }
+    const ast::Expression* Constant(const core::ir::Constant* c) { return Constant(c->Value()); }
 
     const ast::Expression* Constant(const core::constant::Value* c) {
         auto composite = [&](bool can_splat) {
@@ -1020,7 +1011,7 @@
 
     /// @returns the AST name for the given value, creating and returning a new name on the first
     /// call.
-    Symbol NameFor(core::ir::Value* value, std::string_view suggested = {}) {
+    Symbol NameFor(const core::ir::Value* value, std::string_view suggested = {}) {
         return names_.GetOrCreate(value, [&] {
             if (!suggested.empty()) {
                 return b.Symbols().Register(suggested);
@@ -1034,7 +1025,7 @@
 
     /// Associates the IR value @p value with the AST expression @p expr.
     /// @p ptr_kind defines how pointer values are represented by @p expr.
-    void Bind(core::ir::Value* value,
+    void Bind(const core::ir::Value* value,
               const ast::Expression* expr,
               PtrKind ptr_kind = PtrKind::kRef) {
         TINT_ASSERT(value);
@@ -1048,7 +1039,7 @@
                 expr = ToPtrKind(expr, ptr_kind, PtrKind::kPtr);
             }
             auto mod_name = mod.NameOf(value);
-            if (value->Usages().IsEmpty() && !mod_name.IsValid()) {
+            if (!value->IsUsed() && !mod_name.IsValid()) {
                 // Value has no usages and no name.
                 // Assign to a phony. These support more data types than a 'let', and avoids
                 // allocation of unused names.
@@ -1067,7 +1058,7 @@
     /// Associates the IR value @p value with the AST 'var', 'let' or parameter with the name @p
     /// name.
     /// @p ptr_kind defines how pointer values are represented by @p expr.
-    void Bind(core::ir::Value* value, Symbol name, PtrKind ptr_kind) {
+    void Bind(const core::ir::Value* value, Symbol name, PtrKind ptr_kind) {
         TINT_ASSERT(value);
 
         bool added = bindings_.Add(value, VariableValue{name, ptr_kind});
@@ -1079,13 +1070,13 @@
     ////////////////////////////////////////////////////////////////////////////////////////////////
     // Helpers
     ////////////////////////////////////////////////////////////////////////////////////////////////
-    bool AsShortCircuit(core::ir::If* i,
+    bool AsShortCircuit(const core::ir::If* i,
                         const StatementList& true_stmts,
                         const StatementList& false_stmts) {
-        if (!i->HasResults()) {
+        if (i->Results().IsEmpty()) {
             return false;
         }
-        auto* result = i->Result();
+        auto* result = i->Result(0);
         if (!result->Type()->Is<core::type::Bool>()) {
             return false;  // Wrong result type
         }
@@ -1140,7 +1131,7 @@
         return false;
     }
 
-    bool IsConstant(core::ir::Value* val, bool value) {
+    bool IsConstant(const core::ir::Value* val, bool value) {
         if (auto* c = val->As<core::ir::Constant>()) {
             if (c->Type()->Is<core::type::Bool>()) {
                 return c->Value()->ValueAs<bool>() == value;
@@ -1149,7 +1140,8 @@
         return false;
     }
 
-    const ast::Expression* VectorMemberAccess(const ast::Expression* expr, core::ir::Value* index) {
+    const ast::Expression* VectorMemberAccess(const ast::Expression* expr,
+                                              const core::ir::Value* index) {
         if (auto* c = index->As<core::ir::Constant>()) {
             switch (c->Value()->ValueAs<int>()) {
                 case 0:
@@ -1215,14 +1207,14 @@
 
     /// @returns true if the argument @p arg requires the kChromiumExperimentalFullPtrParameters
     /// extension to be enabled.
-    bool ArgRequiresFullPtrParameters(core::ir::Value* arg) {
+    bool ArgRequiresFullPtrParameters(const core::ir::Value* arg) {
         if (!arg->Type()->Is<core::type::Pointer>()) {
             return false;
         }
 
         auto res = arg->As<core::ir::InstructionResult>();
         while (res) {
-            auto* inst = res->Source();
+            auto* inst = res->Instruction();
             if (inst->Is<core::ir::Access>()) {
                 return true;  // Passing pointer into sub-object
             }
@@ -1238,7 +1230,7 @@
 
 }  // namespace
 
-Program IRToProgram(core::ir::Module& i) {
+Program IRToProgram(const core::ir::Module& i) {
     return State{i}.Run();
 }
 
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h
index 267bb29..089a1e7 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h
@@ -40,7 +40,7 @@
 /// @param module the IR module
 /// @return the tint::Program.
 /// @note Check the returned Program::Diagnostics() for any errors.
-Program IRToProgram(core::ir::Module& module);
+Program IRToProgram(const core::ir::Module& module);
 
 }  // namespace tint::wgsl::writer
 
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
index 53af431..d034c0d 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
@@ -1870,11 +1870,11 @@
         auto* va = b.Var("a", ty.ptr<function, i32>());
         va->SetInitializer(b.Constant(42_i));
 
-        auto* la = b.Load(va)->Result();
+        auto* la = b.Load(va)->Result(0);
         auto* vb = b.Var("b", ty.ptr<function, i32>());
         vb->SetInitializer(la);
 
-        auto* lb = b.Load(vb)->Result();
+        auto* lb = b.Load(vb)->Result(0);
         auto* vc = b.Var("c", ty.ptr<function, i32>());
         vc->SetInitializer(lb);
 
@@ -2206,7 +2206,7 @@
         v->SetInitializer(b.Constant(42_i));
 
         auto s = b.Switch(b.Load(v));
-        b.Append(b.Case(s, {core::ir::Switch::CaseSelector{}}), [&] {
+        b.Append(b.DefaultCase(s), [&] {
             b.Call(ty.void_(), fn_a);
             b.ExitSwitch(s);
         });
@@ -2246,20 +2246,15 @@
         v->SetInitializer(b.Constant(42_i));
 
         auto s = b.Switch(b.Load(v));
-        b.Append(b.Case(s, {core::ir::Switch::CaseSelector{b.Constant(0_i)}}), [&] {
+        b.Append(b.Case(s, {b.Constant(0_i)}), [&] {
             b.Call(ty.void_(), fn_a);
             b.ExitSwitch(s);
         });
-        b.Append(b.Case(s,
-                        {
-                            core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                            core::ir::Switch::CaseSelector{},
-                        }),
-                 [&] {
-                     b.Call(ty.void_(), fn_b);
-                     b.ExitSwitch(s);
-                 });
-        b.Append(b.Case(s, {core::ir::Switch::CaseSelector{b.Constant(2_i)}}), [&] {
+        b.Append(b.Case(s, {b.Constant(1_i), nullptr}), [&] {
+            b.Call(ty.void_(), fn_b);
+            b.ExitSwitch(s);
+        });
+        b.Append(b.Case(s, {b.Constant(2_i)}), [&] {
             b.Call(ty.void_(), fn_c);
             b.ExitSwitch(s);
         });
@@ -2305,16 +2300,9 @@
         v->SetInitializer(b.Constant(42_i));
 
         auto s = b.Switch(b.Load(v));
-        b.Append(b.Case(s, {core::ir::Switch::CaseSelector{b.Constant(0_i)}}),
-                 [&] { b.Return(fn); });
-        b.Append(b.Case(s,
-                        {
-                            core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                            core::ir::Switch::CaseSelector{},
-                        }),
-                 [&] { b.Return(fn); });
-        b.Append(b.Case(s, {core::ir::Switch::CaseSelector{b.Constant(2_i)}}),
-                 [&] { b.Return(fn); });
+        b.Append(b.Case(s, {b.Constant(0_i)}), [&] { b.Return(fn); });
+        b.Append(b.Case(s, {b.Constant(1_i), nullptr}), [&] { b.Return(fn); });
+        b.Append(b.Case(s, {b.Constant(2_i)}), [&] { b.Return(fn); });
 
         b.Call(ty.void_(), fn_a);
         b.Return(fn);
@@ -2361,29 +2349,18 @@
         v2->SetInitializer(b.Constant(24_i));
 
         auto s1 = b.Switch(b.Load(v1));
-        b.Append(b.Case(s1, {core::ir::Switch::CaseSelector{b.Constant(0_i)}}), [&] {
+        b.Append(b.Case(s1, {b.Constant(0_i)}), [&] {
             b.Call(ty.void_(), fn_a);
             b.ExitSwitch(s1);
         });
-        b.Append(b.Case(s1,
-                        {
-                            core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                            core::ir::Switch::CaseSelector{},
-                        }),
-                 [&] {
-                     auto s2 = b.Switch(b.Load(v2));
-                     b.Append(b.Case(s2, {core::ir::Switch::CaseSelector{b.Constant(0_i)}}),
-                              [&] { b.ExitSwitch(s2); });
-                     b.Append(b.Case(s2,
-                                     {
-                                         core::ir::Switch::CaseSelector{b.Constant(1_i)},
-                                         core::ir::Switch::CaseSelector{},
-                                     }),
-                              [&] { b.Return(fn); });
+        b.Append(b.Case(s1, {b.Constant(1_i), nullptr}), [&] {
+            auto s2 = b.Switch(b.Load(v2));
+            b.Append(b.Case(s2, {b.Constant(0_i)}), [&] { b.ExitSwitch(s2); });
+            b.Append(b.Case(s2, {b.Constant(1_i), nullptr}), [&] { b.Return(fn); });
 
-                     b.ExitSwitch(s1);
-                 });
-        b.Append(b.Case(s1, {core::ir::Switch::CaseSelector{b.Constant(2_i)}}), [&] {
+            b.ExitSwitch(s1);
+        });
+        b.Append(b.Case(s1, {b.Constant(2_i)}), [&] {
             b.Call(ty.void_(), fn_c);
             b.ExitSwitch(s1);
         });
@@ -2694,7 +2671,7 @@
         auto* loop = b.Loop();
 
         b.Append(loop->Initializer(), [&] {
-            auto* n_0 = b.Call(ty.i32(), fn_n, 0_i)->Result();
+            auto* n_0 = b.Call(ty.i32(), fn_n, 0_i)->Result(0);
             auto* i = b.Var("i", ty.ptr<function, i32>());
             i->SetInitializer(n_0);
             b.NextIteration(loop);
diff --git a/src/tint/lang/wgsl/writer/raise/BUILD.bazel b/src/tint/lang/wgsl/writer/raise/BUILD.bazel
index 4378fbe..e403363 100644
--- a/src/tint/lang/wgsl/writer/raise/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/raise/BUILD.bazel
@@ -40,9 +40,11 @@
   name = "raise",
   srcs = [
     "raise.cc",
+    "rename_conflicts.cc",
   ],
   hdrs = [
     "raise.h",
+    "rename_conflicts.h",
   ],
   deps = [
     "//src/tint/api/common",
@@ -76,6 +78,7 @@
   alwayslink = True,
   srcs = [
     "raise_test.cc",
+    "rename_conflicts_test.cc",
   ],
   deps = [
     "//src/tint/api/common",
diff --git a/src/tint/lang/wgsl/writer/raise/BUILD.cmake b/src/tint/lang/wgsl/writer/raise/BUILD.cmake
index 153221c..cf18c7f 100644
--- a/src/tint/lang/wgsl/writer/raise/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/raise/BUILD.cmake
@@ -41,6 +41,8 @@
 tint_add_target(tint_lang_wgsl_writer_raise lib
   lang/wgsl/writer/raise/raise.cc
   lang/wgsl/writer/raise/raise.h
+  lang/wgsl/writer/raise/rename_conflicts.cc
+  lang/wgsl/writer/raise/rename_conflicts.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_writer_raise lib
@@ -74,6 +76,7 @@
 ################################################################################
 tint_add_target(tint_lang_wgsl_writer_raise_test test
   lang/wgsl/writer/raise/raise_test.cc
+  lang/wgsl/writer/raise/rename_conflicts_test.cc
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_writer_raise_test test
diff --git a/src/tint/lang/wgsl/writer/raise/BUILD.gn b/src/tint/lang/wgsl/writer/raise/BUILD.gn
index db96351..53b013a 100644
--- a/src/tint/lang/wgsl/writer/raise/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/raise/BUILD.gn
@@ -46,6 +46,8 @@
   sources = [
     "raise.cc",
     "raise.h",
+    "rename_conflicts.cc",
+    "rename_conflicts.h",
   ]
   deps = [
     "${tint_src_dir}/api/common",
@@ -74,7 +76,10 @@
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
-    sources = [ "raise_test.cc" ]
+    sources = [
+      "raise_test.cc",
+      "rename_conflicts_test.cc",
+    ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
       "${tint_src_dir}/api/common",
diff --git a/src/tint/lang/wgsl/writer/raise/raise.cc b/src/tint/lang/wgsl/writer/raise/raise.cc
index 407d4e1..cf0f06e 100644
--- a/src/tint/lang/wgsl/writer/raise/raise.cc
+++ b/src/tint/lang/wgsl/writer/raise/raise.cc
@@ -35,6 +35,7 @@
 #include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/wgsl/builtin_fn.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
+#include "src/tint/lang/wgsl/writer/raise/rename_conflicts.h"
 
 namespace tint::wgsl::writer {
 namespace {
@@ -169,7 +170,7 @@
 void ReplaceBuiltinFnCall(core::ir::Module& mod, core::ir::CoreBuiltinCall* call) {
     Vector<core::ir::Value*, 8> args(call->Args());
     auto* replacement = mod.instructions.Create<wgsl::ir::BuiltinCall>(
-        call->Result(), Convert(call->Func()), std::move(args));
+        call->Result(0), Convert(call->Func()), std::move(args));
     call->ReplaceWith(replacement);
     call->ClearResults();
     call->Destroy();
@@ -183,7 +184,7 @@
     // And replace with:
     //    %value = call workgroupUniformLoad %ptr
 
-    auto* load = As<core::ir::Load>(call->next);
+    auto* load = As<core::ir::Load>(call->next.Get());
     if (!load || load->From()->Type()->As<core::type::Pointer>()->AddressSpace() !=
                      core::AddressSpace::kWorkgroup) {
         // No match
@@ -191,7 +192,7 @@
         return;
     }
 
-    auto* post_load = As<core::ir::CoreBuiltinCall>(load->next);
+    auto* post_load = As<core::ir::CoreBuiltinCall>(load->next.Get());
     if (!post_load || post_load->Func() != core::BuiltinFn::kWorkgroupBarrier) {
         // No match
         ReplaceBuiltinFnCall(mod, call);
@@ -204,7 +205,7 @@
 
     // Replace load with workgroupUniformLoad
     auto* replacement = mod.instructions.Create<wgsl::ir::BuiltinCall>(
-        load->Result(), wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{load->From()});
+        load->Result(0), wgsl::BuiltinFn::kWorkgroupUniformLoad, Vector{load->From()});
     load->ReplaceWith(replacement);
     load->ClearResults();
     load->Destroy();
@@ -228,6 +229,10 @@
             }
         }
     }
+    if (auto result = RenameConflicts(&mod); !result) {
+        return result.Failure();
+    }
+
     return Success;
 }
 
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc b/src/tint/lang/wgsl/writer/raise/rename_conflicts.cc
similarity index 97%
rename from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
rename to src/tint/lang/wgsl/writer/raise/rename_conflicts.cc
index a7f210f..7636059 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts.cc
@@ -42,7 +42,7 @@
 #include "src/tint/lang/core/type/scalar.h"
 #include "src/tint/lang/core/type/struct.h"
 #include "src/tint/lang/core/type/vector.h"
-#include "src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h"
+#include "src/tint/lang/wgsl/writer/raise/rename_conflicts.h"
 #include "src/tint/utils/containers/hashset.h"
 #include "src/tint/utils/containers/reverse.h"
 #include "src/tint/utils/containers/scope_stack.h"
@@ -74,7 +74,7 @@
         }
 
         // Process the functions
-        for (auto* fn : ir->functions) {
+        for (core::ir::Function* fn : ir->functions) {
             scopes.Push(Scope{});
             TINT_DEFER(scopes.Pop());
             for (auto* param : fn->Params()) {
@@ -121,7 +121,7 @@
         }
 
         // Declare all the functions
-        for (auto* fn : ir->functions) {
+        for (core::ir::Function* fn : ir->functions) {
             if (auto symbol = ir->NameOf(fn); symbol.IsValid()) {
                 Declare(scopes.Back(), fn, symbol.NameView());
             }
@@ -181,11 +181,11 @@
             },
             [&](core::ir::Var*) {
                 // Ensure the var's type is resolvable
-                EnsureResolvable(inst->Result()->Type());
+                EnsureResolvable(inst->Result(0)->Type());
             },
             [&](core::ir::Construct*) {
                 // Ensure the type of a type constructor is resolvable
-                EnsureResolvable(inst->Result()->Type());
+                EnsureResolvable(inst->Result(0)->Type());
             },
             [&](core::ir::CoreBuiltinCall* call) {
                 // Ensure builtin of a builtin call is resolvable
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/wgsl/writer/raise/rename_conflicts.h
similarity index 90%
rename from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
rename to src/tint/lang/wgsl/writer/raise/rename_conflicts.h
index 6f0f657..9f5d989 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts.h
@@ -25,8 +25,8 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_LANG_WGSL_WRITER_RAISE_RENAME_CONFLICTS_H_
+#define SRC_TINT_LANG_WGSL_WRITER_RAISE_RENAME_CONFLICTS_H_
 
 #include <string>
 
@@ -49,4 +49,4 @@
 
 }  // namespace tint::wgsl::writer
 
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_LANG_WGSL_WRITER_RAISE_RENAME_CONFLICTS_H_
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc b/src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc
similarity index 99%
rename from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
rename to src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc
index 98066fd..bc46235 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h"
+#include "src/tint/lang/wgsl/writer/raise/rename_conflicts.h"
 
 #include <string>
 #include <utility>
@@ -1032,7 +1032,7 @@
 
     auto* fn = b.Function("f", ty.i32());
     b.Append(fn->Block(), [&] {  //
-        auto* res = b.Call(ty.i32(), core::BuiltinFn::kMax, 1_i, 2_i)->Result();
+        auto* res = b.Call(ty.i32(), core::BuiltinFn::kMax, 1_i, 2_i)->Result(0);
         b.Return(fn, res);
     });
 
@@ -1065,7 +1065,7 @@
 
     auto* fn = b.Function("f", ty.i32());
     b.Append(fn->Block(), [&] {  //
-        auto* res = b.Call(ty.i32(), core::BuiltinFn::kMax, 1_i, 2_i)->Result();
+        auto* res = b.Call(ty.i32(), core::BuiltinFn::kMax, 1_i, 2_i)->Result(0);
         b.Return(fn, res);
     });
 
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
index 7a522bd..96fdf82 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
@@ -50,6 +50,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.cmake b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.cmake
index c92f99b..859fa03 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.cmake
@@ -49,6 +49,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.gn b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.gn
index c210fa6..71df0af 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.gn
@@ -49,6 +49,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/wgsl",
     "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/lang/wgsl/features",
     "${tint_src_dir}/lang/wgsl/program",
     "${tint_src_dir}/lang/wgsl/sem",
     "${tint_src_dir}/utils/containers",
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
index b157a0c..529f9c2 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
@@ -27,10 +27,7 @@
 
 #include "src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h"
 
-#include <algorithm>
-
 #include "src/tint/lang/core/texel_format.h"
-#include "src/tint/lang/wgsl/ast/accessor_expression.h"
 #include "src/tint/lang/wgsl/ast/alias.h"
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/binary_expression.h"
@@ -82,7 +79,6 @@
 #include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/lang/wgsl/sem/switch_statement.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
-#include "src/tint/utils/math/math.h"
 #include "src/tint/utils/rtti/switch.h"
 #include "src/tint/utils/strconv/float_to_string.h"
 #include "src/tint/utils/text/string.h"
@@ -263,9 +259,10 @@
                 // NaN and Inf are not allowed to be spelled in literal, it should be fine to emit
                 // f16 literals in this way.
                 if (l->suffix == ast::FloatLiteralExpression::Suffix::kNone) {
-                    Line() << tint::writer::DoubleToBitPreservingString(l->value);
+                    Line() << tint::strconv::DoubleToBitPreservingString(l->value);
                 } else {
-                    Line() << tint::writer::FloatToBitPreservingString(static_cast<float>(l->value))
+                    Line() << tint::strconv::FloatToBitPreservingString(
+                                  static_cast<float>(l->value))
                            << l->suffix;
                 }
             },
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h
index d47ea65..4a94bac 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h
@@ -28,12 +28,8 @@
 #ifndef SRC_TINT_LANG_WGSL_WRITER_SYNTAX_TREE_PRINTER_SYNTAX_TREE_PRINTER_H_
 #define SRC_TINT_LANG_WGSL_WRITER_SYNTAX_TREE_PRINTER_SYNTAX_TREE_PRINTER_H_
 
-#include <string>
-
 #include "src/tint/lang/wgsl/program/program.h"
-#include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/utils/generator/text_generator.h"
-#include "src/tint/utils/text/string_stream.h"
 
 // Forward declarations
 namespace tint::core {
diff --git a/src/tint/tint.gni b/src/tint/tint.gni
index 137def3..8ae6c91 100644
--- a/src/tint/tint.gni
+++ b/src/tint/tint.gni
@@ -29,6 +29,20 @@
 
 import("../../scripts/tint_overrides_with_defaults.gni")
 
+if (tint_has_protobuf) {
+  import("//third_party/protobuf/proto_library.gni")
+}
+
+###############################################################################
+# OS defines
+###############################################################################
+tint_build_is_win = is_win
+tint_build_is_mac = is_mac
+tint_build_is_linux = is_linux
+
+###############################################################################
+# Tint library target
+###############################################################################
 template("libtint_source_set") {
   source_set(target_name) {
     forward_variables_from(invoker, "*", [ "configs" ])
@@ -57,6 +71,21 @@
 }
 
 ###############################################################################
+# Tint protobuf library target
+###############################################################################
+template("tint_proto_library") {
+  if (!tint_has_protobuf) {
+    error("Tint needs protobuf to build a proto library")
+  }
+  proto_library(target_name) {
+    forward_variables_from(invoker, "*", [ "configs" ])
+    generate_cc = true
+    generate_python = false
+    use_protobuf_full = true
+  }
+}
+
+###############################################################################
 # Executables - only built when tint_build_cmds is enabled
 ###############################################################################
 template("tint_executable") {
diff --git a/src/tint/utils/BUILD.cmake b/src/tint/utils/BUILD.cmake
index 659c2a2..badeb73 100644
--- a/src/tint/utils/BUILD.cmake
+++ b/src/tint/utils/BUILD.cmake
@@ -34,6 +34,7 @@
 #                       Do not modify this file directly
 ################################################################################
 
+include(utils/bytes/BUILD.cmake)
 include(utils/cli/BUILD.cmake)
 include(utils/command/BUILD.cmake)
 include(utils/containers/BUILD.cmake)
diff --git a/src/tint/utils/bytes/BUILD.bazel b/src/tint/utils/bytes/BUILD.bazel
new file mode 100644
index 0000000..1b60b1d
--- /dev/null
+++ b/src/tint/utils/bytes/BUILD.bazel
@@ -0,0 +1,92 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "bytes",
+  srcs = [
+    "bytes.cc",
+  ],
+  hdrs = [
+    "decoder.h",
+    "endianness.h",
+    "reader.h",
+    "swap.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/reflection",
+    "//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 = [
+    "decoder_test.cc",
+    "reader_test.cc",
+    "swap_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/bytes",
+    "//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/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ],
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
diff --git a/src/tint/utils/bytes/BUILD.cmake b/src/tint/utils/bytes/BUILD.cmake
new file mode 100644
index 0000000..1012db6
--- /dev/null
+++ b/src/tint/utils/bytes/BUILD.cmake
@@ -0,0 +1,90 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_utils_bytes
+# Kind:      lib
+################################################################################
+tint_add_target(tint_utils_bytes lib
+  utils/bytes/bytes.cc
+  utils/bytes/decoder.h
+  utils/bytes/endianness.h
+  utils/bytes/reader.h
+  utils/bytes/swap.h
+)
+
+tint_target_add_dependencies(tint_utils_bytes lib
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+################################################################################
+# Target:    tint_utils_bytes_test
+# Kind:      test
+################################################################################
+tint_add_target(tint_utils_bytes_test test
+  utils/bytes/decoder_test.cc
+  utils/bytes/reader_test.cc
+  utils/bytes/swap_test.cc
+)
+
+tint_target_add_dependencies(tint_utils_bytes_test test
+  tint_utils_bytes
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_utils_bytes_test test
+  "gtest"
+)
diff --git a/src/tint/utils/bytes/BUILD.gn b/src/tint/utils/bytes/BUILD.gn
new file mode 100644
index 0000000..97931f1
--- /dev/null
+++ b/src/tint/utils/bytes/BUILD.gn
@@ -0,0 +1,90 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+
+libtint_source_set("bytes") {
+  sources = [
+    "bytes.cc",
+    "decoder.h",
+    "endianness.h",
+    "reader.h",
+    "swap.h",
+  ]
+  deps = [
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
+if (tint_build_unittests) {
+  tint_unittests_source_set("unittests") {
+    sources = [
+      "decoder_test.cc",
+      "reader_test.cc",
+      "swap_test.cc",
+    ]
+    deps = [
+      "${tint_src_dir}:gmock_and_gtest",
+      "${tint_src_dir}/utils/bytes",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
+      "${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/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/utils/bytes/bytes.cc
similarity index 65%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/utils/bytes/bytes.cc
index 6f0f657..392f0ea 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/utils/bytes/bytes.cc
@@ -25,28 +25,9 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#if defined(__clang__)
+#pragma clang diagnostic ignored "-Wmissing-variable-declarations"
+#endif
 
-#include <string>
-
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
-
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
-}
-
-namespace tint::wgsl::writer {
-
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
-
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+// A placeholder symbol used to emit a symbol for this lib target.
+int tint_utils_bytes_symbol = 1;
diff --git a/src/tint/utils/bytes/decoder.h b/src/tint/utils/bytes/decoder.h
new file mode 100644
index 0000000..591f255
--- /dev/null
+++ b/src/tint/utils/bytes/decoder.h
@@ -0,0 +1,192 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_BYTES_DECODER_H_
+#define SRC_TINT_UTILS_BYTES_DECODER_H_
+
+#include <string>
+#include <tuple>
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/utils/bytes/reader.h"
+#include "src/tint/utils/result/result.h"
+
+namespace tint::bytes {
+
+template <typename T, typename = void>
+struct Decoder;
+
+/// Decodes T from @p reader.
+/// @param reader the byte reader
+/// @returns the decoded object
+template <typename T>
+Result<T> Decode(Reader& reader) {
+    return Decoder<T>::Decode(reader);
+}
+
+/// Decoder specialization for integer types
+template <typename T>
+struct Decoder<T, std::enable_if_t<std::is_integral_v<T>>> {
+    /// Decode decodes the integer type from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded integer type, or an error if the stream is too short.
+    static Result<T> Decode(Reader& reader) {
+        if (reader.BytesRemaining() < sizeof(T)) {
+            return Failure{"EOF"};
+        }
+        return reader.Int<T>();
+    }
+};
+
+/// Decoder specialization for floating point types
+template <typename T>
+struct Decoder<T, std::enable_if_t<std::is_floating_point_v<T>>> {
+    /// Decode decodes the floating point type from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded floating point type, or an error if the stream is too short.
+    static Result<T> Decode(Reader& reader) {
+        if (reader.BytesRemaining() < sizeof(T)) {
+            return Failure{"EOF"};
+        }
+        return reader.Float<T>();
+    }
+};
+
+/// Decoder specialization for a uint16_t length prefixed string.
+template <typename T>
+struct Decoder<T, std::enable_if_t<std::is_same_v<T, std::string>>> {
+    /// Decode decodes the string from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded string, or an error if the stream is too short.
+    static Result<T> Decode(Reader& reader) {
+        if (reader.BytesRemaining() < sizeof(uint16_t)) {
+            return Failure{"EOF"};
+        }
+        auto len = reader.Int<uint16_t>();
+        if (reader.BytesRemaining() < len) {
+            return Failure{"EOF"};
+        }
+        return reader.String(len);
+    }
+};
+
+/// Decoder specialization for bool types
+template <>
+struct Decoder<bool, void> {
+    static Result<bool> Decode(Reader& reader) {
+        /// Decode decodes the boolean from @p reader.
+        /// @param reader the reader to decode from
+        /// @returns the decoded boolean, or an error if the stream is too short.
+        if (reader.IsEOF()) {
+            return Failure{"EOF"};
+        }
+        return reader.Bool();
+    }
+};
+
+/// Decoder specialization for types that use TINT_REFLECT
+template <typename T>
+struct Decoder<T, std::enable_if_t<HasReflection<T>>> {
+    /// Decode decodes the reflected type from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded reflected type, or an error if the stream is too short.
+    static Result<T> Decode(Reader& reader) {
+        T object{};
+        diag::List errs;
+        ForeachField(object, [&](auto& field) {  //
+            auto value = bytes::Decode<std::decay_t<decltype(field)>>(reader);
+            if (value) {
+                field = value.Get();
+            } else {
+                errs.add(value.Failure().reason);
+            }
+        });
+        if (errs.empty()) {
+            return object;
+        }
+        return Failure{errs};
+    }
+};
+
+/// Decoder specialization for std::unordered_map
+template <typename K, typename V>
+struct Decoder<std::unordered_map<K, V>, void> {
+    /// Decode decodes the map from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded map, or an error if the stream is too short.
+    static Result<std::unordered_map<K, V>> Decode(Reader& reader) {
+        std::unordered_map<K, V> out;
+
+        while (true) {
+            if (reader.IsEOF()) {
+                return Failure{"EOF"};
+            }
+            if (reader.Bool()) {
+                break;
+            }
+            auto key = bytes::Decode<K>(reader);
+            if (!key) {
+                return key.Failure();
+            }
+            auto val = bytes::Decode<V>(reader);
+            if (!val) {
+                return val.Failure();
+            }
+            out.emplace(std::move(key.Get()), std::move(val.Get()));
+        }
+
+        return out;
+    }
+};
+
+/// Decoder specialization for std::tuple
+template <typename FIRST, typename... OTHERS>
+struct Decoder<std::tuple<FIRST, OTHERS...>, void> {
+    /// Decode decodes the tuple from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded tuple, or an error if the stream is too short.
+    static Result<std::tuple<FIRST, OTHERS...>> Decode(Reader& reader) {
+        auto first = bytes::Decode<FIRST>(reader);
+        if (!first) {
+            return first.Failure();
+        }
+        if constexpr (sizeof...(OTHERS) > 0) {
+            auto others = bytes::Decode<std::tuple<OTHERS...>>(reader);
+            if (!others) {
+                return others.Failure();
+            }
+            return std::tuple_cat(std::tuple<FIRST>(first.Get()), others.Get());
+        } else {
+            return std::tuple<FIRST>(first.Get());
+        }
+    }
+};
+
+}  // namespace tint::bytes
+
+#endif  // SRC_TINT_UTILS_BYTES_DECODER_H_
diff --git a/src/tint/utils/bytes/decoder_test.cc b/src/tint/utils/bytes/decoder_test.cc
new file mode 100644
index 0000000..cce8e5a
--- /dev/null
+++ b/src/tint/utils/bytes/decoder_test.cc
@@ -0,0 +1,150 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/bytes/decoder.h"
+
+#include <string>
+#include <tuple>
+#include <unordered_map>
+#include <utility>
+
+#include "gmock/gmock.h"
+
+namespace tint::bytes {
+namespace {
+
+template <typename... ARGS>
+auto Data(ARGS&&... args) {
+    return std::array{std::byte{static_cast<uint8_t>(args)}...};
+}
+
+TEST(BytesDecoderTest, Uint8) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
+    auto reader = Reader{Slice{data}, 0, Endianness::kLittle};
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x10u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x20u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x30u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x40u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x50u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x60u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x70u);
+    EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x80u);
+    EXPECT_FALSE(Decode<uint8_t>(reader));
+}
+
+TEST(BytesDecoderTest, Uint16) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
+    auto reader = Reader{Slice{data}, 0, Endianness::kLittle};
+    EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x2010u);
+    EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x4030u);
+    EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x6050u);
+    EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x8070u);
+    EXPECT_FALSE(Decode<uint16_t>(reader));
+}
+
+TEST(BytesDecoderTest, Uint32) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
+    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    EXPECT_EQ(Decode<uint32_t>(reader).Get(), 0x10203040u);
+    EXPECT_EQ(Decode<uint32_t>(reader).Get(), 0x50607080u);
+    EXPECT_FALSE(Decode<uint32_t>(reader));
+}
+
+TEST(BytesDecoderTest, Float) {
+    auto data = Data(0x00, 0x00, 0x08, 0x41);
+    auto reader = Reader{Slice{data}};
+    EXPECT_EQ(Decode<float>(reader).Get(), 8.5f);
+    EXPECT_FALSE(Decode<float>(reader));
+}
+
+TEST(BytesDecoderTest, Bool) {
+    auto data = Data(0x0, 0x1, 0x2, 0x1, 0x0);
+    auto reader = Reader{Slice{data}};
+    EXPECT_EQ(Decode<bool>(reader).Get(), false);
+    EXPECT_EQ(Decode<bool>(reader).Get(), true);
+    EXPECT_EQ(Decode<bool>(reader).Get(), true);
+    EXPECT_EQ(Decode<bool>(reader).Get(), true);
+    EXPECT_EQ(Decode<bool>(reader).Get(), false);
+    EXPECT_FALSE(Decode<bool>(reader));
+}
+
+TEST(BytesDecoderTest, String) {
+    auto data = Data(0x0, 0x5, 'h', 'e', 'l', 'l', 'o', 0x0, 0x5, 'w', 'o', 'r', 'l', 'd');
+    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    EXPECT_EQ(Decode<std::string>(reader).Get(), "hello");
+    EXPECT_EQ(Decode<std::string>(reader).Get(), "world");
+    EXPECT_FALSE(Decode<std::string>(reader));
+}
+
+struct S {
+    uint8_t a;
+    uint16_t b;
+    uint32_t c;
+    TINT_REFLECT(a, b, c);
+};
+
+TEST(BytesDecoderTest, ReflectedObject) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
+    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    auto got = Decode<S>(reader);
+    EXPECT_EQ(got->a, 0x10u);
+    EXPECT_EQ(got->b, 0x2030u);
+    EXPECT_EQ(got->c, 0x40506070u);
+    EXPECT_FALSE(Decode<S>(reader));
+}
+
+TEST(BytesDecoderTest, UnorderedMap) {
+    using M = std::unordered_map<uint8_t, uint16_t>;
+    auto data = Data(0x00, 0x10, 0x02, 0x20,  //
+                     0x00, 0x30, 0x04, 0x40,  //
+                     0x00, 0x50, 0x06, 0x60,  //
+                     0x00, 0x70, 0x08, 0x80,  //
+                     0x01);
+    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    auto got = Decode<M>(reader);
+    EXPECT_THAT(got.Get(), testing::ContainerEq(M{
+                               std::pair<uint8_t, uint32_t>(0x10u, 0x0220u),
+                               std::pair<uint8_t, uint32_t>(0x30u, 0x0440u),
+                               std::pair<uint8_t, uint32_t>(0x50u, 0x0660u),
+                               std::pair<uint8_t, uint32_t>(0x70u, 0x0880u),
+                           }));
+    EXPECT_FALSE(Decode<M>(reader));
+}
+
+TEST(BytesDecoderTest, Tuple) {
+    using T = std::tuple<uint8_t, uint16_t, uint32_t>;
+    auto data = Data(0x10,                    //
+                     0x20, 0x30,              //
+                     0x40, 0x50, 0x60, 0x70,  //
+                     0x80);
+    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    EXPECT_THAT(Decode<T>(reader).Get(), (T{0x10u, 0x2030u, 0x40506070u}));
+    EXPECT_FALSE(Decode<T>(reader));
+}
+
+}  // namespace
+}  // namespace tint::bytes
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/utils/bytes/endianness.h
similarity index 65%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/utils/bytes/endianness.h
index 6f0f657..81db319 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/utils/bytes/endianness.h
@@ -25,28 +25,23 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_UTILS_BYTES_ENDIANNESS_H_
+#define SRC_TINT_UTILS_BYTES_ENDIANNESS_H_
 
-#include <string>
+#include <cstdint>
+#include <cstring>
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
+namespace tint::bytes {
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
+enum class Endianness : uint8_t { kBig, kLittle };
+
+inline Endianness NativeEndianness() {
+    uint8_t u8[4];
+    uint32_t u32 = 0x01020304;
+    memcpy(u8, &u32, 4);
+    return u8[0] == 1 ? Endianness::kBig : Endianness::kLittle;
 }
 
-namespace tint::wgsl::writer {
+}  // namespace tint::bytes
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
-
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_UTILS_BYTES_ENDIANNESS_H_
diff --git a/src/tint/utils/bytes/reader.h b/src/tint/utils/bytes/reader.h
new file mode 100644
index 0000000..38bbf4e
--- /dev/null
+++ b/src/tint/utils/bytes/reader.h
@@ -0,0 +1,140 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_BYTES_READER_H_
+#define SRC_TINT_UTILS_BYTES_READER_H_
+
+#include <algorithm>
+#include <cstdint>
+#include <string>
+
+#include "src/tint/utils/bytes/endianness.h"
+#include "src/tint/utils/bytes/swap.h"
+#include "src/tint/utils/containers/slice.h"
+#include "src/tint/utils/reflection/reflection.h"
+
+namespace tint::bytes {
+
+/// A binary stream reader.
+struct Reader {
+    /// @returns true if there are no more bytes remaining
+    bool IsEOF() const { return offset >= bytes.len; }
+
+    /// @returns the number of bytes remaining in the stream
+    size_t BytesRemaining() const { return IsEOF() ? 0 : bytes.len - offset; }
+
+    /// Reads an integer from the stream, performing byte swapping if the stream's endianness
+    /// differs from the native endianness. If there are too few bytes remaining in the stream, then
+    /// the missing data will be substituted with zeros.
+    /// @return the deserialized integer
+    template <typename T>
+    T Int() {
+        static_assert(std::is_integral_v<T>);
+        T out = 0;
+        if (!IsEOF()) {
+            size_t n = std::min(sizeof(T), BytesRemaining());
+            memcpy(&out, &bytes[offset], n);
+            offset += n;
+            if (NativeEndianness() != endianness) {
+                out = Swap(out);
+            }
+        }
+        return out;
+    }
+
+    /// Reads a float from the stream. If there are too few bytes remaining in the stream, then
+    /// the missing data will be substituted with zeros.
+    /// @return the deserialized floating point number
+    template <typename T>
+    T Float() {
+        static_assert(std::is_floating_point_v<T>);
+        T out = 0;
+        if (!IsEOF()) {
+            size_t n = std::min(sizeof(T), BytesRemaining());
+            memcpy(&out, &bytes[offset], n);
+            offset += n;
+        }
+        return out;
+    }
+
+    /// Reads a boolean from the stream
+    /// @returns true if the next byte is non-zero
+    bool Bool() {
+        if (IsEOF()) {
+            return false;
+        }
+        return bytes[offset++] != std::byte{0};
+    }
+
+    /// Reads a string of @p len bytes from the stream. If there are too few bytes remaining in the
+    /// stream, then the returned string will be truncated.
+    /// @param len the length of the returned string in bytes
+    /// @return the deserialized string
+    std::string String(size_t len) {
+        if (IsEOF()) {
+            return "";
+        }
+        size_t n = std::min(len, BytesRemaining());
+        std::string out(reinterpret_cast<const char*>(&bytes[offset]), n);
+        offset += n;
+        return out;
+    }
+
+    /// The data to read from
+    Slice<const std::byte> bytes;
+
+    /// The current byte offset
+    size_t offset = 0;
+
+    /// The endianness of integers serialized in the stream
+    Endianness endianness = Endianness::kLittle;
+};
+
+/// Reads the templated type from the reader and assigns it to @p out
+/// @note This function does not
+template <typename T>
+Reader& operator>>(Reader& reader, T& out) {
+    constexpr bool is_numeric = std::is_integral_v<T> || std::is_floating_point_v<T>;
+    static_assert(is_numeric);
+
+    if constexpr (std::is_integral_v<T>) {
+        out = reader.Int<T>();
+        return reader;
+    }
+
+    if constexpr (std::is_floating_point_v<T>) {
+        out = reader.Float<T>();
+        return reader;
+    }
+
+    // Unreachable
+    return reader;
+}
+
+}  // namespace tint::bytes
+
+#endif  // SRC_TINT_UTILS_BYTES_READER_H_
diff --git a/src/tint/utils/bytes/reader_test.cc b/src/tint/utils/bytes/reader_test.cc
new file mode 100644
index 0000000..e8af39d
--- /dev/null
+++ b/src/tint/utils/bytes/reader_test.cc
@@ -0,0 +1,107 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/bytes/reader.h"
+
+#include "gtest/gtest.h"
+
+namespace tint::bytes {
+namespace {
+
+template <typename... ARGS>
+auto Data(ARGS&&... args) {
+    return std::array{std::byte{static_cast<uint8_t>(args)}...};
+}
+
+TEST(BytesReaderTest, IntegerBigEndian) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40);
+    auto u32 = Reader{Slice{data}, 0, Endianness::kBig}.Int<uint32_t>();
+    EXPECT_EQ(u32, 0x10203040u);
+    auto i32 = Reader{Slice{data}, 0, Endianness::kBig}.Int<int32_t>();
+    EXPECT_EQ(i32, 0x10203040);
+}
+
+TEST(BytesReaderTest, IntegerBigEndian_Offset) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60);
+    auto u32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<uint32_t>();
+    EXPECT_EQ(u32, 0x30405060u);
+    auto i32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<int32_t>();
+    EXPECT_EQ(i32, 0x30405060);
+}
+
+TEST(BytesReaderTest, IntegerBigEndian_Clipped) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40);
+    auto u32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<uint32_t>();
+    EXPECT_EQ(u32, 0x30400000u);
+    auto i32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<int32_t>();
+    EXPECT_EQ(i32, 0x30400000);
+}
+
+TEST(BytesReaderTest, IntegerLittleEndian) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40);
+    auto u32 = Reader{Slice{data}, 0, Endianness::kLittle}.Int<uint32_t>();
+    EXPECT_EQ(u32, 0x40302010u);
+    auto i32 = Reader{Slice{data}, 0, Endianness::kLittle}.Int<int32_t>();
+    EXPECT_EQ(i32, 0x40302010);
+}
+
+TEST(BytesReaderTest, IntegerLittleEndian_Offset) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60);
+    auto u32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<uint32_t>();
+    EXPECT_EQ(u32, 0x60504030u);
+    auto i32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<int32_t>();
+    EXPECT_EQ(i32, 0x60504030);
+}
+
+TEST(BytesReaderTest, IntegerLittleEndian_Clipped) {
+    auto data = Data(0x10, 0x20, 0x30, 0x40);
+    auto u32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<uint32_t>();
+    EXPECT_EQ(u32, 0x00004030u);
+    auto i32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<int32_t>();
+    EXPECT_EQ(i32, 0x00004030);
+}
+
+TEST(BytesReaderTest, Float) {
+    auto data = Data(0x00, 0x00, 0x08, 0x41);
+    float f32 = Reader{Slice{data}}.Float<float>();
+    EXPECT_EQ(f32, 8.5f);
+}
+
+TEST(BytesReaderTest, Float_Offset) {
+    auto data = Data(0x00, 0x00, 0x08, 0x41, 0x80, 0x3e);
+    float f32 = Reader{Slice{data}, 2}.Float<float>();
+    EXPECT_EQ(f32, 0.25049614f);
+}
+
+TEST(BytesReaderTest, Float_Clipped) {
+    auto data = Data(0x00, 0x00, 0x08, 0x41);
+    float f32 = Reader{Slice{data}, 2}.Float<float>();
+    EXPECT_EQ(f32, 2.3329e-41f);
+}
+
+}  // namespace
+}  // namespace tint::bytes
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/utils/bytes/swap.h
similarity index 65%
copy from src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
copy to src/tint/utils/bytes/swap.h
index 6f0f657..da4c6e6 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/utils/bytes/swap.h
@@ -25,28 +25,31 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
-#define SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#ifndef SRC_TINT_UTILS_BYTES_SWAP_H_
+#define SRC_TINT_UTILS_BYTES_SWAP_H_
 
-#include <string>
+#include <cstdint>
+#include <cstring>
+#include <type_traits>
+#include <utility>
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/result/result.h"
+namespace tint::bytes {
 
-// Forward declarations.
-namespace tint::core::ir {
-class Module;
+/// @return the input integer value with all bytes reversed
+/// @param value the input value
+template <typename T>
+[[nodiscard]] inline T Swap(T value) {
+    static_assert(std::is_integral_v<T>);
+    uint8_t bytes[sizeof(T)];
+    memcpy(bytes, &value, sizeof(T));
+    for (size_t i = 0; i < sizeof(T) / 2; i++) {
+        std::swap(bytes[i], bytes[sizeof(T) - i - 1]);
+    }
+    T out;
+    memcpy(&out, bytes, sizeof(T));
+    return out;
 }
 
-namespace tint::wgsl::writer {
+}  // namespace tint::bytes
 
-/// RenameConflicts is a transform that renames declarations which prevent identifiers from
-/// resolving to the correct declaration, and those with identical identifiers declared in the same
-/// scope.
-/// @param module the module to transform
-/// @returns success or failure
-Result<SuccessType> RenameConflicts(core::ir::Module* module);
-
-}  // namespace tint::wgsl::writer
-
-#endif  // SRC_TINT_LANG_WGSL_WRITER_IR_TO_PROGRAM_RENAME_CONFLICTS_H_
+#endif  // SRC_TINT_UTILS_BYTES_SWAP_H_
diff --git a/src/tint/utils/bytes/swap_test.cc b/src/tint/utils/bytes/swap_test.cc
new file mode 100644
index 0000000..5d950c0
--- /dev/null
+++ b/src/tint/utils/bytes/swap_test.cc
@@ -0,0 +1,54 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/bytes/swap.h"
+
+#include "gtest/gtest.h"
+
+namespace tint::bytes {
+namespace {
+
+TEST(BytesSwapTest, Uint) {
+    EXPECT_EQ(Swap<uint8_t>(0x41), static_cast<uint8_t>(0x41));
+    EXPECT_EQ(Swap<uint16_t>(0x4152), static_cast<uint16_t>(0x5241));
+    EXPECT_EQ(Swap<uint32_t>(0x41526374), static_cast<uint32_t>(0x74635241));
+    EXPECT_EQ(Swap<uint64_t>(0x415263748596A7B8), static_cast<uint64_t>(0xB8A7968574635241));
+}
+
+TEST(BytesSwapTest, Sint) {
+    EXPECT_EQ(Swap<int8_t>(0x41), static_cast<int8_t>(0x41));
+    EXPECT_EQ(Swap<int8_t>(-0x41), static_cast<int8_t>(-0x41));
+    EXPECT_EQ(Swap<int16_t>(0x4152), static_cast<int16_t>(0x5241));
+    EXPECT_EQ(Swap<int16_t>(-0x4152), static_cast<int16_t>(0xAEBE));
+    EXPECT_EQ(Swap<int32_t>(0x41526374), static_cast<int32_t>(0x74635241));
+    EXPECT_EQ(Swap<int32_t>(-0x41526374), static_cast<int32_t>(0x8C9CADBE));
+    EXPECT_EQ(Swap<int64_t>(0x415263748596A7B8), static_cast<int64_t>(0xB8A7968574635241));
+    EXPECT_EQ(Swap<int64_t>(-0x415263748596A7B8), static_cast<int64_t>(0x4858697A8B9CADBE));
+}
+
+}  // namespace
+}  // namespace tint::bytes
diff --git a/src/tint/utils/cli/cli.h b/src/tint/utils/cli/cli.h
index b315884..b0e89e4 100644
--- a/src/tint/utils/cli/cli.h
+++ b/src/tint/utils/cli/cli.h
@@ -282,13 +282,13 @@
         auto arg = arguments.front();
 
         if constexpr (is_number) {
-            auto result = ParseNumber<T>(arg);
+            auto result = strconv::ParseNumber<T>(arg);
             if (result) {
                 value = result.Get();
                 arguments.pop_front();
                 return Success;
             }
-            if (result.Failure() == ParseNumberError::kResultOutOfRange) {
+            if (result.Failure() == strconv::ParseNumberError::kResultOutOfRange) {
                 return ErrInvalidArgument(arg, "value out of range");
             }
             return ErrInvalidArgument(arg, "failed to parse value");
diff --git a/src/tint/utils/command/BUILD.bazel b/src/tint/utils/command/BUILD.bazel
index f3388db..ffcef85 100644
--- a/src/tint/utils/command/BUILD.bazel
+++ b/src/tint/utils/command/BUILD.bazel
@@ -40,17 +40,17 @@
   name = "command",
   srcs = [
   ] + select({
-    ":_not_is_linux__and__not_is_mac__and__not_is_win_": [
+    ":_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_": [
       "command_other.cc",
     ],
     "//conditions:default": [],
   }) + select({
-    ":is_linux_or_is_mac": [
+    ":tint_build_is_linux_or_tint_build_is_mac": [
       "command_posix.cc",
     ],
     "//conditions:default": [],
   }) + select({
-    ":is_win": [
+    ":tint_build_is_win": [
       "command_windows.cc",
     ],
     "//conditions:default": [],
@@ -80,49 +80,49 @@
 )
 
 alias(
-  name = "is_linux",
-  actual = "//src/tint:is_linux_true",
+  name = "tint_build_is_linux",
+  actual = "//src/tint:tint_build_is_linux_true",
 )
 
 alias(
-  name = "_not_is_linux_",
-  actual = "//src/tint:is_linux_false",
+  name = "_not_tint_build_is_linux_",
+  actual = "//src/tint:tint_build_is_linux_false",
 )
 
 alias(
-  name = "is_mac",
-  actual = "//src/tint:is_mac_true",
+  name = "tint_build_is_mac",
+  actual = "//src/tint:tint_build_is_mac_true",
 )
 
 alias(
-  name = "_not_is_mac_",
-  actual = "//src/tint:is_mac_false",
+  name = "_not_tint_build_is_mac_",
+  actual = "//src/tint:tint_build_is_mac_false",
 )
 
 alias(
-  name = "is_win",
-  actual = "//src/tint:is_win_true",
+  name = "tint_build_is_win",
+  actual = "//src/tint:tint_build_is_win_true",
 )
 
 alias(
-  name = "_not_is_win_",
-  actual = "//src/tint:is_win_false",
+  name = "_not_tint_build_is_win_",
+  actual = "//src/tint:tint_build_is_win_false",
 )
 
 selects.config_setting_group(
-    name = "is_linux_or_is_mac",
+    name = "tint_build_is_linux_or_tint_build_is_mac",
     match_any = [
-        "is_linux",
-        "is_mac",
+        "tint_build_is_linux",
+        "tint_build_is_mac",
     ],
 )
 
 selects.config_setting_group(
-    name = "_not_is_linux__and__not_is_mac__and__not_is_win_",
+    name = "_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_",
     match_all = [
-        ":_not_is_linux_",
-        ":_not_is_mac_",
-        ":_not_is_win_",
+        ":_not_tint_build_is_linux_",
+        ":_not_tint_build_is_mac_",
+        ":_not_tint_build_is_win_",
     ],
 )
 
diff --git a/src/tint/utils/command/BUILD.cmake b/src/tint/utils/command/BUILD.cmake
index 51778af..4e40cc0 100644
--- a/src/tint/utils/command/BUILD.cmake
+++ b/src/tint/utils/command/BUILD.cmake
@@ -47,23 +47,23 @@
   tint_utils_text
 )
 
-if((NOT IS_LINUX) AND (NOT IS_MAC) AND (NOT IS_WIN))
+if((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
   tint_target_add_sources(tint_utils_command lib
     "utils/command/command_other.cc"
   )
-endif((NOT IS_LINUX) AND (NOT IS_MAC) AND (NOT IS_WIN))
+endif((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
 
-if(IS_LINUX OR IS_MAC)
+if(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
   tint_target_add_sources(tint_utils_command lib
     "utils/command/command_posix.cc"
   )
-endif(IS_LINUX OR IS_MAC)
+endif(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
 
-if(IS_WIN)
+if(TINT_BUILD_IS_WIN)
   tint_target_add_sources(tint_utils_command lib
     "utils/command/command_windows.cc"
   )
-endif(IS_WIN)
+endif(TINT_BUILD_IS_WIN)
 
 ################################################################################
 # Target:    tint_utils_command_test
diff --git a/src/tint/utils/command/BUILD.gn b/src/tint/utils/command/BUILD.gn
index 898eebe..4f42d68 100644
--- a/src/tint/utils/command/BUILD.gn
+++ b/src/tint/utils/command/BUILD.gn
@@ -49,15 +49,15 @@
     "${tint_src_dir}/utils/text",
   ]
 
-  if (!is_linux && !is_mac && !is_win) {
+  if (!tint_build_is_linux && !tint_build_is_mac && !tint_build_is_win) {
     sources += [ "command_other.cc" ]
   }
 
-  if (is_linux || is_mac) {
+  if (tint_build_is_linux || tint_build_is_mac) {
     sources += [ "command_posix.cc" ]
   }
 
-  if (is_win) {
+  if (tint_build_is_win) {
     sources += [ "command_windows.cc" ]
   }
 }
diff --git a/src/tint/utils/command/command_other.cc b/src/tint/utils/command/command_other.cc
index 2e47a8b..49a090a 100644
--- a/src/tint/utils/command/command_other.cc
+++ b/src/tint/utils/command/command_other.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION((!is_linux) && (!is_mac) && (!is_win))
+// GEN_BUILD:CONDITION((!tint_build_is_linux) && (!tint_build_is_mac) && (!tint_build_is_win))
 
 #include "src/tint/utils/command/command.h"
 
diff --git a/src/tint/utils/command/command_posix.cc b/src/tint/utils/command/command_posix.cc
index cf9219f..61bb0b1 100644
--- a/src/tint/utils/command/command_posix.cc
+++ b/src/tint/utils/command/command_posix.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_linux || is_mac)
+// GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac)
 
 #include "src/tint/utils/command/command.h"
 
diff --git a/src/tint/utils/command/command_windows.cc b/src/tint/utils/command/command_windows.cc
index 0c12bc8..2916d9a 100644
--- a/src/tint/utils/command/command_windows.cc
+++ b/src/tint/utils/command/command_windows.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_win)
+// GEN_BUILD:CONDITION(tint_build_is_win)
 
 #include "src/tint/utils/command/command.h"
 
diff --git a/src/tint/utils/containers/BUILD.bazel b/src/tint/utils/containers/BUILD.bazel
index c143cd7..36431b1 100644
--- a/src/tint/utils/containers/BUILD.bazel
+++ b/src/tint/utils/containers/BUILD.bazel
@@ -43,6 +43,7 @@
   ],
   hdrs = [
     "bitset.h",
+    "const_propagating_ptr.h",
     "enum_set.h",
     "hashmap.h",
     "hashmap_base.h",
@@ -93,6 +94,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/features",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
diff --git a/src/tint/utils/containers/BUILD.cmake b/src/tint/utils/containers/BUILD.cmake
index b7de673..b5bbaae 100644
--- a/src/tint/utils/containers/BUILD.cmake
+++ b/src/tint/utils/containers/BUILD.cmake
@@ -40,6 +40,7 @@
 ################################################################################
 tint_add_target(tint_utils_containers lib
   utils/containers/bitset.h
+  utils/containers/const_propagating_ptr.h
   utils/containers/containers.cc
   utils/containers/enum_set.h
   utils/containers/hashmap.h
@@ -92,6 +93,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
diff --git a/src/tint/utils/containers/BUILD.gn b/src/tint/utils/containers/BUILD.gn
index 0d47ff8..be16640 100644
--- a/src/tint/utils/containers/BUILD.gn
+++ b/src/tint/utils/containers/BUILD.gn
@@ -45,6 +45,7 @@
 libtint_source_set("containers") {
   sources = [
     "bitset.h",
+    "const_propagating_ptr.h",
     "containers.cc",
     "enum_set.h",
     "hashmap.h",
@@ -94,6 +95,7 @@
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/utils/containers/const_propagating_ptr.h b/src/tint/utils/containers/const_propagating_ptr.h
new file mode 100644
index 0000000..873ce39
--- /dev/null
+++ b/src/tint/utils/containers/const_propagating_ptr.h
@@ -0,0 +1,114 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_CONTAINERS_CONST_PROPAGATING_PTR_H_
+#define SRC_TINT_UTILS_CONTAINERS_CONST_PROPAGATING_PTR_H_
+
+namespace tint {
+
+/// ConstPropagatingPtr is a `const` propagating pointer - if the ConstPropagatingPtr is const, then
+/// you will only be able to obtain a const pointee pointer.
+template <typename T>
+class ConstPropagatingPtr {
+  public:
+    /// Constructor.
+    /// The pointer is initialized to null.
+    ConstPropagatingPtr() : ptr_(nullptr) {}
+
+    /// Constructor.
+    /// @param ptr the pointer value.
+    ConstPropagatingPtr(T* ptr) : ptr_(ptr) {}  // NOLINT(runtime/explicit)
+
+    /// Move constructor.
+    /// @param other the ConstPropagatingPtr to move.
+    ConstPropagatingPtr(ConstPropagatingPtr&& other) = default;
+
+    /// Copy (non-const) constructor.
+    /// @param other the ConstPropagatingPtr to move.
+    ConstPropagatingPtr(ConstPropagatingPtr& other) : ptr_(other.ptr_) {}
+
+    /// Assignment operator
+    /// @param ptr the new pointer value
+    /// @return this ConstPropagatingPtr
+    ConstPropagatingPtr& operator=(T* ptr) {
+        ptr_ = ptr;
+        return *this;
+    }
+
+    /// Move assignment operator.
+    /// @param other the ConstPropagatingPtr to move.
+    /// @return this ConstPropagatingPtr
+    ConstPropagatingPtr& operator=(ConstPropagatingPtr&& other) = default;
+
+    /// Copy (non-const) assignment operator.
+    /// @param other the ConstPropagatingPtr to move.
+    /// @return this ConstPropagatingPtr
+    ConstPropagatingPtr& operator=(ConstPropagatingPtr& other) {
+        ptr_ = other.ptr_;
+        return *this;
+    }
+
+    /// @returns the pointer
+    operator T*() { return ptr_; }
+
+    /// @returns the const pointer
+    operator const T*() const { return ptr_; }
+
+    /// @returns the pointer
+    T* operator->() { return ptr_; }
+
+    /// @returns the const pointer
+    const T* operator->() const { return ptr_; }
+
+    /// @returns the pointer
+    T* Get() { return ptr_; }
+
+    /// @returns the const pointer
+    const T* Get() const { return ptr_; }
+
+    /// Equality operator
+    /// @param ptr the pointer to compare against
+    /// @return true if the pointers are equal
+    bool operator==(T* ptr) const { return ptr_ == ptr; }
+
+    /// Inequality operator
+    /// @param ptr the pointer to compare against
+    /// @return true if the pointers are not equal
+    bool operator!=(T* ptr) const { return ptr_ != ptr; }
+
+  private:
+    // Delete copy constructor and copy-assignment to prevent making a copy of the
+    // ConstPropagatingPtr to break const propagation.
+    ConstPropagatingPtr(const ConstPropagatingPtr&) = delete;
+    ConstPropagatingPtr& operator=(const ConstPropagatingPtr&) = delete;
+
+    T* ptr_ = nullptr;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_CONTAINERS_CONST_PROPAGATING_PTR_H_
diff --git a/src/tint/utils/containers/slice.h b/src/tint/utils/containers/slice.h
index f2cf7c3..3c60913 100644
--- a/src/tint/utils/containers/slice.h
+++ b/src/tint/utils/containers/slice.h
@@ -28,6 +28,7 @@
 #ifndef SRC_TINT_UTILS_CONTAINERS_SLICE_H_
 #define SRC_TINT_UTILS_CONTAINERS_SLICE_H_
 
+#include <array>
 #include <cstdint>
 #include <iterator>
 
@@ -158,6 +159,12 @@
     constexpr Slice(T (&elements)[N])  // NOLINT
         : data(elements), len(N), cap(N) {}
 
+    /// Constructor
+    /// @param array std::array of elements
+    template <size_t N>
+    constexpr Slice(std::array<T, N>& array)  // NOLINT
+        : data(array.data()), len(N), cap(N) {}
+
     /// Reinterprets this slice as `const Slice<TO>&`
     /// @returns the reinterpreted slice
     /// @see CanReinterpretSlice
diff --git a/src/tint/utils/containers/slice_test.cc b/src/tint/utils/containers/slice_test.cc
index 2432aeb..01932fd 100644
--- a/src/tint/utils/containers/slice_test.cc
+++ b/src/tint/utils/containers/slice_test.cc
@@ -118,6 +118,16 @@
     EXPECT_FALSE(slice.IsEmpty());
 }
 
+TEST(TintSliceTest, CtorStdArray) {
+    std::array elements{1, 2, 3};
+
+    auto slice = Slice{elements};
+    EXPECT_EQ(slice.data, &elements[0]);
+    EXPECT_EQ(slice.len, 3u);
+    EXPECT_EQ(slice.cap, 3u);
+    EXPECT_FALSE(slice.IsEmpty());
+}
+
 TEST(TintSliceTest, Index) {
     int elements[] = {1, 2, 3};
 
diff --git a/src/tint/utils/containers/vector.h b/src/tint/utils/containers/vector.h
index deae6fe..663e310 100644
--- a/src/tint/utils/containers/vector.h
+++ b/src/tint/utils/containers/vector.h
@@ -343,7 +343,7 @@
 
     /// Move constructor
     /// @param other the vector to move
-    Vector(Vector&& other) { MoveOrCopy(VectorRef<T>(std::move(other))); }
+    Vector(Vector&& other) { Move(std::move(other)); }
 
     /// Copy constructor (differing N length)
     /// @param other the vector to copy
@@ -356,7 +356,7 @@
     /// @param other the vector to move
     template <size_t N2>
     Vector(Vector<T, N2>&& other) {
-        MoveOrCopy(VectorRef<T>(std::move(other)));
+        Move(std::move(other));
     }
 
     /// Copy constructor with covariance / const conversion
@@ -378,7 +378,7 @@
               ReinterpretMode MODE,
               typename = std::enable_if_t<CanReinterpretSlice<MODE, T, U>>>
     Vector(Vector<U, N2>&& other) {  // NOLINT(runtime/explicit)
-        MoveOrCopy(VectorRef<T>(std::move(other)));
+        Move(std::move(other));
     }
 
     /// Move constructor from a mutable vector reference
@@ -391,7 +391,24 @@
 
     /// Copy constructor from an immutable slice
     /// @param other the slice to copy
-    Vector(const Slice<T>& other) { Copy(other); }  // NOLINT(runtime/explicit)
+    Vector(const Slice<T>& other) {  // NOLINT(runtime/explicit)
+        Copy(other);
+    }
+
+    /// Copy constructor from an immutable slice
+    /// @param other the slice to copy
+    /// @note This overload only exists to keep MSVC happy. The compiler should be able to match
+    /// `Slice<U>`.
+    Vector(const Slice<const T>& other) {  // NOLINT(runtime/explicit)
+        Copy(other);
+    }
+
+    /// Copy constructor from an immutable slice
+    /// @param other the slice to copy
+    template <typename U>
+    Vector(const Slice<U>& other) {  // NOLINT(runtime/explicit)
+        Copy(other);
+    }
 
     /// Destructor
     ~Vector() { ClearAndFree(); }
@@ -411,7 +428,7 @@
     /// @returns this vector so calls can be chained
     Vector& operator=(Vector&& other) {
         if (&other != this) {
-            MoveOrCopy(VectorRef<T>(std::move(other)));
+            Move(std::move(other));
         }
         return *this;
     }
@@ -430,7 +447,7 @@
     /// @returns this vector so calls can be chained
     template <size_t N2>
     Vector& operator=(Vector<T, N2>&& other) {
-        MoveOrCopy(VectorRef<T>(std::move(other)));
+        Move(std::move(other));
         return *this;
     }
 
@@ -824,6 +841,7 @@
     /// Moves 'other' to this vector, if possible, otherwise performs a copy.
     void MoveOrCopy(VectorRef<T>&& other) {
         if (other.can_move_) {
+            // Just steal the slice.
             ClearAndFree();
             impl_.slice = other.slice_;
             other.slice_ = {};
@@ -833,8 +851,9 @@
     }
 
     /// Copies all the elements from `other` to this vector, replacing the content of this vector.
-    /// @param other the
-    void Copy(const tint::Slice<T>& other) {
+    /// @param other the slice to copy
+    template <typename U>
+    void Copy(const tint::Slice<U>& other) {
         if (impl_.slice.cap < other.len) {
             ClearAndFree();
             impl_.Allocate(other.len);
@@ -848,6 +867,41 @@
         }
     }
 
+    /// Moves all the elements from `other` to this vector, replacing the content of this vector.
+    /// @param other the vector to move
+    template <typename U, size_t N2>
+    void Move(Vector<U, N2>&& other) {
+        auto& other_slice = other.impl_.slice;
+        if constexpr (std::is_same_v<T, U>) {
+            if (other.impl_.CanMove()) {
+                // Just steal the slice.
+                ClearAndFree();
+                impl_.slice = other_slice;
+                other_slice = {};
+                return;
+            }
+        }
+
+        // Can't steal the slice, so we have to move the elements instead.
+
+        // Ensure we have capacity for all the elements
+        if (impl_.slice.cap < other_slice.len) {
+            ClearAndFree();
+            impl_.Allocate(other_slice.len);
+        } else {
+            Clear();
+        }
+
+        // Move each of the elements.
+        impl_.slice.len = other_slice.len;
+        for (size_t i = 0; i < impl_.slice.len; i++) {
+            new (&impl_.slice.data[i]) T{std::move(other_slice.data[i])};
+        }
+
+        // Clear other
+        other.Clear();
+    }
+
     /// Clears the vector, then frees the slice data.
     void ClearAndFree() {
         Clear();
@@ -1049,8 +1103,8 @@
     template <typename U,
               size_t N,
               typename = std::enable_if_t<CanReinterpretSlice<ReinterpretMode::kSafe, T, U>>>
-    VectorRef(Vector<U, N>& vector)  // NOLINT(runtime/explicit)
-        : slice_(vector.impl_.slice.template Reinterpret<T>()) {}
+    VectorRef(const Vector<U, N>& vector)  // NOLINT(runtime/explicit)
+        : slice_(const_cast<tint::Slice<U>&>(vector.impl_.slice).template Reinterpret<T>()) {}
 
     /// Constructor from a moved Vector with covariance / const conversion
     /// @param vector the vector to create a reference of
diff --git a/src/tint/utils/diagnostic/BUILD.bazel b/src/tint/utils/diagnostic/BUILD.bazel
index b9e46fb..5cd5f48 100644
--- a/src/tint/utils/diagnostic/BUILD.bazel
+++ b/src/tint/utils/diagnostic/BUILD.bazel
@@ -44,17 +44,17 @@
     "printer.cc",
     "source.cc",
   ] + select({
-    ":_not_is_linux__and__not_is_mac__and__not_is_win_": [
+    ":_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_": [
       "printer_other.cc",
     ],
     "//conditions:default": [],
   }) + select({
-    ":is_linux_or_is_mac": [
+    ":tint_build_is_linux_or_tint_build_is_mac": [
       "printer_posix.cc",
     ],
     "//conditions:default": [],
   }) + select({
-    ":is_win": [
+    ":tint_build_is_win": [
       "printer_windows.cc",
     ],
     "//conditions:default": [],
@@ -104,49 +104,49 @@
 )
 
 alias(
-  name = "is_linux",
-  actual = "//src/tint:is_linux_true",
+  name = "tint_build_is_linux",
+  actual = "//src/tint:tint_build_is_linux_true",
 )
 
 alias(
-  name = "_not_is_linux_",
-  actual = "//src/tint:is_linux_false",
+  name = "_not_tint_build_is_linux_",
+  actual = "//src/tint:tint_build_is_linux_false",
 )
 
 alias(
-  name = "is_mac",
-  actual = "//src/tint:is_mac_true",
+  name = "tint_build_is_mac",
+  actual = "//src/tint:tint_build_is_mac_true",
 )
 
 alias(
-  name = "_not_is_mac_",
-  actual = "//src/tint:is_mac_false",
+  name = "_not_tint_build_is_mac_",
+  actual = "//src/tint:tint_build_is_mac_false",
 )
 
 alias(
-  name = "is_win",
-  actual = "//src/tint:is_win_true",
+  name = "tint_build_is_win",
+  actual = "//src/tint:tint_build_is_win_true",
 )
 
 alias(
-  name = "_not_is_win_",
-  actual = "//src/tint:is_win_false",
+  name = "_not_tint_build_is_win_",
+  actual = "//src/tint:tint_build_is_win_false",
 )
 
 selects.config_setting_group(
-    name = "is_linux_or_is_mac",
+    name = "tint_build_is_linux_or_tint_build_is_mac",
     match_any = [
-        "is_linux",
-        "is_mac",
+        "tint_build_is_linux",
+        "tint_build_is_mac",
     ],
 )
 
 selects.config_setting_group(
-    name = "_not_is_linux__and__not_is_mac__and__not_is_win_",
+    name = "_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_",
     match_all = [
-        ":_not_is_linux_",
-        ":_not_is_mac_",
-        ":_not_is_win_",
+        ":_not_tint_build_is_linux_",
+        ":_not_tint_build_is_mac_",
+        ":_not_tint_build_is_win_",
     ],
 )
 
diff --git a/src/tint/utils/diagnostic/BUILD.cmake b/src/tint/utils/diagnostic/BUILD.cmake
index c11c314..37b0742 100644
--- a/src/tint/utils/diagnostic/BUILD.cmake
+++ b/src/tint/utils/diagnostic/BUILD.cmake
@@ -60,23 +60,23 @@
   tint_utils_traits
 )
 
-if((NOT IS_LINUX) AND (NOT IS_MAC) AND (NOT IS_WIN))
+if((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
   tint_target_add_sources(tint_utils_diagnostic lib
     "utils/diagnostic/printer_other.cc"
   )
-endif((NOT IS_LINUX) AND (NOT IS_MAC) AND (NOT IS_WIN))
+endif((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
 
-if(IS_LINUX OR IS_MAC)
+if(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
   tint_target_add_sources(tint_utils_diagnostic lib
     "utils/diagnostic/printer_posix.cc"
   )
-endif(IS_LINUX OR IS_MAC)
+endif(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
 
-if(IS_WIN)
+if(TINT_BUILD_IS_WIN)
   tint_target_add_sources(tint_utils_diagnostic lib
     "utils/diagnostic/printer_windows.cc"
   )
-endif(IS_WIN)
+endif(TINT_BUILD_IS_WIN)
 
 ################################################################################
 # Target:    tint_utils_diagnostic_test
diff --git a/src/tint/utils/diagnostic/BUILD.gn b/src/tint/utils/diagnostic/BUILD.gn
index 637dc85..4bc6b72 100644
--- a/src/tint/utils/diagnostic/BUILD.gn
+++ b/src/tint/utils/diagnostic/BUILD.gn
@@ -64,15 +64,15 @@
     "${tint_src_dir}/utils/traits",
   ]
 
-  if (!is_linux && !is_mac && !is_win) {
+  if (!tint_build_is_linux && !tint_build_is_mac && !tint_build_is_win) {
     sources += [ "printer_other.cc" ]
   }
 
-  if (is_linux || is_mac) {
+  if (tint_build_is_linux || tint_build_is_mac) {
     sources += [ "printer_posix.cc" ]
   }
 
-  if (is_win) {
+  if (tint_build_is_win) {
     sources += [ "printer_windows.cc" ]
   }
 }
diff --git a/src/tint/utils/diagnostic/diagnostic.cc b/src/tint/utils/diagnostic/diagnostic.cc
index 8a51ac9..ff0170c 100644
--- a/src/tint/utils/diagnostic/diagnostic.cc
+++ b/src/tint/utils/diagnostic/diagnostic.cc
@@ -33,13 +33,29 @@
 
 namespace tint::diag {
 
+namespace {
+size_t CountErrors(VectorRef<Diagnostic> diags) {
+    size_t count = 0;
+    for (auto& diag : diags) {
+        if (diag.severity >= Severity::Error) {
+            count++;
+        }
+    }
+    return count;
+}
+}  // namespace
+
 Diagnostic::Diagnostic() = default;
 Diagnostic::Diagnostic(const Diagnostic&) = default;
 Diagnostic::~Diagnostic() = default;
 Diagnostic& Diagnostic::operator=(const Diagnostic&) = default;
 
 List::List() = default;
-List::List(std::initializer_list<Diagnostic> list) : entries_(list) {}
+List::List(std::initializer_list<Diagnostic> list)
+    : entries_(list), error_count_(CountErrors(entries_)) {}
+List::List(VectorRef<Diagnostic> list)
+    : entries_(std::move(list)), error_count_(CountErrors(entries_)) {}
+
 List::List(const List& rhs) = default;
 
 List::List(List&& rhs) = default;
diff --git a/src/tint/utils/diagnostic/diagnostic.h b/src/tint/utils/diagnostic/diagnostic.h
index 36d6c15..a077f47 100644
--- a/src/tint/utils/diagnostic/diagnostic.h
+++ b/src/tint/utils/diagnostic/diagnostic.h
@@ -108,39 +108,45 @@
     /// Constructs the list with no elements.
     List();
 
-    /// Copy constructor. Copies the diagnostics from `list` into this list.
+    /// Constructor. Copies the diagnostics from @p list into this list.
     /// @param list the list of diagnostics to copy into this list.
     List(std::initializer_list<Diagnostic> list);
 
-    /// Copy constructor. Copies the diagnostics from `list` into this list.
+    /// Constructor. Copies the diagnostics from @p list into this list.
+    /// @param list the list of diagnostics to copy into this list.
+    explicit List(VectorRef<Diagnostic> list);
+
+    /// Copy constructor. Copies the diagnostics from @p list into this list.
     /// @param list the list of diagnostics to copy into this list.
     List(const List& list);
 
-    /// Move constructor. Moves the diagnostics from `list` into this list.
+    /// Move constructor. Moves the diagnostics from @p list into this list.
     /// @param list the list of diagnostics to move into this list.
     List(List&& list);
 
     /// Destructor
     ~List();
 
-    /// Assignment operator. Copies the diagnostics from `list` into this list.
+    /// Assignment operator. Copies the diagnostics from @p list into this list.
     /// @param list the list to copy into this list.
     /// @return this list.
     List& operator=(const List& list);
 
-    /// Assignment move operator. Moves the diagnostics from `list` into this
-    /// list.
+    /// Assignment move operator. Moves the diagnostics from @p list into this list.
     /// @param list the list to move into this list.
     /// @return this list.
     List& operator=(List&& list);
 
     /// adds a diagnostic to the end of this list.
     /// @param diag the diagnostic to append to this list.
-    void add(Diagnostic&& diag) {
+    /// @returns a reference to the new diagnostic.
+    /// @note The returned reference must not be used after the list is mutated again.
+    diag::Diagnostic& add(Diagnostic&& diag) {
         if (diag.severity >= Severity::Error) {
             error_count_++;
         }
         entries_.Push(std::move(diag));
+        return entries_.Back();
     }
 
     /// adds a list of diagnostics to the end of this list.
@@ -155,50 +161,60 @@
     /// @param system the system raising the note message
     /// @param note_msg the note message
     /// @param source the source of the note diagnostic
-    void add_note(System system, std::string_view note_msg, const Source& source) {
+    /// @returns a reference to the new diagnostic.
+    /// @note The returned reference must not be used after the list is mutated again.
+    diag::Diagnostic& add_note(System system, std::string_view note_msg, const Source& source) {
         diag::Diagnostic note{};
         note.severity = diag::Severity::Note;
         note.system = system;
         note.source = source;
         note.message = note_msg;
-        add(std::move(note));
+        return add(std::move(note));
     }
 
     /// adds the warning message with the given Source to the end of this list.
     /// @param system the system raising the warning message
     /// @param warning_msg the warning message
     /// @param source the source of the warning diagnostic
-    void add_warning(System system, std::string_view warning_msg, const Source& source) {
+    /// @returns a reference to the new diagnostic.
+    /// @note The returned reference must not be used after the list is mutated again.
+    diag::Diagnostic& add_warning(System system,
+                                  std::string_view warning_msg,
+                                  const Source& source) {
         diag::Diagnostic warning{};
         warning.severity = diag::Severity::Warning;
         warning.system = system;
         warning.source = source;
         warning.message = warning_msg;
-        add(std::move(warning));
+        return add(std::move(warning));
     }
 
     /// adds the error message without a source to the end of this list.
     /// @param system the system raising the error message
     /// @param err_msg the error message
-    void add_error(System system, std::string_view err_msg) {
+    /// @returns a reference to the new diagnostic.
+    /// @note The returned reference must not be used after the list is mutated again.
+    diag::Diagnostic& add_error(System system, std::string_view err_msg) {
         diag::Diagnostic error{};
         error.severity = diag::Severity::Error;
         error.system = system;
         error.message = err_msg;
-        add(std::move(error));
+        return add(std::move(error));
     }
 
     /// adds the error message with the given Source to the end of this list.
     /// @param system the system raising the error message
     /// @param err_msg the error message
     /// @param source the source of the error diagnostic
-    void add_error(System system, std::string_view err_msg, const Source& source) {
+    /// @returns a reference to the new diagnostic.
+    /// @note The returned reference must not be used after the list is mutated again.
+    diag::Diagnostic& add_error(System system, std::string_view err_msg, const Source& source) {
         diag::Diagnostic error{};
         error.severity = diag::Severity::Error;
         error.system = system;
         error.source = source;
         error.message = err_msg;
-        add(std::move(error));
+        return add(std::move(error));
     }
 
     /// adds an internal compiler error message to the end of this list.
@@ -206,17 +222,19 @@
     /// @param err_msg the error message
     /// @param source the source of the internal compiler error
     /// @param file the Source::File owned by this diagnostic
-    void add_ice(System system,
-                 std::string_view err_msg,
-                 const Source& source,
-                 std::shared_ptr<Source::File> file) {
+    /// @returns a reference to the new diagnostic.
+    /// @note The returned reference must not be used after the list is mutated again.
+    diag::Diagnostic& add_ice(System system,
+                              std::string_view err_msg,
+                              const Source& source,
+                              std::shared_ptr<Source::File> file) {
         diag::Diagnostic ice{};
         ice.severity = diag::Severity::InternalCompilerError;
         ice.system = system;
         ice.source = source;
         ice.message = err_msg;
         ice.owned_file = std::move(file);
-        add(std::move(ice));
+        return add(std::move(ice));
     }
 
     /// @returns true iff the diagnostic list contains errors diagnostics (or of
diff --git a/src/tint/utils/diagnostic/diagnostic_test.cc b/src/tint/utils/diagnostic/diagnostic_test.cc
index a27798e..f947b62 100644
--- a/src/tint/utils/diagnostic/diagnostic_test.cc
+++ b/src/tint/utils/diagnostic/diagnostic_test.cc
@@ -33,12 +33,28 @@
 namespace tint::diag {
 namespace {
 
+TEST(DiagListTest, CtorInitializerList) {
+    Diagnostic err_a, err_b;
+    err_a.severity = Severity::Error;
+    err_b.severity = Severity::Fatal;
+    List list{err_a, err_b};
+    EXPECT_EQ(list.count(), 2u);
+}
+
+TEST(DiagListTest, CtorVectorRef) {
+    Diagnostic err_a, err_b;
+    err_a.severity = Severity::Error;
+    err_b.severity = Severity::Fatal;
+    List list(Vector{err_a, err_b});
+    EXPECT_EQ(list.count(), 2u);
+}
+
 TEST(DiagListTest, OwnedFilesShared) {
     auto file = std::make_shared<Source::File>("path", "content");
 
-    diag::List list_a, list_b;
+    List list_a, list_b;
     {
-        diag::Diagnostic diag{};
+        Diagnostic diag{};
         diag.source = Source{Source::Range{{0, 0}}, file.get()};
         list_a.add(std::move(diag));
     }
diff --git a/src/tint/utils/diagnostic/printer_other.cc b/src/tint/utils/diagnostic/printer_other.cc
index c7d438f..2f93fa7 100644
--- a/src/tint/utils/diagnostic/printer_other.cc
+++ b/src/tint/utils/diagnostic/printer_other.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION((!is_linux) && (!is_mac) && (!is_win))
+// GEN_BUILD:CONDITION((!tint_build_is_linux) && (!tint_build_is_mac) && (!tint_build_is_win))
 
 #include <cstring>
 
diff --git a/src/tint/utils/diagnostic/printer_posix.cc b/src/tint/utils/diagnostic/printer_posix.cc
index 5094681..3c7ba22 100644
--- a/src/tint/utils/diagnostic/printer_posix.cc
+++ b/src/tint/utils/diagnostic/printer_posix.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_linux || is_mac)
+// GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac)
 
 #include <unistd.h>
 
diff --git a/src/tint/utils/diagnostic/printer_windows.cc b/src/tint/utils/diagnostic/printer_windows.cc
index 5b1e68a..52d3790 100644
--- a/src/tint/utils/diagnostic/printer_windows.cc
+++ b/src/tint/utils/diagnostic/printer_windows.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_win)
+// GEN_BUILD:CONDITION(tint_build_is_win)
 
 #include <cstring>
 
diff --git a/src/tint/utils/file/BUILD.bazel b/src/tint/utils/file/BUILD.bazel
index e5c4357..d74bc23 100644
--- a/src/tint/utils/file/BUILD.bazel
+++ b/src/tint/utils/file/BUILD.bazel
@@ -40,17 +40,17 @@
   name = "file",
   srcs = [
   ] + select({
-    ":_not_is_linux__and__not_is_mac__and__not_is_win_": [
+    ":_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_": [
       "tmpfile_other.cc",
     ],
     "//conditions:default": [],
   }) + select({
-    ":is_linux_or_is_mac": [
+    ":tint_build_is_linux_or_tint_build_is_mac": [
       "tmpfile_posix.cc",
     ],
     "//conditions:default": [],
   }) + select({
-    ":is_win": [
+    ":tint_build_is_win": [
       "tmpfile_windows.cc",
     ],
     "//conditions:default": [],
@@ -82,49 +82,49 @@
 )
 
 alias(
-  name = "is_linux",
-  actual = "//src/tint:is_linux_true",
+  name = "tint_build_is_linux",
+  actual = "//src/tint:tint_build_is_linux_true",
 )
 
 alias(
-  name = "_not_is_linux_",
-  actual = "//src/tint:is_linux_false",
+  name = "_not_tint_build_is_linux_",
+  actual = "//src/tint:tint_build_is_linux_false",
 )
 
 alias(
-  name = "is_mac",
-  actual = "//src/tint:is_mac_true",
+  name = "tint_build_is_mac",
+  actual = "//src/tint:tint_build_is_mac_true",
 )
 
 alias(
-  name = "_not_is_mac_",
-  actual = "//src/tint:is_mac_false",
+  name = "_not_tint_build_is_mac_",
+  actual = "//src/tint:tint_build_is_mac_false",
 )
 
 alias(
-  name = "is_win",
-  actual = "//src/tint:is_win_true",
+  name = "tint_build_is_win",
+  actual = "//src/tint:tint_build_is_win_true",
 )
 
 alias(
-  name = "_not_is_win_",
-  actual = "//src/tint:is_win_false",
+  name = "_not_tint_build_is_win_",
+  actual = "//src/tint:tint_build_is_win_false",
 )
 
 selects.config_setting_group(
-    name = "is_linux_or_is_mac",
+    name = "tint_build_is_linux_or_tint_build_is_mac",
     match_any = [
-        "is_linux",
-        "is_mac",
+        "tint_build_is_linux",
+        "tint_build_is_mac",
     ],
 )
 
 selects.config_setting_group(
-    name = "_not_is_linux__and__not_is_mac__and__not_is_win_",
+    name = "_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_",
     match_all = [
-        ":_not_is_linux_",
-        ":_not_is_mac_",
-        ":_not_is_win_",
+        ":_not_tint_build_is_linux_",
+        ":_not_tint_build_is_mac_",
+        ":_not_tint_build_is_win_",
     ],
 )
 
diff --git a/src/tint/utils/file/BUILD.cmake b/src/tint/utils/file/BUILD.cmake
index 93eedbe..0dc6cb8 100644
--- a/src/tint/utils/file/BUILD.cmake
+++ b/src/tint/utils/file/BUILD.cmake
@@ -48,23 +48,23 @@
   tint_utils_text
 )
 
-if((NOT IS_LINUX) AND (NOT IS_MAC) AND (NOT IS_WIN))
+if((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
   tint_target_add_sources(tint_utils_file lib
     "utils/file/tmpfile_other.cc"
   )
-endif((NOT IS_LINUX) AND (NOT IS_MAC) AND (NOT IS_WIN))
+endif((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
 
-if(IS_LINUX OR IS_MAC)
+if(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
   tint_target_add_sources(tint_utils_file lib
     "utils/file/tmpfile_posix.cc"
   )
-endif(IS_LINUX OR IS_MAC)
+endif(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
 
-if(IS_WIN)
+if(TINT_BUILD_IS_WIN)
   tint_target_add_sources(tint_utils_file lib
     "utils/file/tmpfile_windows.cc"
   )
-endif(IS_WIN)
+endif(TINT_BUILD_IS_WIN)
 
 ################################################################################
 # Target:    tint_utils_file_test
diff --git a/src/tint/utils/file/BUILD.gn b/src/tint/utils/file/BUILD.gn
index 612646c..62e1cdc 100644
--- a/src/tint/utils/file/BUILD.gn
+++ b/src/tint/utils/file/BUILD.gn
@@ -50,15 +50,15 @@
     "${tint_src_dir}/utils/text",
   ]
 
-  if (!is_linux && !is_mac && !is_win) {
+  if (!tint_build_is_linux && !tint_build_is_mac && !tint_build_is_win) {
     sources += [ "tmpfile_other.cc" ]
   }
 
-  if (is_linux || is_mac) {
+  if (tint_build_is_linux || tint_build_is_mac) {
     sources += [ "tmpfile_posix.cc" ]
   }
 
-  if (is_win) {
+  if (tint_build_is_win) {
     sources += [ "tmpfile_windows.cc" ]
   }
 }
diff --git a/src/tint/utils/file/tmpfile_other.cc b/src/tint/utils/file/tmpfile_other.cc
index 1fcad3f..d8ecad2 100644
--- a/src/tint/utils/file/tmpfile_other.cc
+++ b/src/tint/utils/file/tmpfile_other.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION((!is_linux) && (!is_mac) && (!is_win))
+// GEN_BUILD:CONDITION((!tint_build_is_linux) && (!tint_build_is_mac) && (!tint_build_is_win))
 
 #include "src/tint/utils/file/tmpfile.h"
 
diff --git a/src/tint/utils/file/tmpfile_posix.cc b/src/tint/utils/file/tmpfile_posix.cc
index 9114ae0..c7b7178 100644
--- a/src/tint/utils/file/tmpfile_posix.cc
+++ b/src/tint/utils/file/tmpfile_posix.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_linux || is_mac)
+// GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac)
 
 #include "src/tint/utils/file/tmpfile.h"
 
diff --git a/src/tint/utils/file/tmpfile_windows.cc b/src/tint/utils/file/tmpfile_windows.cc
index c6ddc33..486c2ea 100644
--- a/src/tint/utils/file/tmpfile_windows.cc
+++ b/src/tint/utils/file/tmpfile_windows.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION(is_win)
+// GEN_BUILD:CONDITION(tint_build_is_win)
 
 #include "src/tint/utils/file/tmpfile.h"
 
diff --git a/src/tint/utils/ice/ice.cc b/src/tint/utils/ice/ice.cc
index 9a4f981..f40d49e 100644
--- a/src/tint/utils/ice/ice.cc
+++ b/src/tint/utils/ice/ice.cc
@@ -54,7 +54,7 @@
 }
 
 std::string InternalCompilerError::Error() const {
-    return std::string(File()) + +":" + std::to_string(Line()) +
+    return std::string(File()) + ":" + std::to_string(Line()) +
            " internal compiler error: " + Message();
 }
 
diff --git a/src/tint/utils/macros/foreach.h b/src/tint/utils/macros/foreach.h
index d889dc8..9ec27cc 100644
--- a/src/tint/utils/macros/foreach.h
+++ b/src/tint/utils/macros/foreach.h
@@ -92,13 +92,13 @@
     TINT_FOREACH_11(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11)          \
     CB(_12)
 #define TINT_FOREACH_13(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \
-    TINT_FOREACH_11(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12)          \
+    TINT_FOREACH_12(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12)          \
     CB(_13)
 #define TINT_FOREACH_14(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \
-    TINT_FOREACH_11(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13)          \
+    TINT_FOREACH_13(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13)          \
     CB(_14)
 #define TINT_FOREACH_15(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \
-    TINT_FOREACH_11(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14)          \
+    TINT_FOREACH_14(CB, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14)          \
     CB(_15)
 
 #endif  // SRC_TINT_UTILS_MACROS_FOREACH_H_
diff --git a/src/tint/utils/socket/BUILD.bazel b/src/tint/utils/socket/BUILD.bazel
index 4c1fb71..c236531 100644
--- a/src/tint/utils/socket/BUILD.bazel
+++ b/src/tint/utils/socket/BUILD.bazel
@@ -48,7 +48,7 @@
   deps = [
     "//src/tint/utils/macros",
   ] + select({
-    ":is_win": [
+    ":tint_build_is_win": [
       
     ],
     "//conditions:default": [],
@@ -58,7 +58,7 @@
 )
 
 alias(
-  name = "is_win",
-  actual = "//src/tint:is_win_true",
+  name = "tint_build_is_win",
+  actual = "//src/tint:tint_build_is_win_true",
 )
 
diff --git a/src/tint/utils/socket/BUILD.cmake b/src/tint/utils/socket/BUILD.cmake
index 2b9b312..6c69f4c 100644
--- a/src/tint/utils/socket/BUILD.cmake
+++ b/src/tint/utils/socket/BUILD.cmake
@@ -48,8 +48,8 @@
   tint_utils_macros
 )
 
-if(IS_WIN)
+if(TINT_BUILD_IS_WIN)
   tint_target_add_external_dependencies(tint_utils_socket lib
     "winsock"
   )
-endif(IS_WIN)
+endif(TINT_BUILD_IS_WIN)
diff --git a/src/tint/utils/socket/BUILD.gn b/src/tint/utils/socket/BUILD.gn
index 0a0ff70..746a415 100644
--- a/src/tint/utils/socket/BUILD.gn
+++ b/src/tint/utils/socket/BUILD.gn
@@ -46,7 +46,7 @@
   ]
   deps = [ "${tint_src_dir}/utils/macros" ]
 
-  if (is_win) {
+  if (tint_build_is_win) {
     deps += [ "${tint_src_dir}:winsock" ]
   }
 }
diff --git a/src/tint/utils/socket/rwmutex.h b/src/tint/utils/socket/rwmutex.h
index 099018a..e43b4e7 100644
--- a/src/tint/utils/socket/rwmutex.h
+++ b/src/tint/utils/socket/rwmutex.h
@@ -35,6 +35,8 @@
 // RWMutex
 ////////////////////////////////////////////////////////////////////////////////
 
+namespace tint::socket {
+
 /// A RWMutex is a reader/writer mutual exclusion lock.
 /// The lock can be held by an arbitrary number of readers or a single writer.
 /// Also known as a shared mutex.
@@ -200,4 +202,6 @@
     return *this;
 }
 
+}  // namespace tint::socket
+
 #endif  // SRC_TINT_UTILS_SOCKET_RWMUTEX_H_
diff --git a/src/tint/utils/socket/socket.cc b/src/tint/utils/socket/socket.cc
index a3bf0bc..bab68f2 100644
--- a/src/tint/utils/socket/socket.cc
+++ b/src/tint/utils/socket/socket.cc
@@ -30,7 +30,7 @@
 #include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/socket/rwmutex.h"
 
-#if defined(_WIN32)
+#if TINT_BUILD_IS_WIN
 #include <winsock2.h>
 #include <ws2tcpip.h>
 #else
@@ -44,7 +44,7 @@
 #include <cstdio>
 #endif
 
-#if defined(_WIN32)
+#if TINT_BUILD_IS_WIN
 #include <atomic>
 namespace {
 std::atomic<int> wsa_init_count = {0};
@@ -56,10 +56,11 @@
 }  // anonymous namespace
 #endif
 
+namespace tint::socket {
 namespace {
 constexpr SOCKET InvalidSocket = static_cast<SOCKET>(-1);
 void Init() {
-#if defined(_WIN32)
+#if TINT_BUILD_IS_WIN
     if (wsa_init_count++ == 0) {
         WSADATA winsock_data;
         (void)WSAStartup(MAKEWORD(2, 2), &winsock_data);
@@ -68,7 +69,7 @@
 }
 
 void Term() {
-#if defined(_WIN32)
+#if TINT_BUILD_IS_WIN
     if (--wsa_init_count == 0) {
         WSACleanup();
     }
@@ -76,7 +77,7 @@
 }
 
 bool SetBlocking(SOCKET s, bool blocking) {
-#if defined(_WIN32)
+#if TINT_BUILD_IS_WIN
     u_long mode = blocking ? 0 : 1;
     return ioctlsocket(s, FIONBIO, &mode) == NO_ERROR;
 #else
@@ -112,7 +113,7 @@
 
         addrinfo* info = nullptr;
         auto err = getaddrinfo(address, port, &hints, &info);
-#if !defined(_WIN32)
+#if !TINT_BUILD_IS_WIN
         if (err) {
             printf("getaddrinfo(%s, %s) error: %s\n", address, port, gai_strerror(err));
         }
@@ -157,7 +158,7 @@
 
         int enable = 1;
 
-#if !defined(_WIN32)
+#if !TINT_BUILD_IS_WIN
         // Prevent sockets lingering after process termination, causing
         // reconnection issues on the same port.
         setsockopt(s, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<char*>(&enable), sizeof(enable));
@@ -167,7 +168,7 @@
             int l_linger; /* how many seconds to linger for */
         } linger = {false, 0};
         setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<char*>(&linger), sizeof(linger));
-#endif  // !defined(_WIN32)
+#endif  // !TINT_BUILD_IS_WIN
 
         // Enable TCP_NODELAY.
         setsockopt(s, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&enable), sizeof(enable));
@@ -189,7 +190,7 @@
         {
             RLock l(mutex);
             if (s != InvalidSocket) {
-#if defined(_WIN32)
+#if TINT_BUILD_IS_WIN
                 closesocket(s);
 #else
                 ::shutdown(s, SHUT_RDWR);
@@ -199,7 +200,7 @@
 
         WLock l(mutex);
         if (s != InvalidSocket) {
-#if !defined(_WIN32)
+#if !TINT_BUILD_IS_WIN
             ::close(s);
 #endif
             s = InvalidSocket;
@@ -333,3 +334,5 @@
 
     return out->IsOpen() ? out : nullptr;
 }
+
+}  // namespace tint::socket
diff --git a/src/tint/utils/socket/socket.h b/src/tint/utils/socket/socket.h
index e22204e..36aa4b3 100644
--- a/src/tint/utils/socket/socket.h
+++ b/src/tint/utils/socket/socket.h
@@ -28,9 +28,10 @@
 #ifndef SRC_TINT_UTILS_SOCKET_SOCKET_H_
 #define SRC_TINT_UTILS_SOCKET_SOCKET_H_
 
-#include <atomic>
 #include <memory>
 
+namespace tint::socket {
+
 /// Socket provides an OS abstraction to a TCP socket.
 class Socket {
   public:
@@ -81,4 +82,6 @@
     virtual std::shared_ptr<Socket> Accept() = 0;
 };
 
+}  // namespace tint::socket
+
 #endif  // SRC_TINT_UTILS_SOCKET_SOCKET_H_
diff --git a/src/tint/utils/strconv/float_to_string.cc b/src/tint/utils/strconv/float_to_string.cc
index ea54abc..ee48a88 100644
--- a/src/tint/utils/strconv/float_to_string.cc
+++ b/src/tint/utils/strconv/float_to_string.cc
@@ -29,14 +29,12 @@
 
 #include <cmath>
 #include <cstring>
-#include <functional>
 #include <iomanip>
-#include <limits>
 
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/text/string_stream.h"
 
-namespace tint::writer {
+namespace tint::strconv {
 
 namespace {
 
@@ -187,4 +185,4 @@
     return ToBitPreservingString(f);
 }
 
-}  // namespace tint::writer
+}  // namespace tint::strconv
diff --git a/src/tint/utils/strconv/float_to_string.h b/src/tint/utils/strconv/float_to_string.h
index b97d74a..c272523 100644
--- a/src/tint/utils/strconv/float_to_string.h
+++ b/src/tint/utils/strconv/float_to_string.h
@@ -30,7 +30,7 @@
 
 #include <string>
 
-namespace tint::writer {
+namespace tint::strconv {
 
 /// Converts the float `f` to a string using fixed-point notation (not
 /// scientific). The float will be printed with the full precision required to
@@ -58,6 +58,6 @@
 /// @return the double f formatted to a string
 std::string DoubleToBitPreservingString(double f);
 
-}  // namespace tint::writer
+}  // namespace tint::strconv
 
 #endif  // SRC_TINT_UTILS_STRCONV_FLOAT_TO_STRING_H_
diff --git a/src/tint/utils/strconv/float_to_string_test.cc b/src/tint/utils/strconv/float_to_string_test.cc
index 043b5f9..d0a0ce5 100644
--- a/src/tint/utils/strconv/float_to_string_test.cc
+++ b/src/tint/utils/strconv/float_to_string_test.cc
@@ -34,7 +34,7 @@
 #include "gtest/gtest.h"
 #include "src/tint/utils/memory/bitcast.h"
 
-namespace tint::writer {
+namespace tint::strconv {
 namespace {
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -345,4 +345,4 @@
 }
 
 }  // namespace
-}  // namespace tint::writer
+}  // namespace tint::strconv
diff --git a/src/tint/utils/strconv/parse_num.cc b/src/tint/utils/strconv/parse_num.cc
index 09fcc3d..c1cbf85 100644
--- a/src/tint/utils/strconv/parse_num.cc
+++ b/src/tint/utils/strconv/parse_num.cc
@@ -31,7 +31,7 @@
 
 #include "absl/strings/charconv.h"
 
-namespace tint {
+namespace tint::strconv {
 
 namespace {
 
@@ -108,4 +108,4 @@
     return Parse<uint8_t>(str);
 }
 
-}  // namespace tint
+}  // namespace tint::strconv
diff --git a/src/tint/utils/strconv/parse_num.h b/src/tint/utils/strconv/parse_num.h
index 7eab873..704bd47 100644
--- a/src/tint/utils/strconv/parse_num.h
+++ b/src/tint/utils/strconv/parse_num.h
@@ -28,16 +28,13 @@
 #ifndef SRC_TINT_UTILS_STRCONV_PARSE_NUM_H_
 #define SRC_TINT_UTILS_STRCONV_PARSE_NUM_H_
 
-#include <optional>
-#include <string>
-
 #include "src/tint/utils/macros/compiler.h"
 #include "src/tint/utils/result/result.h"
 
-namespace tint {
+namespace tint::strconv {
 
 /// Error returned by the number parsing functions
-enum class ParseNumberError {
+enum class ParseNumberError : uint8_t {
     /// The number was unparsable
     kUnparsable,
     /// The parsed number is not representable by the target datatype
@@ -141,6 +138,6 @@
 /// Re-enables the unreachable-code compiler warnings
 TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
 
-}  // namespace tint
+}  // namespace tint::strconv
 
 #endif  // SRC_TINT_UTILS_STRCONV_PARSE_NUM_H_
diff --git a/src/tint/utils/symbol/symbol_table.h b/src/tint/utils/symbol/symbol_table.h
index 9e2c455..7c39512 100644
--- a/src/tint/utils/symbol/symbol_table.h
+++ b/src/tint/utils/symbol/symbol_table.h
@@ -53,17 +53,19 @@
     /// @returns the symbol table
     SymbolTable& operator=(SymbolTable&& other);
 
-    /// Wrap sets this symbol table to hold symbols which point to the allocated names in @p o.
-    /// The symbol table after Wrap is intended to temporarily extend the objects
-    /// of an existing immutable SymbolTable
-    /// As the copied objects are owned by @p o, @p o must not be destructed
-    /// or assigned while using this symbol table.
+    /// @returns a symbol table to hold symbols which point to the allocated names in @p o.
+    /// The symbol table after Wrap is intended to temporarily extend the objects of an existing
+    /// immutable SymbolTable.
+    /// @warning As the copied objects are owned by @p o, @p o must not be destructed or assigned
+    /// while using this symbol table.
     /// @param o the immutable SymbolTable to extend
-    void Wrap(const SymbolTable& o) {
-        next_symbol_ = o.next_symbol_;
-        name_to_symbol_ = o.name_to_symbol_;
-        last_prefix_to_index_ = o.last_prefix_to_index_;
-        generation_id_ = o.generation_id_;
+    static SymbolTable Wrap(const SymbolTable& o) {
+        SymbolTable out(o.generation_id_);
+        out.next_symbol_ = o.next_symbol_;
+        out.name_to_symbol_ = o.name_to_symbol_;
+        out.last_prefix_to_index_ = o.last_prefix_to_index_;
+        out.generation_id_ = o.generation_id_;
+        return out;
     }
 
     /// Registers a name into the symbol table, returning the Symbol.
diff --git a/src/tint/utils/templates/enums.tmpl.inc b/src/tint/utils/templates/enums.tmpl.inc
index dd848a3..23dc6d3 100644
--- a/src/tint/utils/templates/enums.tmpl.inc
+++ b/src/tint/utils/templates/enums.tmpl.inc
@@ -38,34 +38,47 @@
 {{- /* ------------------------------------------------------------------ */ -}}
 {{-                         define "DeclareEnum"                             -}}
 {{- /* Declares the 'enum class' for the provided sem.Enum argument.      */ -}}
+{{- /* The argument can also be a key-value pair with the following keys: */ -}}
+{{- /*   "Enum"        - the sem.Enum argument                            */ -}}
+{{- /*   "EmitOStream" - (default: true) should operator<< be emitted?    */ -}}
 {{- /* ------------------------------------------------------------------ */ -}}
-{{- $enum := Eval "EnumName" $ -}}
-enum class {{$enum}} : uint8_t {
+{{- $enum := $ -}}
+{{- $emit_ostream := true -}}
+{{- if Is $ "Map" -}}
+{{-   $enum = $.Enum -}}
+{{-   $emit_ostream = $.EmitOStream -}}
+{{- end }}
+
+{{- $name := Eval "EnumName" $enum -}}
+enum class {{$name}} : uint8_t {
     kUndefined,
-{{-   range $entry := $.Entries }}
+{{-   range $entry := $enum.Entries }}
     k{{PascalCase $entry.Name}},{{if $entry.IsInternal}}  // Tint-internal enum entry - not parsed{{end}}
 {{-   end }}
 };
 
 /// @param value the enum value
 /// @returns the string for the given enum value
-std::string_view ToString({{$enum}} value);
+std::string_view ToString({{$name}} value);
+
+{{- if $emit_ostream}}
 
 /// @param out the stream to write to
-/// @param value the {{$enum}}
+/// @param value the {{$name}}
 /// @returns @p out so calls can be chained
 template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, {{$enum}} value) {
+auto& operator<<(STREAM& out, {{$name}} value) {
     return out << ToString(value);
 }
+{{- end}}
 
-/// Parse{{$enum}} parses a {{$enum}} from a string.
+/// Parse{{$name}} parses a {{$name}} from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or {{$enum}}::kUndefined if the string could not be parsed.
-{{$enum}} Parse{{$enum}}(std::string_view str);
+/// @returns the parsed enum, or {{$name}}::kUndefined if the string could not be parsed.
+{{$name}} Parse{{$name}}(std::string_view str);
 
-constexpr std::string_view k{{$enum}}Strings[] = {
-{{-   range $entry := $.Entries }}
+constexpr std::string_view k{{$name}}Strings[] = {
+{{-   range $entry := $enum.Entries }}
 {{-     if not $entry.IsInternal}}
     "{{$entry.Name}}",
 {{-     end }}
@@ -163,7 +176,7 @@
 TEST_P({{$enum}}PrintTest, Print) {
     {{$enum}} value = GetParam().value;
     const char* expect = GetParam().string;
-    EXPECT_EQ(expect, tint::ToString(value));
+    EXPECT_EQ(expect, ToString(value));
 }
 
 INSTANTIATE_TEST_SUITE_P(ValidCases, {{$enum}}PrintTest, testing::ValuesIn(kValidCases));
diff --git a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
index 9da4e6c..7ef997c 100644
--- a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
+++ b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
@@ -66,14 +66,18 @@
 {{- with $I.Table -}}
 {{- template "Matchers" $I }}
 
+{{- if .TypeMatcherIndices}}
+
 constexpr TypeMatcherIndex kTypeMatcherIndices[] = {
-{{- range $i, $idx := .TypeMatcherIndices }}
+{{-   range $i, $idx := .TypeMatcherIndices }}
   /* [{{$i}}] */ TypeMatcherIndex({{$idx}}),
-{{- end }}
+{{-   end }}
 };
 
 static_assert(TypeMatcherIndex::CanIndex(kTypeMatcherIndices),
               "TypeMatcherIndex is not large enough to index kTypeMatcherIndices");
+{{- end}}
+{{- if .NumberMatcherIndices}}
 
 constexpr NumberMatcherIndex kNumberMatcherIndices[] = {
 {{- range $i, $idx := .NumberMatcherIndices }}
@@ -83,6 +87,8 @@
 
 static_assert(NumberMatcherIndex::CanIndex(kNumberMatcherIndices),
               "NumberMatcherIndex is not large enough to index kNumberMatcherIndices");
+{{- end}}
+{{- if .Parameters}}
 
 constexpr ParameterInfo kParameters[] = {
 {{- range $i, $p := .Parameters }}
@@ -92,7 +98,10 @@
 {{-   if $p.Usage }}k{{PascalCase $p.Usage}}
 {{-   else        }}kNone
 {{-   end }},
-    /* type_matcher_indices */ TypeMatcherIndicesIndex({{$p.TypeMatcherIndicesOffset}}),
+    /* type_matcher_indices */
+{{-   if ge $p.TypeMatcherIndicesOffset 0 }} TypeMatcherIndicesIndex({{$p.TypeMatcherIndicesOffset}})
+{{-   else                                }} TypeMatcherIndicesIndex(/* invalid */)
+{{-   end }},
     /* number_matcher_indices */
 {{-   if ge $p.NumberMatcherIndicesOffset 0 }} NumberMatcherIndicesIndex({{$p.NumberMatcherIndicesOffset}})
 {{-   else                                  }} NumberMatcherIndicesIndex(/* invalid */)
@@ -103,6 +112,8 @@
 
 static_assert(ParameterIndex::CanIndex(kParameters),
               "ParameterIndex is not large enough to index kParameters");
+{{- end}}
+{{- if .TemplateTypes}}
 
 constexpr TemplateTypeInfo kTemplateTypes[] = {
 {{- range $i, $o := .TemplateTypes }}
@@ -119,6 +130,8 @@
 
 static_assert(TemplateTypeIndex::CanIndex(kTemplateTypes),
               "TemplateTypeIndex is not large enough to index kTemplateTypes");
+{{- end }}
+{{- if .TemplateNumbers}}
 
 constexpr TemplateNumberInfo kTemplateNumbers[] = {
 {{- range $i, $o := .TemplateNumbers }}
@@ -135,6 +148,7 @@
 
 static_assert(TemplateNumberIndex::CanIndex(kTemplateNumbers),
               "TemplateNumberIndex is not large enough to index kTemplateNumbers");
+{{- end }}
 
 {{- if .ConstEvalFunctions}}
 {{/* newline */}}
@@ -271,12 +285,12 @@
 }  // anonymous namespace
 
 const core::intrinsic::TableData {{$.Name}}{
-  /* template_types */ kTemplateTypes,
-  /* template_numbers */ kTemplateNumbers,
-  /* type_matcher_indices */ kTypeMatcherIndices,
-  /* number_matcher_indices */ kNumberMatcherIndices,
-  /* type_matchers */ kTypeMatchers,
-  /* number_matchers */ kNumberMatchers,
+  /* template_types */ {{if .TemplateTypes}}kTemplateTypes{{else}}Empty{{end}},
+  /* template_numbers */ {{if .TemplateNumbers}}kTemplateNumbers{{else}}Empty{{end}},
+  /* type_matcher_indices */ {{if .TypeMatcherIndices}}kTypeMatcherIndices{{else}}Empty{{end}},
+  /* number_matcher_indices */ {{if .NumberMatcherIndices}}kNumberMatcherIndices{{else}}Empty{{end}},
+  /* type_matchers */ {{if .TMatchers}}kTypeMatchers{{else}}Empty{{end}},
+  /* number_matchers */ {{if .NMatchers}}kNumberMatchers{{else}}Empty{{end}},
   /* parameters */ kParameters,
   /* overloads */ kOverloads,
   /* const_eval_functions */ {{if .ConstEvalFunctions}}kConstEvalFunctions{{else}}Empty{{end}},
@@ -436,25 +450,30 @@
 {{-   $n_names.Put . $name }}
 {{- end }}
 
+{{- if $.Table.TMatchers}}
+
 /// The template types, types, and type matchers
 constexpr TypeMatcher kTypeMatchers[] = {
-{{- range $i, $m := $.Table.TMatchers }}
+{{-   range $i, $m := $.Table.TMatchers }}
   /* [{{$i}}] */
-{{-   if $m }} {{$t_names.Get $m}},
-{{-   else  }} {{$t_names.Get $i}},
-{{-   end   }}
-{{- end }}
+{{-     if $m }} {{$t_names.Get $m}},
+{{-     else  }} {{$t_names.Get $i}},
+{{-     end   }}
+{{-   end }}
 };
+{{- end}}
+{{- if $.Table.NMatchers}}
 
 /// The template numbers, and number matchers
 constexpr NumberMatcher kNumberMatchers[] = {
-{{- range $i, $m := $.Table.NMatchers }}
+{{-   range $i, $m := $.Table.NMatchers }}
   /* [{{$i}}] */
-{{-   if $m }} {{$n_names.Get $m}},
-{{-   else  }} {{$n_names.Get $i}},
-{{-   end   }}
-{{- end }}
+{{-     if $m }} {{$n_names.Get $m}},
+{{-     else  }} {{$n_names.Get $i}},
+{{-     end   }}
+{{-   end }}
 };
+{{- end}}
 
 {{- end -}}
 
diff --git a/src/tint/utils/text/BUILD.bazel b/src/tint/utils/text/BUILD.bazel
index 384af8e..21ae073 100644
--- a/src/tint/utils/text/BUILD.bazel
+++ b/src/tint/utils/text/BUILD.bazel
@@ -39,11 +39,13 @@
 cc_library(
   name = "text",
   srcs = [
+    "base64.cc",
     "string.cc",
     "string_stream.cc",
     "unicode.cc",
   ],
   hdrs = [
+    "base64.h",
     "string.h",
     "string_stream.h",
     "unicode.h",
@@ -64,6 +66,7 @@
   name = "test",
   alwayslink = True,
   srcs = [
+    "base64_test.cc",
     "string_stream_test.cc",
     "string_test.cc",
     "unicode_test.cc",
diff --git a/src/tint/utils/text/BUILD.cmake b/src/tint/utils/text/BUILD.cmake
index cd7e6b8..f5f8c10 100644
--- a/src/tint/utils/text/BUILD.cmake
+++ b/src/tint/utils/text/BUILD.cmake
@@ -39,6 +39,8 @@
 # Kind:      lib
 ################################################################################
 tint_add_target(tint_utils_text lib
+  utils/text/base64.cc
+  utils/text/base64.h
   utils/text/string.cc
   utils/text/string.h
   utils/text/string_stream.cc
@@ -62,6 +64,7 @@
 # Kind:      test
 ################################################################################
 tint_add_target(tint_utils_text_test test
+  utils/text/base64_test.cc
   utils/text/string_stream_test.cc
   utils/text/string_test.cc
   utils/text/unicode_test.cc
diff --git a/src/tint/utils/text/BUILD.gn b/src/tint/utils/text/BUILD.gn
index df4c510..c13c8c0 100644
--- a/src/tint/utils/text/BUILD.gn
+++ b/src/tint/utils/text/BUILD.gn
@@ -44,6 +44,8 @@
 
 libtint_source_set("text") {
   sources = [
+    "base64.cc",
+    "base64.h",
     "string.cc",
     "string.h",
     "string_stream.cc",
@@ -64,6 +66,7 @@
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
     sources = [
+      "base64_test.cc",
       "string_stream_test.cc",
       "string_test.cc",
       "unicode_test.cc",
diff --git a/src/tint/utils/text/base64.cc b/src/tint/utils/text/base64.cc
new file mode 100644
index 0000000..7ae85cc
--- /dev/null
+++ b/src/tint/utils/text/base64.cc
@@ -0,0 +1,71 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <string>
+
+#include "src/tint/utils/text/base64.h"
+
+namespace tint {
+
+Vector<std::byte, 0> DecodeBase64FromComments(std::string_view wgsl) {
+    Vector<std::byte, 0> out;
+    size_t block_nesting = 0;
+    bool line_comment = false;
+    for (size_t i = 0, n = wgsl.length(); i < n; i++) {
+        char curr = wgsl[i];
+        if (curr == '\n') {
+            line_comment = false;
+            continue;
+        }
+
+        char next = (i + 1) < n ? wgsl[i + 1] : 0;
+        if (curr == '/' && next == '*') {
+            block_nesting++;
+            i++;  // skip '*'
+            continue;
+        }
+        if (block_nesting > 0 && curr == '*' && next == '/') {
+            block_nesting--;
+            i++;  // skip '/'
+            continue;
+        }
+        if (block_nesting == 0 && curr == '/' && next == '/') {
+            line_comment = true;
+            i++;  // skip '/'
+            continue;
+        }
+
+        if (block_nesting > 0 || line_comment) {
+            if (auto v = DecodeBase64(curr)) {
+                out.Push(std::byte{*v});
+            }
+        }
+    }
+    return out;
+}
+
+}  // namespace tint
diff --git a/src/tint/utils/text/base64.h b/src/tint/utils/text/base64.h
new file mode 100644
index 0000000..1047c33
--- /dev/null
+++ b/src/tint/utils/text/base64.h
@@ -0,0 +1,68 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_TEXT_BASE64_H_
+#define SRC_TINT_UTILS_TEXT_BASE64_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "src/tint/utils/containers/vector.h"
+
+namespace tint {
+
+/// Decodes a byte from a base64 encoded character
+/// @param c the character to decode
+/// @return the decoded value, or std::nullopt if the character is padding ('=') or an invalid
+/// character.
+inline std::optional<uint8_t> DecodeBase64(char c) {
+    if (c >= 'A' && c <= 'Z') {
+        return static_cast<uint8_t>(c - 'A');
+    }
+    if (c >= 'a' && c <= 'z') {
+        return static_cast<uint8_t>(26 + c - 'a');
+    }
+    if (c >= '0' && c <= '9') {
+        return static_cast<uint8_t>(52 + c - '0');
+    }
+    if (c == '+') {
+        return 62;
+    }
+    if (c == '/') {
+        return 63;
+    }
+    return std::nullopt;
+}
+/// DecodeBase64FromComments parses all the comments from the WGSL source string as a base64 byte
+/// stream. Non-base64 characters are skipped
+/// @param wgsl the WGSL source
+/// @return the base64 decoded bytes
+Vector<std::byte, 0> DecodeBase64FromComments(std::string_view wgsl);
+
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_TEXT_BASE64_H_
diff --git a/src/tint/utils/text/base64_test.cc b/src/tint/utils/text/base64_test.cc
new file mode 100644
index 0000000..3eb282f
--- /dev/null
+++ b/src/tint/utils/text/base64_test.cc
@@ -0,0 +1,222 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/text/base64.h"
+
+#include <optional>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/utils/containers/transform.h"
+#include "src/tint/utils/text/string.h"
+
+namespace tint::utils {
+namespace {
+
+struct DecodeBase64Case {
+    char in;
+    std::optional<uint8_t> out;
+};
+
+using DecodeBase64Test = testing::TestWithParam<DecodeBase64Case>;
+
+TEST_P(DecodeBase64Test, Char) {
+    EXPECT_EQ(DecodeBase64(GetParam().in), GetParam().out);
+}
+
+INSTANTIATE_TEST_SUITE_P(Valid,
+                         DecodeBase64Test,
+                         testing::Values(DecodeBase64Case{'A', 0},
+                                         DecodeBase64Case{'B', 1},
+                                         DecodeBase64Case{'C', 2},
+                                         DecodeBase64Case{'D', 3},
+                                         DecodeBase64Case{'E', 4},
+                                         DecodeBase64Case{'F', 5},
+                                         DecodeBase64Case{'G', 6},
+                                         DecodeBase64Case{'H', 7},
+                                         DecodeBase64Case{'I', 8},
+                                         DecodeBase64Case{'J', 9},
+                                         DecodeBase64Case{'K', 10},
+                                         DecodeBase64Case{'L', 11},
+                                         DecodeBase64Case{'M', 12},
+                                         DecodeBase64Case{'N', 13},
+                                         DecodeBase64Case{'O', 14},
+                                         DecodeBase64Case{'P', 15},
+                                         DecodeBase64Case{'Q', 16},
+                                         DecodeBase64Case{'R', 17},
+                                         DecodeBase64Case{'S', 18},
+                                         DecodeBase64Case{'T', 19},
+                                         DecodeBase64Case{'U', 20},
+                                         DecodeBase64Case{'V', 21},
+                                         DecodeBase64Case{'W', 22},
+                                         DecodeBase64Case{'X', 23},
+                                         DecodeBase64Case{'Y', 24},
+                                         DecodeBase64Case{'Z', 25},
+                                         DecodeBase64Case{'a', 26},
+                                         DecodeBase64Case{'b', 27},
+                                         DecodeBase64Case{'c', 28},
+                                         DecodeBase64Case{'d', 29},
+                                         DecodeBase64Case{'e', 30},
+                                         DecodeBase64Case{'f', 31},
+                                         DecodeBase64Case{'g', 32},
+                                         DecodeBase64Case{'h', 33},
+                                         DecodeBase64Case{'i', 34},
+                                         DecodeBase64Case{'j', 35},
+                                         DecodeBase64Case{'k', 36},
+                                         DecodeBase64Case{'l', 37},
+                                         DecodeBase64Case{'m', 38},
+                                         DecodeBase64Case{'n', 39},
+                                         DecodeBase64Case{'o', 40},
+                                         DecodeBase64Case{'p', 41},
+                                         DecodeBase64Case{'q', 42},
+                                         DecodeBase64Case{'r', 43},
+                                         DecodeBase64Case{'s', 44},
+                                         DecodeBase64Case{'t', 45},
+                                         DecodeBase64Case{'u', 46},
+                                         DecodeBase64Case{'v', 47},
+                                         DecodeBase64Case{'w', 48},
+                                         DecodeBase64Case{'x', 49},
+                                         DecodeBase64Case{'y', 50},
+                                         DecodeBase64Case{'z', 51},
+                                         DecodeBase64Case{'0', 52},
+                                         DecodeBase64Case{'1', 53},
+                                         DecodeBase64Case{'2', 54},
+                                         DecodeBase64Case{'3', 55},
+                                         DecodeBase64Case{'4', 56},
+                                         DecodeBase64Case{'5', 57},
+                                         DecodeBase64Case{'6', 58},
+                                         DecodeBase64Case{'7', 59},
+                                         DecodeBase64Case{'8', 60},
+                                         DecodeBase64Case{'9', 61},
+                                         DecodeBase64Case{'+', 62},
+                                         DecodeBase64Case{'/', 63}));
+
+INSTANTIATE_TEST_SUITE_P(Invalid,
+                         DecodeBase64Test,
+                         testing::Values(DecodeBase64Case{'@', std::nullopt},
+                                         DecodeBase64Case{'#', std::nullopt},
+                                         DecodeBase64Case{'^', std::nullopt},
+                                         DecodeBase64Case{'&', std::nullopt},
+                                         DecodeBase64Case{'!', std::nullopt},
+                                         DecodeBase64Case{'*', std::nullopt},
+                                         DecodeBase64Case{'(', std::nullopt},
+                                         DecodeBase64Case{')', std::nullopt},
+                                         DecodeBase64Case{'-', std::nullopt},
+                                         DecodeBase64Case{'.', std::nullopt},
+                                         DecodeBase64Case{'\0', std::nullopt},
+                                         DecodeBase64Case{'\n', std::nullopt}));
+
+INSTANTIATE_TEST_SUITE_P(Padding,
+                         DecodeBase64Test,
+                         testing::Values(DecodeBase64Case{'=', std::nullopt}));
+
+struct DecodeBase64FromCommentsCase {
+    std::string_view wgsl;
+    Vector<int, 0> expected;
+};
+
+static std::ostream& operator<<(std::ostream& o, const DecodeBase64FromCommentsCase& c) {
+    return o << "'" << ReplaceAll(c.wgsl, "\n", "␤") << "'";
+}
+
+using DecodeBase64FromCommentsTest = ::testing::TestWithParam<DecodeBase64FromCommentsCase>;
+
+TEST_P(DecodeBase64FromCommentsTest, None) {
+    auto got_bytes = DecodeBase64FromComments(GetParam().wgsl);
+    auto got = Transform(got_bytes, [](std::byte byte) { return static_cast<int>(byte); });
+    EXPECT_EQ(got, GetParam().expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         DecodeBase64FromCommentsTest,
+                         testing::ValuesIn(std::vector<DecodeBase64FromCommentsCase>{
+                             {"", Empty},
+                             {"//", Empty},
+                             {"abc", Empty},
+                             {"abc//", Empty},
+                             {
+                                 R"(a
+b
+c)",
+                                 Empty,
+                             },
+                             {"// abc", {26, 27, 28}},
+                             {"a // bc", {27, 28}},
+                             {"ab // c", {28}},
+                             {"// a.b.c", {26, 27, 28}},
+                             {
+                                 R"(a
+b
+c)",
+                                 Empty,
+                             },
+                             {
+                                 R"(a
+// b
+c)",
+                                 {27},
+                             },
+                             {
+                                 R"(// a
+// b
+// c)",
+                                 {26, 27, 28},
+                             },
+                             {
+                                 R"(/* a
+b
+c
+*/)",
+                                 {26, 27, 28},
+                             },
+                             {
+                                 R"(/* a
+b
+*/
+c)",
+                                 {26, 27},
+                             },
+                             {
+                                 R"(a/*
+b
+*/
+c)",
+                                 {27},
+                             },
+                             {
+                                 "x/*a*/b/*c*/y",
+                                 {26, 28},
+                             },
+                             {
+                                 "x/*a/*b*/c*/z",
+                                 {26, 27, 28},
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::utils