Import Tint changes from Dawn

Changes:
  - b32f9ca75b28abdbc0b8c25817de6641f2c71989 [tint][wgsl] Simplify IRToProgramTest by Ben Clayton <bclayton@google.com>
  - d56afc4261e2d046a7d8e424be372487ec149c18 Remove materialize from core defs. by dan sinclair <dsinclair@chromium.org>
  - a3430744b3c198bc3ad7e28dc94297f3b8e04ff9 [tint][ir] Validate user call arguments by Ben Clayton <bclayton@google.com>
  - addf27edd4bd0f14ad3c459099f51195078929f6 [tint] Minor code refactor by Ben Clayton <bclayton@google.com>
  - 47236f1ef5f29b90d32c7a591fadb38d321ba6ff [tint] Fix the datatype conversion in socket.cc by Sonakshi Saxena <nexa@google.com>
  - becf2538e8896d1538d0e02e05c5e8022337b505 [tint] Improve unit test failure output. by Ben Clayton <bclayton@google.com>
  - 9887c6ea389dfd01bec84e39aecbf07ead777d94 [tint][ir] Validate store target type by Ben Clayton <bclayton@google.com>
  - fa41628b03f1c048b024e32581d4ec9404e88d1c [spirv-reader] Handle UBO variable decorations by James Price <jrprice@google.com>
  - 2e2bf7b923ee30485fd134326345a6233e0884c2 [spirv-reader] Handle Op{AccessChain,Load,Store} by James Price <jrprice@google.com>
  - bd9da033ea5b4242217b1343d16eaf9c2d098d44 [spirv-reader] Add Emit() and AddValue() helpers by James Price <jrprice@google.com>
  - df41dad13f79f66e81d8693a10f44d67a2edbec7 Add new SPIRV AST fuzzer. by dan sinclair <dsinclair@chromium.org>
  - 41cdde73fbc97afbe4de286d3180ad3420ca1fbc [tint][lang] Standardise lower / raise namespaces by Ben Clayton <bclayton@google.com>
  - 31295c47ea224ae63b63bb78bd3f989074224f46 Add new GLSL fuzzer by dan sinclair <dsinclair@chromium.org>
  - 91c5d6a2295dc2edd533de1935b92754690e23bb Add new HLSL fuzzer by dan sinclair <dsinclair@chromium.org>
  - 2fe1f267e72e4e4e1b903b7c70f517c6d690c4e1 Add reflection for Access enum. by dan sinclair <dsinclair@chromium.org>
  - 8a000ce7786b702fa12cee14160588dda6f6af5a [ir]: polyfill dot4I8Packed, dot4UPacked when needed by David Neto <dneto@google.com>
  - 6140a5998637a8b64a83c4da189d02af11a503f2 Add Vector to byte decoder. by dan sinclair <dsinclair@chromium.org>
  - d98c29e4403851696da11684c6f45ca840defd16 Additions to generator code. by dan sinclair <dsinclair@chromium.org>
  - 514a18aed3fc6cbef08cfab57fc3b5155f6f84b9 [tint][ir] Use intrinsic table for binary ops. by Ben Clayton <bclayton@google.com>
  - fa72454f21104406d53b6ca324c5fa65f03097a5 Add new MSL fuzzer by dan sinclair <dsinclair@chromium.org>
  - 53edc77f378a6ff6837c75a8150fae179008e210 dp4a: fix Tint IR polyfills for unpack4x[IU]8 by David Neto <dneto@google.com>
  - 1dc345789819bfa6509fe4fb3ecae600ddd6de2f [tint][ir] Add an IR binary roundtrip fuzzer by Ben Clayton <bclayton@google.com>
  - b865a1e88666f21dc094e0218f12459e0743660c [tint][ir] Use intrinsic table for unary ops. by Ben Clayton <bclayton@google.com>
  - 825f6265f4435d0aba4eaa60e5b6fa7938c513fc Add toggle to disable Tint polyfills on integer division ... by Jiawei Shao <jiawei.shao@intel.com>
GitOrigin-RevId: b32f9ca75b28abdbc0b8c25817de6641f2c71989
Change-Id: Ib87d37962a5220b36e44adc9989e61f001c7b599
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/168760
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/api/common/BUILD.bazel b/src/tint/api/common/BUILD.bazel
index 44c7d10..dca847a 100644
--- a/src/tint/api/common/BUILD.bazel
+++ b/src/tint/api/common/BUILD.bazel
@@ -49,7 +49,6 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/reflection",
-    "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
   copts = COPTS,
diff --git a/src/tint/api/common/BUILD.cmake b/src/tint/api/common/BUILD.cmake
index 019534d..96daf61 100644
--- a/src/tint/api/common/BUILD.cmake
+++ b/src/tint/api/common/BUILD.cmake
@@ -48,6 +48,5 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_reflection
-  tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/api/common/BUILD.gn b/src/tint/api/common/BUILD.gn
index 0d1ea88..7e710c8 100644
--- a/src/tint/api/common/BUILD.gn
+++ b/src/tint/api/common/BUILD.gn
@@ -48,7 +48,6 @@
     "${tint_src_dir}/utils/macros",
     "${tint_src_dir}/utils/math",
     "${tint_src_dir}/utils/reflection",
-    "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
 }
diff --git a/src/tint/api/common/binding_point.h b/src/tint/api/common/binding_point.h
index f86d525..64d61f6 100644
--- a/src/tint/api/common/binding_point.h
+++ b/src/tint/api/common/binding_point.h
@@ -34,7 +34,6 @@
 
 #include "src/tint/utils/math/hash.h"
 #include "src/tint/utils/reflection/reflection.h"
-#include "src/tint/utils/text/string_stream.h"
 #include "src/tint/utils/traits/traits.h"
 
 namespace tint {
diff --git a/src/tint/api/options/BUILD.bazel b/src/tint/api/options/BUILD.bazel
index 3e1de60..b0e099b 100644
--- a/src/tint/api/options/BUILD.bazel
+++ b/src/tint/api/options/BUILD.bazel
@@ -53,7 +53,6 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/reflection",
-    "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
   copts = COPTS,
diff --git a/src/tint/api/options/BUILD.cmake b/src/tint/api/options/BUILD.cmake
index 9cf5e92..9de3f17 100644
--- a/src/tint/api/options/BUILD.cmake
+++ b/src/tint/api/options/BUILD.cmake
@@ -52,6 +52,5 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_reflection
-  tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/api/options/BUILD.gn b/src/tint/api/options/BUILD.gn
index 3b567ca..35d9806 100644
--- a/src/tint/api/options/BUILD.gn
+++ b/src/tint/api/options/BUILD.gn
@@ -52,7 +52,6 @@
     "${tint_src_dir}/utils/macros",
     "${tint_src_dir}/utils/math",
     "${tint_src_dir}/utils/reflection",
-    "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
 }
diff --git a/src/tint/api/options/pixel_local.h b/src/tint/api/options/pixel_local.h
index 61e177b..1b4e200 100644
--- a/src/tint/api/options/pixel_local.h
+++ b/src/tint/api/options/pixel_local.h
@@ -28,6 +28,7 @@
 #ifndef SRC_TINT_API_OPTIONS_PIXEL_LOCAL_H_
 #define SRC_TINT_API_OPTIONS_PIXEL_LOCAL_H_
 
+#include <cstdint>
 #include <unordered_map>
 
 #include "src/tint/utils/reflection/reflection.h"
diff --git a/src/tint/cmd/bench/BUILD.bazel b/src/tint/cmd/bench/BUILD.bazel
index 48b45c3..a273bc9 100644
--- a/src/tint/cmd/bench/BUILD.bazel
+++ b/src/tint/cmd/bench/BUILD.bazel
@@ -113,6 +113,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/rtti:bench",
diff --git a/src/tint/cmd/bench/BUILD.cmake b/src/tint/cmd/bench/BUILD.cmake
index 9df1c7a..ee4b122 100644
--- a/src/tint/cmd/bench/BUILD.cmake
+++ b/src/tint/cmd/bench/BUILD.cmake
@@ -60,6 +60,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_rtti_bench
diff --git a/src/tint/cmd/bench/BUILD.gn b/src/tint/cmd/bench/BUILD.gn
index 8697f0c..7eb287d 100644
--- a/src/tint/cmd/bench/BUILD.gn
+++ b/src/tint/cmd/bench/BUILD.gn
@@ -114,6 +114,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/rtti:bench",
diff --git a/src/tint/cmd/fuzz/ir/fuzz.h b/src/tint/cmd/fuzz/ir/fuzz.h
index fa1ffd0..183ebe1 100644
--- a/src/tint/cmd/fuzz/ir/fuzz.h
+++ b/src/tint/cmd/fuzz/ir/fuzz.h
@@ -57,8 +57,8 @@
                     return;
                 }
                 bytes::BufferReader reader{data};
-                if (auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader);
-                    data_args == Success) {
+                auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader);
+                if (data_args == Success) {
                     auto all_args =
                         std::tuple_cat(std::tuple<core::ir::Module&>{module}, data_args.Get());
                     std::apply(*fn, all_args);
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.bazel b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
index c3cbe15..2b19c04 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.bazel
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.bazel
@@ -38,6 +38,26 @@
 load("@bazel_skylib//lib:selects.bzl", "selects")
 
 alias(
+  name = "tint_build_glsl_writer",
+  actual = "//src/tint:tint_build_glsl_writer_true",
+)
+
+alias(
+  name = "tint_build_hlsl_writer",
+  actual = "//src/tint:tint_build_hlsl_writer_true",
+)
+
+alias(
+  name = "tint_build_ir_binary",
+  actual = "//src/tint:tint_build_ir_binary_true",
+)
+
+alias(
+  name = "tint_build_msl_writer",
+  actual = "//src/tint:tint_build_msl_writer_true",
+)
+
+alias(
   name = "tint_build_spv_writer",
   actual = "//src/tint:tint_build_spv_writer_true",
 )
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.cmake b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
index b0546c5..853fd5d 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.cmake
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
@@ -73,6 +73,30 @@
   tint_utils_traits
 )
 
+if(TINT_BUILD_GLSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_lang_glsl_writer_fuzz
+  )
+endif(TINT_BUILD_GLSL_WRITER)
+
+if(TINT_BUILD_HLSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_lang_hlsl_writer_fuzz
+  )
+endif(TINT_BUILD_HLSL_WRITER)
+
+if(TINT_BUILD_IR_BINARY)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_lang_core_ir_binary_fuzz
+  )
+endif(TINT_BUILD_IR_BINARY)
+
+if(TINT_BUILD_MSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
+    tint_lang_msl_writer_fuzz
+  )
+endif(TINT_BUILD_MSL_WRITER)
+
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_cmd_fuzz_wgsl_fuzz_cmd fuzz_cmd
     tint_lang_spirv_writer_fuzz
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.gn b/src/tint/cmd/fuzz/wgsl/BUILD.gn
index 5dc14cb..97cec04 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.gn
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.gn
@@ -110,6 +110,22 @@
       "${tint_src_dir}/utils/traits",
     ]
 
+    if (tint_build_glsl_writer) {
+      deps += [ "${tint_src_dir}/lang/glsl/writer:fuzz" ]
+    }
+
+    if (tint_build_hlsl_writer) {
+      deps += [ "${tint_src_dir}/lang/hlsl/writer:fuzz" ]
+    }
+
+    if (tint_build_ir_binary) {
+      deps += [ "${tint_src_dir}/lang/core/ir/binary:fuzz" ]
+    }
+
+    if (tint_build_msl_writer) {
+      deps += [ "${tint_src_dir}/lang/msl/writer:fuzz" ]
+    }
+
     if (tint_build_spv_writer) {
       deps += [ "${tint_src_dir}/lang/spirv/writer:fuzz" ]
     }
diff --git a/src/tint/lang/core/BUILD.bazel b/src/tint/lang/core/BUILD.bazel
index a26c37f..ade8daa 100644
--- a/src/tint/lang/core/BUILD.bazel
+++ b/src/tint/lang/core/BUILD.bazel
@@ -78,6 +78,7 @@
     "//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",
@@ -143,6 +144,8 @@
   ],
   deps = [
     "//src/tint/lang/core",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/traits",
     "@benchmark",
   ],
diff --git a/src/tint/lang/core/BUILD.cmake b/src/tint/lang/core/BUILD.cmake
index a5c1610..ea40bb5 100644
--- a/src/tint/lang/core/BUILD.cmake
+++ b/src/tint/lang/core/BUILD.cmake
@@ -82,6 +82,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_text
@@ -150,6 +151,8 @@
 
 tint_target_add_dependencies(tint_lang_core_bench bench
   tint_lang_core
+  tint_utils_macros
+  tint_utils_reflection
   tint_utils_traits
 )
 
diff --git a/src/tint/lang/core/BUILD.gn b/src/tint/lang/core/BUILD.gn
index 88de4fd..b1186f8 100644
--- a/src/tint/lang/core/BUILD.gn
+++ b/src/tint/lang/core/BUILD.gn
@@ -81,6 +81,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/text",
@@ -142,6 +143,8 @@
     deps = [
       "${tint_src_dir}:google_benchmark",
       "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/reflection",
       "${tint_src_dir}/utils/traits",
     ]
   }
diff --git a/src/tint/lang/core/access.h b/src/tint/lang/core/access.h
index 9ace935..d03c07b 100644
--- a/src/tint/lang/core/access.h
+++ b/src/tint/lang/core/access.h
@@ -39,6 +39,7 @@
 
 #include <cstdint>
 
+#include "src/tint/utils/reflection/reflection.h"
 #include "src/tint/utils/traits/traits.h"
 
 namespace tint::core {
@@ -76,4 +77,11 @@
 
 }  // namespace tint::core
 
+namespace tint {
+
+/// Access reflection information
+TINT_REFLECT_ENUM_RANGE(core::Access, kUndefined, kWrite);
+
+}  // namespace tint
+
 #endif  // SRC_TINT_LANG_CORE_ACCESS_H_
diff --git a/src/tint/lang/core/access.h.tmpl b/src/tint/lang/core/access.h.tmpl
index 852f978..9e9112e 100644
--- a/src/tint/lang/core/access.h.tmpl
+++ b/src/tint/lang/core/access.h.tmpl
@@ -20,6 +20,7 @@
 
 #include <cstdint>
 
+#include "src/tint/utils/reflection/reflection.h"
 #include "src/tint/utils/traits/traits.h"
 
 namespace tint::core {
@@ -29,4 +30,11 @@
 
 }  // namespace tint::core
 
+namespace tint {
+
+/// Access reflection information
+TINT_REFLECT_ENUM_RANGE(core::{{ Eval "EnumName" $enum }}, {{Eval "EnumFirst" $enum }}, {{Eval "EnumLast" $enum}});
+
+}  // namespace tint
+
 #endif  // SRC_TINT_LANG_CORE_ACCESS_H_
diff --git a/src/tint/lang/core/builtin_fn.cc b/src/tint/lang/core/builtin_fn.cc
index 5344117..82e1e37 100644
--- a/src/tint/lang/core/builtin_fn.cc
+++ b/src/tint/lang/core/builtin_fn.cc
@@ -402,9 +402,6 @@
     if (name == "subgroupBroadcast") {
         return BuiltinFn::kSubgroupBroadcast;
     }
-    if (name == "_tint_materialize") {
-        return BuiltinFn::kTintMaterialize;
-    }
     return BuiltinFn::kNone;
 }
 
@@ -654,8 +651,6 @@
             return "subgroupBallot";
         case BuiltinFn::kSubgroupBroadcast:
             return "subgroupBroadcast";
-        case BuiltinFn::kTintMaterialize:
-            return "_tint_materialize";
     }
     return "<unknown>";
 }
diff --git a/src/tint/lang/core/builtin_fn.h b/src/tint/lang/core/builtin_fn.h
index 277b908..7a87054 100644
--- a/src/tint/lang/core/builtin_fn.h
+++ b/src/tint/lang/core/builtin_fn.h
@@ -168,7 +168,6 @@
     kAtomicCompareExchangeWeak,
     kSubgroupBallot,
     kSubgroupBroadcast,
-    kTintMaterialize,
     kNone,
 };
 
@@ -312,7 +311,6 @@
     BuiltinFn::kAtomicCompareExchangeWeak,
     BuiltinFn::kSubgroupBallot,
     BuiltinFn::kSubgroupBroadcast,
-    BuiltinFn::kTintMaterialize,
 };
 
 /// All builtin function names
@@ -438,7 +436,6 @@
     "atomicCompareExchangeWeak",
     "subgroupBallot",
     "subgroupBroadcast",
-    "_tint_materialize",
 };
 
 /// Determines if the given `f` is a coarse derivative.
diff --git a/src/tint/lang/core/constant/BUILD.bazel b/src/tint/lang/core/constant/BUILD.bazel
index b7967f5..b092c56 100644
--- a/src/tint/lang/core/constant/BUILD.bazel
+++ b/src/tint/lang/core/constant/BUILD.bazel
@@ -67,6 +67,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/core/constant/BUILD.cmake b/src/tint/lang/core/constant/BUILD.cmake
index 3e86cd2..b7dbca1 100644
--- a/src/tint/lang/core/constant/BUILD.cmake
+++ b/src/tint/lang/core/constant/BUILD.cmake
@@ -66,6 +66,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/core/constant/BUILD.gn b/src/tint/lang/core/constant/BUILD.gn
index 39c11d9..8549c0b 100644
--- a/src/tint/lang/core/constant/BUILD.gn
+++ b/src/tint/lang/core/constant/BUILD.gn
@@ -70,6 +70,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/core/core.def b/src/tint/lang/core/core.def
index 20eb59e..2606174 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -1115,8 +1115,3 @@
 
 @must_use @const op >> <T: ia_iu32>(T, u32) -> T
 @must_use @const op >> <T: ia_iu32, N: num> (vec<N, T>, vec<N, u32>) -> vec<N, T>
-
-////////////////////////////////////////////////////////////////////////////////
-// Tint internal builtins                                                     //
-////////////////////////////////////////////////////////////////////////////////
-@const("Identity") fn _tint_materialize<T>(T) -> T
diff --git a/src/tint/lang/core/intrinsic/BUILD.bazel b/src/tint/lang/core/intrinsic/BUILD.bazel
index ea936ea..141bff2 100644
--- a/src/tint/lang/core/intrinsic/BUILD.bazel
+++ b/src/tint/lang/core/intrinsic/BUILD.bazel
@@ -61,6 +61,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/core/intrinsic/BUILD.cmake b/src/tint/lang/core/intrinsic/BUILD.cmake
index 4a6f199..6af46bb 100644
--- a/src/tint/lang/core/intrinsic/BUILD.cmake
+++ b/src/tint/lang/core/intrinsic/BUILD.cmake
@@ -60,6 +60,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/core/intrinsic/BUILD.gn b/src/tint/lang/core/intrinsic/BUILD.gn
index 011a65a..fbfb8ce 100644
--- a/src/tint/lang/core/intrinsic/BUILD.gn
+++ b/src/tint/lang/core/intrinsic/BUILD.gn
@@ -64,6 +64,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/core/intrinsic/data.cc b/src/tint/lang/core/intrinsic/data.cc
index bf2983c..97482d5 100644
--- a/src/tint/lang/core/intrinsic/data.cc
+++ b/src/tint/lang/core/intrinsic/data.cc
@@ -4859,32 +4859,32 @@
   /* [76] */ &core::constant::Eval::unpack4x8unorm,
   /* [77] */ &core::constant::Eval::unpack4xI8,
   /* [78] */ &core::constant::Eval::unpack4xU8,
-  /* [79] */ &core::constant::Eval::Identity,
-  /* [80] */ &core::constant::Eval::Not,
-  /* [81] */ &core::constant::Eval::Complement,
-  /* [82] */ &core::constant::Eval::UnaryMinus,
-  /* [83] */ &core::constant::Eval::Plus,
-  /* [84] */ &core::constant::Eval::Minus,
-  /* [85] */ &core::constant::Eval::Multiply,
-  /* [86] */ &core::constant::Eval::MultiplyMatVec,
-  /* [87] */ &core::constant::Eval::MultiplyVecMat,
-  /* [88] */ &core::constant::Eval::MultiplyMatMat,
-  /* [89] */ &core::constant::Eval::Divide,
-  /* [90] */ &core::constant::Eval::Modulo,
-  /* [91] */ &core::constant::Eval::Xor,
-  /* [92] */ &core::constant::Eval::And,
-  /* [93] */ &core::constant::Eval::Or,
-  /* [94] */ &core::constant::Eval::LogicalAnd,
-  /* [95] */ &core::constant::Eval::LogicalOr,
-  /* [96] */ &core::constant::Eval::Equal,
-  /* [97] */ &core::constant::Eval::NotEqual,
-  /* [98] */ &core::constant::Eval::LessThan,
-  /* [99] */ &core::constant::Eval::GreaterThan,
-  /* [100] */ &core::constant::Eval::LessThanEqual,
-  /* [101] */ &core::constant::Eval::GreaterThanEqual,
-  /* [102] */ &core::constant::Eval::ShiftLeft,
-  /* [103] */ &core::constant::Eval::ShiftRight,
-  /* [104] */ &core::constant::Eval::Zero,
+  /* [79] */ &core::constant::Eval::Not,
+  /* [80] */ &core::constant::Eval::Complement,
+  /* [81] */ &core::constant::Eval::UnaryMinus,
+  /* [82] */ &core::constant::Eval::Plus,
+  /* [83] */ &core::constant::Eval::Minus,
+  /* [84] */ &core::constant::Eval::Multiply,
+  /* [85] */ &core::constant::Eval::MultiplyMatVec,
+  /* [86] */ &core::constant::Eval::MultiplyVecMat,
+  /* [87] */ &core::constant::Eval::MultiplyMatMat,
+  /* [88] */ &core::constant::Eval::Divide,
+  /* [89] */ &core::constant::Eval::Modulo,
+  /* [90] */ &core::constant::Eval::Xor,
+  /* [91] */ &core::constant::Eval::And,
+  /* [92] */ &core::constant::Eval::Or,
+  /* [93] */ &core::constant::Eval::LogicalAnd,
+  /* [94] */ &core::constant::Eval::LogicalOr,
+  /* [95] */ &core::constant::Eval::Equal,
+  /* [96] */ &core::constant::Eval::NotEqual,
+  /* [97] */ &core::constant::Eval::LessThan,
+  /* [98] */ &core::constant::Eval::GreaterThan,
+  /* [99] */ &core::constant::Eval::LessThanEqual,
+  /* [100] */ &core::constant::Eval::GreaterThanEqual,
+  /* [101] */ &core::constant::Eval::ShiftLeft,
+  /* [102] */ &core::constant::Eval::ShiftRight,
+  /* [103] */ &core::constant::Eval::Zero,
+  /* [104] */ &core::constant::Eval::Identity,
   /* [105] */ &core::constant::Eval::Conv,
   /* [106] */ &core::constant::Eval::VecSplat,
   /* [107] */ &core::constant::Eval::VecInitS,
@@ -5532,7 +5532,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(94),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [49] */
@@ -5545,7 +5545,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [50] */
@@ -5558,7 +5558,7 @@
     /* parameters */ ParameterIndex(217),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(52),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [51] */
@@ -6429,7 +6429,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(88),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [118] */
@@ -6442,7 +6442,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [119] */
@@ -6455,7 +6455,7 @@
     /* parameters */ ParameterIndex(213),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(10),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [120] */
@@ -6715,7 +6715,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(82),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [140] */
@@ -6728,7 +6728,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [141] */
@@ -6741,7 +6741,7 @@
     /* parameters */ ParameterIndex(209),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(72),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [142] */
@@ -6845,7 +6845,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [150] */
@@ -6858,7 +6858,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [151] */
@@ -6871,7 +6871,7 @@
     /* parameters */ ParameterIndex(223),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [152] */
@@ -6884,7 +6884,7 @@
     /* parameters */ ParameterIndex(350),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [153] */
@@ -6897,7 +6897,7 @@
     /* parameters */ ParameterIndex(355),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(2),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [154] */
@@ -6910,7 +6910,7 @@
     /* parameters */ ParameterIndex(354),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(2),
-    /* const_eval_fn */ ConstEvalFunctionIndex(85),
+    /* const_eval_fn */ ConstEvalFunctionIndex(84),
   },
   {
     /* [155] */
@@ -6923,7 +6923,7 @@
     /* parameters */ ParameterIndex(356),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(3),
-    /* const_eval_fn */ ConstEvalFunctionIndex(86),
+    /* const_eval_fn */ ConstEvalFunctionIndex(85),
   },
   {
     /* [156] */
@@ -6936,7 +6936,7 @@
     /* parameters */ ParameterIndex(358),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(87),
+    /* const_eval_fn */ ConstEvalFunctionIndex(86),
   },
   {
     /* [157] */
@@ -6949,7 +6949,7 @@
     /* parameters */ ParameterIndex(360),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(18),
-    /* const_eval_fn */ ConstEvalFunctionIndex(88),
+    /* const_eval_fn */ ConstEvalFunctionIndex(87),
   },
   {
     /* [158] */
@@ -7404,7 +7404,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [193] */
@@ -7417,7 +7417,7 @@
     /* parameters */ ParameterIndex(385),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(102),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [194] */
@@ -7482,7 +7482,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [199] */
@@ -7495,7 +7495,7 @@
     /* parameters */ ParameterIndex(388),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(108),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [200] */
@@ -7560,7 +7560,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [205] */
@@ -7573,7 +7573,7 @@
     /* parameters */ ParameterIndex(391),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(114),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [206] */
@@ -7638,7 +7638,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [211] */
@@ -7651,7 +7651,7 @@
     /* parameters */ ParameterIndex(394),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(120),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [212] */
@@ -7716,7 +7716,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [217] */
@@ -7729,7 +7729,7 @@
     /* parameters */ ParameterIndex(397),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(126),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [218] */
@@ -7794,7 +7794,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [223] */
@@ -7807,7 +7807,7 @@
     /* parameters */ ParameterIndex(400),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(132),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [224] */
@@ -7872,7 +7872,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [229] */
@@ -7885,7 +7885,7 @@
     /* parameters */ ParameterIndex(403),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(138),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [230] */
@@ -7950,7 +7950,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [235] */
@@ -7963,7 +7963,7 @@
     /* parameters */ ParameterIndex(406),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(144),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [236] */
@@ -8028,7 +8028,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [241] */
@@ -8041,7 +8041,7 @@
     /* parameters */ ParameterIndex(409),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(150),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [242] */
@@ -8171,7 +8171,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [252] */
@@ -8184,7 +8184,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [253] */
@@ -8197,7 +8197,7 @@
     /* parameters */ ParameterIndex(223),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [254] */
@@ -8210,7 +8210,7 @@
     /* parameters */ ParameterIndex(350),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [255] */
@@ -8223,7 +8223,7 @@
     /* parameters */ ParameterIndex(353),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(2),
-    /* const_eval_fn */ ConstEvalFunctionIndex(83),
+    /* const_eval_fn */ ConstEvalFunctionIndex(82),
   },
   {
     /* [256] */
@@ -8236,7 +8236,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [257] */
@@ -8249,7 +8249,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [258] */
@@ -8262,7 +8262,7 @@
     /* parameters */ ParameterIndex(223),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [259] */
@@ -8275,7 +8275,7 @@
     /* parameters */ ParameterIndex(350),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [260] */
@@ -8288,7 +8288,7 @@
     /* parameters */ ParameterIndex(353),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(12),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(2),
-    /* const_eval_fn */ ConstEvalFunctionIndex(84),
+    /* const_eval_fn */ ConstEvalFunctionIndex(83),
   },
   {
     /* [261] */
@@ -8301,7 +8301,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(88),
   },
   {
     /* [262] */
@@ -8314,7 +8314,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(88),
   },
   {
     /* [263] */
@@ -8327,7 +8327,7 @@
     /* parameters */ ParameterIndex(223),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(88),
   },
   {
     /* [264] */
@@ -8340,7 +8340,7 @@
     /* parameters */ ParameterIndex(350),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(89),
+    /* const_eval_fn */ ConstEvalFunctionIndex(88),
   },
   {
     /* [265] */
@@ -8353,7 +8353,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(90),
+    /* const_eval_fn */ ConstEvalFunctionIndex(89),
   },
   {
     /* [266] */
@@ -8366,7 +8366,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(90),
+    /* const_eval_fn */ ConstEvalFunctionIndex(89),
   },
   {
     /* [267] */
@@ -8379,7 +8379,7 @@
     /* parameters */ ParameterIndex(223),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(90),
+    /* const_eval_fn */ ConstEvalFunctionIndex(89),
   },
   {
     /* [268] */
@@ -8392,7 +8392,7 @@
     /* parameters */ ParameterIndex(350),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(90),
+    /* const_eval_fn */ ConstEvalFunctionIndex(89),
   },
   {
     /* [269] */
@@ -8405,7 +8405,7 @@
     /* parameters */ ParameterIndex(226),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [270] */
@@ -8418,7 +8418,7 @@
     /* parameters */ ParameterIndex(233),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [271] */
@@ -8431,7 +8431,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [272] */
@@ -8444,7 +8444,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(92),
+    /* const_eval_fn */ ConstEvalFunctionIndex(91),
   },
   {
     /* [273] */
@@ -8457,7 +8457,7 @@
     /* parameters */ ParameterIndex(226),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(93),
+    /* const_eval_fn */ ConstEvalFunctionIndex(92),
   },
   {
     /* [274] */
@@ -8470,7 +8470,7 @@
     /* parameters */ ParameterIndex(233),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(93),
+    /* const_eval_fn */ ConstEvalFunctionIndex(92),
   },
   {
     /* [275] */
@@ -8483,7 +8483,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(93),
+    /* const_eval_fn */ ConstEvalFunctionIndex(92),
   },
   {
     /* [276] */
@@ -8496,7 +8496,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(93),
+    /* const_eval_fn */ ConstEvalFunctionIndex(92),
   },
   {
     /* [277] */
@@ -8587,7 +8587,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(31),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [284] */
@@ -8600,7 +8600,7 @@
     /* parameters */ ParameterIndex(380),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(31),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [285] */
@@ -8626,7 +8626,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [287] */
@@ -8639,7 +8639,7 @@
     /* parameters */ ParameterIndex(17),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(33),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [288] */
@@ -8665,7 +8665,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [290] */
@@ -8678,7 +8678,7 @@
     /* parameters */ ParameterIndex(370),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(15),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [291] */
@@ -8704,7 +8704,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(85),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [293] */
@@ -8717,7 +8717,7 @@
     /* parameters */ ParameterIndex(381),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(85),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [294] */
@@ -8743,7 +8743,7 @@
     /* parameters */ ParameterIndex(/* invalid */),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(104),
+    /* const_eval_fn */ ConstEvalFunctionIndex(103),
   },
   {
     /* [296] */
@@ -8756,7 +8756,7 @@
     /* parameters */ ParameterIndex(226),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(104),
   },
   {
     /* [297] */
@@ -10212,7 +10212,7 @@
     /* parameters */ ParameterIndex(226),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(80),
+    /* const_eval_fn */ ConstEvalFunctionIndex(79),
   },
   {
     /* [409] */
@@ -10225,7 +10225,7 @@
     /* parameters */ ParameterIndex(233),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(80),
+    /* const_eval_fn */ ConstEvalFunctionIndex(79),
   },
   {
     /* [410] */
@@ -10238,7 +10238,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(81),
+    /* const_eval_fn */ ConstEvalFunctionIndex(80),
   },
   {
     /* [411] */
@@ -10251,7 +10251,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(81),
+    /* const_eval_fn */ ConstEvalFunctionIndex(80),
   },
   {
     /* [412] */
@@ -10264,7 +10264,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(81),
   },
   {
     /* [413] */
@@ -10277,7 +10277,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(82),
+    /* const_eval_fn */ ConstEvalFunctionIndex(81),
   },
   {
     /* [414] */
@@ -10290,7 +10290,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(91),
+    /* const_eval_fn */ ConstEvalFunctionIndex(90),
   },
   {
     /* [415] */
@@ -10303,7 +10303,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(91),
+    /* const_eval_fn */ ConstEvalFunctionIndex(90),
   },
   {
     /* [416] */
@@ -10316,7 +10316,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(96),
+    /* const_eval_fn */ ConstEvalFunctionIndex(95),
   },
   {
     /* [417] */
@@ -10329,7 +10329,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(96),
+    /* const_eval_fn */ ConstEvalFunctionIndex(95),
   },
   {
     /* [418] */
@@ -10342,7 +10342,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(97),
+    /* const_eval_fn */ ConstEvalFunctionIndex(96),
   },
   {
     /* [419] */
@@ -10355,7 +10355,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(97),
+    /* const_eval_fn */ ConstEvalFunctionIndex(96),
   },
   {
     /* [420] */
@@ -10368,7 +10368,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(98),
+    /* const_eval_fn */ ConstEvalFunctionIndex(97),
   },
   {
     /* [421] */
@@ -10381,7 +10381,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(98),
+    /* const_eval_fn */ ConstEvalFunctionIndex(97),
   },
   {
     /* [422] */
@@ -10394,7 +10394,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(99),
+    /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
   {
     /* [423] */
@@ -10407,7 +10407,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(99),
+    /* const_eval_fn */ ConstEvalFunctionIndex(98),
   },
   {
     /* [424] */
@@ -10420,7 +10420,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(100),
+    /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
   {
     /* [425] */
@@ -10433,7 +10433,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(100),
+    /* const_eval_fn */ ConstEvalFunctionIndex(99),
   },
   {
     /* [426] */
@@ -10446,7 +10446,7 @@
     /* parameters */ ParameterIndex(1),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(101),
+    /* const_eval_fn */ ConstEvalFunctionIndex(100),
   },
   {
     /* [427] */
@@ -10459,7 +10459,7 @@
     /* parameters */ ParameterIndex(149),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(8),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(101),
+    /* const_eval_fn */ ConstEvalFunctionIndex(100),
   },
   {
     /* [428] */
@@ -10472,7 +10472,7 @@
     /* parameters */ ParameterIndex(16),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(102),
+    /* const_eval_fn */ ConstEvalFunctionIndex(101),
   },
   {
     /* [429] */
@@ -10485,7 +10485,7 @@
     /* parameters */ ParameterIndex(351),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(102),
+    /* const_eval_fn */ ConstEvalFunctionIndex(101),
   },
   {
     /* [430] */
@@ -10498,7 +10498,7 @@
     /* parameters */ ParameterIndex(16),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
   {
     /* [431] */
@@ -10511,7 +10511,7 @@
     /* parameters */ ParameterIndex(351),
     /* return_type_matcher_indices */ TypeMatcherIndicesIndex(6),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(1),
-    /* const_eval_fn */ ConstEvalFunctionIndex(103),
+    /* const_eval_fn */ ConstEvalFunctionIndex(102),
   },
   {
     /* [432] */
@@ -10957,16 +10957,16 @@
   },
   {
     /* [466] */
-    /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
-    /* num_parameters */ 1,
-    /* num_template_types */ 1,
+    /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
+    /* num_parameters */ 2,
+    /* num_template_types */ 0,
     /* num_template_numbers */ 0,
-    /* template_types */ TemplateTypeIndex(25),
+    /* template_types */ TemplateTypeIndex(/* invalid */),
     /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(1),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(2),
+    /* parameters */ ParameterIndex(226),
+    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
     /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(79),
+    /* const_eval_fn */ ConstEvalFunctionIndex(93),
   },
   {
     /* [467] */
@@ -10983,19 +10983,6 @@
   },
   {
     /* [468] */
-    /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
-    /* num_parameters */ 2,
-    /* num_template_types */ 0,
-    /* num_template_numbers */ 0,
-    /* template_types */ TemplateTypeIndex(/* invalid */),
-    /* template_numbers */ TemplateNumberIndex(/* invalid */),
-    /* parameters */ ParameterIndex(226),
-    /* return_type_matcher_indices */ TypeMatcherIndicesIndex(9),
-    /* return_number_matcher_indices */ NumberMatcherIndicesIndex(/* invalid */),
-    /* const_eval_fn */ ConstEvalFunctionIndex(95),
-  },
-  {
-    /* [469] */
     /* flags */ OverloadFlags(OverloadFlag::kIsConverter, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* num_parameters */ 1,
     /* num_template_types */ 1,
@@ -11943,12 +11930,6 @@
     /* num overloads */ 1,
     /* overloads */ OverloadIndex(465),
   },
-  {
-    /* [121] */
-    /* fn _tint_materialize<T>(T) -> T */
-    /* num overloads */ 1,
-    /* overloads */ OverloadIndex(466),
-  },
 };
 
 constexpr IntrinsicInfo kUnaryOperators[] = {
@@ -12060,13 +12041,13 @@
     /* [8] */
     /* op &&(bool, bool) -> bool */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(467),
+    /* overloads */ OverloadIndex(466),
   },
   {
     /* [9] */
     /* op ||(bool, bool) -> bool */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(468),
+    /* overloads */ OverloadIndex(467),
   },
   {
     /* [10] */
@@ -12341,7 +12322,7 @@
     /* [17] */
     /* conv packedVec3<T : concrete_scalar>(vec3<T>) -> packedVec3<T> */
     /* num overloads */ 1,
-    /* overloads */ OverloadIndex(469),
+    /* overloads */ OverloadIndex(468),
   },
 };
 
diff --git a/src/tint/lang/core/ir/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel
index 3c62fe9..a02930e 100644
--- a/src/tint/lang/core/ir/BUILD.bazel
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -54,7 +54,9 @@
     "continue.cc",
     "control_instruction.cc",
     "convert.cc",
+    "core_binary.cc",
     "core_builtin_call.cc",
+    "core_unary.cc",
     "disassembler.cc",
     "discard.cc",
     "exit.cc",
@@ -104,7 +106,9 @@
     "continue.h",
     "control_instruction.h",
     "convert.h",
+    "core_binary.h",
     "core_builtin_call.h",
+    "core_unary.h",
     "disassembler.h",
     "discard.h",
     "exit.h",
@@ -169,7 +173,6 @@
   alwayslink = True,
   srcs = [
     "access_test.cc",
-    "binary_test.cc",
     "bitcast_test.cc",
     "block_param_test.cc",
     "block_test.cc",
@@ -178,7 +181,9 @@
     "construct_test.cc",
     "continue_test.cc",
     "convert_test.cc",
+    "core_binary_test.cc",
     "core_builtin_call_test.cc",
+    "core_unary_test.cc",
     "discard_test.cc",
     "exit_if_test.cc",
     "exit_loop_test.cc",
@@ -204,7 +209,6 @@
     "swizzle_test.cc",
     "terminate_invocation_test.cc",
     "traverse_test.cc",
-    "unary_test.cc",
     "unreachable_test.cc",
     "user_call_test.cc",
     "validator_test.cc",
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index c901636..94365b4 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -72,8 +72,12 @@
   lang/core/ir/control_instruction.h
   lang/core/ir/convert.cc
   lang/core/ir/convert.h
+  lang/core/ir/core_binary.cc
+  lang/core/ir/core_binary.h
   lang/core/ir/core_builtin_call.cc
   lang/core/ir/core_builtin_call.h
+  lang/core/ir/core_unary.cc
+  lang/core/ir/core_unary.h
   lang/core/ir/disassembler.cc
   lang/core/ir/disassembler.h
   lang/core/ir/discard.cc
@@ -170,7 +174,6 @@
 ################################################################################
 tint_add_target(tint_lang_core_ir_test test
   lang/core/ir/access_test.cc
-  lang/core/ir/binary_test.cc
   lang/core/ir/bitcast_test.cc
   lang/core/ir/block_param_test.cc
   lang/core/ir/block_test.cc
@@ -179,7 +182,9 @@
   lang/core/ir/construct_test.cc
   lang/core/ir/continue_test.cc
   lang/core/ir/convert_test.cc
+  lang/core/ir/core_binary_test.cc
   lang/core/ir/core_builtin_call_test.cc
+  lang/core/ir/core_unary_test.cc
   lang/core/ir/discard_test.cc
   lang/core/ir/exit_if_test.cc
   lang/core/ir/exit_loop_test.cc
@@ -205,7 +210,6 @@
   lang/core/ir/swizzle_test.cc
   lang/core/ir/terminate_invocation_test.cc
   lang/core/ir/traverse_test.cc
-  lang/core/ir/unary_test.cc
   lang/core/ir/unreachable_test.cc
   lang/core/ir/user_call_test.cc
   lang/core/ir/validator_test.cc
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index 9bcb139..ac59268 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -74,8 +74,12 @@
     "control_instruction.h",
     "convert.cc",
     "convert.h",
+    "core_binary.cc",
+    "core_binary.h",
     "core_builtin_call.cc",
     "core_builtin_call.h",
+    "core_unary.cc",
+    "core_unary.h",
     "disassembler.cc",
     "disassembler.h",
     "discard.cc",
@@ -169,7 +173,6 @@
   tint_unittests_source_set("unittests") {
     sources = [
       "access_test.cc",
-      "binary_test.cc",
       "bitcast_test.cc",
       "block_param_test.cc",
       "block_test.cc",
@@ -178,7 +181,9 @@
       "construct_test.cc",
       "continue_test.cc",
       "convert_test.cc",
+      "core_binary_test.cc",
       "core_builtin_call_test.cc",
+      "core_unary_test.cc",
       "discard_test.cc",
       "exit_if_test.cc",
       "exit_loop_test.cc",
@@ -204,7 +209,6 @@
       "swizzle_test.cc",
       "terminate_invocation_test.cc",
       "traverse_test.cc",
-      "unary_test.cc",
       "unreachable_test.cc",
       "user_call_test.cc",
       "validator_test.cc",
diff --git a/src/tint/lang/core/ir/binary.cc b/src/tint/lang/core/ir/binary.cc
index 732f029..2738b01 100644
--- a/src/tint/lang/core/ir/binary.cc
+++ b/src/tint/lang/core/ir/binary.cc
@@ -44,49 +44,4 @@
 
 Binary::~Binary() = default;
 
-Binary* Binary::Clone(CloneContext& ctx) {
-    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);
-}
-
-std::string_view ToString(enum BinaryOp op) {
-    switch (op) {
-        case BinaryOp::kAdd:
-            return "add";
-        case BinaryOp::kSubtract:
-            return "subtract";
-        case BinaryOp::kMultiply:
-            return "multiply";
-        case BinaryOp::kDivide:
-            return "divide";
-        case BinaryOp::kModulo:
-            return "modulo";
-        case BinaryOp::kAnd:
-            return "and";
-        case BinaryOp::kOr:
-            return "or";
-        case BinaryOp::kXor:
-            return "xor";
-        case BinaryOp::kEqual:
-            return "equal";
-        case BinaryOp::kNotEqual:
-            return "not equal";
-        case BinaryOp::kLessThan:
-            return "less than";
-        case BinaryOp::kGreaterThan:
-            return "greater than";
-        case BinaryOp::kLessThanEqual:
-            return "less than equal";
-        case BinaryOp::kGreaterThanEqual:
-            return "greater than equal";
-        case BinaryOp::kShiftLeft:
-            return "shift left";
-        case BinaryOp::kShiftRight:
-            return "shift right";
-    }
-    return "<unknown>";
-}
-
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/binary.h b/src/tint/lang/core/ir/binary.h
index 5f8c26a..378dae4 100644
--- a/src/tint/lang/core/ir/binary.h
+++ b/src/tint/lang/core/ir/binary.h
@@ -30,36 +30,18 @@
 
 #include <string>
 
+#include "src/tint/lang/core/binary_op.h"
 #include "src/tint/lang/core/ir/operand_instruction.h"
-#include "src/tint/utils/rtti/castable.h"
+
+// Forward declarations
+namespace tint::core::intrinsic {
+struct TableData;
+}
 
 namespace tint::core::ir {
 
-/// A binary operator.
-enum class BinaryOp {
-    kAdd,
-    kSubtract,
-    kMultiply,
-    kDivide,
-    kModulo,
-
-    kAnd,
-    kOr,
-    kXor,
-
-    kEqual,
-    kNotEqual,
-    kLessThan,
-    kGreaterThan,
-    kLessThanEqual,
-    kGreaterThanEqual,
-
-    kShiftLeft,
-    kShiftRight
-};
-
-/// A binary instruction in the IR.
-class Binary final : public Castable<Binary, OperandInstruction<2, 1>> {
+/// The abstract base class for dialect-specific binary-op instructions in the IR.
+class Binary : public Castable<Binary, OperandInstruction<2, 1>> {
   public:
     /// The offset in Operands() for the LHS
     static constexpr size_t kLhsOperandOffset = 0;
@@ -78,9 +60,6 @@
     Binary(InstructionResult* result, BinaryOp op, Value* lhs, Value* rhs);
     ~Binary() override;
 
-    /// @copydoc Instruction::Clone()
-    Binary* Clone(CloneContext& ctx) override;
-
     /// @returns the binary operator
     BinaryOp Op() const { return op_; }
 
@@ -102,20 +81,13 @@
     /// @returns the friendly name for the instruction
     std::string FriendlyName() const override { return "binary"; }
 
+    /// @returns the table data to validate this builtin
+    virtual const core::intrinsic::TableData& TableData() const = 0;
+
   private:
     BinaryOp op_ = BinaryOp::kAdd;
 };
 
-/// @param kind the enum value
-/// @returns the string for the given enum value
-std::string_view ToString(BinaryOp kind);
-
-/// Emits the name of the intrinsic type.
-template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, BinaryOp kind) {
-    return out << ToString(kind);
-}
-
 }  // namespace tint::core::ir
 
 #endif  // SRC_TINT_LANG_CORE_IR_BINARY_H_
diff --git a/src/tint/lang/core/ir/binary/BUILD.cmake b/src/tint/lang/core/ir/binary/BUILD.cmake
index 61650da..c749148 100644
--- a/src/tint/lang/core/ir/binary/BUILD.cmake
+++ b/src/tint/lang/core/ir/binary/BUILD.cmake
@@ -130,4 +130,44 @@
   )
 endif(TINT_BUILD_IR_BINARY)
 
+endif(TINT_BUILD_IR_BINARY)
+if(TINT_BUILD_IR_BINARY)
+################################################################################
+# Target:    tint_lang_core_ir_binary_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_IR_BINARY
+################################################################################
+tint_add_target(tint_lang_core_ir_binary_fuzz fuzz
+  lang/core/ir/binary/roundtrip_fuzz.cc
+)
+
+tint_target_add_dependencies(tint_lang_core_ir_binary_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_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_IR_BINARY)
+  tint_target_add_dependencies(tint_lang_core_ir_binary_fuzz fuzz
+    tint_lang_core_ir_binary
+  )
+endif(TINT_BUILD_IR_BINARY)
+
 endif(TINT_BUILD_IR_BINARY)
\ No newline at end of file
diff --git a/src/tint/lang/core/ir/binary/BUILD.gn b/src/tint/lang/core/ir/binary/BUILD.gn
index d0333c8..0e3dd8e 100644
--- a/src/tint/lang/core/ir/binary/BUILD.gn
+++ b/src/tint/lang/core/ir/binary/BUILD.gn
@@ -116,3 +116,34 @@
     }
   }
 }
+if (tint_build_ir_binary) {
+  tint_fuzz_source_set("fuzz") {
+    sources = [ "roundtrip_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}/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_ir_binary) {
+      deps += [ "${tint_src_dir}/lang/core/ir/binary" ]
+    }
+  }
+}
diff --git a/src/tint/lang/core/ir/binary/decode.cc b/src/tint/lang/core/ir/binary/decode.cc
index fb77a96..8b3d75d 100644
--- a/src/tint/lang/core/ir/binary/decode.cc
+++ b/src/tint/lang/core/ir/binary/decode.cc
@@ -347,8 +347,8 @@
         return mod_out_.instructions.Create<ir::Access>();
     }
 
-    ir::Binary* CreateInstructionBinary(const pb::InstructionBinary& binary_in) {
-        auto* binary_out = mod_out_.instructions.Create<ir::Binary>();
+    ir::CoreBinary* CreateInstructionBinary(const pb::InstructionBinary& binary_in) {
+        auto* binary_out = mod_out_.instructions.Create<ir::CoreBinary>();
         binary_out->SetOp(BinaryOp(binary_in.op()));
         return binary_out;
     }
@@ -494,8 +494,8 @@
         return switch_out;
     }
 
-    ir::Unary* CreateInstructionUnary(const pb::InstructionUnary& unary_in) {
-        auto* unary_out = mod_out_.instructions.Create<ir::Unary>();
+    ir::CoreUnary* CreateInstructionUnary(const pb::InstructionUnary& unary_in) {
+        auto* unary_out = mod_out_.instructions.Create<ir::CoreUnary>();
         unary_out->SetOp(UnaryOp(unary_in.op()));
         return unary_out;
     }
@@ -894,57 +894,63 @@
         }
     }
 
-    core::ir::UnaryOp UnaryOp(pb::UnaryOp in) {
+    core::UnaryOp UnaryOp(pb::UnaryOp in) {
         switch (in) {
             case pb::UnaryOp::complement:
-                return core::ir::UnaryOp::kComplement;
+                return core::UnaryOp::kComplement;
             case pb::UnaryOp::negation:
-                return core::ir::UnaryOp::kNegation;
+                return core::UnaryOp::kNegation;
+            case pb::UnaryOp::address_of:
+                return core::UnaryOp::kAddressOf;
+            case pb::UnaryOp::indirection:
+                return core::UnaryOp::kIndirection;
+            case pb::UnaryOp::not_:
+                return core::UnaryOp::kNot;
 
             default:
                 TINT_ICE() << "invalid UnaryOp: " << in;
-                return core::ir::UnaryOp::kComplement;
+                return core::UnaryOp::kComplement;
         }
     }
 
-    core::ir::BinaryOp BinaryOp(pb::BinaryOp in) {
+    core::BinaryOp BinaryOp(pb::BinaryOp in) {
         switch (in) {
             case pb::BinaryOp::add_:
-                return core::ir::BinaryOp::kAdd;
+                return core::BinaryOp::kAdd;
             case pb::BinaryOp::subtract:
-                return core::ir::BinaryOp::kSubtract;
+                return core::BinaryOp::kSubtract;
             case pb::BinaryOp::multiply:
-                return core::ir::BinaryOp::kMultiply;
+                return core::BinaryOp::kMultiply;
             case pb::BinaryOp::divide:
-                return core::ir::BinaryOp::kDivide;
+                return core::BinaryOp::kDivide;
             case pb::BinaryOp::modulo:
-                return core::ir::BinaryOp::kModulo;
+                return core::BinaryOp::kModulo;
             case pb::BinaryOp::and_:
-                return core::ir::BinaryOp::kAnd;
+                return core::BinaryOp::kAnd;
             case pb::BinaryOp::or_:
-                return core::ir::BinaryOp::kOr;
+                return core::BinaryOp::kOr;
             case pb::BinaryOp::xor_:
-                return core::ir::BinaryOp::kXor;
+                return core::BinaryOp::kXor;
             case pb::BinaryOp::equal:
-                return core::ir::BinaryOp::kEqual;
+                return core::BinaryOp::kEqual;
             case pb::BinaryOp::not_equal:
-                return core::ir::BinaryOp::kNotEqual;
+                return core::BinaryOp::kNotEqual;
             case pb::BinaryOp::less_than:
-                return core::ir::BinaryOp::kLessThan;
+                return core::BinaryOp::kLessThan;
             case pb::BinaryOp::greater_than:
-                return core::ir::BinaryOp::kGreaterThan;
+                return core::BinaryOp::kGreaterThan;
             case pb::BinaryOp::less_than_equal:
-                return core::ir::BinaryOp::kLessThanEqual;
+                return core::BinaryOp::kLessThanEqual;
             case pb::BinaryOp::greater_than_equal:
-                return core::ir::BinaryOp::kGreaterThanEqual;
+                return core::BinaryOp::kGreaterThanEqual;
             case pb::BinaryOp::shift_left:
-                return core::ir::BinaryOp::kShiftLeft;
+                return core::BinaryOp::kShiftLeft;
             case pb::BinaryOp::shift_right:
-                return core::ir::BinaryOp::kShiftRight;
+                return core::BinaryOp::kShiftRight;
 
             default:
                 TINT_ICE() << "invalid BinaryOp: " << in;
-                return core::ir::BinaryOp::kAdd;
+                return core::BinaryOp::kAdd;
         }
     }
 
diff --git a/src/tint/lang/core/ir/binary/encode.cc b/src/tint/lang/core/ir/binary/encode.cc
index fa72a55..6f57223 100644
--- a/src/tint/lang/core/ir/binary/encode.cc
+++ b/src/tint/lang/core/ir/binary/encode.cc
@@ -35,13 +35,14 @@
 #include "src/tint/lang/core/constant/scalar.h"
 #include "src/tint/lang/core/constant/splat.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/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_binary.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/core_unary.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"
@@ -60,7 +61,6 @@
 #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/unary.h"
 #include "src/tint/lang/core/ir/unreachable.h"
 #include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/var.h"
@@ -198,12 +198,13 @@
         tint::Switch(
             inst_in,  //
             [&](const ir::Access* i) { InstructionAccess(*inst_out.mutable_access(), i); },
-            [&](const ir::Binary* i) { InstructionBinary(*inst_out.mutable_binary(), i); },
             [&](const ir::Bitcast* i) { InstructionBitcast(*inst_out.mutable_bitcast(), i); },
             [&](const ir::BreakIf* i) { InstructionBreakIf(*inst_out.mutable_break_if(), i); },
+            [&](const ir::CoreBinary* i) { InstructionBinary(*inst_out.mutable_binary(), i); },
             [&](const ir::CoreBuiltinCall* i) {
                 InstructionBuiltinCall(*inst_out.mutable_builtin_call(), i);
             },
+            [&](const ir::CoreUnary* i) { InstructionUnary(*inst_out.mutable_unary(), i); },
             [&](const ir::Construct* i) { InstructionConstruct(*inst_out.mutable_construct(), i); },
             [&](const ir::Continue* i) { InstructionContinue(*inst_out.mutable_continue_(), i); },
             [&](const ir::Convert* i) { InstructionConvert(*inst_out.mutable_convert(), i); },
@@ -230,7 +231,6 @@
             },
             [&](const ir::Switch* i) { InstructionSwitch(*inst_out.mutable_switch_(), i); },
             [&](const ir::Swizzle* i) { InstructionSwizzle(*inst_out.mutable_swizzle(), i); },
-            [&](const ir::Unary* i) { InstructionUnary(*inst_out.mutable_unary(), i); },
             [&](const ir::UserCall* i) { InstructionUserCall(*inst_out.mutable_user_call(), i); },
             [&](const ir::Var* i) { InstructionVar(*inst_out.mutable_var(), i); },
             [&](const ir::Unreachable* i) {
@@ -247,7 +247,7 @@
 
     void InstructionAccess(pb::InstructionAccess&, const ir::Access*) {}
 
-    void InstructionBinary(pb::InstructionBinary& binary_out, const ir::Binary* binary_in) {
+    void InstructionBinary(pb::InstructionBinary& binary_out, const ir::CoreBinary* binary_in) {
         binary_out.set_op(BinaryOp(binary_in->Op()));
     }
 
@@ -329,7 +329,7 @@
         }
     }
 
-    void InstructionUnary(pb::InstructionUnary& unary_out, const ir::Unary* unary_in) {
+    void InstructionUnary(pb::InstructionUnary& unary_out, const ir::CoreUnary* unary_in) {
         unary_out.set_op(UnaryOp(unary_in->Op()));
     }
 
@@ -675,51 +675,61 @@
         }
     }
 
-    pb::UnaryOp UnaryOp(core::ir::UnaryOp in) {
+    pb::UnaryOp UnaryOp(core::UnaryOp in) {
         switch (in) {
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 return pb::UnaryOp::complement;
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 return pb::UnaryOp::negation;
+            case core::UnaryOp::kAddressOf:
+                return pb::UnaryOp::address_of;
+            case core::UnaryOp::kIndirection:
+                return pb::UnaryOp::indirection;
+            case core::UnaryOp::kNot:
+                return pb::UnaryOp::not_;
         }
         TINT_ICE() << "invalid UnaryOp: " << in;
         return pb::UnaryOp::complement;
     }
 
-    pb::BinaryOp BinaryOp(core::ir::BinaryOp in) {
+    pb::BinaryOp BinaryOp(core::BinaryOp in) {
         switch (in) {
-            case core::ir::BinaryOp::kAdd:
+            case core::BinaryOp::kAdd:
                 return pb::BinaryOp::add_;
-            case core::ir::BinaryOp::kSubtract:
+            case core::BinaryOp::kSubtract:
                 return pb::BinaryOp::subtract;
-            case core::ir::BinaryOp::kMultiply:
+            case core::BinaryOp::kMultiply:
                 return pb::BinaryOp::multiply;
-            case core::ir::BinaryOp::kDivide:
+            case core::BinaryOp::kDivide:
                 return pb::BinaryOp::divide;
-            case core::ir::BinaryOp::kModulo:
+            case core::BinaryOp::kModulo:
                 return pb::BinaryOp::modulo;
-            case core::ir::BinaryOp::kAnd:
+            case core::BinaryOp::kAnd:
                 return pb::BinaryOp::and_;
-            case core::ir::BinaryOp::kOr:
+            case core::BinaryOp::kOr:
                 return pb::BinaryOp::or_;
-            case core::ir::BinaryOp::kXor:
+            case core::BinaryOp::kXor:
                 return pb::BinaryOp::xor_;
-            case core::ir::BinaryOp::kEqual:
+            case core::BinaryOp::kEqual:
                 return pb::BinaryOp::equal;
-            case core::ir::BinaryOp::kNotEqual:
+            case core::BinaryOp::kNotEqual:
                 return pb::BinaryOp::not_equal;
-            case core::ir::BinaryOp::kLessThan:
+            case core::BinaryOp::kLessThan:
                 return pb::BinaryOp::less_than;
-            case core::ir::BinaryOp::kGreaterThan:
+            case core::BinaryOp::kGreaterThan:
                 return pb::BinaryOp::greater_than;
-            case core::ir::BinaryOp::kLessThanEqual:
+            case core::BinaryOp::kLessThanEqual:
                 return pb::BinaryOp::less_than_equal;
-            case core::ir::BinaryOp::kGreaterThanEqual:
+            case core::BinaryOp::kGreaterThanEqual:
                 return pb::BinaryOp::greater_than_equal;
-            case core::ir::BinaryOp::kShiftLeft:
+            case core::BinaryOp::kShiftLeft:
                 return pb::BinaryOp::shift_left;
-            case core::ir::BinaryOp::kShiftRight:
+            case core::BinaryOp::kShiftRight:
                 return pb::BinaryOp::shift_right;
+            case core::BinaryOp::kLogicalAnd:
+                return pb::BinaryOp::logical_and;
+            case core::BinaryOp::kLogicalOr:
+                return pb::BinaryOp::logical_or;
         }
 
         TINT_ICE() << "invalid BinaryOp: " << in;
diff --git a/src/tint/lang/core/ir/binary/ir.proto b/src/tint/lang/core/ir/binary/ir.proto
index 8eb5c20..20933ce 100644
--- a/src/tint/lang/core/ir/binary/ir.proto
+++ b/src/tint/lang/core/ir/binary/ir.proto
@@ -413,6 +413,9 @@
 enum UnaryOp {
     complement = 0;
     negation = 1;
+    address_of = 2;
+    indirection = 3;
+    not = 4;
 }
 
 enum BinaryOp {
@@ -432,6 +435,8 @@
     greater_than_equal = 13;
     shift_left = 14;
     shift_right = 15;
+    logical_and = 16;
+    logical_or = 17;
 }
 
 enum TextureDimension {
diff --git a/src/tint/lang/core/ir/binary/roundtrip_fuzz.cc b/src/tint/lang/core/ir/binary/roundtrip_fuzz.cc
new file mode 100644
index 0000000..32886de
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/roundtrip_fuzz.cc
@@ -0,0 +1,69 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+#include "src/tint/lang/core/ir/binary/decode.h"
+#include "src/tint/lang/core/ir/binary/encode.h"
+#include "src/tint/lang/core/ir/disassembler.h"
+
+namespace tint::core::ir::binary {
+namespace {
+
+void IRBinaryRoundtripFuzzer(core::ir::Module& module) {
+    auto encoded = Encode(module);
+    if (encoded != Success) {
+        TINT_ICE() << "Encode() failed\n" << encoded.Failure();
+        return;
+    }
+
+    auto decoded = Decode(encoded->Slice());
+    if (decoded != Success) {
+        TINT_ICE() << "Decode() failed\n" << decoded.Failure();
+        return;
+    }
+
+    auto in = Disassemble(module);
+    auto out = Disassemble(decoded.Get());
+    if (in != out) {
+        TINT_ICE() << "Roundtrip produced different disassembly\n"
+                   << "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"
+                   << "-=                     In                      =-\n"
+                   << "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"
+                   << in << "\n"
+                   << "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"
+                   << "-=                     Out                     =-\n"
+                   << "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"
+                   << out << "\n"
+                   << "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n";
+        return;
+    }
+}
+
+}  // namespace
+}  // namespace tint::core::ir::binary
+
+TINT_IR_MODULE_FUZZER(tint::core::ir::binary::IRBinaryRoundtripFuzzer);
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index fc847c7..dd44c4f 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -34,7 +34,6 @@
 #include "src/tint/lang/core/constant/scalar.h"
 #include "src/tint/lang/core/constant/splat.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/block_param.h"
 #include "src/tint/lang/core/ir/break_if.h"
@@ -42,7 +41,9 @@
 #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_binary.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/core_unary.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"
@@ -64,7 +65,6 @@
 #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/unary.h"
 #include "src/tint/lang/core/ir/unreachable.h"
 #include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/value.h"
@@ -472,12 +472,12 @@
     /// @param rhs the right-hand-side of the operation
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Binary(BinaryOp op, const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Binary(BinaryOp op, const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         CheckForNonDeterministicEvaluation<LHS, RHS>();
         auto* lhs_val = Value(std::forward<LHS>(lhs));
         auto* rhs_val = Value(std::forward<RHS>(rhs));
         return Append(
-            ir.instructions.Create<ir::Binary>(InstructionResult(type), op, lhs_val, rhs_val));
+            ir.instructions.Create<ir::CoreBinary>(InstructionResult(type), op, lhs_val, rhs_val));
     }
 
     /// Creates an And operation
@@ -486,7 +486,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* And(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* And(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kAnd, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -496,7 +496,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* And(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* And(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return And(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -507,7 +507,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Or(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Or(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kOr, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -517,7 +517,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Or(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Or(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Or(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -528,7 +528,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Xor(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Xor(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kXor, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -538,7 +538,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Xor(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Xor(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Xor(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -549,7 +549,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Equal(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Equal(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kEqual, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -559,7 +559,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Equal(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Equal(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Equal(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -570,7 +570,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* NotEqual(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* NotEqual(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kNotEqual, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -580,7 +580,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* NotEqual(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* NotEqual(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return NotEqual(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -591,7 +591,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* LessThan(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* LessThan(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kLessThan, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -601,7 +601,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* LessThan(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* LessThan(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return LessThan(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -612,7 +612,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* GreaterThan(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* GreaterThan(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kGreaterThan, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -622,7 +622,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* GreaterThan(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* GreaterThan(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return GreaterThan(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -633,7 +633,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* LessThanEqual(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* LessThanEqual(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kLessThanEqual, type, std::forward<LHS>(lhs),
                       std::forward<RHS>(rhs));
     }
@@ -644,7 +644,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* LessThanEqual(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* LessThanEqual(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return LessThanEqual(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -655,7 +655,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* GreaterThanEqual(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* GreaterThanEqual(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kGreaterThanEqual, type, std::forward<LHS>(lhs),
                       std::forward<RHS>(rhs));
     }
@@ -666,7 +666,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* GreaterThanEqual(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* GreaterThanEqual(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return GreaterThanEqual(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -677,7 +677,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* ShiftLeft(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* ShiftLeft(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kShiftLeft, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -687,7 +687,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* ShiftLeft(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* ShiftLeft(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return ShiftLeft(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -698,7 +698,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* ShiftRight(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* ShiftRight(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kShiftRight, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -708,7 +708,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* ShiftRight(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* ShiftRight(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return ShiftRight(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -719,7 +719,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Add(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Add(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kAdd, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -729,7 +729,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Add(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Add(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Add(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -740,7 +740,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Subtract(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Subtract(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kSubtract, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -750,7 +750,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Subtract(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Subtract(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Subtract(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -761,7 +761,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Multiply(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Multiply(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kMultiply, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -771,7 +771,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Multiply(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Multiply(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Multiply(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -782,7 +782,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Divide(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Divide(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kDivide, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -792,7 +792,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Divide(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Divide(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Divide(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -803,7 +803,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename LHS, typename RHS>
-    ir::Binary* Modulo(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Modulo(const core::type::Type* type, LHS&& lhs, RHS&& rhs) {
         return Binary(BinaryOp::kModulo, type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
 
@@ -813,7 +813,7 @@
     /// @param rhs the rhs of the add
     /// @returns the operation
     template <typename TYPE, typename LHS, typename RHS>
-    ir::Binary* Modulo(LHS&& lhs, RHS&& rhs) {
+    ir::CoreBinary* Modulo(LHS&& lhs, RHS&& rhs) {
         auto* type = ir.Types().Get<TYPE>();
         return Modulo(type, std::forward<LHS>(lhs), std::forward<RHS>(rhs));
     }
@@ -824,9 +824,9 @@
     /// @param val the value of the operation
     /// @returns the operation
     template <typename VAL>
-    ir::Unary* Unary(UnaryOp op, const core::type::Type* type, VAL&& val) {
+    ir::CoreUnary* Unary(UnaryOp op, const core::type::Type* type, VAL&& val) {
         auto* value = Value(std::forward<VAL>(val));
-        return Append(ir.instructions.Create<ir::Unary>(InstructionResult(type), op, value));
+        return Append(ir.instructions.Create<ir::CoreUnary>(InstructionResult(type), op, value));
     }
 
     /// Creates an op for `op val`
@@ -835,7 +835,7 @@
     /// @param val the value of the operation
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Unary* Unary(UnaryOp op, VAL&& val) {
+    ir::CoreUnary* Unary(UnaryOp op, VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Unary(op, type, std::forward<VAL>(val));
     }
@@ -845,8 +845,8 @@
     /// @param val the value
     /// @returns the operation
     template <typename VAL>
-    ir::Unary* Complement(const core::type::Type* type, VAL&& val) {
-        return Unary(ir::UnaryOp::kComplement, type, std::forward<VAL>(val));
+    ir::CoreUnary* Complement(const core::type::Type* type, VAL&& val) {
+        return Unary(UnaryOp::kComplement, type, std::forward<VAL>(val));
     }
 
     /// Creates a Complement operation
@@ -854,7 +854,7 @@
     /// @param val the value
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Unary* Complement(VAL&& val) {
+    ir::CoreUnary* Complement(VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Complement(type, std::forward<VAL>(val));
     }
@@ -864,8 +864,8 @@
     /// @param val the value
     /// @returns the operation
     template <typename VAL>
-    ir::Unary* Negation(const core::type::Type* type, VAL&& val) {
-        return Unary(ir::UnaryOp::kNegation, type, std::forward<VAL>(val));
+    ir::CoreUnary* Negation(const core::type::Type* type, VAL&& val) {
+        return Unary(UnaryOp::kNegation, type, std::forward<VAL>(val));
     }
 
     /// Creates a Negation operation
@@ -873,7 +873,7 @@
     /// @param val the value
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Unary* Negation(VAL&& val) {
+    ir::CoreUnary* Negation(VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Negation(type, std::forward<VAL>(val));
     }
@@ -883,7 +883,7 @@
     /// @param val the value
     /// @returns the operation
     template <typename VAL>
-    ir::Binary* Not(const core::type::Type* type, VAL&& val) {
+    ir::CoreBinary* Not(const core::type::Type* type, VAL&& val) {
         if (auto* vec = type->As<core::type::Vector>()) {
             return Equal(type, std::forward<VAL>(val), Splat(vec, false, vec->Width()));
         } else {
@@ -896,7 +896,7 @@
     /// @param val the value
     /// @returns the operation
     template <typename TYPE, typename VAL>
-    ir::Binary* Not(VAL&& val) {
+    ir::CoreBinary* Not(VAL&& val) {
         auto* type = ir.Types().Get<TYPE>();
         return Not(type, std::forward<VAL>(val));
     }
@@ -1297,6 +1297,15 @@
         return FunctionParam(name, type);
     }
 
+    /// Creates a new `FunctionParam`
+    /// @tparam TYPE the parameter type
+    /// @returns the value
+    template <typename TYPE>
+    ir::FunctionParam* FunctionParam() {
+        auto* type = ir.Types().Get<TYPE>();
+        return FunctionParam(type);
+    }
+
     /// Creates a new `Access`
     /// @param type the return type
     /// @param object the object being accessed
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/core/ir/core_binary.cc
similarity index 62%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/core/ir/core_binary.cc
index 421db7b..0bab152 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/core/ir/core_binary.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +25,32 @@
 // 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/writer.h"
+#include "src/tint/lang/core/ir/core_binary.h"
 
-#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/lang/core/intrinsic/dialect.h"
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 
-namespace tint::spirv::writer {
-namespace {
+TINT_INSTANTIATE_TYPEINFO(tint::core::ir::CoreBinary);
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
-        return;
-    }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
-    }
+namespace tint::core::ir {
+
+CoreBinary::CoreBinary() = default;
+
+CoreBinary::CoreBinary(InstructionResult* result, BinaryOp op, Value* lhs, Value* rhs)
+    : Base(result, op, lhs, rhs) {}
+
+CoreBinary::~CoreBinary() = default;
+
+CoreBinary* CoreBinary::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result(0));
+    auto* lhs = ctx.Remap(LHS());
+    auto* rhs = ctx.Remap(RHS());
+    return ctx.ir.instructions.Create<CoreBinary>(new_result, Op(), lhs, rhs);
 }
 
-}  // namespace
-}  // namespace tint::spirv::writer
+const core::intrinsic::TableData& CoreBinary::TableData() const {
+    return core::intrinsic::Dialect::kData;
+}
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/core_binary.h b/src/tint/lang/core/ir/core_binary.h
new file mode 100644
index 0000000..7329ce6
--- /dev/null
+++ b/src/tint/lang/core/ir/core_binary.h
@@ -0,0 +1,61 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_CORE_BINARY_H_
+#define SRC_TINT_LANG_CORE_IR_CORE_BINARY_H_
+
+#include "src/tint/lang/core/ir/binary.h"
+
+namespace tint::core::ir {
+
+/// A core-dialect binary-op instruction in the IR.
+class CoreBinary final : public Castable<CoreBinary, Binary> {
+  public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
+    /// Constructor (no results, no operands)
+    CoreBinary();
+
+    /// Constructor
+    /// @param result the result value
+    /// @param op the Binary operator
+    /// @param lhs the lhs of the instruction
+    /// @param rhs the rhs of the instruction
+    CoreBinary(InstructionResult* result, BinaryOp op, Value* lhs, Value* rhs);
+    ~CoreBinary() override;
+
+    /// @copydoc Instruction::Clone()
+    CoreBinary* Clone(CloneContext& ctx) override;
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() const override;
+};
+
+}  // namespace tint::core::ir
+
+#endif  // SRC_TINT_LANG_CORE_IR_CORE_BINARY_H_
diff --git a/src/tint/lang/core/ir/binary_test.cc b/src/tint/lang/core/ir/core_binary_test.cc
similarity index 100%
rename from src/tint/lang/core/ir/binary_test.cc
rename to src/tint/lang/core/ir/core_binary_test.cc
diff --git a/src/tint/lang/core/ir/core_builtin_call.cc b/src/tint/lang/core/ir/core_builtin_call.cc
index bb876cf..b8114ba 100644
--- a/src/tint/lang/core/ir/core_builtin_call.cc
+++ b/src/tint/lang/core/ir/core_builtin_call.cc
@@ -44,7 +44,6 @@
                                  VectorRef<Value*> arguments)
     : Base(result, arguments), func_(func) {
     TINT_ASSERT(func != core::BuiltinFn::kNone);
-    TINT_ASSERT(func != core::BuiltinFn::kTintMaterialize);
 }
 
 CoreBuiltinCall::~CoreBuiltinCall() = default;
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 05965f8..29c5913 100644
--- a/src/tint/lang/core/ir/core_builtin_call_test.cc
+++ b/src/tint/lang/core/ir/core_builtin_call_test.cc
@@ -75,16 +75,6 @@
         "");
 }
 
-TEST_F(IR_CoreBuiltinCallTest, Fail_TintMaterializeFunction) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Module mod;
-            Builder b{mod};
-            b.Call(mod.Types().f32(), core::BuiltinFn::kTintMaterialize);
-        },
-        "");
-}
-
 TEST_F(IR_CoreBuiltinCallTest, Clone) {
     auto* builtin = b.Call(mod.Types().f32(), core::BuiltinFn::kAbs, 1_u, 2_u);
 
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/core/ir/core_unary.cc
similarity index 62%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/core/ir/core_unary.cc
index 421db7b..5ab130d 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/core/ir/core_unary.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +25,30 @@
 // 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/writer.h"
+#include "src/tint/lang/core/ir/core_unary.h"
 
-#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/lang/core/intrinsic/dialect.h"
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 
-namespace tint::spirv::writer {
-namespace {
+TINT_INSTANTIATE_TYPEINFO(tint::core::ir::CoreUnary);
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
-        return;
-    }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
-    }
+namespace tint::core::ir {
+
+CoreUnary::CoreUnary() = default;
+
+CoreUnary::CoreUnary(InstructionResult* result, UnaryOp op, Value* val) : Base(result, op, val) {}
+
+CoreUnary::~CoreUnary() = default;
+
+CoreUnary* CoreUnary::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result(0));
+    auto* val = ctx.Remap(Val());
+    return ctx.ir.instructions.Create<CoreUnary>(new_result, Op(), val);
 }
 
-}  // namespace
-}  // namespace tint::spirv::writer
+const core::intrinsic::TableData& CoreUnary::TableData() const {
+    return core::intrinsic::Dialect::kData;
+}
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/core_unary.h b/src/tint/lang/core/ir/core_unary.h
new file mode 100644
index 0000000..04a02f4
--- /dev/null
+++ b/src/tint/lang/core/ir/core_unary.h
@@ -0,0 +1,60 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_CORE_UNARY_H_
+#define SRC_TINT_LANG_CORE_IR_CORE_UNARY_H_
+
+#include "src/tint/lang/core/ir/unary.h"
+
+namespace tint::core::ir {
+
+/// A core-dialect unary instruction in the IR.
+class CoreUnary final : public Castable<CoreUnary, Unary> {
+  public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
+    /// Constructor (no results, no operands)
+    CoreUnary();
+
+    /// Constructor
+    /// @param result the result value
+    /// @param op the unary operator
+    /// @param val the input value for the instruction
+    CoreUnary(InstructionResult* result, UnaryOp op, Value* val);
+    ~CoreUnary() override;
+
+    /// @copydoc Instruction::Clone()
+    CoreUnary* Clone(CloneContext& ctx) override;
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() const override;
+};
+
+}  // namespace tint::core::ir
+
+#endif  // SRC_TINT_LANG_CORE_IR_CORE_UNARY_H_
diff --git a/src/tint/lang/core/ir/unary_test.cc b/src/tint/lang/core/ir/core_unary_test.cc
similarity index 100%
rename from src/tint/lang/core/ir/unary_test.cc
rename to src/tint/lang/core/ir/core_unary_test.cc
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 9292f50..cfa2e30 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -852,6 +852,12 @@
         case BinaryOp::kShiftRight:
             out_ << "shr";
             break;
+        case BinaryOp::kLogicalAnd:
+            out_ << "logical-and";
+            break;
+        case BinaryOp::kLogicalOr:
+            out_ << "logical-or";
+            break;
     }
     out_ << " ";
     EmitOperandList(b);
@@ -870,6 +876,15 @@
         case UnaryOp::kNegation:
             out_ << "negation";
             break;
+        case UnaryOp::kAddressOf:
+            out_ << "ref-to-ptr";
+            break;
+        case UnaryOp::kIndirection:
+            out_ << "ptr-to-ref";
+            break;
+        case UnaryOp::kNot:
+            out_ << "not";
+            break;
     }
     out_ << " ";
     EmitOperandList(u);
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index b75b761..467e451 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -151,6 +151,7 @@
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/lang/wgsl/writer/ir_to_program",
+    "//src/tint/lang/wgsl/writer/raise",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index 76a01e3..01120f4 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -141,6 +141,7 @@
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_lang_wgsl_writer_ir_to_program
+  tint_lang_wgsl_writer_raise
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index fbb325a..56910e6 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -143,6 +143,7 @@
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
+      "${tint_src_dir}/lang/wgsl/writer/raise",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
diff --git a/src/tint/lang/core/ir/transform/binary_polyfill.cc b/src/tint/lang/core/ir/transform/binary_polyfill.cc
index 14c6756..83fb233 100644
--- a/src/tint/lang/core/ir/transform/binary_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/binary_polyfill.cc
@@ -66,12 +66,12 @@
     /// Process the module.
     void Process() {
         // Find the binary instructions that need to be polyfilled.
-        Vector<ir::Binary*, 64> worklist;
+        Vector<ir::CoreBinary*, 64> worklist;
         for (auto* inst : ir.instructions.Objects()) {
             if (!inst->Alive()) {
                 continue;
             }
-            if (auto* binary = inst->As<ir::Binary>()) {
+            if (auto* binary = inst->As<ir::CoreBinary>()) {
                 switch (binary->Op()) {
                     case BinaryOp::kDivide:
                     case BinaryOp::kModulo:
@@ -149,7 +149,7 @@
     /// divide-by-zero and signed integer overflow.
     /// @param binary the binary instruction
     /// @returns the replacement value
-    ir::Value* IntDivMod(ir::Binary* binary) {
+    ir::Value* IntDivMod(ir::CoreBinary* binary) {
         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();
@@ -232,13 +232,13 @@
     /// Mask the RHS of a shift instruction to ensure it is modulo the bitwidth of the LHS.
     /// @param binary the binary instruction
     /// @returns the replacement value
-    ir::Value* MaskShiftAmount(ir::Binary* binary) {
+    ir::Value* MaskShiftAmount(ir::CoreBinary* binary) {
         auto* lhs = binary->LHS();
         auto* rhs = binary->RHS();
         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(0));
+        binary->SetOperand(ir::CoreBinary::kRhsOperandOffset, masked->Result(0));
         return binary->Result(0);
     }
 };
diff --git a/src/tint/lang/core/ir/transform/binary_polyfill_test.cc b/src/tint/lang/core/ir/transform/binary_polyfill_test.cc
index b582411..314ac0c 100644
--- a/src/tint/lang/core/ir/transform/binary_polyfill_test.cc
+++ b/src/tint/lang/core/ir/transform/binary_polyfill_test.cc
@@ -671,9 +671,9 @@
 }
 
 TEST_F(IR_BinaryPolyfillTest, Divide_Scalar_Vector) {
-    Build(BinaryOp::kDivide, ty.vec4<i32>(), ty.i32(), ty.vec2<i32>());
+    Build(BinaryOp::kDivide, ty.vec4<i32>(), ty.i32(), ty.vec4<i32>());
     auto* src = R"(
-%foo = func(%lhs:i32, %rhs:vec2<i32>):vec4<i32> -> %b1 {
+%foo = func(%lhs:i32, %rhs:vec4<i32>):vec4<i32> -> %b1 {
   %b1 = block {
     %result:vec4<i32> = div %lhs, %rhs
     ret %result
@@ -681,7 +681,7 @@
 }
 )";
     auto* expect = R"(
-%foo = func(%lhs:i32, %rhs:vec2<i32>):vec4<i32> -> %b1 {
+%foo = func(%lhs:i32, %rhs:vec4<i32>):vec4<i32> -> %b1 {
   %b1 = block {
     %4:vec4<i32> = construct %lhs
     %result:vec4<i32> = call %tint_div_v4i32, %4, %rhs
@@ -711,9 +711,9 @@
 }
 
 TEST_F(IR_BinaryPolyfillTest, Divide_Vector_Scalar) {
-    Build(BinaryOp::kDivide, ty.vec4<i32>(), ty.vec2<i32>(), ty.i32());
+    Build(BinaryOp::kDivide, ty.vec4<i32>(), ty.vec4<i32>(), ty.i32());
     auto* src = R"(
-%foo = func(%lhs:vec2<i32>, %rhs:i32):vec4<i32> -> %b1 {
+%foo = func(%lhs:vec4<i32>, %rhs:i32):vec4<i32> -> %b1 {
   %b1 = block {
     %result:vec4<i32> = div %lhs, %rhs
     ret %result
@@ -721,7 +721,7 @@
 }
 )";
     auto* expect = R"(
-%foo = func(%lhs:vec2<i32>, %rhs:i32):vec4<i32> -> %b1 {
+%foo = func(%lhs:vec4<i32>, %rhs:i32):vec4<i32> -> %b1 {
   %b1 = block {
     %4:vec4<i32> = construct %rhs
     %result:vec4<i32> = call %tint_div_v4i32, %lhs, %4
@@ -751,9 +751,9 @@
 }
 
 TEST_F(IR_BinaryPolyfillTest, Modulo_Scalar_Vector) {
-    Build(BinaryOp::kModulo, ty.vec4<i32>(), ty.i32(), ty.vec2<i32>());
+    Build(BinaryOp::kModulo, ty.vec4<i32>(), ty.i32(), ty.vec4<i32>());
     auto* src = R"(
-%foo = func(%lhs:i32, %rhs:vec2<i32>):vec4<i32> -> %b1 {
+%foo = func(%lhs:i32, %rhs:vec4<i32>):vec4<i32> -> %b1 {
   %b1 = block {
     %result:vec4<i32> = mod %lhs, %rhs
     ret %result
@@ -761,7 +761,7 @@
 }
 )";
     auto* expect = R"(
-%foo = func(%lhs:i32, %rhs:vec2<i32>):vec4<i32> -> %b1 {
+%foo = func(%lhs:i32, %rhs:vec4<i32>):vec4<i32> -> %b1 {
   %b1 = block {
     %4:vec4<i32> = construct %lhs
     %result:vec4<i32> = call %tint_mod_v4i32, %4, %rhs
@@ -793,9 +793,9 @@
 }
 
 TEST_F(IR_BinaryPolyfillTest, Modulo_Vector_Scalar) {
-    Build(BinaryOp::kModulo, ty.vec4<i32>(), ty.vec2<i32>(), ty.i32());
+    Build(BinaryOp::kModulo, ty.vec4<i32>(), ty.vec4<i32>(), ty.i32());
     auto* src = R"(
-%foo = func(%lhs:vec2<i32>, %rhs:i32):vec4<i32> -> %b1 {
+%foo = func(%lhs:vec4<i32>, %rhs:i32):vec4<i32> -> %b1 {
   %b1 = block {
     %result:vec4<i32> = mod %lhs, %rhs
     ret %result
@@ -803,7 +803,7 @@
 }
 )";
     auto* expect = R"(
-%foo = func(%lhs:vec2<i32>, %rhs:i32):vec4<i32> -> %b1 {
+%foo = func(%lhs:vec4<i32>, %rhs:i32):vec4<i32> -> %b1 {
   %b1 = block {
     %4:vec4<i32> = construct %rhs
     %result:vec4<i32> = call %tint_mod_v4i32, %lhs, %4
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill.cc b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
index f321fe1..4ff5cbf 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
@@ -119,6 +119,13 @@
                             }
                         }
                         break;
+                    case core::BuiltinFn::kDot4U8Packed:
+                    case core::BuiltinFn::kDot4I8Packed: {
+                        if (config.dot_4x8_packed) {
+                            worklist.Push(builtin);
+                        }
+                        break;
+                    }
                     case core::BuiltinFn::kPack4XI8:
                     case core::BuiltinFn::kPack4XU8:
                     case core::BuiltinFn::kPack4XI8Clamp:
@@ -167,6 +174,12 @@
                 case core::BuiltinFn::kTextureSampleBaseClampToEdge:
                     replacement = TextureSampleBaseClampToEdge_2d_f32(builtin);
                     break;
+                case core::BuiltinFn::kDot4I8Packed:
+                    replacement = Dot4I8Packed(builtin);
+                    break;
+                case core::BuiltinFn::kDot4U8Packed:
+                    replacement = Dot4U8Packed(builtin);
+                    break;
                 case core::BuiltinFn::kPack4XI8:
                     replacement = Pack4xI8(builtin);
                     break;
@@ -601,6 +614,44 @@
         return result;
     }
 
+    /// Polyfill a `dot4I8Packed()` builtin call
+    /// @param call the builtin call instruction
+    /// @returns the replacement value
+    ir::Value* Dot4I8Packed(ir::CoreBuiltinCall* call) {
+        // Replace `dot4I8Packed(%x,%y)` with:
+        //   %unpacked_x = unpack4xI8(%x);
+        //   %unpacked_y = unpack4xI8(%y);
+        //   %result = dot(%unpacked_x, %unpacked_y);
+        auto* x = call->Args()[0];
+        auto* y = call->Args()[1];
+        auto* unpacked_x = Unpack4xI8OnValue(call, x);
+        auto* unpacked_y = Unpack4xI8OnValue(call, y);
+        ir::Value* result = nullptr;
+        b.InsertBefore(call, [&] {
+            result = b.Call(ty.i32(), core::BuiltinFn::kDot, unpacked_x, unpacked_y)->Result(0);
+        });
+        return result;
+    }
+
+    /// Polyfill a `dot4U8Packed()` builtin call
+    /// @param call the builtin call instruction
+    /// @returns the replacement value
+    ir::Value* Dot4U8Packed(ir::CoreBuiltinCall* call) {
+        // Replace `dot4U8Packed(%x,%y)` with:
+        //   %unpacked_x = unpack4xU8(%x);
+        //   %unpacked_y = unpack4xU8(%y);
+        //   %result = dot(%unpacked_x, %unpacked_y);
+        auto* x = call->Args()[0];
+        auto* y = call->Args()[1];
+        auto* unpacked_x = Unpack4xU8OnValue(call, x);
+        auto* unpacked_y = Unpack4xU8OnValue(call, y);
+        ir::Value* result = nullptr;
+        b.InsertBefore(call, [&] {
+            result = b.Call(ty.u32(), core::BuiltinFn::kDot, unpacked_x, unpacked_y)->Result(0);
+        });
+        return result;
+    }
+
     /// Polyfill a `pack4xI8()` builtin call
     /// @param call the builtin call instruction
     /// @returns the replacement value
@@ -713,50 +764,64 @@
         return result;
     }
 
-    /// Polyfill a `unpack4xI8()` builtin call
-    /// @param call the builtin call instruction
-    /// @returns the replacement value
-    ir::Value* Unpack4xI8(ir::CoreBuiltinCall* call) {
+    /// Emit code for `unpack4xI8` on u32 value `x`, before the given call.
+    /// @param call the instruction that should follow the emitted code
+    /// @param x the u32 value to be unpacked
+    ir::Value* Unpack4xI8OnValue(ir::CoreBuiltinCall* call, ir::Value* x) {
         // Replace `unpack4xI8(%x)` with:
         //   %n       = vec4u(24, 16, 8, 0);
-        //   %x_vec4u = vec4u(x);
-        //   %x_vec4i = bitcast<vec4i>(%x_vec4u << n);
+        //   %x_splat = vec4u(%x); // splat the scalar to a vector
+        //   %x_vec4i = bitcast<vec4i>(%x_splat << n);
         //   %result  = %x_vec4i >> vec4u(24);
         ir::Value* result = nullptr;
-        auto* x = call->Args()[0];
         b.InsertBefore(call, [&] {
             auto* vec4i = ty.vec4<i32>();
             auto* vec4u = ty.vec4<u32>();
 
             auto* n = b.Construct(vec4u, b.Constant(u32(24)), b.Constant(u32(16)),
                                   b.Constant(u32(8)), b.Constant(u32(0)));
-            auto* x_vec4u = b.Convert(vec4u, x);
-            auto* x_vec4i = b.Bitcast(vec4i, b.ShiftLeft(vec4u, x_vec4u, n));
+            auto* x_splat = b.Construct(vec4u, x);
+            auto* x_vec4i = b.Bitcast(vec4i, b.ShiftLeft(vec4u, x_splat, n));
             result =
                 b.ShiftRight(vec4i, x_vec4i, b.Construct(vec4u, b.Constant(u32(24))))->Result(0);
         });
         return result;
     }
 
+    /// Polyfill a `unpack4xI8()` builtin call
+    /// @param call the builtin call instruction
+    /// @returns the replacement value
+    ir::Value* Unpack4xI8(ir::CoreBuiltinCall* call) {
+        return Unpack4xI8OnValue(call, call->Args()[0]);
+    }
+
+    /// Emit code for `unpack4xU8` on u32 value `x`, before the given call.
+    /// @param call the instruction that should follow the emitted code
+    /// @param x the u32 value to be unpacked
+    ir::Value* Unpack4xU8OnValue(ir::CoreBuiltinCall* call, ir::Value* x) {
+        // Replace `unpack4xU8(%x)` with:
+        //   %n       = vec4u(0, 8, 16, 24);
+        //   %x_splat = vec4u(%x); // splat the scalar to a vector
+        //   %x_vec4u = %x_splat >> n;
+        //   %result  = %x_vec4u & vec4u(0xff);
+        ir::Value* result = nullptr;
+        b.InsertBefore(call, [&] {
+            auto* vec4u = ty.vec4<u32>();
+
+            auto* n = b.Construct(vec4u, b.Constant(u32(0)), b.Constant(u32(8)),
+                                  b.Constant(u32(16)), b.Constant(u32(24)));
+            auto* x_splat = b.Construct(vec4u, x);
+            auto* x_vec4u = b.ShiftRight(vec4u, x_splat, n);
+            result = b.And(vec4u, x_vec4u, b.Construct(vec4u, b.Constant(u32(0xff))))->Result(0);
+        });
+        return result;
+    }
+
     /// Polyfill a `unpack4xU8()` builtin call
     /// @param call the builtin call instruction
     /// @returns the replacement value
     ir::Value* Unpack4xU8(ir::CoreBuiltinCall* call) {
-        // Replace `unpack4xU8(%x)` with:
-        //   %n       = vec4u(0, 8, 16, 24);
-        //   %x_vec4u = vec4u(x) >> n;
-        //   %result  = %x_vec4u & vec4u(0xff);
-        ir::Value* result = nullptr;
-        auto* x = call->Args()[0];
-        b.InsertBefore(call, [&] {
-            auto* vec4u = ty.vec4<u32>();
-
-            auto* n = b.Construct(vec4u, b.Constant(u32(0)), b.Constant(u32(8)),
-                                  b.Constant(u32(16)), b.Constant(u32(24)));
-            auto* x_vec4u = b.ShiftRight(vec4u, b.Convert(vec4u, x), n);
-            result = b.And(vec4u, x_vec4u, b.Construct(vec4u, b.Constant(u32(0xff))))->Result(0);
-        });
-        return result;
+        return Unpack4xU8OnValue(call, call->Args()[0]);
     }
 };
 
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill.h b/src/tint/lang/core/ir/transform/builtin_polyfill.h
index 2f90486..cb3c054 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill.h
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill.h
@@ -69,6 +69,8 @@
     bool saturate = false;
     /// Should `textureSampleBaseClampToEdge()` be polyfilled for texture_2d<f32> textures?
     bool texture_sample_base_clamp_to_edge_2d_f32 = false;
+    /// Should `dot4U8Packed()` and `dot4I8Packed()` be polyfilled?
+    bool dot_4x8_packed = false;
     /// Should `pack4xI8()` and `pack4xU8()` be polyfilled?
     bool pack_unpack_4x8 = false;
 };
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill_test.cc b/src/tint/lang/core/ir/transform/builtin_polyfill_test.cc
index 37a3fdf..9dfaee0 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill_test.cc
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill_test.cc
@@ -1572,7 +1572,7 @@
 %foo = func(%arg:u32):vec4<i32> -> %b1 {
   %b1 = block {
     %3:vec4<u32> = construct 24u, 16u, 8u, 0u
-    %4:vec4<u32> = convert %arg
+    %4:vec4<u32> = construct %arg
     %5:vec4<u32> = shl %4, %3
     %6:vec4<i32> = bitcast %5
     %7:vec4<u32> = construct 24u
@@ -1606,7 +1606,7 @@
 %foo = func(%arg:u32):vec4<u32> -> %b1 {
   %b1 = block {
     %3:vec4<u32> = construct 0u, 8u, 16u, 24u
-    %4:vec4<u32> = convert %arg
+    %4:vec4<u32> = construct %arg
     %5:vec4<u32> = shr %4, %3
     %6:vec4<u32> = construct 255u
     %result:vec4<u32> = and %5, %6
@@ -1622,5 +1622,85 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_BuiltinPolyfillTest, Dot4I8Packed) {
+    Build(core::BuiltinFn::kDot4I8Packed, ty.i32(), Vector{ty.u32(), ty.u32()});
+
+    auto* src = R"(
+%foo = func(%arg:u32, %arg_1:u32):i32 -> %b1 {  # %arg_1: 'arg'
+  %b1 = block {
+    %result:i32 = dot4I8Packed %arg, %arg_1
+    ret %result
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%arg:u32, %arg_1:u32):i32 -> %b1 {  # %arg_1: 'arg'
+  %b1 = block {
+    %4:vec4<u32> = construct 24u, 16u, 8u, 0u
+    %5:vec4<u32> = construct %arg
+    %6:vec4<u32> = shl %5, %4
+    %7:vec4<i32> = bitcast %6
+    %8:vec4<u32> = construct 24u
+    %9:vec4<i32> = shr %7, %8
+    %10:vec4<u32> = construct 24u, 16u, 8u, 0u
+    %11:vec4<u32> = construct %arg_1
+    %12:vec4<u32> = shl %11, %10
+    %13:vec4<i32> = bitcast %12
+    %14:vec4<u32> = construct 24u
+    %15:vec4<i32> = shr %13, %14
+    %result:i32 = dot %9, %15
+    ret %result
+  }
+}
+)";
+
+    BuiltinPolyfillConfig config;
+    config.dot_4x8_packed = true;
+    Run(BuiltinPolyfill, config);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_BuiltinPolyfillTest, Dot4U8Packed) {
+    Build(core::BuiltinFn::kDot4U8Packed, ty.u32(), Vector{ty.u32(), ty.u32()});
+
+    auto* src = R"(
+%foo = func(%arg:u32, %arg_1:u32):u32 -> %b1 {  # %arg_1: 'arg'
+  %b1 = block {
+    %result:u32 = dot4U8Packed %arg, %arg_1
+    ret %result
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func(%arg:u32, %arg_1:u32):u32 -> %b1 {  # %arg_1: 'arg'
+  %b1 = block {
+    %4:vec4<u32> = construct 0u, 8u, 16u, 24u
+    %5:vec4<u32> = construct %arg
+    %6:vec4<u32> = shr %5, %4
+    %7:vec4<u32> = construct 255u
+    %8:vec4<u32> = and %6, %7
+    %9:vec4<u32> = construct 0u, 8u, 16u, 24u
+    %10:vec4<u32> = construct %arg_1
+    %11:vec4<u32> = shr %10, %9
+    %12:vec4<u32> = construct 255u
+    %13:vec4<u32> = and %11, %12
+    %result:u32 = dot %8, %13
+    ret %result
+  }
+}
+)";
+
+    BuiltinPolyfillConfig config;
+    config.dot_4x8_packed = true;
+    Run(BuiltinPolyfill, config);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::core::ir::transform
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 9f39916..49780b7 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
@@ -36,6 +36,7 @@
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
+#include "src/tint/lang/wgsl/writer/raise/raise.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
 
 namespace tint::core::ir::transform {
@@ -62,12 +63,12 @@
         wgsl::reader::Options parser_options;
         parser_options.allowed_features = wgsl::AllowedFeatures::Everything();
         Source::File file{"test", in};
-        auto program = wgsl::reader::Parse(&file, parser_options);
-        if (!program.IsValid()) {
-            return "wgsl::reader::Parse() failed: \n" + program.Diagnostics().str();
+        auto program_in = wgsl::reader::Parse(&file, parser_options);
+        if (!program_in.IsValid()) {
+            return "wgsl::reader::Parse() failed: \n" + program_in.Diagnostics().str();
         }
 
-        auto module = wgsl::reader::ProgramToIR(program);
+        auto module = wgsl::reader::ProgramToIR(program_in);
         if (module != Success) {
             return "ProgramToIR() failed:\n" + module.Failure().reason.str();
         }
@@ -77,19 +78,27 @@
             return "DirectVariableAccess failed:\n" + res.Failure().reason.str();
         }
 
+        auto pre_raise = ir::Disassemble(module.Get());
+
+        if (auto raise = wgsl::writer::Raise(module.Get()); raise != Success) {
+            return "wgsl::writer::Raise failed:\n" + res.Failure().reason.str();
+        }
+
         wgsl::writer::ProgramOptions program_options;
         program_options.allowed_features.extensions.insert(
             wgsl::Extension::kChromiumExperimentalFullPtrParameters);
-        auto transformed = wgsl::writer::IRToProgram(module.Get(), program_options);
-        if (!transformed.IsValid()) {
-            return "wgsl::writer::IRToProgram() failed: \n" + transformed.Diagnostics().str() +
-                   "\n\nIR:\n" + ir::Disassemble(module.Get()) +  //
-                   "\n\nAST:\n" + Program::printer(transformed);
+        auto program_out = wgsl::writer::IRToProgram(module.Get(), program_options);
+        if (!program_out.IsValid()) {
+            return "wgsl::writer::IRToProgram() failed: \n" + program_out.Diagnostics().str() +
+                   "\n\nIR (pre):\n" + pre_raise +                       //
+                   "\n\nIR (post):\n" + ir::Disassemble(module.Get()) +  //
+                   "\n\nAST:\n" + Program::printer(program_out);
         }
 
-        auto output = wgsl::writer::Generate(transformed, wgsl::writer::Options{});
+        auto output = wgsl::writer::Generate(program_out, wgsl::writer::Options{});
         if (output != Success) {
-            return "wgsl::writer::Generate() failed: \n" + output.Failure().reason.str();
+            return "wgsl::writer::IRToProgram() failed: \n" + output.Failure().reason.str() +
+                   "\n\nIR:\n" + ir::Disassemble(module.Get());
         }
 
         return "\n" + output->wgsl;
diff --git a/src/tint/lang/core/ir/unary.cc b/src/tint/lang/core/ir/unary.cc
index d4de4e1..9fdfe0f 100644
--- a/src/tint/lang/core/ir/unary.cc
+++ b/src/tint/lang/core/ir/unary.cc
@@ -43,19 +43,4 @@
 
 Unary::~Unary() = default;
 
-Unary* Unary::Clone(CloneContext& ctx) {
-    auto* new_result = ctx.Clone(Result(0));
-    auto* val = ctx.Remap(Val());
-    return ctx.ir.instructions.Create<Unary>(new_result, op_, val);
-}
-
-std::string_view ToString(enum UnaryOp op) {
-    switch (op) {
-        case UnaryOp::kComplement:
-            return "complement";
-        case UnaryOp::kNegation:
-            return "negation";
-    }
-    return "<unknown>";
-}
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unary.h b/src/tint/lang/core/ir/unary.h
index 41a4875..8ade151 100644
--- a/src/tint/lang/core/ir/unary.h
+++ b/src/tint/lang/core/ir/unary.h
@@ -31,18 +31,17 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/operand_instruction.h"
-#include "src/tint/utils/rtti/castable.h"
+#include "src/tint/lang/core/unary_op.h"
+
+// Forward declarations
+namespace tint::core::intrinsic {
+struct TableData;
+}
 
 namespace tint::core::ir {
 
-/// A unary operator.
-enum class UnaryOp {
-    kComplement,
-    kNegation,
-};
-
-/// A unary instruction in the IR.
-class Unary final : public Castable<Unary, OperandInstruction<1, 1>> {
+/// The abstract base class for dialect-specific unary-op instructions in the IR.
+class Unary : public Castable<Unary, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
@@ -57,9 +56,6 @@
     Unary(InstructionResult* result, UnaryOp op, Value* val);
     ~Unary() override;
 
-    /// @copydoc Instruction::Clone()
-    Unary* Clone(CloneContext& ctx) override;
-
     /// @returns the value for the instruction
     Value* Val() { return operands_[kValueOperandOffset]; }
 
@@ -75,20 +71,13 @@
     /// @returns the friendly name for the instruction
     std::string FriendlyName() const override { return "unary"; }
 
+    /// @returns the table data to validate this builtin
+    virtual const core::intrinsic::TableData& TableData() const = 0;
+
   private:
     UnaryOp op_ = UnaryOp::kComplement;
 };
 
-/// @param kind the enum value
-/// @returns the string for the given enum value
-std::string_view ToString(UnaryOp kind);
-
-/// Emits the name of the intrinsic type.
-template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, UnaryOp kind) {
-    return out << ToString(kind);
-}
-
 }  // namespace tint::core::ir
 
 #endif  // SRC_TINT_LANG_CORE_IR_UNARY_H_
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index dd7925a..b6e96e5 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -594,6 +594,24 @@
         AddError(call, UserCall::kFunctionOperandOffset,
                  InstError(call, "call target is not part of the module"));
     }
+    auto args = call->Args();
+    auto params = call->Target()->Params();
+    if (args.Length() != params.Length()) {
+        StringStream err;
+        err << "function has " << params.Length() << " parameters, but call provides "
+            << args.Length() << " arguments";
+        AddError(call, UserCall::kFunctionOperandOffset, InstError(call, err.str()));
+        return;
+    }
+
+    for (size_t i = 0; i < args.Length(); i++) {
+        if (args[i]->Type() != params[i]->Type()) {
+            StringStream err;
+            err << "function parameter " << i << " is of type " << params[i]->Type()->FriendlyName()
+                << ", but argument is of type " << args[i]->Type()->FriendlyName();
+            AddError(call, UserCall::kArgsOperandOffset + i, InstError(call, err.str()));
+        }
+    }
 }
 
 void Validator::CheckAccess(const Access* a) {
@@ -667,14 +685,61 @@
 
 void Validator::CheckBinary(const Binary* b) {
     CheckOperandsNotNull(b, Binary::kLhsOperandOffset, Binary::kRhsOperandOffset);
+    if (b->LHS() && b->RHS()) {
+        auto symbols = SymbolTable::Wrap(mod_.symbols);
+        auto type_mgr = type::Manager::Wrap(mod_.Types());
+        intrinsic::Context context{
+            b->TableData(),
+            type_mgr,
+            symbols,
+        };
+
+        auto overload =
+            core::intrinsic::LookupBinary(context, b->Op(), b->LHS()->Type(), b->RHS()->Type(),
+                                          core::EvaluationStage::kRuntime, /* is_compound */ false);
+        if (overload != Success) {
+            AddError(b, InstError(b, overload.Failure()));
+            return;
+        }
+
+        if (auto* result = b->Result(0)) {
+            if (overload->return_type != result->Type()) {
+                StringStream err;
+                err << "binary instruction result type (" << result->Type()->FriendlyName()
+                    << ") does not match overload result type ("
+                    << overload->return_type->FriendlyName() << ")";
+                AddError(b, InstError(b, err.str()));
+            }
+        }
+    }
 }
 
 void Validator::CheckUnary(const Unary* u) {
     CheckOperandNotNull(u, u->Val(), Unary::kValueOperandOffset);
+    if (u->Val()) {
+        auto symbols = SymbolTable::Wrap(mod_.symbols);
+        auto type_mgr = type::Manager::Wrap(mod_.Types());
+        intrinsic::Context context{
+            u->TableData(),
+            type_mgr,
+            symbols,
+        };
 
-    if (u->Result(0) && u->Val()) {
-        if (u->Result(0)->Type() != u->Val()->Type()) {
-            AddError(u, InstError(u, "result type must match value type"));
+        auto overload = core::intrinsic::LookupUnary(context, u->Op(), u->Val()->Type(),
+                                                     core::EvaluationStage::kRuntime);
+        if (overload != Success) {
+            AddError(u, InstError(u, overload.Failure()));
+            return;
+        }
+
+        if (auto* result = u->Result(0)) {
+            if (overload->return_type != result->Type()) {
+                StringStream err;
+                err << "unary instruction result type (" << result->Type()->FriendlyName()
+                    << ") does not match overload result type ("
+                    << overload->return_type->FriendlyName() << ")";
+                AddError(u, InstError(u, err.str()));
+            }
         }
     }
 }
@@ -856,9 +921,13 @@
 
     if (auto* from = s->From()) {
         if (auto* to = s->To()) {
-            if (from->Type() != to->Type()->UnwrapPtr()) {
-                AddError(s, Store::kFromOperandOffset,
-                         "value type does not match pointer element type");
+            auto* mv = to->Type()->As<core::type::MemoryView>();
+            if (!mv) {
+                AddError(s, Store::kFromOperandOffset, "store target operand is not a memory view");
+                return;
+            }
+            if (from->Type() != mv->StoreType()) {
+                AddError(s, Store::kFromOperandOffset, "value type does not match store type");
             }
         }
     }
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 7dd1d69..9989e2a 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -179,6 +179,117 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, CallToFunctionTooFewArguments) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, 42_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:8:20 error: call: function has 2 parameters, but call provides 1 arguments
+    %5:void = call %g, 42i
+                   ^^
+
+:7:3 note: In block
+  %b2 = block {
+  ^^^^^^^^^^^
+
+note: # Disassembly
+%g = func(%2:i32, %3:i32):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+%f = func():void -> %b2 {
+  %b2 = block {
+    %5:void = call %g, 42i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionTooManyArguments) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, 1_i, 2_i, 3_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:8:20 error: call: function has 2 parameters, but call provides 3 arguments
+    %5:void = call %g, 1i, 2i, 3i
+                   ^^
+
+:7:3 note: In block
+  %b2 = block {
+  ^^^^^^^^^^^
+
+note: # Disassembly
+%g = func(%2:i32, %3:i32):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+%f = func():void -> %b2 {
+  %b2 = block {
+    %5:void = call %g, 1i, 2i, 3i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, CallToFunctionWrongArgType) {
+    auto* g = b.Function("g", ty.void_());
+    g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>(), b.FunctionParam<i32>()});
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto* f = b.Function("f", ty.void_());
+    b.Append(f->Block(), [&] {
+        b.Call(g, 1_i, 2_f, 3_i);
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:8:28 error: call: function parameter 1 is of type i32, but argument is of type f32
+    %6:void = call %g, 1i, 2.0f, 3i
+                           ^^^^
+
+:7:3 note: In block
+  %b2 = block {
+  ^^^^^^^^^^^
+
+note: # Disassembly
+%g = func(%2:i32, %3:i32, %4:i32):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+%f = func():void -> %b2 {
+  %b2 = block {
+    %6:void = call %g, 1i, 2.0f, 3i
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, Block_NoTerminator) {
     b.Function("my_func", ty.void_());
 
@@ -1271,8 +1382,8 @@
 }
 
 TEST_F(IR_ValidatorTest, Binary_Result_Nullptr) {
-    auto* bin = mod.instructions.Create<ir::Binary>(nullptr, BinaryOp::kAdd, b.Constant(3_i),
-                                                    b.Constant(2_i));
+    auto* bin = mod.instructions.Create<ir::CoreBinary>(nullptr, BinaryOp::kAdd, b.Constant(3_i),
+                                                        b.Constant(2_i));
 
     auto* f = b.Function("my_func", ty.void_());
 
@@ -1329,7 +1440,7 @@
 
 TEST_F(IR_ValidatorTest, Unary_Result_Nullptr) {
     auto* bin =
-        mod.instructions.Create<ir::Unary>(nullptr, ir::UnaryOp::kNegation, b.Constant(2_i));
+        mod.instructions.Create<ir::CoreUnary>(nullptr, UnaryOp::kNegation, b.Constant(2_i));
 
     auto* f = b.Function("my_func", ty.void_());
 
@@ -1368,7 +1479,9 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: unary: result type must match value type
+    EXPECT_EQ(
+        res.Failure().reason.str(),
+        R"(:3:5 error: unary: unary instruction result type (f32) does not match overload result type (i32)
     %2:f32 = complement 2i
     ^^^^^^^^^^^^^^^^^^^^^^
 
@@ -2931,6 +3044,37 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, Store_TargetNotMemoryView) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    b.Append(f->Block(), [&] {
+        auto* let = b.Let("l", 1_i);
+        b.Append(mod.instructions.Create<ir::Store>(let->Result(0), b.Constant(42_u)));
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:4:15 error: store target operand is not a memory view
+    store %l, 42u
+              ^^^
+
+:2:3 note: In block
+  %b1 = block {
+  ^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    %l:i32 = let 1i
+    store %l, 42u
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, Store_TypeMismatch) {
     auto* f = b.Function("my_func", ty.void_());
 
@@ -2943,7 +3087,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.str(),
-              R"(:4:15 error: value type does not match pointer element type
+              R"(:4:15 error: value type does not match store type
     store %2, 42u
               ^^^
 
diff --git a/src/tint/lang/core/type/BUILD.bazel b/src/tint/lang/core/type/BUILD.bazel
index e3c6b72..485a4a8 100644
--- a/src/tint/lang/core/type/BUILD.bazel
+++ b/src/tint/lang/core/type/BUILD.bazel
@@ -122,6 +122,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/core/type/BUILD.cmake b/src/tint/lang/core/type/BUILD.cmake
index d212503..7c9b819 100644
--- a/src/tint/lang/core/type/BUILD.cmake
+++ b/src/tint/lang/core/type/BUILD.cmake
@@ -121,6 +121,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/core/type/BUILD.gn b/src/tint/lang/core/type/BUILD.gn
index 8942f2a..2f0568b 100644
--- a/src/tint/lang/core/type/BUILD.gn
+++ b/src/tint/lang/core/type/BUILD.gn
@@ -125,6 +125,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/glsl/writer/BUILD.bazel b/src/tint/lang/glsl/writer/BUILD.bazel
index 3dac23b..0d24e42 100644
--- a/src/tint/lang/glsl/writer/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/BUILD.bazel
@@ -131,3 +131,8 @@
   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 cb302d8..35e7e21 100644
--- a/src/tint/lang/glsl/writer/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/BUILD.cmake
@@ -138,4 +138,58 @@
   )
 endif(TINT_BUILD_GLSL_WRITER)
 
+endif(TINT_BUILD_GLSL_WRITER)
+if(TINT_BUILD_GLSL_WRITER)
+################################################################################
+# Target:    tint_lang_glsl_writer_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_GLSL_WRITER
+################################################################################
+tint_add_target(tint_lang_glsl_writer_fuzz fuzz
+)
+
+tint_target_add_dependencies(tint_lang_glsl_writer_fuzz fuzz
+  tint_api_common
+  tint_api_options
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_features
+  tint_lang_wgsl_inspector
+  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_GLSL_WRITER)
+  tint_target_add_dependencies(tint_lang_glsl_writer_fuzz fuzz
+    tint_lang_glsl_writer
+    tint_lang_glsl_writer_common
+  )
+endif(TINT_BUILD_GLSL_WRITER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_glsl_writer_fuzz fuzz
+    "lang/glsl/writer/writer_ast_fuzz.cc"
+  )
+  tint_target_add_dependencies(tint_lang_glsl_writer_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 endif(TINT_BUILD_GLSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/glsl/writer/BUILD.gn b/src/tint/lang/glsl/writer/BUILD.gn
index 4d5bfda..2f908e4 100644
--- a/src/tint/lang/glsl/writer/BUILD.gn
+++ b/src/tint/lang/glsl/writer/BUILD.gn
@@ -127,3 +127,47 @@
     }
   }
 }
+if (tint_build_glsl_writer) {
+  tint_fuzz_source_set("fuzz") {
+    sources = []
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/api/options",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/features",
+      "${tint_src_dir}/lang/wgsl/inspector",
+      "${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_glsl_writer) {
+      deps += [
+        "${tint_src_dir}/lang/glsl/writer",
+        "${tint_src_dir}/lang/glsl/writer/common",
+      ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "writer_ast_fuzz.cc" ]
+      deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
+    }
+  }
+}
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 dde83cd..8d96bc7 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -182,7 +182,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = ast::transform::BuiltinPolyfill::Level::kClampParameters;
-        polyfills.int_div_mod = true;
+        polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
         polyfills.saturate = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.workgroup_uniform_load = true;
diff --git a/src/tint/lang/glsl/writer/common/options.h b/src/tint/lang/glsl/writer/common/options.h
index 8068595..b795d10 100644
--- a/src/tint/lang/glsl/writer/common/options.h
+++ b/src/tint/lang/glsl/writer/common/options.h
@@ -61,6 +61,9 @@
     /// Set to `true` to disable workgroup memory zero initialization
     bool disable_workgroup_init = false;
 
+    /// Set to `true` to disable the polyfills on integer division and modulo.
+    bool disable_polyfill_integer_div_mod = false;
+
     /// The GLSL version to emit
     Version version;
 
@@ -85,6 +88,7 @@
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
                  disable_workgroup_init,
+                 disable_polyfill_integer_div_mod,
                  version,
                  binding_map,
                  placeholder_binding_point,
diff --git a/src/tint/lang/glsl/writer/common/version.h b/src/tint/lang/glsl/writer/common/version.h
index 84a5a9c..9c3463a 100644
--- a/src/tint/lang/glsl/writer/common/version.h
+++ b/src/tint/lang/glsl/writer/common/version.h
@@ -73,4 +73,11 @@
 
 }  // namespace tint::glsl::writer
 
+namespace tint {
+
+/// Relect enum information for Version
+TINT_REFLECT_ENUM_RANGE(glsl::writer::Version::Standard, kDesktop, kES);
+
+}  // namespace tint
+
 #endif  // SRC_TINT_LANG_GLSL_WRITER_COMMON_VERSION_H_
diff --git a/src/tint/lang/glsl/writer/printer/helper_test.h b/src/tint/lang/glsl/writer/printer/helper_test.h
index f6b31dd..a720241 100644
--- a/src/tint/lang/glsl/writer/printer/helper_test.h
+++ b/src/tint/lang/glsl/writer/printer/helper_test.h
@@ -62,7 +62,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 != Success) {
+        if (auto raised = Raise(mod); raised != Success) {
             err_ = raised.Failure().reason.str();
             return false;
         }
diff --git a/src/tint/lang/glsl/writer/raise/raise.cc b/src/tint/lang/glsl/writer/raise/raise.cc
index 33c17e9..1985ec7 100644
--- a/src/tint/lang/glsl/writer/raise/raise.cc
+++ b/src/tint/lang/glsl/writer/raise/raise.cc
@@ -27,7 +27,7 @@
 
 #include "src/tint/lang/glsl/writer/raise/raise.h"
 
-namespace tint::glsl::raise {
+namespace tint::glsl::writer {
 
 Result<SuccessType> Raise(core::ir::Module&) {
     // #define RUN_TRANSFORM(name)
@@ -41,4 +41,4 @@
     return Success;
 }
 
-}  // namespace tint::glsl::raise
+}  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/raise/raise.h b/src/tint/lang/glsl/writer/raise/raise.h
index 2ff1489..846790d 100644
--- a/src/tint/lang/glsl/writer/raise/raise.h
+++ b/src/tint/lang/glsl/writer/raise/raise.h
@@ -35,13 +35,13 @@
 class Module;
 }  // namespace tint::core::ir
 
-namespace tint::glsl::raise {
+namespace tint::glsl::writer {
 
 /// Raise a core IR module to the MSL dialect of the IR.
 /// @param mod the core IR module to raise to MSL dialect
 /// @returns success or failure
 Result<SuccessType> Raise(core::ir::Module& mod);
 
-}  // namespace tint::glsl::raise
+}  // namespace tint::glsl::writer
 
 #endif  // SRC_TINT_LANG_GLSL_WRITER_RAISE_RAISE_H_
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index 1402afe..3f8437d 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -40,7 +40,7 @@
     Output output;
 
     // Raise from core-dialect to GLSL-dialect.
-    if (auto res = raise::Raise(ir); res != Success) {
+    if (auto res = Raise(ir); res != Success) {
         return res.Failure();
     }
 
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/glsl/writer/writer_ast_fuzz.cc
similarity index 65%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/glsl/writer/writer_ast_fuzz.cc
index 421db7b..096c259 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/glsl/writer/writer_ast_fuzz.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +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.
 
-#include "src/tint/lang/spirv/writer/writer.h"
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
 
-#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/cmd/fuzz/wgsl/fuzz.h"
+#include "src/tint/lang/glsl/writer/writer.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/inspector/inspector.h"
 
-namespace tint::spirv::writer {
+namespace tint::glsl::writer {
 namespace {
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
+void ASTFuzzer(const tint::Program& program, Options options) {
+    if (program.AST().HasOverrides()) {
         return;
     }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
+
+    auto inspector = tint::inspector::Inspector(program);
+    auto entrypoints = inspector.GetEntryPoints();
+
+    // Test all of the entry points as GLSL requires specifying which one to generate.
+    for (const auto& ep : entrypoints) {
+        [[maybe_unused]] auto res = tint::glsl::writer::Generate(program, options, ep.name);
     }
 }
 
 }  // namespace
-}  // namespace tint::spirv::writer
+}  // namespace tint::glsl::writer
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+TINT_WGSL_PROGRAM_FUZZER(tint::glsl::writer::ASTFuzzer);
diff --git a/src/tint/lang/hlsl/writer/BUILD.bazel b/src/tint/lang/hlsl/writer/BUILD.bazel
index 71c0cbd..affbe22 100644
--- a/src/tint/lang/hlsl/writer/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/BUILD.bazel
@@ -130,3 +130,8 @@
   actual = "//src/tint:tint_build_hlsl_writer_true",
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
diff --git a/src/tint/lang/hlsl/writer/BUILD.cmake b/src/tint/lang/hlsl/writer/BUILD.cmake
index b9d641d..5a19a3c 100644
--- a/src/tint/lang/hlsl/writer/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/BUILD.cmake
@@ -135,4 +135,57 @@
   )
 endif(TINT_BUILD_HLSL_WRITER)
 
+endif(TINT_BUILD_HLSL_WRITER)
+if(TINT_BUILD_HLSL_WRITER)
+################################################################################
+# Target:    tint_lang_hlsl_writer_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_HLSL_WRITER
+################################################################################
+tint_add_target(tint_lang_hlsl_writer_fuzz fuzz
+)
+
+tint_target_add_dependencies(tint_lang_hlsl_writer_fuzz fuzz
+  tint_api_common
+  tint_api_options
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_type
+  tint_lang_hlsl_writer_common
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_features
+  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_HLSL_WRITER)
+  tint_target_add_dependencies(tint_lang_hlsl_writer_fuzz fuzz
+    tint_lang_hlsl_writer
+  )
+endif(TINT_BUILD_HLSL_WRITER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_hlsl_writer_fuzz fuzz
+    "lang/hlsl/writer/writer_ast_fuzz.cc"
+  )
+  tint_target_add_dependencies(tint_lang_hlsl_writer_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 endif(TINT_BUILD_HLSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/hlsl/writer/BUILD.gn b/src/tint/lang/hlsl/writer/BUILD.gn
index 6b8a8fc..4c8f704 100644
--- a/src/tint/lang/hlsl/writer/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/BUILD.gn
@@ -124,3 +124,44 @@
     }
   }
 }
+if (tint_build_hlsl_writer) {
+  tint_fuzz_source_set("fuzz") {
+    sources = []
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/api/options",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/hlsl/writer/common",
+      "${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/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_hlsl_writer) {
+      deps += [ "${tint_src_dir}/lang/hlsl/writer" ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "writer_ast_fuzz.cc" ]
+      deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
+    }
+  }
+}
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 6932c07..452abc5 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -250,7 +250,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = ast::transform::BuiltinPolyfill::Level::kFull;
-        polyfills.int_div_mod = true;
+        polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
         polyfills.precise_float_mod = true;
         polyfills.reflect_vec2_f32 = options.polyfill_reflect_vec2_f32;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.bazel b/src/tint/lang/hlsl/writer/common/BUILD.bazel
index f9bbbad..93cb98f 100644
--- a/src/tint/lang/hlsl/writer/common/BUILD.bazel
+++ b/src/tint/lang/hlsl/writer/common/BUILD.bazel
@@ -51,7 +51,6 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/reflection",
-    "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
   copts = COPTS,
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.cmake b/src/tint/lang/hlsl/writer/common/BUILD.cmake
index aa8415c..1a5415e 100644
--- a/src/tint/lang/hlsl/writer/common/BUILD.cmake
+++ b/src/tint/lang/hlsl/writer/common/BUILD.cmake
@@ -50,6 +50,5 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_reflection
-  tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/lang/hlsl/writer/common/BUILD.gn b/src/tint/lang/hlsl/writer/common/BUILD.gn
index eeb1335..39c9b06 100644
--- a/src/tint/lang/hlsl/writer/common/BUILD.gn
+++ b/src/tint/lang/hlsl/writer/common/BUILD.gn
@@ -50,7 +50,6 @@
     "${tint_src_dir}/utils/macros",
     "${tint_src_dir}/utils/math",
     "${tint_src_dir}/utils/reflection",
-    "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
 }
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index 44cb403..b1d8535 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -74,6 +74,9 @@
     /// Set to `true` to generate polyfill for `dot4I8Packed` and `dot4U8Packed` builtins
     bool polyfill_dot_4x8_packed = false;
 
+    /// Set to `true` to disable the polyfills on integer division and modulo.
+    bool disable_polyfill_integer_div_mod = false;
+
     /// Options used to specify a mapping of binding points to indices into a UBO
     /// from which to load buffer sizes.
     ArrayLengthFromUniformOptions array_length_from_uniform = {};
@@ -106,6 +109,7 @@
                  truncate_interstage_variables,
                  polyfill_reflect_vec2_f32,
                  polyfill_dot_4x8_packed,
+                 disable_polyfill_integer_div_mod,
                  array_length_from_uniform,
                  interstage_locations,
                  root_constant_binding_point,
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/hlsl/writer/writer_ast_fuzz.cc
similarity index 64%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/hlsl/writer/writer_ast_fuzz.cc
index 421db7b..a2865f9 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/hlsl/writer/writer_ast_fuzz.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +25,24 @@
 // 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/writer.h"
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
 
-#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/cmd/fuzz/wgsl/fuzz.h"
+#include "src/tint/lang/hlsl/writer/writer.h"
+#include "src/tint/lang/wgsl/ast/module.h"
 
-namespace tint::spirv::writer {
+namespace tint::hlsl::writer {
 namespace {
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
+void ASTFuzzer(const tint::Program& program, Options options) {
+    if (program.AST().HasOverrides()) {
         return;
     }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
-    }
+
+    [[maybe_unused]] auto res = tint::hlsl::writer::Generate(program, options);
 }
 
 }  // namespace
-}  // namespace tint::spirv::writer
+}  // namespace tint::hlsl::writer
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+TINT_WGSL_PROGRAM_FUZZER(tint::hlsl::writer::ASTFuzzer);
diff --git a/src/tint/lang/msl/intrinsic/BUILD.bazel b/src/tint/lang/msl/intrinsic/BUILD.bazel
index de60b66..d4b9fab 100644
--- a/src/tint/lang/msl/intrinsic/BUILD.bazel
+++ b/src/tint/lang/msl/intrinsic/BUILD.bazel
@@ -57,6 +57,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/msl/intrinsic/BUILD.cmake b/src/tint/lang/msl/intrinsic/BUILD.cmake
index 60cca31..3ede760 100644
--- a/src/tint/lang/msl/intrinsic/BUILD.cmake
+++ b/src/tint/lang/msl/intrinsic/BUILD.cmake
@@ -56,6 +56,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/msl/intrinsic/BUILD.gn b/src/tint/lang/msl/intrinsic/BUILD.gn
index 61d0771..2351f59 100644
--- a/src/tint/lang/msl/intrinsic/BUILD.gn
+++ b/src/tint/lang/msl/intrinsic/BUILD.gn
@@ -56,6 +56,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/msl/validate/BUILD.bazel b/src/tint/lang/msl/validate/BUILD.bazel
index 0606047..7e5730a 100644
--- a/src/tint/lang/msl/validate/BUILD.bazel
+++ b/src/tint/lang/msl/validate/BUILD.bazel
@@ -67,6 +67,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/msl/validate/BUILD.cmake b/src/tint/lang/msl/validate/BUILD.cmake
index e51eab9..698e899 100644
--- a/src/tint/lang/msl/validate/BUILD.cmake
+++ b/src/tint/lang/msl/validate/BUILD.cmake
@@ -63,6 +63,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/msl/validate/BUILD.gn b/src/tint/lang/msl/validate/BUILD.gn
index 2d587b1..cbf7c61 100644
--- a/src/tint/lang/msl/validate/BUILD.gn
+++ b/src/tint/lang/msl/validate/BUILD.gn
@@ -61,6 +61,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/msl/writer/BUILD.cmake b/src/tint/lang/msl/writer/BUILD.cmake
index d1b3c1a..18bec4c 100644
--- a/src/tint/lang/msl/writer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/BUILD.cmake
@@ -148,4 +148,58 @@
   )
 endif(TINT_BUILD_MSL_WRITER)
 
+endif(TINT_BUILD_MSL_WRITER)
+if(TINT_BUILD_MSL_WRITER)
+################################################################################
+# Target:    tint_lang_msl_writer_fuzz
+# Kind:      fuzz
+# Condition: TINT_BUILD_MSL_WRITER
+################################################################################
+tint_add_target(tint_lang_msl_writer_fuzz fuzz
+)
+
+tint_target_add_dependencies(tint_lang_msl_writer_fuzz fuzz
+  tint_api_common
+  tint_api_options
+  tint_lang_core
+  tint_lang_core_constant
+  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_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_MSL_WRITER)
+  tint_target_add_dependencies(tint_lang_msl_writer_fuzz fuzz
+    tint_lang_msl_writer
+    tint_lang_msl_writer_common
+    tint_lang_msl_writer_helpers
+  )
+endif(TINT_BUILD_MSL_WRITER)
+
+if(TINT_BUILD_WGSL_READER)
+  tint_target_add_sources(tint_lang_msl_writer_fuzz fuzz
+    "lang/msl/writer/writer_ast_fuzz.cc"
+  )
+  tint_target_add_dependencies(tint_lang_msl_writer_fuzz fuzz
+    tint_cmd_fuzz_wgsl_fuzz
+  )
+endif(TINT_BUILD_WGSL_READER)
+
 endif(TINT_BUILD_MSL_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/msl/writer/BUILD.gn b/src/tint/lang/msl/writer/BUILD.gn
index e3f3066..e26ed1f 100644
--- a/src/tint/lang/msl/writer/BUILD.gn
+++ b/src/tint/lang/msl/writer/BUILD.gn
@@ -134,3 +134,47 @@
     }
   }
 }
+if (tint_build_msl_writer) {
+  tint_fuzz_source_set("fuzz") {
+    sources = []
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/api/options",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${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/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_msl_writer) {
+      deps += [
+        "${tint_src_dir}/lang/msl/writer",
+        "${tint_src_dir}/lang/msl/writer/common",
+        "${tint_src_dir}/lang/msl/writer/helpers",
+      ]
+    }
+
+    if (tint_build_wgsl_reader) {
+      sources += [ "writer_ast_fuzz.cc" ]
+      deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
+    }
+  }
+}
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 517885a..b5991d0 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -184,7 +184,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = ast::transform::BuiltinPolyfill::Level::kClampParameters;
-        polyfills.int_div_mod = true;
+        polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
         polyfills.sign_int = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.workgroup_uniform_load = true;
diff --git a/src/tint/lang/msl/writer/common/options.h b/src/tint/lang/msl/writer/common/options.h
index 66623d9..1004293 100644
--- a/src/tint/lang/msl/writer/common/options.h
+++ b/src/tint/lang/msl/writer/common/options.h
@@ -130,6 +130,9 @@
     /// for all vertex shaders in the module.
     bool emit_vertex_point_size = false;
 
+    /// Set to `true` to disable the polyfills on integer division and modulo.
+    bool disable_polyfill_integer_div_mod = 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;
@@ -152,6 +155,7 @@
     TINT_REFLECT(disable_robustness,
                  disable_workgroup_init,
                  emit_vertex_point_size,
+                 disable_polyfill_integer_div_mod,
                  buffer_size_ubo_index,
                  fixed_sample_mask,
                  pixel_local_options,
diff --git a/src/tint/lang/msl/writer/printer/binary_test.cc b/src/tint/lang/msl/writer/printer/binary_test.cc
index 74808bf..495b156 100644
--- a/src/tint/lang/msl/writer/printer/binary_test.cc
+++ b/src/tint/lang/msl/writer/printer/binary_test.cc
@@ -38,7 +38,7 @@
 
 struct BinaryData {
     const char* result;
-    core::ir::BinaryOp op;
+    core::BinaryOp op;
 };
 inline std::ostream& operator<<(std::ostream& out, BinaryData data) {
     StringStream str;
@@ -70,22 +70,21 @@
 }
 )");
 }
-INSTANTIATE_TEST_SUITE_P(
-    MslPrinterTest,
-    MslPrinterBinaryTest,
-    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::kAnd},
-                    BinaryData{"(left | right)", core::ir::BinaryOp::kOr},
-                    BinaryData{"(left ^ right)", core::ir::BinaryOp::kXor}));
+INSTANTIATE_TEST_SUITE_P(MslPrinterTest,
+                         MslPrinterBinaryTest,
+                         testing::Values(BinaryData{"(left + right)", core::BinaryOp::kAdd},
+                                         BinaryData{"(left - right)", core::BinaryOp::kSubtract},
+                                         BinaryData{"(left * right)", core::BinaryOp::kMultiply},
+                                         BinaryData{"(left & right)", core::BinaryOp::kAnd},
+                                         BinaryData{"(left | right)", core::BinaryOp::kOr},
+                                         BinaryData{"(left ^ right)", core::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);
+        auto* bin = b.Binary(core::BinaryOp::kDivide, ty.u32(), l, r);
         b.Let("val", bin);
         b.Return(func);
     });
@@ -108,7 +107,7 @@
     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);
+        auto* bin = b.Binary(core::BinaryOp::kModulo, ty.u32(), l, r);
         b.Let("val", bin);
         b.Return(func);
     });
@@ -132,7 +131,7 @@
     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);
+        auto* bin = b.Binary(core::BinaryOp::kShiftLeft, ty.u32(), l, r);
         b.Let("val", bin);
         b.Return(func);
     });
@@ -152,7 +151,7 @@
     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);
+        auto* bin = b.Binary(core::BinaryOp::kShiftRight, ty.u32(), l, r);
         b.Let("val", bin);
         b.Return(func);
     });
@@ -193,12 +192,12 @@
 INSTANTIATE_TEST_SUITE_P(
     MslPrinterTest,
     MslPrinterBinaryBoolTest,
-    testing::Values(BinaryData{"(left == right)", core::ir::BinaryOp::kEqual},
-                    BinaryData{"(left != right)", core::ir::BinaryOp::kNotEqual},
-                    BinaryData{"(left < right)", core::ir::BinaryOp::kLessThan},
-                    BinaryData{"(left > right)", core::ir::BinaryOp::kGreaterThan},
-                    BinaryData{"(left <= right)", core::ir::BinaryOp::kLessThanEqual},
-                    BinaryData{"(left >= right)", core::ir::BinaryOp::kGreaterThanEqual}));
+    testing::Values(BinaryData{"(left == right)", core::BinaryOp::kEqual},
+                    BinaryData{"(left != right)", core::BinaryOp::kNotEqual},
+                    BinaryData{"(left < right)", core::BinaryOp::kLessThan},
+                    BinaryData{"(left > right)", core::BinaryOp::kGreaterThan},
+                    BinaryData{"(left <= right)", core::BinaryOp::kLessThanEqual},
+                    BinaryData{"(left >= right)", core::BinaryOp::kGreaterThanEqual}));
 
 // TODO(dsinclair): Needs transform
 // TODO(dsinclair): Requires `bitcast` support
@@ -228,9 +227,9 @@
 }
 
 constexpr BinaryData signed_overflow_defined_behaviour_cases[] = {
-    {"as_type<int>((as_type<uint>(left) + as_type<uint>(right)))", core::ir::BinaryOp::kAdd},
-    {"as_type<int>((as_type<uint>(left) - as_type<uint>(right)))", core::ir::BinaryOp::kSubtract},
-    {"as_type<int>((as_type<uint>(left) * as_type<uint>(right)))", core::ir::BinaryOp::kMultiply}};
+    {"as_type<int>((as_type<uint>(left) + as_type<uint>(right)))", core::BinaryOp::kAdd},
+    {"as_type<int>((as_type<uint>(left) - as_type<uint>(right)))", core::BinaryOp::kSubtract},
+    {"as_type<int>((as_type<uint>(left) * as_type<uint>(right)))", core::BinaryOp::kMultiply}};
 INSTANTIATE_TEST_SUITE_P(MslPrinterTest,
                          MslPrinterBinaryTest_SignedOverflowDefinedBehaviour,
                          testing::ValuesIn(signed_overflow_defined_behaviour_cases));
@@ -263,8 +262,8 @@
 }
 
 constexpr BinaryData shift_signed_overflow_defined_behaviour_cases[] = {
-    {"as_type<int>((as_type<uint>(left) << right))", core::ir::BinaryOp::kShiftLeft},
-    {"(left >> right)", core::ir::BinaryOp::kShiftRight}};
+    {"as_type<int>((as_type<uint>(left) << right))", core::BinaryOp::kShiftLeft},
+    {"(left >> right)", core::BinaryOp::kShiftRight}};
 INSTANTIATE_TEST_SUITE_P(MslPrinterTest,
                          MslPrinterBinaryTest_ShiftSignedOverflowDefinedBehaviour,
                          testing::ValuesIn(shift_signed_overflow_defined_behaviour_cases));
@@ -299,13 +298,13 @@
 constexpr BinaryData signed_overflow_defined_behaviour_chained_cases[] = {
     {R"(as_type<int>((as_type<uint>(as_type<int>((as_type<uint>(left) + as_type<uint>(right)))) +
     as_type<uint>(right))))",
-     core::ir::BinaryOp::kAdd},
+     core::BinaryOp::kAdd},
     {R"(as_type<int>((as_type<uint>(as_type<int>((as_type<uint>(left) - as_type<uint>(right)))) -
     as_type<uint>(right))))",
-     core::ir::BinaryOp::kSubtract},
+     core::BinaryOp::kSubtract},
     {R"(as_type<int>((as_type<uint>(as_type<int>((as_type<uint>(left) * as_type<uint>(right)))) *
     as_type<uint>(right))))",
-     core::ir::BinaryOp::kMultiply}};
+     core::BinaryOp::kMultiply}};
 INSTANTIATE_TEST_SUITE_P(MslPrinterTest,
                          MslPrinterBinaryTest_SignedOverflowDefinedBehaviour_Chained,
                          testing::ValuesIn(signed_overflow_defined_behaviour_chained_cases));
@@ -339,8 +338,8 @@
 }
 constexpr BinaryData shift_signed_overflow_defined_behaviour_chained_cases[] = {
     {R"(as_type<int>((as_type<uint>(as_type<int>((as_type<uint>(left) << right))) << right)))",
-     core::ir::BinaryOp::kShiftLeft},
-    {R"(((left >> right) >> right))", core::ir::BinaryOp::kShiftRight},
+     core::BinaryOp::kShiftLeft},
+    {R"(((left >> right) >> right))", core::BinaryOp::kShiftRight},
 };
 INSTANTIATE_TEST_SUITE_P(MslPrinterTest,
                          MslPrinterBinaryTest_ShiftSignedOverflowDefinedBehaviour_Chained,
@@ -353,7 +352,7 @@
         auto* left = b.Var("left", ty.ptr<core::AddressSpace::kFunction, f32>());
         auto* right = b.Var("right", ty.ptr<core::AddressSpace::kFunction, f32>());
 
-        auto* expr1 = b.Binary(core::ir::BinaryOp::kModulo, ty.f32(), left, right);
+        auto* expr1 = b.Binary(core::BinaryOp::kModulo, ty.f32(), left, right);
 
         b.Let("val", expr1);
     });
@@ -376,7 +375,7 @@
         auto* left = b.Var("left", ty.ptr<core::AddressSpace::kFunction, f16>());
         auto* right = b.Var("right", ty.ptr<core::AddressSpace::kFunction, f16>());
 
-        auto* expr1 = b.Binary(core::ir::BinaryOp::kModulo, ty.f16(), left, right);
+        auto* expr1 = b.Binary(core::BinaryOp::kModulo, ty.f16(), left, right);
 
         b.Let("val", expr1);
     });
@@ -397,7 +396,7 @@
         auto* left = b.Var("left", ty.ptr(core::AddressSpace::kFunction, ty.vec3<f32>()));
         auto* right = b.Var("right", ty.ptr(core::AddressSpace::kFunction, ty.vec3<f32>()));
 
-        auto* expr1 = b.Binary(core::ir::BinaryOp::kModulo, ty.vec3<f32>(), left, right);
+        auto* expr1 = b.Binary(core::BinaryOp::kModulo, ty.vec3<f32>(), left, right);
 
         b.Let("val", expr1);
     });
@@ -420,7 +419,7 @@
         auto* left = b.Var("left", ty.ptr(core::AddressSpace::kFunction, ty.vec3<f16>()));
         auto* right = b.Var("right", ty.ptr(core::AddressSpace::kFunction, ty.vec3<f16>()));
 
-        auto* expr1 = b.Binary(core::ir::BinaryOp::kModulo, ty.vec3<f16>(), left, right);
+        auto* expr1 = b.Binary(core::BinaryOp::kModulo, ty.vec3<f16>(), left, right);
 
         b.Let("val", expr1);
     });
@@ -441,7 +440,7 @@
         auto* left = b.Var("left", ty.ptr(core::AddressSpace::kFunction, ty.bool_()));
         auto* right = b.Var("right", ty.ptr(core::AddressSpace::kFunction, ty.bool_()));
 
-        auto* expr1 = b.Binary(core::ir::BinaryOp::kAdd, ty.bool_(), left, right);
+        auto* expr1 = b.Binary(core::BinaryOp::kAdd, ty.bool_(), left, right);
 
         b.Let("val", expr1);
     });
@@ -462,7 +461,7 @@
         auto* left = b.Var("left", ty.ptr(core::AddressSpace::kFunction, ty.bool_()));
         auto* right = b.Var("right", ty.ptr(core::AddressSpace::kFunction, ty.bool_()));
 
-        auto* expr1 = b.Binary(core::ir::BinaryOp::kOr, ty.bool_(), left, right);
+        auto* expr1 = b.Binary(core::BinaryOp::kOr, ty.bool_(), left, right);
 
         b.Let("val", expr1);
     });
diff --git a/src/tint/lang/msl/writer/printer/helper_test.h b/src/tint/lang/msl/writer/printer/helper_test.h
index b52e60d..410153b 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 != Success) {
+        if (auto raised = Raise(mod, {}); raised != Success) {
             err_ = raised.Failure().reason.str();
             return false;
         }
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index 6cfa838..e2950c5 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -35,14 +35,15 @@
 #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_binary.h"
 #include "src/tint/lang/core/ir/core_builtin_call.h"
+#include "src/tint/lang/core/ir/core_unary.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"
@@ -349,8 +350,8 @@
                 [&](core::ir::LoadVectorElement*) { /* inlined */ },  //
                 [&](core::ir::Swizzle*) { /* inlined */ },            //
                 [&](core::ir::Bitcast*) { /* inlined */ },            //
-                [&](core::ir::Unary*) { /* inlined */ },              //
-                [&](core::ir::Binary*) { /* inlined */ },             //
+                [&](core::ir::CoreBinary*) { /* inlined */ },         //
+                [&](core::ir::CoreUnary*) { /* inlined */ },          //
                 [&](core::ir::Load*) { /* inlined */ },               //
                 [&](core::ir::Construct*) { /* inlined */ },          //
                 [&](core::ir::Access*) { /* inlined */ },             //
@@ -365,8 +366,8 @@
             [&](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::CoreBinary* b) { EmitBinary(out, b); },                //
+                    [&](const core::ir::CoreUnary* u) { EmitUnary(out, u); },                  //
                     [&](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()); },               //
@@ -387,14 +388,17 @@
             TINT_ICE_ON_NO_MATCH);
     }
 
-    void EmitUnary(StringStream& out, const core::ir::Unary* u) {
+    void EmitUnary(StringStream& out, const core::ir::CoreUnary* u) {
         switch (u->Op()) {
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 out << "-";
                 break;
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 out << "~";
                 break;
+            default:
+                TINT_UNIMPLEMENTED() << u->Op();
+                break;
         }
         out << "(";
         EmitValue(out, u->Val());
@@ -403,8 +407,8 @@
 
     /// Emit a binary instruction
     /// @param b the binary instruction
-    void EmitBinary(StringStream& out, const core::ir::Binary* b) {
-        if (b->Op() == core::ir::BinaryOp::kEqual) {
+    void EmitBinary(StringStream& out, const core::ir::CoreBinary* b) {
+        if (b->Op() == core::BinaryOp::kEqual) {
             auto* rhs = b->RHS()->As<core::ir::Constant>();
             if (rhs && rhs->Type()->Is<core::type::Bool>() &&
                 rhs->Value()->ValueAs<bool>() == false) {
@@ -418,38 +422,42 @@
 
         auto kind = [&] {
             switch (b->Op()) {
-                case core::ir::BinaryOp::kAdd:
+                case core::BinaryOp::kAdd:
                     return "+";
-                case core::ir::BinaryOp::kSubtract:
+                case core::BinaryOp::kSubtract:
                     return "-";
-                case core::ir::BinaryOp::kMultiply:
+                case core::BinaryOp::kMultiply:
                     return "*";
-                case core::ir::BinaryOp::kDivide:
+                case core::BinaryOp::kDivide:
                     return "/";
-                case core::ir::BinaryOp::kModulo:
+                case core::BinaryOp::kModulo:
                     return "%";
-                case core::ir::BinaryOp::kAnd:
+                case core::BinaryOp::kAnd:
                     return "&";
-                case core::ir::BinaryOp::kOr:
+                case core::BinaryOp::kOr:
                     return "|";
-                case core::ir::BinaryOp::kXor:
+                case core::BinaryOp::kXor:
                     return "^";
-                case core::ir::BinaryOp::kEqual:
+                case core::BinaryOp::kEqual:
                     return "==";
-                case core::ir::BinaryOp::kNotEqual:
+                case core::BinaryOp::kNotEqual:
                     return "!=";
-                case core::ir::BinaryOp::kLessThan:
+                case core::BinaryOp::kLessThan:
                     return "<";
-                case core::ir::BinaryOp::kGreaterThan:
+                case core::BinaryOp::kGreaterThan:
                     return ">";
-                case core::ir::BinaryOp::kLessThanEqual:
+                case core::BinaryOp::kLessThanEqual:
                     return "<=";
-                case core::ir::BinaryOp::kGreaterThanEqual:
+                case core::BinaryOp::kGreaterThanEqual:
                     return ">=";
-                case core::ir::BinaryOp::kShiftLeft:
+                case core::BinaryOp::kShiftLeft:
                     return "<<";
-                case core::ir::BinaryOp::kShiftRight:
+                case core::BinaryOp::kShiftRight:
                     return ">>";
+                case core::BinaryOp::kLogicalAnd:
+                    return "&&";
+                case core::BinaryOp::kLogicalOr:
+                    return "||";
             }
             return "<error>";
         };
diff --git a/src/tint/lang/msl/writer/raise/raise.cc b/src/tint/lang/msl/writer/raise/raise.cc
index 2d970cc..63591e7 100644
--- a/src/tint/lang/msl/writer/raise/raise.cc
+++ b/src/tint/lang/msl/writer/raise/raise.cc
@@ -43,7 +43,7 @@
 #include "src/tint/lang/msl/writer/common/option_helpers.h"
 #include "src/tint/lang/msl/writer/raise/builtin_polyfill.h"
 
-namespace tint::msl::writer::raise {
+namespace tint::msl::writer {
 
 Result<SuccessType> Raise(core::ir::Module& module, const Options& options) {
 #define RUN_TRANSFORM(name, ...)                   \
@@ -61,7 +61,7 @@
 
     {
         core::ir::transform::BinaryPolyfillConfig binary_polyfills{};
-        binary_polyfills.int_div_mod = true;
+        binary_polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
         binary_polyfills.bitshift_modulo = true;  // crbug.com/tint/1543
         RUN_TRANSFORM(core::ir::transform::BinaryPolyfill, binary_polyfills);
     }
@@ -103,9 +103,9 @@
     RUN_TRANSFORM(core::ir::transform::DemoteToHelper);
 
     RUN_TRANSFORM(core::ir::transform::ValueToLet);
-    RUN_TRANSFORM(BuiltinPolyfill);
+    RUN_TRANSFORM(raise::BuiltinPolyfill);
 
     return Success;
 }
 
-}  // namespace tint::msl::writer::raise
+}  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/raise/raise.h b/src/tint/lang/msl/writer/raise/raise.h
index 256925d..a4d5895 100644
--- a/src/tint/lang/msl/writer/raise/raise.h
+++ b/src/tint/lang/msl/writer/raise/raise.h
@@ -39,7 +39,7 @@
 class Module;
 }  // namespace tint::core::ir
 
-namespace tint::msl::writer::raise {
+namespace tint::msl::writer {
 
 /// Raise a core IR module to the MSL dialect of the IR.
 /// @param module the core IR module to raise to MSL dialect
@@ -47,6 +47,6 @@
 /// @returns success or failure
 Result<SuccessType> Raise(core::ir::Module& module, const Options& options);
 
-}  // namespace tint::msl::writer::raise
+}  // namespace tint::msl::writer
 
 #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 e997959..c0f9d41 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -53,7 +53,7 @@
     Output output;
 
     // Raise from core-dialect to MSL-dialect.
-    if (auto res = raise::Raise(ir, options); res != Success) {
+    if (auto res = Raise(ir, options); res != Success) {
         return res.Failure();
     }
 
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/msl/writer/writer_ast_fuzz.cc
similarity index 64%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/msl/writer/writer_ast_fuzz.cc
index 421db7b..937a4a8 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/msl/writer/writer_ast_fuzz.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +25,27 @@
 // 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/writer.h"
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
 
-#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/cmd/fuzz/wgsl/fuzz.h"
+#include "src/tint/lang/msl/writer/helpers/generate_bindings.h"
+#include "src/tint/lang/msl/writer/writer.h"
+#include "src/tint/lang/wgsl/ast/module.h"
 
-namespace tint::spirv::writer {
+namespace tint::msl::writer {
 namespace {
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
+void ASTFuzzer(const tint::Program& program, Options options) {
+    if (program.AST().HasOverrides()) {
         return;
     }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
-    }
+
+    options.bindings = GenerateBindings(program);
+
+    [[maybe_unused]] auto res = tint::msl::writer::Generate(program, options);
 }
 
 }  // namespace
-}  // namespace tint::spirv::writer
+}  // namespace tint::msl::writer
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+TINT_WGSL_PROGRAM_FUZZER(tint::msl::writer::ASTFuzzer);
diff --git a/src/tint/lang/spirv/intrinsic/BUILD.bazel b/src/tint/lang/spirv/intrinsic/BUILD.bazel
index 6ebd70e..7b2f0e8 100644
--- a/src/tint/lang/spirv/intrinsic/BUILD.bazel
+++ b/src/tint/lang/spirv/intrinsic/BUILD.bazel
@@ -60,6 +60,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/spirv/intrinsic/BUILD.cmake b/src/tint/lang/spirv/intrinsic/BUILD.cmake
index 92e0bc5..541c0f5 100644
--- a/src/tint/lang/spirv/intrinsic/BUILD.cmake
+++ b/src/tint/lang/spirv/intrinsic/BUILD.cmake
@@ -59,6 +59,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/spirv/intrinsic/BUILD.gn b/src/tint/lang/spirv/intrinsic/BUILD.gn
index eb892d0..e99baee 100644
--- a/src/tint/lang/spirv/intrinsic/BUILD.gn
+++ b/src/tint/lang/spirv/intrinsic/BUILD.gn
@@ -59,6 +59,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/spirv/reader/parser/BUILD.bazel b/src/tint/lang/spirv/reader/parser/BUILD.bazel
index 6990fb1..d41f46f 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/parser/BUILD.bazel
@@ -82,6 +82,7 @@
     "constant_test.cc",
     "function_test.cc",
     "helper_test.h",
+    "memory_test.cc",
     "struct_test.cc",
     "var_test.cc",
   ],
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.cmake b/src/tint/lang/spirv/reader/parser/BUILD.cmake
index ac508eb..e82f651 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/parser/BUILD.cmake
@@ -88,6 +88,7 @@
   lang/spirv/reader/parser/constant_test.cc
   lang/spirv/reader/parser/function_test.cc
   lang/spirv/reader/parser/helper_test.h
+  lang/spirv/reader/parser/memory_test.cc
   lang/spirv/reader/parser/struct_test.cc
   lang/spirv/reader/parser/var_test.cc
 )
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.gn b/src/tint/lang/spirv/reader/parser/BUILD.gn
index 8ffc520..430c576 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/parser/BUILD.gn
@@ -89,6 +89,7 @@
         "constant_test.cc",
         "function_test.cc",
         "helper_test.h",
+        "memory_test.cc",
         "struct_test.cc",
         "var_test.cc",
       ]
diff --git a/src/tint/lang/spirv/reader/parser/function_test.cc b/src/tint/lang/spirv/reader/parser/function_test.cc
index b9ec90a..a66b1e0 100644
--- a/src/tint/lang/spirv/reader/parser/function_test.cc
+++ b/src/tint/lang/spirv/reader/parser/function_test.cc
@@ -398,6 +398,56 @@
 )");
 }
 
+TEST_F(SpirvParserTest, FunctionCall_ReturnValueChain) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+    %fn_type = OpTypeFunction %bool
+  %main_type = OpTypeFunction %void
+
+        %bar = OpFunction %bool None %fn_type
+  %bar_start = OpLabel
+               OpReturnValue %true
+               OpFunctionEnd
+
+        %foo = OpFunction %bool None %fn_type
+  %foo_start = OpLabel
+       %call = OpFunctionCall %bool %foo
+               OpReturnValue %call
+               OpFunctionEnd
+
+       %main = OpFunction %void None %main_type
+ %main_start = OpLabel
+          %1 = OpFunctionCall %bool %bar
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%1 = func():bool -> %b1 {
+  %b1 = block {
+    ret true
+  }
+}
+%2 = func():bool -> %b2 {
+  %b2 = block {
+    %3:bool = call %2
+    ret %3
+  }
+}
+%main = @compute @workgroup_size(1, 1, 1) func():void -> %b3 {
+  %b3 = block {
+    %5:bool = call %1
+    ret
+  }
+}
+)");
+}
+
 TEST_F(SpirvParserTest, FunctionCall_ParamAndReturnValue) {
     EXPECT_IR(R"(
                OpCapability Shader
diff --git a/src/tint/lang/spirv/reader/parser/memory_test.cc b/src/tint/lang/spirv/reader/parser/memory_test.cc
new file mode 100644
index 0000000..2031696
--- /dev/null
+++ b/src/tint/lang/spirv/reader/parser/memory_test.cc
@@ -0,0 +1,766 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/parser/helper_test.h"
+
+namespace tint::spirv::reader {
+namespace {
+
+TEST_F(SpirvParserTest, Load_Scalar) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+    %u32_ptr = OpTypePointer Function %u32
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %u32_ptr Function
+       %load = OpLoad %u32 %var
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, u32, read_write> = var
+    %3:u32 = load %2
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_Vector) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %vec4u = OpTypeVector %u32 4
+  %vec4u_ptr = OpTypePointer Function %vec4u
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %vec4u_ptr Function
+       %load = OpLoad %vec4u %var
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, vec4<u32>, read_write> = var
+    %3:vec4<u32> = load %2
+    ret
+  }
+)");
+}
+
+// TODO(jrprice): We need to handle pointer-to-vector component somewhere.
+TEST_F(SpirvParserTest, DISABLED_Load_VectorComponent) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %vec4u = OpTypeVector %u32 4
+    %u32_ptr = OpTypePointer Function %u32
+  %vec4u_ptr = OpTypePointer Function %vec4u
+      %u32_2 = OpConstant %u32 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %vec4u_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_2
+       %load = OpLoad %u32 %access
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 2u
+    %4:u32 = load %3
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_Matrix) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %f32 = OpTypeFloat 32
+      %vec4f = OpTypeVector %f32 4
+    %mat3x4f = OpTypeMatrix %vec4f 3
+%mat3x4f_ptr = OpTypePointer Function %mat3x4f
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %mat3x4f_ptr Function
+       %load = OpLoad %mat3x4f %var
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, mat3x4<f32>, read_write> = var
+    %3:mat3x4<f32> = load %2
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_MatrixColumn) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %f32 = OpTypeFloat 32
+      %vec4f = OpTypeVector %f32 4
+    %mat3x4f = OpTypeMatrix %vec4f 3
+  %vec4f_ptr = OpTypePointer Function %vec4f
+%mat3x4f_ptr = OpTypePointer Function %mat3x4f
+      %u32_2 = OpConstant %u32 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %mat3x4f_ptr Function
+     %access = OpAccessChain %vec4f_ptr %var %u32_2
+       %load = OpLoad %vec4f %access
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, mat3x4<f32>, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %2, 2u
+    %4:vec4<f32> = load %3
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_Array) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_4 = OpConstant %u32 4
+        %arr = OpTypeArray %u32 %u32_4
+    %arr_ptr = OpTypePointer Function %arr
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_ptr Function
+       %load = OpLoad %arr %var
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<u32, 4>, read_write> = var
+    %3:array<u32, 4> = load %2
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_ArrayElement) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_2 = OpConstant %u32 2
+      %u32_4 = OpConstant %u32 4
+        %arr = OpTypeArray %u32 %u32_4
+    %u32_ptr = OpTypePointer Function %u32
+    %arr_ptr = OpTypePointer Function %arr
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_2
+       %load = OpLoad %u32 %access
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<u32, 4>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 2u
+    %4:u32 = load %3
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_Struct) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %str = OpTypeStruct %u32 %u32
+    %str_ptr = OpTypePointer Function %str
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %str_ptr Function
+       %load = OpLoad %str %var
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, tint_symbol_2, read_write> = var
+    %3:tint_symbol_2 = load %2
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Load_StructMember) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_1 = OpConstant %u32 1
+        %str = OpTypeStruct %u32 %u32
+    %u32_ptr = OpTypePointer Function %u32
+    %str_ptr = OpTypePointer Function %str
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %str_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_1
+       %load = OpLoad %u32 %access
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, tint_symbol_2, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 1u
+    %4:u32 = load %3
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_Scalar) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+     %u32_42 = OpConstant %u32 42
+    %u32_ptr = OpTypePointer Function %u32
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %u32_ptr Function
+               OpStore %var %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, u32, read_write> = var
+    store %2, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_Vector) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %vec4u = OpTypeVector %u32 4
+       %null = OpConstantNull %vec4u
+  %vec4u_ptr = OpTypePointer Function %vec4u
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %vec4u_ptr Function
+               OpStore %var %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, vec4<u32>, read_write> = var
+    store %2, vec4<u32>(0u)
+    ret
+  }
+)");
+}
+
+// TODO(jrprice): We need to handle pointer-to-vector component somewhere.
+TEST_F(SpirvParserTest, DISABLED_Store_VectorComponent) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+     %u32_42 = OpConstantInt %u32 42
+      %vec4u = OpTypeVector %u32 4
+    %u32_ptr = OpTypePointer Function %u32
+  %vec4u_ptr = OpTypePointer Function %vec4u
+      %u32_2 = OpConstant %u32 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %vec4u_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_2
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, vec4<u32>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 2u
+    store %3, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_Matrix) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %f32 = OpTypeFloat 32
+      %vec4f = OpTypeVector %f32 4
+    %mat3x4f = OpTypeMatrix %vec4f 3
+       %null = OpConstantNull %mat3x4f
+%mat3x4f_ptr = OpTypePointer Function %mat3x4f
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %mat3x4f_ptr Function
+               OpStore %var %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, mat3x4<f32>, read_write> = var
+    store %2, mat3x4<f32>(vec4<f32>(0.0f))
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_MatrixColumn) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %f32 = OpTypeFloat 32
+      %vec4f = OpTypeVector %f32 4
+    %mat3x4f = OpTypeMatrix %vec4f 3
+       %null = OpConstantNull %vec4f
+  %vec4f_ptr = OpTypePointer Function %vec4f
+%mat3x4f_ptr = OpTypePointer Function %mat3x4f
+      %u32_2 = OpConstant %u32 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %mat3x4f_ptr Function
+     %access = OpAccessChain %vec4f_ptr %var %u32_2
+               OpStore %access %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, mat3x4<f32>, read_write> = var
+    %3:ptr<function, vec4<f32>, read_write> = access %2, 2u
+    store %3, vec4<f32>(0.0f)
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_Array) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_4 = OpConstant %u32 4
+        %arr = OpTypeArray %u32 %u32_4
+       %null = OpConstantNull %arr
+    %arr_ptr = OpTypePointer Function %arr
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_ptr Function
+               OpStore %var %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<u32, 4>, read_write> = var
+    store %2, array<u32, 4>(0u)
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_ArrayElement) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_2 = OpConstant %u32 2
+      %u32_4 = OpConstant %u32 4
+     %u32_42 = OpConstant %u32 42
+        %arr = OpTypeArray %u32 %u32_4
+    %u32_ptr = OpTypePointer Function %u32
+    %arr_ptr = OpTypePointer Function %arr
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_2
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<u32, 4>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 2u
+    store %3, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_Struct) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %str = OpTypeStruct %u32 %u32
+       %null = OpConstantNull %str
+    %str_ptr = OpTypePointer Function %str
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %str_ptr Function
+               OpStore %var %null
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, tint_symbol_2, read_write> = var
+    store %2, tint_symbol_2(0u)
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Store_StructMember) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_1 = OpConstant %u32 1
+     %u32_42 = OpConstant %u32 42
+        %str = OpTypeStruct %u32 %u32
+    %u32_ptr = OpTypePointer Function %u32
+    %str_ptr = OpTypePointer Function %str
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %str_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_1
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, tint_symbol_2, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 1u
+    store %3, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Access_Nested_SingleAccessInstruction) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_1 = OpConstant %u32 1
+      %u32_2 = OpConstant %u32 2
+      %u32_3 = OpConstant %u32 3
+      %u32_4 = OpConstant %u32 4
+     %u32_42 = OpConstant %u32 42
+  %arr_inner = OpTypeArray %u32 %u32_4
+        %str = OpTypeStruct %arr_inner %arr_inner %arr_inner %arr_inner
+  %arr_outer = OpTypeArray %str %u32_4
+    %u32_ptr = OpTypePointer Function %u32
+%arr_outer_ptr = OpTypePointer Function %arr_outer
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_outer_ptr Function
+     %access = OpAccessChain %u32_ptr %var %u32_1 %u32_2 %u32_3
+       %load = OpLoad %u32 %access
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<tint_symbol_4, 4>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 1u, 2u, 3u
+    %4:u32 = load %3
+    store %3, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Access_Nested_SeparateAccessInstructions) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_1 = OpConstant %u32 1
+      %u32_2 = OpConstant %u32 2
+      %u32_3 = OpConstant %u32 3
+      %u32_4 = OpConstant %u32 4
+     %u32_42 = OpConstant %u32 42
+  %arr_inner = OpTypeArray %u32 %u32_4
+        %str = OpTypeStruct %arr_inner %arr_inner %arr_inner %arr_inner
+  %arr_outer = OpTypeArray %str %u32_4
+    %u32_ptr = OpTypePointer Function %u32
+%arr_inner_ptr = OpTypePointer Function %arr_inner
+    %str_ptr = OpTypePointer Function %str
+%arr_outer_ptr = OpTypePointer Function %arr_outer
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_outer_ptr Function
+   %access_1 = OpAccessChain %str_ptr %var %u32_1
+   %access_2 = OpAccessChain %arr_inner_ptr %access_1 %u32_2
+   %access_3 = OpAccessChain %u32_ptr %access_2 %u32_3
+       %load = OpLoad %u32 %access_3
+               OpStore %access_3 %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<tint_symbol_4, 4>, read_write> = var
+    %3:ptr<function, tint_symbol_4, read_write> = access %2, 1u
+    %4:ptr<function, array<u32, 4>, read_write> = access %3, 2u
+    %5:ptr<function, u32, read_write> = access %4, 3u
+    %6:u32 = load %5
+    store %5, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Access_NoIndices) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+    %u32_ptr = OpTypePointer Function %u32
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %u32_ptr Function
+   %access_1 = OpAccessChain %u32_ptr %var
+   %access_2 = OpAccessChain %u32_ptr %access_1
+       %load = OpLoad %u32 %access_2
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, u32, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2
+    %4:ptr<function, u32, read_write> = access %3
+    %5:u32 = load %4
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, Access_SignedIndices) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+        %i32 = OpTypeInt 32 1
+      %i32_1 = OpConstant %i32 1
+      %u32_2 = OpConstant %u32 2
+      %i32_3 = OpConstant %i32 3
+      %u32_4 = OpConstant %u32 4
+     %u32_42 = OpConstant %u32 42
+  %arr_inner = OpTypeArray %u32 %u32_4
+        %str = OpTypeStruct %arr_inner %arr_inner %arr_inner %arr_inner
+  %arr_outer = OpTypeArray %str %u32_4
+    %u32_ptr = OpTypePointer Function %u32
+%arr_outer_ptr = OpTypePointer Function %arr_outer
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_outer_ptr Function
+     %access = OpAccessChain %u32_ptr %var %i32_1 %u32_2 %i32_3
+       %load = OpLoad %u32 %access
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<tint_symbol_4, 4>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 1i, 2u, 3i
+    %4:u32 = load %3
+    store %3, 42u
+    ret
+  }
+)");
+}
+
+TEST_F(SpirvParserTest, InBoundsAccessChain) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+        %u32 = OpTypeInt 32 0
+      %u32_1 = OpConstant %u32 1
+      %u32_2 = OpConstant %u32 2
+      %u32_3 = OpConstant %u32 3
+      %u32_4 = OpConstant %u32 4
+     %u32_42 = OpConstant %u32 42
+  %arr_inner = OpTypeArray %u32 %u32_4
+        %str = OpTypeStruct %arr_inner %arr_inner %arr_inner %arr_inner
+  %arr_outer = OpTypeArray %str %u32_4
+    %u32_ptr = OpTypePointer Function %u32
+%arr_outer_ptr = OpTypePointer Function %arr_outer
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+        %var = OpVariable %arr_outer_ptr Function
+     %access = OpInBoundsAccessChain %u32_ptr %var %u32_1 %u32_2 %u32_3
+       %load = OpLoad %u32 %access
+               OpStore %access %u32_42
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+  %b1 = block {
+    %2:ptr<function, array<tint_symbol_4, 4>, read_write> = var
+    %3:ptr<function, u32, read_write> = access %2, 1u, 2u, 3u
+    %4:u32 = load %3
+    store %3, 42u
+    ret
+  }
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index f9057b3..56a3d47 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -76,7 +76,10 @@
             return Failure("failed to build the internal representation of the module");
         }
 
-        EmitModuleScopeVariables();
+        {
+            TINT_SCOPED_ASSIGNMENT(current_block_, ir_.root_block);
+            EmitModuleScopeVariables();
+        }
 
         EmitFunctions();
 
@@ -316,11 +319,27 @@
         return nullptr;
     }
 
+    /// Register an IR value for a SPIR-V result ID.
+    /// @param result_id the SPIR-V result ID
+    /// @param value the IR value
+    void AddValue(uint32_t result_id, core::ir::Value* value) { values_.Add(result_id, value); }
+
+    /// Emit an instruction to the current block.
+    /// @param inst the instruction to emit
+    /// @param result_id an optional SPIR-V result ID to register the instruction result for
+    void Emit(core::ir::Instruction* inst, uint32_t result_id = 0) {
+        current_block_->Append(inst);
+        if (result_id != 0) {
+            TINT_ASSERT_OR_RETURN(inst->Results().Length() == 1u);
+            AddValue(result_id, inst->Result(0));
+        }
+    }
+
     /// Emit the module-scope variables.
     void EmitModuleScopeVariables() {
         for (auto& inst : spirv_context_->module()->types_values()) {
             if (inst.opcode() == spv::Op::OpVariable) {
-                ir_.root_block->Append(EmitVar(inst));
+                EmitVar(inst);
             }
         }
     }
@@ -394,19 +413,31 @@
     /// @param dst the Tint IR block to append to
     /// @param src the SPIR-V block to emit
     void EmitBlock(core::ir::Block* dst, const spvtools::opt::BasicBlock& src) {
+        TINT_SCOPED_ASSIGNMENT(current_block_, dst);
         for (auto& inst : src) {
             switch (inst.opcode()) {
+                case spv::Op::OpAccessChain:
+                case spv::Op::OpInBoundsAccessChain:
+                    EmitAccess(inst);
+                    break;
                 case spv::Op::OpFunctionCall:
-                    dst->Append(EmitFunctionCall(inst));
+                    EmitFunctionCall(inst);
+                    break;
+                case spv::Op::OpLoad:
+                    Emit(b_.Load(Value(inst.GetSingleWordOperand(2))), inst.result_id());
                     break;
                 case spv::Op::OpReturn:
-                    dst->Append(b_.Return(current_function_));
+                    Emit(b_.Return(current_function_));
                     break;
                 case spv::Op::OpReturnValue:
-                    dst->Append(b_.Return(current_function_, Value(inst.GetSingleWordOperand(0))));
+                    Emit(b_.Return(current_function_, Value(inst.GetSingleWordOperand(0))));
+                    break;
+                case spv::Op::OpStore:
+                    Emit(b_.Store(Value(inst.GetSingleWordOperand(0)),
+                                  Value(inst.GetSingleWordOperand(1))));
                     break;
                 case spv::Op::OpVariable:
-                    dst->Append(EmitVar(inst));
+                    EmitVar(inst);
                     break;
                 default:
                     TINT_UNIMPLEMENTED()
@@ -415,25 +446,60 @@
         }
     }
 
+    /// @param inst the SPIR-V instruction for OpAccessChain
+    void EmitAccess(const spvtools::opt::Instruction& inst) {
+        Vector<core::ir::Value*, 4> indices;
+        for (uint32_t i = 3; i < inst.NumOperandWords(); i++) {
+            indices.Push(Value(inst.GetSingleWordOperand(i)));
+        }
+        auto* base = Value(inst.GetSingleWordOperand(2));
+        auto* access = b_.Access(Type(inst.type_id()), base, std::move(indices));
+        Emit(access, inst.result_id());
+    }
+
     /// @param inst the SPIR-V instruction for OpFunctionCall
-    /// @returns the Tint IR instruction
-    core::ir::UserCall* EmitFunctionCall(const spvtools::opt::Instruction& inst) {
+    void EmitFunctionCall(const spvtools::opt::Instruction& inst) {
         // TODO(crbug.com/tint/1907): Capture result.
         Vector<core::ir::Value*, 4> args;
         for (uint32_t i = 3; i < inst.NumOperandWords(); i++) {
             args.Push(Value(inst.GetSingleWordOperand(i)));
         }
-        return b_.Call(Function(inst.GetSingleWordInOperand(0)), std::move(args));
+        Emit(b_.Call(Function(inst.GetSingleWordInOperand(0)), std::move(args)), inst.result_id());
     }
 
     /// @param inst the SPIR-V instruction for OpVariable
-    /// @returns the Tint IR instruction
-    core::ir::Var* EmitVar(const spvtools::opt::Instruction& inst) {
+    void EmitVar(const spvtools::opt::Instruction& inst) {
         auto* var = b_.Var(Type(inst.type_id())->As<core::type::Pointer>());
         if (inst.NumOperands() > 3) {
             var->SetInitializer(Value(inst.GetSingleWordOperand(3)));
         }
-        return var;
+
+        // Handle decorations.
+        std::optional<uint32_t> group;
+        std::optional<uint32_t> binding;
+        for (auto* deco :
+             spirv_context_->get_decoration_mgr()->GetDecorationsFor(inst.result_id(), false)) {
+            auto d = deco->GetSingleWordOperand(1);
+            switch (spv::Decoration(d)) {
+                case spv::Decoration::NonWritable:
+                    break;
+                case spv::Decoration::DescriptorSet:
+                    group = deco->GetSingleWordOperand(2);
+                    break;
+                case spv::Decoration::Binding:
+                    binding = deco->GetSingleWordOperand(2);
+                    break;
+                default:
+                    TINT_UNIMPLEMENTED() << "unhandled decoration " << d;
+                    break;
+            }
+        }
+        if (group || binding) {
+            TINT_ASSERT(group && binding);
+            var->SetBindingPoint(group.value(), binding.value());
+        }
+
+        Emit(var, inst.result_id());
     }
 
   private:
@@ -446,6 +512,8 @@
 
     /// The Tint IR function that is currently being emitted.
     core::ir::Function* current_function_ = nullptr;
+    /// The Tint IR block that is currently being emitted.
+    core::ir::Block* current_block_ = nullptr;
     /// A map from a SPIR-V type declaration result ID to the corresponding Tint type object.
     Hashmap<const spvtools::opt::analysis::Type*, const core::type::Type*, 16> types_;
     /// A map from a SPIR-V function definition result ID to the corresponding Tint function object.
diff --git a/src/tint/lang/spirv/reader/parser/var_test.cc b/src/tint/lang/spirv/reader/parser/var_test.cc
index 28d2a66..b2963dd 100644
--- a/src/tint/lang/spirv/reader/parser/var_test.cc
+++ b/src/tint/lang/spirv/reader/parser/var_test.cc
@@ -150,6 +150,9 @@
                OpExecutionMode %1 LocalSize 1 1 1
                OpDecorate %str Block
                OpMemberDecorate %str 0 Offset 0
+               OpDecorate %6 NonWritable
+               OpDecorate %6 DescriptorSet 1
+               OpDecorate %6 Binding 2
        %void = OpTypeVoid
        %uint = OpTypeInt 32 0
         %str = OpTypeStruct %uint
@@ -167,7 +170,7 @@
 }
 
 %b1 = block {  # root
-  %1:ptr<uniform, tint_symbol_1, read_write> = var
+  %1:ptr<uniform, tint_symbol_1, read_write> = var @binding_point(1, 2)
 }
 
 %main = @compute @workgroup_size(1, 1, 1) func():void -> %b2 {
diff --git a/src/tint/lang/spirv/type/BUILD.bazel b/src/tint/lang/spirv/type/BUILD.bazel
index 9bbee2d..53c59e9 100644
--- a/src/tint/lang/spirv/type/BUILD.bazel
+++ b/src/tint/lang/spirv/type/BUILD.bazel
@@ -56,6 +56,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/spirv/type/BUILD.cmake b/src/tint/lang/spirv/type/BUILD.cmake
index 92c1ede..7d21774 100644
--- a/src/tint/lang/spirv/type/BUILD.cmake
+++ b/src/tint/lang/spirv/type/BUILD.cmake
@@ -55,6 +55,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/spirv/type/BUILD.gn b/src/tint/lang/spirv/type/BUILD.gn
index 8c03245..2834593 100644
--- a/src/tint/lang/spirv/type/BUILD.gn
+++ b/src/tint/lang/spirv/type/BUILD.gn
@@ -55,6 +55,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/spirv/writer/BUILD.cmake b/src/tint/lang/spirv/writer/BUILD.cmake
index 3700f48..0afdfec 100644
--- a/src/tint/lang/spirv/writer/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/BUILD.cmake
@@ -232,7 +232,7 @@
 # Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_fuzz fuzz
-  lang/spirv/writer/writer_fuzz.cc
+  lang/spirv/writer/writer_ir_fuzz.cc
 )
 
 tint_target_add_dependencies(tint_lang_spirv_writer_fuzz fuzz
@@ -244,6 +244,7 @@
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
+  tint_lang_wgsl_features
   tint_lang_wgsl_helpers
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
@@ -283,6 +284,7 @@
 if(TINT_BUILD_WGSL_READER)
   tint_target_add_sources(tint_lang_spirv_writer_fuzz fuzz
     "lang/spirv/writer/ast_writer_fuzz.cc"
+    "lang/spirv/writer/writer_ast_fuzz.cc"
   )
   tint_target_add_dependencies(tint_lang_spirv_writer_fuzz fuzz
     tint_cmd_fuzz_wgsl_fuzz
diff --git a/src/tint/lang/spirv/writer/BUILD.gn b/src/tint/lang/spirv/writer/BUILD.gn
index 120b4f4..02f722a 100644
--- a/src/tint/lang/spirv/writer/BUILD.gn
+++ b/src/tint/lang/spirv/writer/BUILD.gn
@@ -205,7 +205,7 @@
 }
 if (tint_build_spv_writer) {
   tint_fuzz_source_set("fuzz") {
-    sources = [ "writer_fuzz.cc" ]
+    sources = [ "writer_ir_fuzz.cc" ]
     deps = [
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/cmd/fuzz/ir:fuzz",
@@ -215,6 +215,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/helpers",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
@@ -251,7 +252,10 @@
     }
 
     if (tint_build_wgsl_reader) {
-      sources += [ "ast_writer_fuzz.cc" ]
+      sources += [
+        "ast_writer_fuzz.cc",
+        "writer_ast_fuzz.cc",
+      ]
       deps += [ "${tint_src_dir}/cmd/fuzz/wgsl:fuzz" ]
     }
   }
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index 31a300e..2b3c5fa 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -28,8 +28,6 @@
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
 
 #include <unordered_map>
-#include <utility>
-#include <vector>
 
 #include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
 #include "src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h"
@@ -140,7 +138,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = ast::transform::BuiltinPolyfill::Level::kClampParameters;
-        polyfills.int_div_mod = true;
+        polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
         polyfills.saturate = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.quantize_to_vec_f16 = true;  // crbug.com/tint/1741
diff --git a/src/tint/lang/spirv/writer/binary_test.cc b/src/tint/lang/spirv/writer/binary_test.cc
index 607b04b..a14b39b 100644
--- a/src/tint/lang/spirv/writer/binary_test.cc
+++ b/src/tint/lang/spirv/writer/binary_test.cc
@@ -38,10 +38,38 @@
 
 /// A parameterized test case.
 struct BinaryTestCase {
-    /// The element type to test.
-    TestElementType type;
+    BinaryTestCase(TestElementType type_,
+                   core::BinaryOp op_,
+                   std::string spirv_inst_,
+                   std::string spirv_type_name_)
+        : res_type(type_),
+          lhs_type(type_),
+          rhs_type(type_),
+          op(op_),
+          spirv_inst(spirv_inst_),
+          spirv_type_name(spirv_type_name_) {}
+
+    BinaryTestCase(TestElementType res_type_,
+                   TestElementType lhs_type_,
+                   TestElementType rhs_type_,
+                   core::BinaryOp op_,
+                   std::string spirv_inst_,
+                   std::string spirv_type_name_)
+        : res_type(res_type_),
+          lhs_type(lhs_type_),
+          rhs_type(rhs_type_),
+          op(op_),
+          spirv_inst(spirv_inst_),
+          spirv_type_name(spirv_type_name_) {}
+
+    /// The result type of the binary op.
+    TestElementType res_type;
+    /// The LHS type of the binary op.
+    TestElementType lhs_type;
+    /// The RHS type of the binary op.
+    TestElementType rhs_type;
     /// The binary operation.
-    core::ir::BinaryOp op;
+    core::BinaryOp op;
     /// The expected SPIR-V instruction.
     std::string spirv_inst;
     /// The expected SPIR-V result type name.
@@ -54,9 +82,9 @@
 
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
-        auto* lhs = MakeScalarValue(params.type);
-        auto* rhs = MakeScalarValue(params.type);
-        auto* result = b.Binary(params.op, MakeScalarType(params.type), lhs, rhs);
+        auto* lhs = MakeScalarValue(params.lhs_type);
+        auto* rhs = MakeScalarValue(params.rhs_type);
+        auto* result = b.Binary(params.op, MakeScalarType(params.res_type), lhs, rhs);
         b.Return(func);
         mod.SetName(result, "result");
     });
@@ -69,9 +97,9 @@
 
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
-        auto* lhs = MakeVectorValue(params.type);
-        auto* rhs = MakeVectorValue(params.type);
-        auto* result = b.Binary(params.op, MakeVectorType(params.type), lhs, rhs);
+        auto* lhs = MakeVectorValue(params.lhs_type);
+        auto* rhs = MakeVectorValue(params.rhs_type);
+        auto* result = b.Binary(params.op, MakeVectorType(params.res_type), lhs, rhs);
         b.Return(func);
         mod.SetName(result, "result");
     });
@@ -82,48 +110,49 @@
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_I32,
     Arithmetic_Bitwise,
-    testing::Values(
-        BinaryTestCase{kI32, core::ir::BinaryOp::kAdd, "OpIAdd", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kSubtract, "OpISub", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kMultiply, "OpIMul", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kAnd, "OpBitwiseAnd", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kOr, "OpBitwiseOr", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kXor, "OpBitwiseXor", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kShiftLeft, "OpShiftLeftLogical", "int"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kShiftRight, "OpShiftRightArithmetic", "int"}));
+    testing::Values(BinaryTestCase{kI32, core::BinaryOp::kAdd, "OpIAdd", "int"},
+                    BinaryTestCase{kI32, core::BinaryOp::kSubtract, "OpISub", "int"},
+                    BinaryTestCase{kI32, core::BinaryOp::kMultiply, "OpIMul", "int"},
+                    BinaryTestCase{kI32, core::BinaryOp::kAnd, "OpBitwiseAnd", "int"},
+                    BinaryTestCase{kI32, core::BinaryOp::kOr, "OpBitwiseOr", "int"},
+                    BinaryTestCase{kI32, core::BinaryOp::kXor, "OpBitwiseXor", "int"},
+                    BinaryTestCase{kI32, kI32, kU32, core::BinaryOp::kShiftLeft,
+                                   "OpShiftLeftLogical", "int"},
+                    BinaryTestCase{kI32, kI32, kU32, core::BinaryOp::kShiftRight,
+                                   "OpShiftRightArithmetic", "int"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_U32,
     Arithmetic_Bitwise,
-    testing::Values(
-        BinaryTestCase{kU32, core::ir::BinaryOp::kAdd, "OpIAdd", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kSubtract, "OpISub", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kMultiply, "OpIMul", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kAnd, "OpBitwiseAnd", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kOr, "OpBitwiseOr", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kXor, "OpBitwiseXor", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kShiftLeft, "OpShiftLeftLogical", "uint"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kShiftRight, "OpShiftRightLogical", "uint"}));
+    testing::Values(BinaryTestCase{kU32, core::BinaryOp::kAdd, "OpIAdd", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kSubtract, "OpISub", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kMultiply, "OpIMul", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kAnd, "OpBitwiseAnd", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kOr, "OpBitwiseOr", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kXor, "OpBitwiseXor", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kShiftLeft, "OpShiftLeftLogical", "uint"},
+                    BinaryTestCase{kU32, core::BinaryOp::kShiftRight, "OpShiftRightLogical",
+                                   "uint"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_F32,
     Arithmetic_Bitwise,
-    testing::Values(BinaryTestCase{kF32, core::ir::BinaryOp::kAdd, "OpFAdd", "float"},
-                    BinaryTestCase{kF32, core::ir::BinaryOp::kSubtract, "OpFSub", "float"},
-                    BinaryTestCase{kF32, core::ir::BinaryOp::kMultiply, "OpFMul", "float"},
-                    BinaryTestCase{kF32, core::ir::BinaryOp::kDivide, "OpFDiv", "float"},
-                    BinaryTestCase{kF32, core::ir::BinaryOp::kModulo, "OpFRem", "float"}));
+    testing::Values(BinaryTestCase{kF32, core::BinaryOp::kAdd, "OpFAdd", "float"},
+                    BinaryTestCase{kF32, core::BinaryOp::kSubtract, "OpFSub", "float"},
+                    BinaryTestCase{kF32, core::BinaryOp::kMultiply, "OpFMul", "float"},
+                    BinaryTestCase{kF32, core::BinaryOp::kDivide, "OpFDiv", "float"},
+                    BinaryTestCase{kF32, core::BinaryOp::kModulo, "OpFRem", "float"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_F16,
     Arithmetic_Bitwise,
-    testing::Values(BinaryTestCase{kF16, core::ir::BinaryOp::kAdd, "OpFAdd", "half"},
-                    BinaryTestCase{kF16, core::ir::BinaryOp::kSubtract, "OpFSub", "half"},
-                    BinaryTestCase{kF16, core::ir::BinaryOp::kMultiply, "OpFMul", "half"},
-                    BinaryTestCase{kF16, core::ir::BinaryOp::kDivide, "OpFDiv", "half"},
-                    BinaryTestCase{kF16, core::ir::BinaryOp::kModulo, "OpFRem", "half"}));
+    testing::Values(BinaryTestCase{kF16, core::BinaryOp::kAdd, "OpFAdd", "half"},
+                    BinaryTestCase{kF16, core::BinaryOp::kSubtract, "OpFSub", "half"},
+                    BinaryTestCase{kF16, core::BinaryOp::kMultiply, "OpFMul", "half"},
+                    BinaryTestCase{kF16, core::BinaryOp::kDivide, "OpFDiv", "half"},
+                    BinaryTestCase{kF16, core::BinaryOp::kModulo, "OpFRem", "half"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_Bool,
     Arithmetic_Bitwise,
-    testing::Values(BinaryTestCase{kBool, core::ir::BinaryOp::kAnd, "OpLogicalAnd", "bool"},
-                    BinaryTestCase{kBool, core::ir::BinaryOp::kOr, "OpLogicalOr", "bool"}));
+    testing::Values(BinaryTestCase{kBool, core::BinaryOp::kAnd, "OpLogicalAnd", "bool"},
+                    BinaryTestCase{kBool, core::BinaryOp::kOr, "OpLogicalOr", "bool"}));
 
 TEST_F(SpirvWriterTest, Binary_ScalarTimesVector_F32) {
     auto* scalar = b.FunctionParam("scalar", ty.f32());
@@ -236,8 +265,8 @@
 
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
-        auto* lhs = MakeScalarValue(params.type);
-        auto* rhs = MakeScalarValue(params.type);
+        auto* lhs = MakeScalarValue(params.lhs_type);
+        auto* rhs = MakeScalarValue(params.rhs_type);
         auto* result = b.Binary(params.op, ty.bool_(), lhs, rhs);
         b.Return(func);
         mod.SetName(result, "result");
@@ -252,8 +281,8 @@
 
     auto* func = b.Function("foo", ty.void_());
     b.Append(func->Block(), [&] {
-        auto* lhs = MakeVectorValue(params.type);
-        auto* rhs = MakeVectorValue(params.type);
+        auto* lhs = MakeVectorValue(params.lhs_type);
+        auto* rhs = MakeVectorValue(params.rhs_type);
         auto* result = b.Binary(params.op, ty.vec2<bool>(), lhs, rhs);
         b.Return(func);
         mod.SetName(result, "result");
@@ -266,50 +295,47 @@
     SpirvWriterTest_Binary_I32,
     Comparison,
     testing::Values(
-        BinaryTestCase{kI32, core::ir::BinaryOp::kEqual, "OpIEqual", "bool"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kNotEqual, "OpINotEqual", "bool"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kGreaterThan, "OpSGreaterThan", "bool"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kGreaterThanEqual, "OpSGreaterThanEqual", "bool"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kLessThan, "OpSLessThan", "bool"},
-        BinaryTestCase{kI32, core::ir::BinaryOp::kLessThanEqual, "OpSLessThanEqual", "bool"}));
+        BinaryTestCase{kI32, core::BinaryOp::kEqual, "OpIEqual", "bool"},
+        BinaryTestCase{kI32, core::BinaryOp::kNotEqual, "OpINotEqual", "bool"},
+        BinaryTestCase{kI32, core::BinaryOp::kGreaterThan, "OpSGreaterThan", "bool"},
+        BinaryTestCase{kI32, core::BinaryOp::kGreaterThanEqual, "OpSGreaterThanEqual", "bool"},
+        BinaryTestCase{kI32, core::BinaryOp::kLessThan, "OpSLessThan", "bool"},
+        BinaryTestCase{kI32, core::BinaryOp::kLessThanEqual, "OpSLessThanEqual", "bool"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_U32,
     Comparison,
     testing::Values(
-        BinaryTestCase{kU32, core::ir::BinaryOp::kEqual, "OpIEqual", "bool"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kNotEqual, "OpINotEqual", "bool"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kGreaterThan, "OpUGreaterThan", "bool"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kGreaterThanEqual, "OpUGreaterThanEqual", "bool"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kLessThan, "OpULessThan", "bool"},
-        BinaryTestCase{kU32, core::ir::BinaryOp::kLessThanEqual, "OpULessThanEqual", "bool"}));
+        BinaryTestCase{kU32, core::BinaryOp::kEqual, "OpIEqual", "bool"},
+        BinaryTestCase{kU32, core::BinaryOp::kNotEqual, "OpINotEqual", "bool"},
+        BinaryTestCase{kU32, core::BinaryOp::kGreaterThan, "OpUGreaterThan", "bool"},
+        BinaryTestCase{kU32, core::BinaryOp::kGreaterThanEqual, "OpUGreaterThanEqual", "bool"},
+        BinaryTestCase{kU32, core::BinaryOp::kLessThan, "OpULessThan", "bool"},
+        BinaryTestCase{kU32, core::BinaryOp::kLessThanEqual, "OpULessThanEqual", "bool"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_F32,
     Comparison,
     testing::Values(
-        BinaryTestCase{kF32, core::ir::BinaryOp::kEqual, "OpFOrdEqual", "bool"},
-        BinaryTestCase{kF32, core::ir::BinaryOp::kNotEqual, "OpFOrdNotEqual", "bool"},
-        BinaryTestCase{kF32, core::ir::BinaryOp::kGreaterThan, "OpFOrdGreaterThan", "bool"},
-        BinaryTestCase{kF32, core::ir::BinaryOp::kGreaterThanEqual, "OpFOrdGreaterThanEqual",
-                       "bool"},
-        BinaryTestCase{kF32, core::ir::BinaryOp::kLessThan, "OpFOrdLessThan", "bool"},
-        BinaryTestCase{kF32, core::ir::BinaryOp::kLessThanEqual, "OpFOrdLessThanEqual", "bool"}));
+        BinaryTestCase{kF32, core::BinaryOp::kEqual, "OpFOrdEqual", "bool"},
+        BinaryTestCase{kF32, core::BinaryOp::kNotEqual, "OpFOrdNotEqual", "bool"},
+        BinaryTestCase{kF32, core::BinaryOp::kGreaterThan, "OpFOrdGreaterThan", "bool"},
+        BinaryTestCase{kF32, core::BinaryOp::kGreaterThanEqual, "OpFOrdGreaterThanEqual", "bool"},
+        BinaryTestCase{kF32, core::BinaryOp::kLessThan, "OpFOrdLessThan", "bool"},
+        BinaryTestCase{kF32, core::BinaryOp::kLessThanEqual, "OpFOrdLessThanEqual", "bool"}));
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Binary_F16,
     Comparison,
     testing::Values(
-        BinaryTestCase{kF16, core::ir::BinaryOp::kEqual, "OpFOrdEqual", "bool"},
-        BinaryTestCase{kF16, core::ir::BinaryOp::kNotEqual, "OpFOrdNotEqual", "bool"},
-        BinaryTestCase{kF16, core::ir::BinaryOp::kGreaterThan, "OpFOrdGreaterThan", "bool"},
-        BinaryTestCase{kF16, core::ir::BinaryOp::kGreaterThanEqual, "OpFOrdGreaterThanEqual",
-                       "bool"},
-        BinaryTestCase{kF16, core::ir::BinaryOp::kLessThan, "OpFOrdLessThan", "bool"},
-        BinaryTestCase{kF16, core::ir::BinaryOp::kLessThanEqual, "OpFOrdLessThanEqual", "bool"}));
-INSTANTIATE_TEST_SUITE_P(SpirvWriterTest_Binary_Bool,
-                         Comparison,
-                         testing::Values(BinaryTestCase{kBool, core::ir::BinaryOp::kEqual,
-                                                        "OpLogicalEqual", "bool"},
-                                         BinaryTestCase{kBool, core::ir::BinaryOp::kNotEqual,
-                                                        "OpLogicalNotEqual", "bool"}));
+        BinaryTestCase{kF16, core::BinaryOp::kEqual, "OpFOrdEqual", "bool"},
+        BinaryTestCase{kF16, core::BinaryOp::kNotEqual, "OpFOrdNotEqual", "bool"},
+        BinaryTestCase{kF16, core::BinaryOp::kGreaterThan, "OpFOrdGreaterThan", "bool"},
+        BinaryTestCase{kF16, core::BinaryOp::kGreaterThanEqual, "OpFOrdGreaterThanEqual", "bool"},
+        BinaryTestCase{kF16, core::BinaryOp::kLessThan, "OpFOrdLessThan", "bool"},
+        BinaryTestCase{kF16, core::BinaryOp::kLessThanEqual, "OpFOrdLessThanEqual", "bool"}));
+INSTANTIATE_TEST_SUITE_P(
+    SpirvWriterTest_Binary_Bool,
+    Comparison,
+    testing::Values(BinaryTestCase{kBool, core::BinaryOp::kEqual, "OpLogicalEqual", "bool"},
+                    BinaryTestCase{kBool, core::BinaryOp::kNotEqual, "OpLogicalNotEqual", "bool"}));
 
 TEST_F(SpirvWriterTest, Binary_Chain) {
     auto* func = b.Function("foo", ty.void_());
@@ -334,7 +360,7 @@
     auto* func = b.Function("foo", ty.u32());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kDivide, ty.u32(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kDivide, ty.u32(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -370,7 +396,7 @@
     auto* func = b.Function("foo", ty.i32());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kDivide, ty.i32(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kDivide, ty.i32(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -410,7 +436,7 @@
     auto* func = b.Function("foo", ty.vec4<i32>());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kDivide, ty.vec4<i32>(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kDivide, ty.vec4<i32>(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -452,7 +478,7 @@
     auto* func = b.Function("foo", ty.vec4<i32>());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kDivide, ty.vec4<i32>(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kDivide, ty.vec4<i32>(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -494,7 +520,7 @@
     auto* func = b.Function("foo", ty.u32());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kModulo, ty.u32(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kModulo, ty.u32(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -532,7 +558,7 @@
     auto* func = b.Function("foo", ty.i32());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kModulo, ty.i32(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kModulo, ty.i32(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -574,7 +600,7 @@
     auto* func = b.Function("foo", ty.vec4<i32>());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kModulo, ty.vec4<i32>(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kModulo, ty.vec4<i32>(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
@@ -618,7 +644,7 @@
     auto* func = b.Function("foo", ty.vec4<i32>());
     func->SetParams(args);
     b.Append(func->Block(), [&] {
-        auto* result = b.Binary(core::ir::BinaryOp::kModulo, ty.vec4<i32>(), args[0], args[1]);
+        auto* result = b.Binary(core::BinaryOp::kModulo, ty.vec4<i32>(), args[0], args[1]);
         b.Return(func, result);
         mod.SetName(result, "result");
     });
diff --git a/src/tint/lang/spirv/writer/common/helper_test.h b/src/tint/lang/spirv/writer/common/helper_test.h
index df4936e..1c08ab9 100644
--- a/src/tint/lang/spirv/writer/common/helper_test.h
+++ b/src/tint/lang/spirv/writer/common/helper_test.h
@@ -115,7 +115,7 @@
     /// storage class with OpConstantNull
     /// @returns true if generation and validation succeeded
     bool Generate(Options options = {}, bool zero_init_workgroup_memory = false) {
-        auto raised = raise::Raise(mod, options);
+        auto raised = Raise(mod, options);
         if (raised != Success) {
             err_ = raised.Failure().reason.str();
             return false;
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index 2c8a117..79ec0e7 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -146,6 +146,9 @@
     /// Set to `true` to generate polyfill for `dot4I8Packed` and `dot4U8Packed` builtins
     bool polyfill_dot_4x8_packed = false;
 
+    /// Set to `true` to disable the polyfills on integer division and modulo.
+    bool disable_polyfill_integer_div_mod = false;
+
     /// The bindings
     Bindings bindings;
 
@@ -159,6 +162,7 @@
                  clamp_frag_depth,
                  experimental_require_subgroup_uniform_control_flow,
                  polyfill_dot_4x8_packed,
+                 disable_polyfill_integer_div_mod,
                  bindings);
 };
 
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index e5e9945..8eec980 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -879,8 +879,8 @@
             Switch(
                 inst,                                                                 //
                 [&](core::ir::Access* a) { EmitAccess(a); },                          //
-                [&](core::ir::Binary* b) { EmitBinary(b); },                          //
                 [&](core::ir::Bitcast* b) { EmitBitcast(b); },                        //
+                [&](core::ir::CoreBinary* b) { EmitBinary(b); },                      //
                 [&](core::ir::CoreBuiltinCall* b) { EmitCoreBuiltinCall(b); },        //
                 [&](spirv::ir::BuiltinCall* b) { EmitSpirvBuiltinCall(b); },          //
                 [&](core::ir::Construct* c) { EmitConstruct(c); },                    //
@@ -893,7 +893,7 @@
                 [&](core::ir::Store* s) { EmitStore(s); },                            //
                 [&](core::ir::StoreVectorElement* s) { EmitStoreVectorElement(s); },  //
                 [&](core::ir::UserCall* c) { EmitUserCall(c); },                      //
-                [&](core::ir::Unary* u) { EmitUnary(u); },                            //
+                [&](core::ir::CoreUnary* u) { EmitUnary(u); },                        //
                 [&](core::ir::Var* v) { EmitVar(v); },                                //
                 [&](core::ir::Let* l) { EmitLet(l); },                                //
                 [&](core::ir::If* i) { EmitIf(i); },                                  //
@@ -1060,7 +1060,7 @@
 
     /// Emit a binary instruction.
     /// @param binary the binary instruction to emit
-    void EmitBinary(core::ir::Binary* binary) {
+    void EmitBinary(core::ir::CoreBinary* binary) {
         auto id = Value(binary);
         auto lhs = Value(binary->LHS());
         auto rhs = Value(binary->RHS());
@@ -1070,11 +1070,11 @@
         // Determine the opcode.
         spv::Op op = spv::Op::Max;
         switch (binary->Op()) {
-            case core::ir::BinaryOp::kAdd: {
+            case core::BinaryOp::kAdd: {
                 op = ty->is_integer_scalar_or_vector() ? spv::Op::OpIAdd : spv::Op::OpFAdd;
                 break;
             }
-            case core::ir::BinaryOp::kDivide: {
+            case core::BinaryOp::kDivide: {
                 if (ty->is_signed_integer_scalar_or_vector()) {
                     op = spv::Op::OpSDiv;
                 } else if (ty->is_unsigned_integer_scalar_or_vector()) {
@@ -1084,7 +1084,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kMultiply: {
+            case core::BinaryOp::kMultiply: {
                 if (ty->is_integer_scalar_or_vector()) {
                     op = spv::Op::OpIMul;
                 } else if (ty->is_float_scalar_or_vector()) {
@@ -1092,11 +1092,11 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kSubtract: {
+            case core::BinaryOp::kSubtract: {
                 op = ty->is_integer_scalar_or_vector() ? spv::Op::OpISub : spv::Op::OpFSub;
                 break;
             }
-            case core::ir::BinaryOp::kModulo: {
+            case core::BinaryOp::kModulo: {
                 if (ty->is_signed_integer_scalar_or_vector()) {
                     op = spv::Op::OpSRem;
                 } else if (ty->is_unsigned_integer_scalar_or_vector()) {
@@ -1107,7 +1107,7 @@
                 break;
             }
 
-            case core::ir::BinaryOp::kAnd: {
+            case core::BinaryOp::kAnd: {
                 if (ty->is_integer_scalar_or_vector()) {
                     op = spv::Op::OpBitwiseAnd;
                 } else if (ty->is_bool_scalar_or_vector()) {
@@ -1115,7 +1115,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kOr: {
+            case core::BinaryOp::kOr: {
                 if (ty->is_integer_scalar_or_vector()) {
                     op = spv::Op::OpBitwiseOr;
                 } else if (ty->is_bool_scalar_or_vector()) {
@@ -1123,16 +1123,16 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kXor: {
+            case core::BinaryOp::kXor: {
                 op = spv::Op::OpBitwiseXor;
                 break;
             }
 
-            case core::ir::BinaryOp::kShiftLeft: {
+            case core::BinaryOp::kShiftLeft: {
                 op = spv::Op::OpShiftLeftLogical;
                 break;
             }
-            case core::ir::BinaryOp::kShiftRight: {
+            case core::BinaryOp::kShiftRight: {
                 if (ty->is_signed_integer_scalar_or_vector()) {
                     op = spv::Op::OpShiftRightArithmetic;
                 } else if (ty->is_unsigned_integer_scalar_or_vector()) {
@@ -1141,7 +1141,7 @@
                 break;
             }
 
-            case core::ir::BinaryOp::kEqual: {
+            case core::BinaryOp::kEqual: {
                 if (lhs_ty->is_bool_scalar_or_vector()) {
                     op = spv::Op::OpLogicalEqual;
                 } else if (lhs_ty->is_float_scalar_or_vector()) {
@@ -1151,7 +1151,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kNotEqual: {
+            case core::BinaryOp::kNotEqual: {
                 if (lhs_ty->is_bool_scalar_or_vector()) {
                     op = spv::Op::OpLogicalNotEqual;
                 } else if (lhs_ty->is_float_scalar_or_vector()) {
@@ -1161,7 +1161,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kGreaterThan: {
+            case core::BinaryOp::kGreaterThan: {
                 if (lhs_ty->is_float_scalar_or_vector()) {
                     op = spv::Op::OpFOrdGreaterThan;
                 } else if (lhs_ty->is_signed_integer_scalar_or_vector()) {
@@ -1171,7 +1171,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kGreaterThanEqual: {
+            case core::BinaryOp::kGreaterThanEqual: {
                 if (lhs_ty->is_float_scalar_or_vector()) {
                     op = spv::Op::OpFOrdGreaterThanEqual;
                 } else if (lhs_ty->is_signed_integer_scalar_or_vector()) {
@@ -1181,7 +1181,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kLessThan: {
+            case core::BinaryOp::kLessThan: {
                 if (lhs_ty->is_float_scalar_or_vector()) {
                     op = spv::Op::OpFOrdLessThan;
                 } else if (lhs_ty->is_signed_integer_scalar_or_vector()) {
@@ -1191,7 +1191,7 @@
                 }
                 break;
             }
-            case core::ir::BinaryOp::kLessThanEqual: {
+            case core::BinaryOp::kLessThanEqual: {
                 if (lhs_ty->is_float_scalar_or_vector()) {
                     op = spv::Op::OpFOrdLessThanEqual;
                 } else if (lhs_ty->is_signed_integer_scalar_or_vector()) {
@@ -1201,6 +1201,9 @@
                 }
                 break;
             }
+            default:
+                TINT_UNIMPLEMENTED() << binary->Op();
+                break;
         }
 
         // Emit the instruction.
@@ -1961,21 +1964,24 @@
 
     /// Emit a unary instruction.
     /// @param unary the unary instruction to emit
-    void EmitUnary(core::ir::Unary* unary) {
+    void EmitUnary(core::ir::CoreUnary* unary) {
         auto id = Value(unary);
         auto* ty = unary->Result(0)->Type();
         spv::Op op = spv::Op::Max;
         switch (unary->Op()) {
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 op = spv::Op::OpNot;
                 break;
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 if (ty->is_float_scalar_or_vector()) {
                     op = spv::Op::OpFNegate;
                 } else if (ty->is_signed_integer_scalar_or_vector()) {
                     op = spv::Op::OpSNegate;
                 }
                 break;
+            default:
+                TINT_UNIMPLEMENTED() << unary->Op();
+                break;
         }
         current_function_.push_inst(op, {Type(ty), id, Value(unary->Val())});
     }
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 083a98b..3385ad0 100644
--- a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
+++ b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
@@ -46,7 +46,7 @@
 
     // Find the instructions that use implicit splats and either modify them in place or record them
     // to be replaced in a second pass.
-    Vector<core::ir::Binary*, 4> binary_worklist;
+    Vector<core::ir::CoreBinary*, 4> binary_worklist;
     Vector<core::ir::CoreBuiltinCall*, 4> builtin_worklist;
     for (auto* inst : ir.instructions.Objects()) {
         if (!inst->Alive()) {
@@ -63,7 +63,7 @@
                     construct->AppendArg(construct->Args()[0]);
                 }
             }
-        } else if (auto* binary = inst->As<core::ir::Binary>()) {
+        } else if (auto* binary = inst->As<core::ir::CoreBinary>()) {
             // A binary instruction that mixes vector and scalar operands needs to have the scalar
             // operand replaced with an explicit vector constructor.
             if (binary->Result(0)->Type()->Is<core::type::Vector>()) {
@@ -101,7 +101,7 @@
     // Replace scalar operands to binary instructions that produce vectors.
     for (auto* binary : binary_worklist) {
         auto* result_ty = binary->Result(0)->Type();
-        if (result_ty->is_float_vector() && binary->Op() == core::ir::BinaryOp::kMultiply) {
+        if (result_ty->is_float_vector() && binary->Op() == core::BinaryOp::kMultiply) {
             // Use OpVectorTimesScalar for floating point multiply.
             auto* vts =
                 b.Call<spirv::ir::BuiltinCall>(result_ty, spirv::BuiltinFn::kVectorTimesScalar);
@@ -121,9 +121,9 @@
         } else {
             // Expand the scalar argument into an explicitly constructed vector.
             if (binary->LHS()->Type()->Is<core::type::Scalar>()) {
-                expand_operand(binary, core::ir::Binary::kLhsOperandOffset);
+                expand_operand(binary, core::ir::CoreBinary::kLhsOperandOffset);
             } else if (binary->RHS()->Type()->Is<core::type::Scalar>()) {
-                expand_operand(binary, core::ir::Binary::kRhsOperandOffset);
+                expand_operand(binary, core::ir::CoreBinary::kRhsOperandOffset);
             }
         }
     }
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 6be5daf..3e77d21 100644
--- a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
+++ b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
@@ -48,13 +48,13 @@
     core::ir::Builder b{ir};
 
     // Find the instructions that need to be modified.
-    Vector<core::ir::Binary*, 4> binary_worklist;
+    Vector<core::ir::CoreBinary*, 4> binary_worklist;
     Vector<core::ir::Convert*, 4> convert_worklist;
     for (auto* inst : ir.instructions.Objects()) {
         if (!inst->Alive()) {
             continue;
         }
-        if (auto* binary = inst->As<core::ir::Binary>()) {
+        if (auto* binary = inst->As<core::ir::CoreBinary>()) {
             TINT_ASSERT(binary->Operands().Length() == 2);
             if (binary->LHS()->Type()->Is<core::type::Matrix>() ||
                 binary->RHS()->Type()->Is<core::type::Matrix>()) {
@@ -101,13 +101,13 @@
         };
 
         switch (binary->Op()) {
-            case core::ir::BinaryOp::kAdd:
-                column_wise(core::ir::BinaryOp::kAdd);
+            case core::BinaryOp::kAdd:
+                column_wise(core::BinaryOp::kAdd);
                 break;
-            case core::ir::BinaryOp::kSubtract:
-                column_wise(core::ir::BinaryOp::kSubtract);
+            case core::BinaryOp::kSubtract:
+                column_wise(core::BinaryOp::kSubtract);
                 break;
-            case core::ir::BinaryOp::kMultiply:
+            case core::BinaryOp::kMultiply:
                 // Select the SPIR-V intrinsic that corresponds to the operation being performed.
                 if (lhs_ty->Is<core::type::Matrix>()) {
                     if (rhs_ty->Is<core::type::Scalar>()) {
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 1c88637..6372390 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -54,7 +54,7 @@
 #include "src/tint/lang/spirv/writer/raise/shader_io.h"
 #include "src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h"
 
-namespace tint::spirv::writer::raise {
+namespace tint::spirv::writer {
 
 Result<SuccessType> Raise(core::ir::Module& module, const Options& options) {
 #define RUN_TRANSFORM(name, ...)         \
@@ -73,7 +73,7 @@
 
     core::ir::transform::BinaryPolyfillConfig binary_polyfills;
     binary_polyfills.bitshift_modulo = true;
-    binary_polyfills.int_div_mod = true;
+    binary_polyfills.int_div_mod = !options.disable_polyfill_integer_div_mod;
     RUN_TRANSFORM(core::ir::transform::BinaryPolyfill, module, binary_polyfills);
 
     core::ir::transform::BuiltinPolyfillConfig core_polyfills;
@@ -86,6 +86,7 @@
     core_polyfills.insert_bits = core::ir::transform::BuiltinPolyfillLevel::kClampOrRangeCheck;
     core_polyfills.saturate = true;
     core_polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
+    core_polyfills.dot_4x8_packed = options.polyfill_dot_4x8_packed;
     core_polyfills.pack_unpack_4x8 = true;
     RUN_TRANSFORM(core::ir::transform::BuiltinPolyfill, module, core_polyfills);
 
@@ -121,7 +122,7 @@
 
     if (options.pass_matrix_by_pointer) {
         // PassMatrixByPointer must come after PreservePadding+DirectVariableAccess.
-        RUN_TRANSFORM(PassMatrixByPointer, module);
+        RUN_TRANSFORM(raise::PassMatrixByPointer, module);
     }
 
     RUN_TRANSFORM(core::ir::transform::AddEmptyEntryPoint, module);
@@ -137,16 +138,16 @@
     // 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);
-    RUN_TRANSFORM(ShaderIO, module,
-                  ShaderIOConfig{options.clamp_frag_depth, options.emit_vertex_point_size});
+    RUN_TRANSFORM(raise::BuiltinPolyfill, module);
+    RUN_TRANSFORM(raise::ExpandImplicitSplats, module);
+    RUN_TRANSFORM(raise::HandleMatrixArithmetic, module);
+    RUN_TRANSFORM(raise::MergeReturn, module);
+    RUN_TRANSFORM(raise::ShaderIO, module,
+                  raise::ShaderIOConfig{options.clamp_frag_depth, options.emit_vertex_point_size});
     RUN_TRANSFORM(core::ir::transform::Std140, module);
-    RUN_TRANSFORM(VarForDynamicIndex, module);
+    RUN_TRANSFORM(raise::VarForDynamicIndex, module);
 
     return Success;
 }
 
-}  // namespace tint::spirv::writer::raise
+}  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/raise/raise.h b/src/tint/lang/spirv/writer/raise/raise.h
index ba6270d..64854f3 100644
--- a/src/tint/lang/spirv/writer/raise/raise.h
+++ b/src/tint/lang/spirv/writer/raise/raise.h
@@ -39,7 +39,7 @@
 class Module;
 }
 
-namespace tint::spirv::writer::raise {
+namespace tint::spirv::writer {
 
 /// Raise a core IR module to the SPIR-V dialect of the IR.
 /// @param module the core IR module to raise to SPIR-V dialect
@@ -47,6 +47,6 @@
 /// @returns success or failure
 Result<SuccessType> Raise(core::ir::Module& module, const Options& options);
 
-}  // namespace tint::spirv::writer::raise
+}  // namespace tint::spirv::writer
 
 #endif  // SRC_TINT_LANG_SPIRV_WRITER_RAISE_RAISE_H_
diff --git a/src/tint/lang/spirv/writer/unary_test.cc b/src/tint/lang/spirv/writer/unary_test.cc
index 72fff71..43fb04e 100644
--- a/src/tint/lang/spirv/writer/unary_test.cc
+++ b/src/tint/lang/spirv/writer/unary_test.cc
@@ -39,7 +39,7 @@
     /// The element type to test.
     TestElementType type;
     /// The unary operation.
-    enum core::ir::UnaryOp op;
+    core::UnaryOp op;
     /// The expected SPIR-V instruction.
     std::string spirv_inst;
     /// The expected SPIR-V result type name.
@@ -80,11 +80,11 @@
 INSTANTIATE_TEST_SUITE_P(
     SpirvWriterTest_Unary,
     Arithmetic,
-    testing::Values(UnaryTestCase{kI32, core::ir::UnaryOp::kComplement, "OpNot", "int"},
-                    UnaryTestCase{kU32, core::ir::UnaryOp::kComplement, "OpNot", "uint"},
-                    UnaryTestCase{kI32, core::ir::UnaryOp::kNegation, "OpSNegate", "int"},
-                    UnaryTestCase{kF32, core::ir::UnaryOp::kNegation, "OpFNegate", "float"},
-                    UnaryTestCase{kF16, core::ir::UnaryOp::kNegation, "OpFNegate", "half"}));
+    testing::Values(UnaryTestCase{kI32, core::UnaryOp::kComplement, "OpNot", "int"},
+                    UnaryTestCase{kU32, core::UnaryOp::kComplement, "OpNot", "uint"},
+                    UnaryTestCase{kI32, core::UnaryOp::kNegation, "OpSNegate", "int"},
+                    UnaryTestCase{kF32, core::UnaryOp::kNegation, "OpFNegate", "float"},
+                    UnaryTestCase{kF16, core::UnaryOp::kNegation, "OpFNegate", "half"}));
 
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index a605c36..88f8233 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -54,7 +54,7 @@
     Output output;
 
     // Raise from core-dialect to SPIR-V-dialect.
-    if (auto res = raise::Raise(ir, options); res != Success) {
+    if (auto res = Raise(ir, options); res != Success) {
         return std::move(res.Failure());
     }
 
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/spirv/writer/writer_ast_fuzz.cc
similarity index 69%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/spirv/writer/writer_ast_fuzz.cc
index 421db7b..8a7d6e9 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/spirv/writer/writer_ast_fuzz.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +25,27 @@
 // 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/writer.h"
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader)
 
-#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/cmd/fuzz/wgsl/fuzz.h"
+#include "src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h"
+#include "src/tint/lang/spirv/writer/writer.h"
+#include "src/tint/lang/wgsl/ast/module.h"
 
 namespace tint::spirv::writer {
 namespace {
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
+void ASTFuzzer(const tint::Program& program, Options options) {
+    if (program.AST().HasOverrides()) {
         return;
     }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
-    }
+
+    options.bindings = GenerateBindings(program);
+
+    [[maybe_unused]] auto res = tint::spirv::writer::Generate(program, options);
 }
 
 }  // namespace
 }  // namespace tint::spirv::writer
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+TINT_WGSL_PROGRAM_FUZZER(tint::spirv::writer::ASTFuzzer);
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/spirv/writer/writer_ir_fuzz.cc
similarity index 94%
rename from src/tint/lang/spirv/writer/writer_fuzz.cc
rename to src/tint/lang/spirv/writer/writer_ir_fuzz.cc
index 421db7b..dcc848c 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/spirv/writer/writer_ir_fuzz.cc
@@ -34,7 +34,7 @@
 namespace tint::spirv::writer {
 namespace {
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
+void IRFuzzer(core::ir::Module& module, Options options) {
     options.bindings = GenerateBindings(module);
     auto output = Generate(module, options);
     if (output != Success) {
@@ -51,4 +51,4 @@
 }  // namespace
 }  // namespace tint::spirv::writer
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRFuzzer);
diff --git a/src/tint/lang/wgsl/BUILD.bazel b/src/tint/lang/wgsl/BUILD.bazel
index f30a202..b945e60 100644
--- a/src/tint/lang/wgsl/BUILD.bazel
+++ b/src/tint/lang/wgsl/BUILD.bazel
@@ -94,12 +94,10 @@
     "//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",
-    "//src/tint/lang/wgsl/resolver",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/lang/wgsl/writer/ir_to_program",
+    "//src/tint/lang/wgsl/writer/raise",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
diff --git a/src/tint/lang/wgsl/BUILD.cmake b/src/tint/lang/wgsl/BUILD.cmake
index 17f4d73..63b67a8 100644
--- a/src/tint/lang/wgsl/BUILD.cmake
+++ b/src/tint/lang/wgsl/BUILD.cmake
@@ -97,12 +97,10 @@
   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
-  tint_lang_wgsl_resolver
   tint_lang_wgsl_sem
   tint_lang_wgsl_writer_ir_to_program
+  tint_lang_wgsl_writer_raise
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
diff --git a/src/tint/lang/wgsl/BUILD.gn b/src/tint/lang/wgsl/BUILD.gn
index 1b7590c..c63b21c 100644
--- a/src/tint/lang/wgsl/BUILD.gn
+++ b/src/tint/lang/wgsl/BUILD.gn
@@ -86,12 +86,10 @@
       "${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",
-      "${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/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
diff --git a/src/tint/lang/wgsl/intrinsic/BUILD.bazel b/src/tint/lang/wgsl/intrinsic/BUILD.bazel
index be578ca..bfb10a6 100644
--- a/src/tint/lang/wgsl/intrinsic/BUILD.bazel
+++ b/src/tint/lang/wgsl/intrinsic/BUILD.bazel
@@ -59,6 +59,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/symbol",
diff --git a/src/tint/lang/wgsl/intrinsic/BUILD.cmake b/src/tint/lang/wgsl/intrinsic/BUILD.cmake
index f77068d..275c8b0 100644
--- a/src/tint/lang/wgsl/intrinsic/BUILD.cmake
+++ b/src/tint/lang/wgsl/intrinsic/BUILD.cmake
@@ -58,6 +58,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/intrinsic/BUILD.gn b/src/tint/lang/wgsl/intrinsic/BUILD.gn
index 61a2bb1..bae4bd0 100644
--- a/src/tint/lang/wgsl/intrinsic/BUILD.gn
+++ b/src/tint/lang/wgsl/intrinsic/BUILD.gn
@@ -58,6 +58,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/ir/BUILD.bazel b/src/tint/lang/wgsl/ir/BUILD.bazel
index e764df7..259b851 100644
--- a/src/tint/lang/wgsl/ir/BUILD.bazel
+++ b/src/tint/lang/wgsl/ir/BUILD.bazel
@@ -40,9 +40,11 @@
   name = "ir",
   srcs = [
     "builtin_call.cc",
+    "unary.cc",
   ],
   hdrs = [
     "builtin_call.h",
+    "unary.h",
   ],
   deps = [
     "//src/tint/api/common",
diff --git a/src/tint/lang/wgsl/ir/BUILD.cmake b/src/tint/lang/wgsl/ir/BUILD.cmake
index f41c7a4..d0192da 100644
--- a/src/tint/lang/wgsl/ir/BUILD.cmake
+++ b/src/tint/lang/wgsl/ir/BUILD.cmake
@@ -41,6 +41,8 @@
 tint_add_target(tint_lang_wgsl_ir lib
   lang/wgsl/ir/builtin_call.cc
   lang/wgsl/ir/builtin_call.h
+  lang/wgsl/ir/unary.cc
+  lang/wgsl/ir/unary.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_ir lib
diff --git a/src/tint/lang/wgsl/ir/BUILD.gn b/src/tint/lang/wgsl/ir/BUILD.gn
index 462ecfa..1194bb7 100644
--- a/src/tint/lang/wgsl/ir/BUILD.gn
+++ b/src/tint/lang/wgsl/ir/BUILD.gn
@@ -42,6 +42,8 @@
   sources = [
     "builtin_call.cc",
     "builtin_call.h",
+    "unary.cc",
+    "unary.h",
   ]
   deps = [
     "${tint_src_dir}/api/common",
diff --git a/src/tint/lang/spirv/writer/writer_fuzz.cc b/src/tint/lang/wgsl/ir/unary.cc
similarity index 62%
copy from src/tint/lang/spirv/writer/writer_fuzz.cc
copy to src/tint/lang/wgsl/ir/unary.cc
index 421db7b..f8c90f3 100644
--- a/src/tint/lang/spirv/writer/writer_fuzz.cc
+++ b/src/tint/lang/wgsl/ir/unary.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,30 +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.
 
-#include "src/tint/lang/spirv/writer/writer.h"
+#include "src/tint/lang/wgsl/ir/unary.h"
 
-#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/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/wgsl/intrinsic/dialect.h"
 
-namespace tint::spirv::writer {
-namespace {
+TINT_INSTANTIATE_TYPEINFO(tint::wgsl::ir::Unary);
 
-void IRPrinterFuzzer(core::ir::Module& module, Options options) {
-    options.bindings = GenerateBindings(module);
-    auto output = Generate(module, options);
-    if (output != Success) {
-        return;
-    }
-    auto& spirv = output->spirv;
-    if (auto res = validate::Validate(Slice(spirv.data(), spirv.size()), SPV_ENV_VULKAN_1_1);
-        res != Success) {
-        TINT_ICE() << "Output of SPIR-V writer failed to validate with SPIR-V Tools\n"
-                   << res.Failure();
-    }
+namespace tint::wgsl::ir {
+
+Unary::Unary() = default;
+
+Unary::Unary(core::ir::InstructionResult* result, core::UnaryOp op, core::ir::Value* val)
+    : Base(result, op, val) {}
+
+Unary::~Unary() = default;
+
+Unary* Unary::Clone(core::ir::CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result(0));
+    auto* val = ctx.Remap(Val());
+    return ctx.ir.instructions.Create<Unary>(new_result, Op(), val);
 }
 
-}  // namespace
-}  // namespace tint::spirv::writer
+const core::intrinsic::TableData& Unary::TableData() const {
+    return wgsl::intrinsic::Dialect::kData;
+}
 
-TINT_IR_MODULE_FUZZER(tint::spirv::writer::IRPrinterFuzzer);
+}  // namespace tint::wgsl::ir
diff --git a/src/tint/lang/wgsl/ir/unary.h b/src/tint/lang/wgsl/ir/unary.h
new file mode 100644
index 0000000..9fbeeea
--- /dev/null
+++ b/src/tint/lang/wgsl/ir/unary.h
@@ -0,0 +1,60 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_IR_UNARY_H_
+#define SRC_TINT_LANG_WGSL_IR_UNARY_H_
+
+#include "src/tint/lang/core/ir/unary.h"
+
+namespace tint::wgsl::ir {
+
+/// A WGSL-dialect unary instruction in the IR.
+class Unary final : public Castable<Unary, core::ir::Unary> {
+  public:
+    /// The offset in Operands() for the value
+    static constexpr size_t kValueOperandOffset = 0;
+
+    /// Constructor (no results, no operands)
+    Unary();
+
+    /// Constructor
+    /// @param result the result value
+    /// @param op the unary operator
+    /// @param val the input value for the instruction
+    Unary(core::ir::InstructionResult* result, core::UnaryOp op, core::ir::Value* val);
+    ~Unary() override;
+
+    /// @copydoc core::ir::Instruction::Clone()
+    Unary* Clone(core::ir::CloneContext& ctx) override;
+
+    /// @returns the table data to validate this builtin
+    const core::intrinsic::TableData& TableData() const override;
+};
+
+}  // namespace tint::wgsl::ir
+
+#endif  // SRC_TINT_LANG_WGSL_IR_UNARY_H_
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index ab1b3d6..d3497d0 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -27,10 +27,13 @@
 
 // GEN_BUILD:CONDITION(tint_build_wgsl_reader && tint_build_wgsl_writer)
 
-#include "src/tint/lang/wgsl/helpers/ir_program_test.h"
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/core/ir/disassembler.h"
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
+#include "src/tint/lang/wgsl/writer/raise/raise.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
 #include "src/tint/utils/text/string.h"
 
@@ -39,60 +42,146 @@
 
 using namespace tint::core::number_suffixes;  // NOLINT
 
-class IRToProgramRoundtripTest : public helpers::IRProgramTest {
+class IRToProgramRoundtripTest : public testing::Test {
   public:
-    void Test(std::string_view input_wgsl, std::string_view expected_wgsl) {
+    struct Result {
+        /// The resulting WGSL
+        std::string wgsl;
+        /// The resulting AST
+        std::string ast;
+        /// The resulting IR before raising
+        std::string ir_pre_raise;
+        /// The resulting IR after raising
+        std::string ir_post_raise;
+        /// The resulting error
+        std::string err;
+        /// The expected WGSL
+        std::string expected;
+    };
+
+    /// @return the round-tripped string and the expected string
+    Result Run(std::string input_wgsl, std::string expected_wgsl) {
+        std::string input{tint::TrimSpace(input_wgsl)};
+
+        Result result;
+
         wgsl::reader::Options options;
         options.allowed_features = wgsl::AllowedFeatures::Everything();
-        auto input = tint::TrimSpace(input_wgsl);
         Source::File file("test.wgsl", std::string(input));
         auto ir_module = wgsl::reader::WgslToIR(&file, options);
-        ASSERT_EQ(ir_module, Success);
+        if (ir_module != Success) {
+            return result;
+        }
 
-        auto disassembly = tint::core::ir::Disassemble(ir_module.Get());
+        result.ir_pre_raise = core::ir::Disassemble(ir_module.Get());
+
+        if (auto res = tint::wgsl::writer::Raise(ir_module.Get()); res != Success) {
+            result.err = res.Failure().reason.str();
+            return result;
+        }
+
+        result.ir_post_raise = core::ir::Disassemble(ir_module.Get());
 
         writer::ProgramOptions program_options;
         program_options.allowed_features = AllowedFeatures::Everything();
-        auto output = wgsl::writer::WgslFromIR(ir_module.Get(), program_options);
-        if (output != Success) {
-            FAIL() << output.Failure() << std::endl  //
-                   << "IR:" << std::endl             //
-                   << disassembly << std::endl;
+        auto output_program = wgsl::writer::IRToProgram(ir_module.Get(), program_options);
+        if (!output_program.IsValid()) {
+            result.err = output_program.Diagnostics().str();
+            result.ast = Program::printer(output_program);
+            return result;
         }
 
-        auto expected = expected_wgsl.empty() ? input : tint::TrimSpace(expected_wgsl);
-        auto got = tint::TrimSpace(output->wgsl);
-        EXPECT_EQ(expected, got) << "IR:" << std::endl << disassembly;
+        auto output = wgsl::writer::Generate(output_program, {});
+        if (output != Success) {
+            std::stringstream ss;
+            ss << "wgsl::Generate() errored: " << output.Failure();
+            result.err = ss.str();
+            result.ast = Program::printer(output_program);
+            return result;
+        }
+
+        result.expected = expected_wgsl.empty() ? input : tint::TrimSpace(expected_wgsl);
+        if (!result.expected.empty()) {
+            result.expected = "\n" + result.expected + "\n";
+        }
+
+        result.wgsl = std::string(tint::TrimSpace(output->wgsl));
+        if (!result.wgsl.empty()) {
+            result.wgsl = "\n" + result.wgsl + "\n";
+        }
+
+        return result;
     }
 
-    void Test(std::string_view wgsl) { Test(wgsl, wgsl); }
+    Result Run(std::string wgsl) { return Run(wgsl, wgsl); }
 };
 
+std::ostream& operator<<(std::ostream& o, const IRToProgramRoundtripTest::Result& res) {
+    if (!res.err.empty()) {
+        o << "============================" << std::endl
+          << "== Error                  ==" << std::endl
+          << "============================" << std::endl
+          << res.err << std::endl
+          << std::endl;
+    }
+    if (!res.ir_pre_raise.empty()) {
+        o << "============================" << std::endl
+          << "== IR (pre-raise)         ==" << std::endl
+          << "============================" << std::endl
+          << res.ir_pre_raise << std::endl
+          << std::endl;
+    }
+    if (!res.ir_post_raise.empty()) {
+        o << "============================" << std::endl
+          << "== IR (post-raise)        ==" << std::endl
+          << "============================" << std::endl
+          << res.ir_post_raise << std::endl
+          << std::endl;
+    }
+    if (!res.ast.empty()) {
+        o << "============================" << std::endl
+          << "== AST                    ==" << std::endl
+          << "============================" << std::endl
+          << res.ast << std::endl
+          << std::endl;
+    }
+    return o;
+}
+
+#define RUN_TEST(...)                                       \
+    do {                                                    \
+        if (auto res = Run(__VA_ARGS__); res.err.empty()) { \
+            EXPECT_EQ(res.expected, res.wgsl) << res;       \
+        } else {                                            \
+            FAIL() << res;                                  \
+        }                                                   \
+    } while (false)
+
 TEST_F(IRToProgramRoundtripTest, EmptyModule) {
-    Test("");
+    RUN_TEST("");
 }
 
 TEST_F(IRToProgramRoundtripTest, SingleFunction_Empty) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
 }
 )");
 }
 
 TEST_F(IRToProgramRoundtripTest, SingleFunction_Return) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   return;
 }
 )",
-         R"(
+             R"(
 fn f() {
 }
 )");
 }
 
 TEST_F(IRToProgramRoundtripTest, SingleFunction_Return_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   return 42i;
 }
@@ -100,7 +189,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, SingleFunction_Parameters) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32, u : u32) -> i32 {
   return i;
 }
@@ -111,7 +200,7 @@
 // Struct declaration
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, StructDecl_Scalars) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   b : u32,
@@ -123,7 +212,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberAlign) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @align(32u)
@@ -136,7 +225,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberSize) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @size(32u)
@@ -149,7 +238,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberLocation) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @location(1u)
@@ -162,7 +251,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberIndex) {
-    Test(R"(
+    RUN_TEST(R"(
 enable chromium_internal_dual_source_blending;
 
 struct S {
@@ -177,7 +266,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberBuiltin) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @builtin(position)
@@ -190,7 +279,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberInterpolateType) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @location(1u) @interpolate(flat)
@@ -203,7 +292,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberInterpolateTypeSampling) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @location(1u) @interpolate(perspective, centroid)
@@ -216,7 +305,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, StructDecl_MemberInvariant) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   @builtin(position) @invariant
@@ -232,7 +321,7 @@
 // Function Call
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, FnCall_NoArgs_NoRet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -243,7 +332,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FnCall_NoArgs_Ret_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> i32 {
   return 1i;
 }
@@ -255,7 +344,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FnCall_3Args_NoRet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(x : i32, y : u32, z : f32) {
 }
 
@@ -266,7 +355,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FnCall_3Args_Ret_f32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(x : i32, y : u32, z : f32) -> f32 {
   return z;
 }
@@ -278,7 +367,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FnCall_PtrArgs) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> y : i32 = 2i;
 
 fn a(px : ptr<function, i32>, py : ptr<private, i32>) -> i32 {
@@ -296,7 +385,7 @@
 // Core Builtin Call
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_Stmt) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   workgroupBarrier();
 }
@@ -304,7 +393,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_Expr) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) {
   var i : i32 = max(a, b);
 }
@@ -312,7 +401,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_PhonyAssignment) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) {
   _ = max(a, b);
 }
@@ -320,7 +409,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_UnusedLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) {
   let unused = max(a, b);
 }
@@ -328,7 +417,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_PtrArg) {
-    Test(R"(
+    RUN_TEST(R"(
 @group(0) @binding(0) var<storage, read> v : array<u32>;
 
 fn foo() -> u32 {
@@ -338,13 +427,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CoreBuiltinCall_DisableDerivativeUniformity) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(in : f32) {
   let x = dpdx(in);
   let y = dpdy(in);
 }
 )",
-         R"(
+             R"(
 diagnostic(off, derivative_uniformity);
 
 fn f(in : f32) {
@@ -358,7 +447,7 @@
 // Type Construct
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) {
   var v : i32 = i32(i);
 }
@@ -366,7 +455,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_u32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : u32) {
   var v : u32 = u32(i);
 }
@@ -374,7 +463,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_f32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : f32) {
   var v : f32 = f32(i);
 }
@@ -382,7 +471,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_bool) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : bool) {
   var v : bool = bool(i);
 }
@@ -390,7 +479,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_struct) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   b : u32,
@@ -404,7 +493,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_array) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) {
   var v : array<i32, 3u> = array<i32, 3u>(i, i, i);
 }
@@ -412,7 +501,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_vec3i_Splat) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) {
   var v : vec3<i32> = vec3<i32>(i);
 }
@@ -420,7 +509,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_vec3i_Scalars) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) {
   var v : vec3<i32> = vec3<i32>(i, i, i);
 }
@@ -428,7 +517,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_mat2x3f_Scalars) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : f32) {
   var v : mat2x3<f32> = mat2x3<f32>(i, i, i, i, i, i);
 }
@@ -436,7 +525,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConstruct_mat2x3f_Columns) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : f32) {
   var v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(i, i, i), vec3<f32>(i, i, i));
 }
@@ -447,7 +536,7 @@
 // Type Convert
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, TypeConvert_i32_to_u32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) {
   var v : u32 = u32(i);
 }
@@ -455,7 +544,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConvert_u32_to_f32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : u32) {
   var v : f32 = f32(i);
 }
@@ -463,7 +552,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConvert_f32_to_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : f32) {
   var v : i32 = i32(i);
 }
@@ -471,7 +560,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConvert_bool_to_u32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : bool) {
   var v : u32 = u32(i);
 }
@@ -479,7 +568,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConvert_vec3i_to_vec3u) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : vec3<i32>) {
   var v : vec3<u32> = vec3<u32>(i);
 }
@@ -487,7 +576,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConvert_vec3u_to_vec3f) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : vec3<u32>) {
   var v : vec3<f32> = vec3<f32>(i);
 }
@@ -495,7 +584,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, TypeConvert_mat2x3f_to_mat2x3h) {
-    Test(R"(
+    RUN_TEST(R"(
 enable f16;
 
 fn f(i : mat2x3<f32>) {
@@ -508,7 +597,7 @@
 // Bitcast
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Bitcast_i32_to_u32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) {
   var v : u32 = bitcast<u32>(i);
 }
@@ -516,7 +605,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Bitcast_u32_to_f32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : u32) {
   var v : f32 = bitcast<f32>(i);
 }
@@ -524,7 +613,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Bitcast_f32_to_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : f32) {
   var v : i32 = bitcast<i32>(i);
 }
@@ -535,7 +624,7 @@
 // Discard
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Discard) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   discard;
 }
@@ -546,12 +635,12 @@
 // Access
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Access_Value_vec3f_1) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> f32 {
   return v[1];
 }
 )",
-         R"(
+             R"(
 fn f(v : vec3<f32>) -> f32 {
   return v.y;
 }
@@ -559,14 +648,14 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_vec3f_1) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> v : vec3<f32>;
 
 fn f() -> f32 {
   return v[1];
 }
 )",
-         R"(
+             R"(
 var<private> v : vec3<f32>;
 
 fn f() -> f32 {
@@ -576,7 +665,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_vec3f_z) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> f32 {
   return v.z;
 }
@@ -584,7 +673,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_vec3f_z) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> v : vec3<f32>;
 
 fn f() -> f32 {
@@ -594,12 +683,12 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_vec3f_g) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> f32 {
   return v.g;
 }
 )",
-         R"(
+             R"(
 fn f(v : vec3<f32>) -> f32 {
   return v.y;
 }
@@ -607,14 +696,14 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_vec3f_g) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> v : vec3<f32>;
 
 fn f() -> f32 {
   return v.g;
 }
 )",
-         R"(
+             R"(
 var<private> v : vec3<f32>;
 
 fn f() -> f32 {
@@ -624,7 +713,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_vec3f_i) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>, i : i32) -> f32 {
   return v[i];
 }
@@ -632,7 +721,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_vec3f_i) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> v : vec3<f32>;
 
 fn f(i : i32) -> f32 {
@@ -642,12 +731,12 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_mat3x2f_1_0) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(m : mat3x2<f32>) -> f32 {
   return m[1][0];
 }
 )",
-         R"(
+             R"(
 fn f(m : mat3x2<f32>) -> f32 {
   return m[1i].x;
 }
@@ -655,14 +744,14 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_mat3x2f_1_0) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> m : mat3x2<f32>;
 
 fn f() -> f32 {
   return m[1][0];
 }
 )",
-         R"(
+             R"(
 var<private> m : mat3x2<f32>;
 
 fn f() -> f32 {
@@ -672,12 +761,12 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_mat3x2f_u_0) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(m : mat3x2<f32>, u : u32) -> f32 {
   return m[u][0];
 }
 )",
-         R"(
+             R"(
 fn f(m : mat3x2<f32>, u : u32) -> f32 {
   return m[u].x;
 }
@@ -685,14 +774,14 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_mat3x2f_u_0) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> m : mat3x2<f32>;
 
 fn f(u : u32) -> f32 {
   return m[u][0];
 }
 )",
-         R"(
+             R"(
 var<private> m : mat3x2<f32>;
 
 fn f(u : u32) -> f32 {
@@ -702,7 +791,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_mat3x2f_u_i) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(m : mat3x2<f32>, u : u32, i : i32) -> f32 {
   return m[u][i];
 }
@@ -710,7 +799,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_mat3x2f_u_i) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> m : mat3x2<f32>;
 
 fn f(u : u32, i : i32) -> f32 {
@@ -720,7 +809,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_array_0u) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : array<i32, 4u>) -> i32 {
   return a[0u];
 }
@@ -728,7 +817,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_array_0u) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> a : array<i32, 4u>;
 
 fn f() -> i32 {
@@ -738,7 +827,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Value_array_i) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : array<i32, 4u>, i : i32) -> i32 {
   return a[i];
 }
@@ -746,7 +835,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Ref_array_i) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> a : array<i32, 4u>;
 
 fn f(i : i32) -> i32 {
@@ -756,7 +845,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ValueStruct) {
-    Test(R"(
+    RUN_TEST(R"(
 struct Y {
   a : i32,
   b : i32,
@@ -776,7 +865,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ReferenceStruct) {
-    Test(R"(
+    RUN_TEST(R"(
 struct Y {
   a : i32,
   b : i32,
@@ -797,7 +886,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfArrayOfArray_123) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -810,7 +899,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfArrayOfArray_213) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -824,7 +913,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfArrayOfArray_312) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -838,7 +927,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfArrayOfArray_321) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -853,7 +942,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfMat3x4f_123) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -865,7 +954,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfMat3x4f_213) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -879,7 +968,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfMat3x4f_312) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -893,7 +982,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_ArrayOfMat3x4f_321) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> i32 {
   return 1i;
 }
@@ -908,7 +997,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_UsePartialChains) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> a : array<array<array<i32, 4u>, 5u>, 6u>;
 
 fn f(i : i32) -> i32 {
@@ -927,7 +1016,7 @@
 // Swizzle
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Value_xy) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> vec2<f32> {
   return v.xy;
 }
@@ -935,7 +1024,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Value_yz) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> vec2<f32> {
   return v.yz;
 }
@@ -943,7 +1032,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Value_yzx) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> vec3<f32> {
   return v.yzx;
 }
@@ -951,7 +1040,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Value_yzxy) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : vec3<f32>) -> vec4<f32> {
   return v.yzxy;
 }
@@ -959,7 +1048,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Pointer_xy) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : ptr<function, vec3<f32>>) -> vec2<f32> {
   return (*(v)).xy;
 }
@@ -967,7 +1056,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Pointer_yz) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : ptr<function, vec3<f32>>) -> vec2<f32> {
   return (*(v)).yz;
 }
@@ -975,7 +1064,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Pointer_yzx) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : ptr<function, vec3<f32>>) -> vec3<f32> {
   return (*(v)).yzx;
 }
@@ -983,7 +1072,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Access_Vec3_Pointer_yzxy) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(v : ptr<function, vec3<f32>>) -> vec4<f32> {
   return (*(v)).yzxy;
 }
@@ -994,7 +1083,7 @@
 // Unary ops
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, UnaryOp_Negate) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) -> i32 {
   return -(i);
 }
@@ -1002,7 +1091,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, UnaryOp_Complement) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : u32) -> u32 {
   return ~(i);
 }
@@ -1010,7 +1099,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, UnaryOp_Not) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(b : bool) -> bool {
   return !(b);
 }
@@ -1021,7 +1110,7 @@
 // Binary ops
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Add) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a + b);
 }
@@ -1029,7 +1118,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Subtract) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a - b);
 }
@@ -1037,7 +1126,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Multiply) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a * b);
 }
@@ -1045,7 +1134,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Divide) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a / b);
 }
@@ -1053,7 +1142,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Modulo) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a % b);
 }
@@ -1061,7 +1150,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_And) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a & b);
 }
@@ -1069,7 +1158,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Or) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a | b);
 }
@@ -1077,7 +1166,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Xor) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> i32 {
   return (a ^ b);
 }
@@ -1085,7 +1174,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_Equal) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> bool {
   return (a == b);
 }
@@ -1093,7 +1182,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_NotEqual) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> bool {
   return (a != b);
 }
@@ -1101,7 +1190,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_LessThan) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> bool {
   return (a < b);
 }
@@ -1109,7 +1198,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_GreaterThan) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> bool {
   return (a > b);
 }
@@ -1117,7 +1206,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_LessThanEqual) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> bool {
   return (a <= b);
 }
@@ -1125,7 +1214,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_GreaterThanEqual) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : i32) -> bool {
   return (a >= b);
 }
@@ -1133,7 +1222,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_ShiftLeft) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : u32) -> i32 {
   return (a << b);
 }
@@ -1141,7 +1230,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, BinaryOp_ShiftRight) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : i32, b : u32) -> i32 {
   return (a >> b);
 }
@@ -1152,7 +1241,7 @@
 // Short-circuiting binary ops
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Param_2) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool) -> bool {
   return (a && b);
 }
@@ -1160,7 +1249,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Param_3_ab_c) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   return ((a && b) && c);
 }
@@ -1168,7 +1257,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Param_3_a_bc) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   return ((a && b) && c);
 }
@@ -1176,7 +1265,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Let_2) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool) -> bool {
   let l = (a && b);
   return l;
@@ -1185,7 +1274,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Let_3_ab_c) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   let l = ((a && b) && c);
   return l;
@@ -1194,7 +1283,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Let_3_a_bc) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   let l = (a && (b && c));
   return l;
@@ -1203,7 +1292,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Call_2) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> bool {
   return true;
 }
@@ -1219,7 +1308,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Call_3_ab_c) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> bool {
   return true;
 }
@@ -1239,7 +1328,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_And_Call_3_a_bc) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> bool {
   return true;
 }
@@ -1259,7 +1348,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Param_2) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool) -> bool {
   return (a || b);
 }
@@ -1267,7 +1356,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Param_3_ab_c) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   return ((a || b) || c);
 }
@@ -1275,7 +1364,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Param_3_a_bc) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   return (a || (b || c));
 }
@@ -1283,7 +1372,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Let_2) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool) -> bool {
   let l = (a || b);
   return l;
@@ -1292,7 +1381,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Let_3_ab_c) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   let l = ((a || b) || c);
   return l;
@@ -1301,7 +1390,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Let_3_a_bc) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(a : bool, b : bool, c : bool) -> bool {
   let l = (a || (b || c));
   return l;
@@ -1310,7 +1399,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Call_2) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> bool {
   return true;
 }
@@ -1326,7 +1415,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Call_3_ab_c) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> bool {
   return true;
 }
@@ -1346,7 +1435,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Or_Call_3_a_bc) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() -> bool {
   return true;
 }
@@ -1366,7 +1455,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ShortCircuit_Mixed) {
-    Test(R"(
+    RUN_TEST(R"(
 fn b() -> bool {
   return true;
 }
@@ -1386,7 +1475,7 @@
 // Assignment
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Assign_ArrayOfArrayOfArrayAccess_123456) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1399,7 +1488,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Assign_ArrayOfArrayOfArrayAccess_261345) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1414,7 +1503,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Assign_ArrayOfArrayOfArrayAccess_532614) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1431,7 +1520,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Assign_ArrayOfMatrixAccess_123456) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1444,7 +1533,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Assign_ArrayOfMatrixAccess_261345) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1459,7 +1548,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Assign_ArrayOfMatrixAccess_532614) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1479,13 +1568,13 @@
 // Compound assignment
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Increment) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v++;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v + 1i);
@@ -1494,13 +1583,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Decrement) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v++;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v + 1i);
@@ -1509,13 +1598,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Add) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v += 8i;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v + 8i);
@@ -1524,13 +1613,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Subtract) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v -= 8i;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v - 8i);
@@ -1539,13 +1628,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Multiply) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v *= 8i;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v * 8i);
@@ -1554,13 +1643,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Divide) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v /= 8i;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v / 8i);
@@ -1569,13 +1658,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_Xor) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var v : i32;
   v ^= 8i;
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : i32;
   v = (v ^ 8i);
@@ -1584,7 +1673,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_ArrayOfArrayOfArrayAccess_123456) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1594,7 +1683,8 @@
   v[e(1i)][e(2i)][e(3i)] += v[e(4i)][e(5i)][e(6i)];
 }
 )",
-         R"(fn e(i : i32) -> i32 {
+             R"(
+fn e(i : i32) -> i32 {
   return i;
 }
 
@@ -1607,7 +1697,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_ArrayOfArrayOfArrayAccess_261345) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1619,7 +1709,8 @@
   v[e(1i)][v_2][e(3i)] += v[e(4i)][e(5i)][v_3];
 }
 )",
-         R"(fn e(i : i32) -> i32 {
+             R"(
+fn e(i : i32) -> i32 {
   return i;
 }
 
@@ -1634,7 +1725,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_ArrayOfArrayOfArrayAccess_532614) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1648,7 +1739,8 @@
   v[e(1i)][v_4][v_3] += v[e(4i)][v_2][v_5];
 }
 )",
-         R"(fn e(i : i32) -> i32 {
+             R"(
+fn e(i : i32) -> i32 {
   return i;
 }
 
@@ -1665,7 +1757,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_ArrayOfMatrixAccess_123456) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1675,7 +1767,8 @@
   v[e(1i)][e(2i)][e(3i)] += v[e(4i)][e(5i)][e(6i)];
 }
 )",
-         R"(fn e(i : i32) -> i32 {
+             R"(
+fn e(i : i32) -> i32 {
   return i;
 }
 
@@ -1689,7 +1782,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_ArrayOfMatrixAccess_261345) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1701,7 +1794,8 @@
   v[e(1i)][v_2][e(3i)] += v[e(4i)][e(5i)][v_3];
 }
 )",
-         R"(fn e(i : i32) -> i32 {
+             R"(
+fn e(i : i32) -> i32 {
   return i;
 }
 
@@ -1717,7 +1811,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, CompoundAssign_ArrayOfMatrixAccess_532614) {
-    Test(R"(
+    RUN_TEST(R"(
 fn e(i : i32) -> i32 {
   return i;
 }
@@ -1731,7 +1825,8 @@
   v[e(1i)][v_4][v_3] += v[e(4i)][v_2][v_5];
 }
 )",
-         R"(fn e(i : i32) -> i32 {
+             R"(
+fn e(i : i32) -> i32 {
   return i;
 }
 
@@ -1751,7 +1846,7 @@
 // Phony Assignment
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, PhonyAssign_PrivateVar) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> p : i32;
 
 fn f() {
@@ -1761,7 +1856,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, PhonyAssign_FunctionVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var i : i32;
   _ = i;
@@ -1770,13 +1865,13 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, PhonyAssign_FunctionLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   let i : i32 = 42i;
   _ = i;
 }
 )",
-         R"(
+             R"(
 fn f() {
   let i = 42i;
 }
@@ -1784,7 +1879,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, PhonyAssign_HandleVar) {
-    Test(R"(
+    RUN_TEST(R"(
 @group(0) @binding(0) var t : texture_2d<f32>;
 
 fn f() {
@@ -1794,19 +1889,19 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, PhonyAssign_Constant) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   _ = 42i;
 }
 )",
-         R"(
+             R"(
 fn f() {
 }
 )");
 }
 
 TEST_F(IRToProgramRoundtripTest, PhonyAssign_Call) {
-    Test(R"(
+    RUN_TEST(R"(
 fn v() -> i32 {
   return 42;
 }
@@ -1815,7 +1910,7 @@
   _ = v();
 }
 )",
-         R"(
+             R"(
 fn v() -> i32 {
   return 42i;
 }
@@ -1830,7 +1925,7 @@
 // let
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, LetUsedOnce) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : u32) -> u32 {
   let v = ~(i);
   return v;
@@ -1839,7 +1934,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, LetUsedTwice) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) -> i32 {
   let v = (i * 2i);
   return (v + v);
@@ -1851,19 +1946,25 @@
 // Module-scope var
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_i32) {
-    Test("var<private> v : i32 = 1i;");
+    RUN_TEST(R"(
+var<private> v : i32 = 1i;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_u32) {
-    Test("var<private> v : u32 = 1u;");
+    RUN_TEST(R"(
+var<private> v : u32 = 1u;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_f32) {
-    Test("var<private> v : f32 = 1.0f;");
+    RUN_TEST(R"(
+var<private> v : f32 = 1.0f;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_f16) {
-    Test(R"(
+    RUN_TEST(R"(
 enable f16;
 
 var<private> v : f16 = 1.0h;
@@ -1871,28 +1972,38 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_bool) {
-    Test("var<private> v : bool = true;");
+    RUN_TEST(R"(
+var<private> v : bool = true;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_array_NoArgs) {
-    Test("var<private> v : array<i32, 4u> = array<i32, 4u>();");
+    RUN_TEST(R"(
+var<private> v : array<i32, 4u> = array<i32, 4u>();
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_array_Zero) {
-    Test("var<private> v : array<i32, 4u> = array<i32, 4u>(0i, 0i, 0i, 0i);",
-         "var<private> v : array<i32, 4u> = array<i32, 4u>();");
+    RUN_TEST(R"(
+var<private> v : array<i32, 4u> = array<i32, 4u>(0i, 0i, 0i, 0i);
+var<private> v : array<i32, 4u> = array<i32, 4u>();
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_array_SameValue) {
-    Test("var<private> v : array<i32, 4u> = array<i32, 4u>(4i, 4i, 4i, 4i);");
+    RUN_TEST(R"(
+var<private> v : array<i32, 4u> = array<i32, 4u>(4i, 4i, 4i, 4i);
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_array_DifferentValues) {
-    Test("var<private> v : array<i32, 4u> = array<i32, 4u>(1i, 2i, 3i, 4i);");
+    RUN_TEST(R"(
+var<private> v : array<i32, 4u> = array<i32, 4u>(1i, 2i, 3i, 4i);
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_struct_NoArgs) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   i : i32,
   u : u32,
@@ -1904,7 +2015,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_struct_Zero) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   i : i32,
   u : u32,
@@ -1913,7 +2024,7 @@
 
 var<private> s : S = S(0i, 0u, 0f);
 )",
-         R"(
+             R"(
 struct S {
   i : i32,
   u : u32,
@@ -1925,7 +2036,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_struct_SameValue) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   b : i32,
@@ -1937,7 +2048,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_struct_DifferentValues) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   a : i32,
   b : i32,
@@ -1949,78 +2060,104 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_vec3f_NoArgs) {
-    Test("var<private> v : vec3<f32> = vec3<f32>();");
+    RUN_TEST(R"(
+var<private> v : vec3<f32> = vec3<f32>();
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_vec3f_Zero) {
-    Test("var<private> v : vec3<f32> = vec3<f32>(0f);",
-         "var<private> v : vec3<f32> = vec3<f32>();");
+    RUN_TEST(R"(
+var<private> v : vec3<f32> = vec3<f32>(0f);",
+             "var<private> v : vec3<f32> = vec3<f32>();
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_vec3f_Splat) {
-    Test("var<private> v : vec3<f32> = vec3<f32>(1.0f);");
+    RUN_TEST(R"(
+var<private> v : vec3<f32> = vec3<f32>(1.0f);
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_vec3f_Scalars) {
-    Test("var<private> v : vec3<f32> = vec3<f32>(1.0f, 2.0f, 3.0f);");
+    RUN_TEST(R"(
+var<private> v : vec3<f32> = vec3<f32>(1.0f, 2.0f, 3.0f);
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_mat2x3f_NoArgs) {
-    Test("var<private> v : mat2x3<f32> = mat2x3<f32>();");
+    RUN_TEST(R"(
+var<private> v : mat2x3<f32> = mat2x3<f32>();
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_mat2x3f_Scalars_SameValue) {
-    Test("var<private> v : mat2x3<f32> = mat2x3<f32>(4.0f, 4.0f, 4.0f, 4.0f, 4.0f, 4.0f);",
-         "var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(4.0f), vec3<f32>(4.0f));");
+    RUN_TEST(R"(
+var<private> v : mat2x3<f32> = mat2x3<f32>(4.0f, 4.0f, 4.0f, 4.0f, 4.0f, 4.0f);
+var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(4.0f), vec3<f32>(4.0f));
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_mat2x3f_Scalars) {
-    Test("var<private> v : mat2x3<f32> = mat2x3<f32>(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f);",
-         "var<private> v : mat2x3<f32> = "
-         "mat2x3<f32>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));");
+    RUN_TEST(R"(
+var<private> v : mat2x3<f32> = mat2x3<f32>(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f);
+var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_mat2x3f_Columns) {
-    Test(
-        "var<private> v : mat2x3<f32> = "
-        "mat2x3<f32>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));");
+    RUN_TEST(
+        R"(
+var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(1.0f, 2.0f, 3.0f), vec3<f32>(4.0f, 5.0f, 6.0f));
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Private_mat2x3f_Columns_SameValue) {
-    Test(
-        "var<private> v : mat2x3<f32> = "
-        "mat2x3<f32>(vec3<f32>(4.0f, 4.0f, 4.0f), vec3<f32>(4.0f, 4.0f, 4.0f));",
-        "var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(4.0f), vec3<f32>(4.0f));");
+    RUN_TEST(R"(
+var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(4.0f, 4.0f, 4.0f), vec3<f32>(4.0f, 4.0f, 4.0f));
+var<private> v : mat2x3<f32> = mat2x3<f32>(vec3<f32>(4.0f), vec3<f32>(4.0f));
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Uniform_vec4i) {
-    Test("@group(10) @binding(20) var<uniform> v : vec4<i32>;");
+    RUN_TEST(R"(
+@group(10) @binding(20) var<uniform> v : vec4<i32>;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_StorageRead_u32) {
-    Test("@group(10) @binding(20) var<storage, read> v : u32;");
+    RUN_TEST(R"(
+@group(10) @binding(20) var<storage, read> v : u32;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_StorageReadWrite_i32) {
-    Test("@group(10) @binding(20) var<storage, read_write> v : i32;");
+    RUN_TEST(R"(
+@group(10) @binding(20) var<storage, read_write> v : i32;
+)");
 }
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Handle_Texture2D) {
-    Test("@group(0) @binding(0) var t : texture_2d<f32>;");
+    RUN_TEST(R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Handle_Sampler) {
-    Test("@group(0) @binding(0) var s : sampler;");
+    RUN_TEST(R"(
+@group(0) @binding(0) var s : sampler;
+)");
 }
 
 TEST_F(IRToProgramRoundtripTest, ModuleScopeVar_Handle_SamplerCmp) {
-    Test("@group(0) @binding(0) var s : sampler_comparison;");
+    RUN_TEST(R"(
+@group(0) @binding(0) var s : sampler_comparison;
+)");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Function-scope var
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, FunctionScopeVar_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var i : i32;
 }
@@ -2028,7 +2165,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FunctionScopeVar_i32_InitLiteral) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var i : i32 = 42i;
 }
@@ -2036,7 +2173,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FunctionScopeVar_Chained) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var a : i32 = 42i;
   var b : i32 = a;
@@ -2049,7 +2186,7 @@
 // Function-scope let
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, FunctionScopeLet_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(i : i32) -> i32 {
   let a = (42i + i);
   let b = (24i + i);
@@ -2060,7 +2197,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, FunctionScopeLet_ptr) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var a : array<i32, 3u>;
   let b = &(a[1i]);
@@ -2075,7 +2212,7 @@
     // If their constant values were inlined, then the initializer for 'c' would be treated as a
     // constant expression instead of the authored runtime expression. Evaluating '1 / 0' as a
     // constant expression is a WGSL validation error.
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   let a = 1i;
   let b = 0i;
@@ -2088,7 +2225,7 @@
 // If
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, If_CallFn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2101,7 +2238,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_Return) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(cond : bool) {
   if (cond) {
     return;
@@ -2111,7 +2248,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_Return_i32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var cond : bool = true;
   if (cond) {
@@ -2123,7 +2260,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_CallFn_Else_CallFn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2141,7 +2278,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_Return_f32_Else_Return_f32) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> f32 {
   var cond : bool = true;
   if (cond) {
@@ -2154,7 +2291,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_Return_u32_Else_CallFn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2175,7 +2312,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_CallFn_ElseIf_CallFn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2198,7 +2335,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, If_Else_Chain) {
-    Test(R"(
+    RUN_TEST(R"(
 fn x(i : i32) -> bool {
   return true;
 }
@@ -2221,7 +2358,7 @@
 // Switch
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Switch_Default) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2237,7 +2374,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Switch_3_Cases) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2265,7 +2402,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Switch_3_Cases_AllReturn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2285,7 +2422,7 @@
   a();
 }
 )",
-         R"(
+             R"(
 fn a() {
 }
 
@@ -2307,7 +2444,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Switch_Nested) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a() {
 }
 
@@ -2345,7 +2482,7 @@
 // For
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, For_Empty) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   for(var i : i32 = 0i; (i < 5i); i = (i + 1i)) {
   }
@@ -2354,7 +2491,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_Empty_NoInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var i : i32 = 0i;
   for(; (i < 5i); i = (i + 1i)) {
@@ -2364,14 +2501,14 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_Empty_NoCond) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   for(var i : i32 = 0i; ; i = (i + 1i)) {
     break;
   }
 }
 )",
-         R"(
+             R"(
 fn f() {
   {
     var i : i32 = 0i;
@@ -2388,7 +2525,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_Empty_NoCont) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   for(var i : i32 = 0i; (i < 5i); ) {
   }
@@ -2397,7 +2534,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_ComplexBody) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> bool {
   return (v == 1i);
 }
@@ -2416,7 +2553,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_ComplexBody_NoInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> bool {
   return (v == 1i);
 }
@@ -2436,7 +2573,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_ComplexBody_NoCond) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> bool {
   return (v == 1i);
 }
@@ -2451,7 +2588,7 @@
   }
 }
 )",
-         R"(
+             R"(
 fn a(v : i32) -> bool {
   return (v == 1i);
 }
@@ -2476,7 +2613,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_ComplexBody_NoCont) {
-    Test(R"(
+    RUN_TEST(R"(
 fn a(v : i32) -> bool {
   return (v == 1i);
 }
@@ -2495,7 +2632,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_CallInInitCondCont) {
-    Test(R"(
+    RUN_TEST(R"(
 fn n(v : i32) -> i32 {
   return (v + 1i);
 }
@@ -2508,7 +2645,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_AssignAsInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn n() {
 }
 
@@ -2521,7 +2658,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_CompoundAssignAsInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn n() {
 }
 
@@ -2531,7 +2668,7 @@
   }
 }
 )",
-         R"(
+             R"(
 fn n() {
 }
 
@@ -2544,7 +2681,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_IncrementAsInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn n() {
 }
 
@@ -2554,7 +2691,7 @@
   }
 }
 )",
-         R"(
+             R"(
 fn n() {
 }
 
@@ -2567,7 +2704,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_DecrementAsInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn n() {
 }
 
@@ -2577,7 +2714,7 @@
   }
 }
 )",
-         R"(
+             R"(
 fn n() {
 }
 
@@ -2590,7 +2727,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, For_CallAsInit) {
-    Test(R"(
+    RUN_TEST(R"(
 fn n() {
 }
 
@@ -2606,7 +2743,7 @@
 // While
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, While_Empty) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   while(true) {
   }
@@ -2615,7 +2752,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, While_Cond) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(cond : bool) {
   while(cond) {
   }
@@ -2624,7 +2761,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, While_Break) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   while(true) {
     break;
@@ -2634,7 +2771,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, While_IfBreak) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(cond : bool) {
   while(true) {
     if (cond) {
@@ -2646,7 +2783,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, While_IfReturn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(cond : bool) {
   while(true) {
     if (cond) {
@@ -2661,7 +2798,7 @@
 // Loop
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Loop_Break) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   loop {
     break;
@@ -2671,7 +2808,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_IfBreak) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(cond : bool) {
   loop {
     if (cond) {
@@ -2683,7 +2820,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_IfReturn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f(cond : bool) {
   loop {
     if (cond) {
@@ -2695,7 +2832,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_IfContinuing) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var cond : bool = false;
   loop {
@@ -2712,7 +2849,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_VarsDeclaredOutsideAndInside) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   var b : i32 = 1i;
   loop {
@@ -2730,7 +2867,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_BreakIf_EmptyBody) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   loop {
 
@@ -2743,7 +2880,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_BreakIf_NotFalse) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   loop {
     if (false) {
@@ -2757,7 +2894,7 @@
   }
 }
 )",
-         R"(
+             R"(
 fn f() {
   loop {
     if (!(false)) {
@@ -2773,7 +2910,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_BreakIf_NotTrue) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   loop {
     if (false) {
@@ -2787,7 +2924,7 @@
   }
 }
 )",
-         R"(
+             R"(
 fn f() {
   loop {
     if (!(false)) {
@@ -2803,7 +2940,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Loop_WithReturn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() {
   loop {
     let i = 42i;
@@ -2817,12 +2954,12 @@
 // Shadowing tests
 ////////////////////////////////////////////////////////////////////////////////
 TEST_F(IRToProgramRoundtripTest, Shadow_f32_With_Fn) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f32() {
   var v = mat4x4f();
 }
 )",
-         R"(
+             R"(
 fn f32_1() {
   var v : mat4x4<f32> = mat4x4<f32>();
 }
@@ -2830,7 +2967,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_f32_With_Struct) {
-    Test(R"(
+    RUN_TEST(R"(
 struct f32 {
   v : i32,
 }
@@ -2839,7 +2976,7 @@
   let f = vec2f(1.0f);
 }
 )",
-         R"(
+             R"(
 struct f32_1 {
   v : i32,
 }
@@ -2851,21 +2988,21 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_f32_With_ModVar) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> f32 : vec2f = vec2f(0.0f, 1.0f);
 )",
-         R"(
+             R"(
 var<private> f32_1 : vec2<f32> = vec2<f32>(0.0f, 1.0f);
 )");
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_f32_With_ModVar2) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> f32 : i32 = 1i;
 
 var<private> v = vec2(1.0).x;
 )",
-         R"(
+             R"(
 var<private> f32_1 : i32 = 1i;
 
 var<private> v : f32 = 1.0f;
@@ -2873,14 +3010,14 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_f32_With_Alias) {
-    Test(R"(
+    RUN_TEST(R"(
 alias f32 = i32;
 
 fn f() {
   var v = vec3(1.0f, 2.0f, 3.0f);
 }
 )",
-         R"(
+             R"(
 fn f() {
   var v : vec3<f32> = vec3<f32>(1.0f, 2.0f, 3.0f);
 }
@@ -2888,7 +3025,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_Struct_With_FnVar) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   i : i32,
 }
@@ -2901,7 +3038,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_Struct_With_Param) {
-    Test(R"(
+    RUN_TEST(R"(
 struct S {
   i : i32,
 }
@@ -2913,7 +3050,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_ModVar_With_FnVar) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> i : i32 = 1i;
 
 fn f() -> i32 {
@@ -2925,7 +3062,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_ModVar_With_FnLet) {
-    Test(R"(
+    RUN_TEST(R"(
 var<private> i : i32 = 1i;
 
 fn f() -> i32 {
@@ -2937,7 +3074,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_IfVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   if (true) {
@@ -2951,7 +3088,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_IfLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   if (true) {
@@ -2965,7 +3102,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_WhileVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   while((i < 4i)) {
@@ -2978,7 +3115,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_WhileLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   while((i < 4i)) {
@@ -2991,7 +3128,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_ForInitVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   for(var i : f32 = 0.0f; (i < 4.0f); ) {
@@ -3003,7 +3140,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_ForInitLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   for(let i = 0.0f; (i < 4.0f); ) {
@@ -3015,7 +3152,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_ForBodyVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   for(var x : i32 = 0i; (i < 4i); ) {
@@ -3028,7 +3165,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_ForBodyLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   for(var x : i32 = 0i; (i < 4i); ) {
@@ -3041,7 +3178,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_LoopBodyVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   loop {
@@ -3059,7 +3196,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_LoopBodyLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   loop {
@@ -3077,7 +3214,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_LoopContinuingVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   loop {
@@ -3096,7 +3233,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_LoopContinuingLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   loop {
@@ -3115,7 +3252,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_SwitchCaseVar) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   switch(i) {
@@ -3135,7 +3272,7 @@
 }
 
 TEST_F(IRToProgramRoundtripTest, Shadow_FnVar_With_SwitchCaseLet) {
-    Test(R"(
+    RUN_TEST(R"(
 fn f() -> i32 {
   var i : i32;
   switch(i) {
diff --git a/src/tint/lang/wgsl/reader/lower/lower_test.cc b/src/tint/lang/wgsl/reader/lower/lower_test.cc
index dd05a5d..d8ec462 100644
--- a/src/tint/lang/wgsl/reader/lower/lower_test.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower_test.cc
@@ -33,7 +33,7 @@
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
 
-namespace tint::wgsl::reader::lower {
+namespace tint::wgsl::reader {
 namespace {
 
 using namespace tint::core::fluent_types;     // NOLINT
@@ -124,4 +124,4 @@
 }
 
 }  // namespace
-}  // namespace tint::wgsl::reader::lower
+}  // namespace tint::wgsl::reader
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 0ae2eea..740c918 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
@@ -950,7 +950,7 @@
                 if (!rhs) {
                     return;
                 }
-                core::ir::Binary* inst = impl.BinaryOp(ty, lhs, rhs, b->op);
+                auto* inst = impl.BinaryOp(ty, lhs, rhs, b->op);
                 if (!inst) {
                     return;
                 }
@@ -1288,10 +1288,10 @@
             TINT_ICE_ON_NO_MATCH);
     }
 
-    core::ir::Binary* BinaryOp(const core::type::Type* ty,
-                               core::ir::Value* lhs,
-                               core::ir::Value* rhs,
-                               core::BinaryOp op) {
+    core::ir::CoreBinary* BinaryOp(const core::type::Type* ty,
+                                   core::ir::Value* lhs,
+                                   core::ir::Value* rhs,
+                                   core::BinaryOp op) {
         switch (op) {
             case core::BinaryOp::kAnd:
                 return builder_.And(ty, lhs, rhs);
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
index f4308d6..f580105 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.bazel
@@ -61,6 +61,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/strconv",
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
index 1f2d210..7ce8c41 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.cmake
@@ -62,6 +62,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_strconv
diff --git a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
index 796fc83..3d484a5 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/ast_printer/BUILD.gn
@@ -64,6 +64,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",
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 f7b6321..47b6468 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
@@ -749,12 +749,15 @@
     void Unary(const core::ir::Unary* u) {
         const ast::Expression* expr = nullptr;
         switch (u->Op()) {
-            case core::ir::UnaryOp::kComplement:
+            case core::UnaryOp::kComplement:
                 expr = b.Complement(Expr(u->Val()));
                 break;
-            case core::ir::UnaryOp::kNegation:
+            case core::UnaryOp::kNegation:
                 expr = b.Negation(Expr(u->Val()));
                 break;
+            default:
+                TINT_UNIMPLEMENTED() << u->Op();
+                break;
         }
         Bind(u->Result(0), expr);
     }
@@ -809,7 +812,7 @@
     }
 
     void Binary(const core::ir::Binary* e) {
-        if (e->Op() == core::ir::BinaryOp::kEqual) {
+        if (e->Op() == core::BinaryOp::kEqual) {
             auto* rhs = e->RHS()->As<core::ir::Constant>();
             if (rhs && rhs->Type()->Is<core::type::Bool>() &&
                 rhs->Value()->ValueAs<bool>() == false) {
@@ -822,54 +825,60 @@
         auto* rhs = Expr(e->RHS());
         const ast::Expression* expr = nullptr;
         switch (e->Op()) {
-            case core::ir::BinaryOp::kAdd:
+            case core::BinaryOp::kAdd:
                 expr = b.Add(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kSubtract:
+            case core::BinaryOp::kSubtract:
                 expr = b.Sub(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kMultiply:
+            case core::BinaryOp::kMultiply:
                 expr = b.Mul(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kDivide:
+            case core::BinaryOp::kDivide:
                 expr = b.Div(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kModulo:
+            case core::BinaryOp::kModulo:
                 expr = b.Mod(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kAnd:
+            case core::BinaryOp::kAnd:
                 expr = b.And(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kOr:
+            case core::BinaryOp::kOr:
                 expr = b.Or(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kXor:
+            case core::BinaryOp::kXor:
                 expr = b.Xor(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kEqual:
+            case core::BinaryOp::kEqual:
                 expr = b.Equal(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kNotEqual:
+            case core::BinaryOp::kNotEqual:
                 expr = b.NotEqual(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kLessThan:
+            case core::BinaryOp::kLessThan:
                 expr = b.LessThan(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kGreaterThan:
+            case core::BinaryOp::kGreaterThan:
                 expr = b.GreaterThan(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kLessThanEqual:
+            case core::BinaryOp::kLessThanEqual:
                 expr = b.LessThanEqual(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kGreaterThanEqual:
+            case core::BinaryOp::kGreaterThanEqual:
                 expr = b.GreaterThanEqual(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kShiftLeft:
+            case core::BinaryOp::kShiftLeft:
                 expr = b.Shl(lhs, rhs);
                 break;
-            case core::ir::BinaryOp::kShiftRight:
+            case core::BinaryOp::kShiftRight:
                 expr = b.Shr(lhs, rhs);
                 break;
+            case core::BinaryOp::kLogicalAnd:
+                expr = b.LogicalAnd(lhs, rhs);
+                break;
+            case core::BinaryOp::kLogicalOr:
+                expr = b.LogicalOr(lhs, rhs);
+                break;
         }
         Bind(e->Result(0), expr);
     }
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 f379020..cd77dba 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
@@ -33,6 +33,7 @@
 #include "src/tint/lang/core/ir/disassembler.h"
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
+#include "src/tint/lang/wgsl/ir/unary.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
@@ -926,7 +927,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 // Short-circuiting binary ops
 ////////////////////////////////////////////////////////////////////////////////
-TEST_F(IRToProgramTest, ShortCircuit_And_Param_2) {
+TEST_F(IRToProgramTest, ShortCircuit_And_2) {
     auto* fn = b.Function("f", ty.bool_());
     auto* pa = b.FunctionParam("a", ty.bool_());
     auto* pb = b.FunctionParam("b", ty.bool_());
@@ -938,7 +939,7 @@
         b.Append(if_->True(), [&] { b.ExitIf(if_, pb); });
         b.Append(if_->False(), [&] { b.ExitIf(if_, false); });
 
-        b.Return(fn, if_->Result(0));
+        b.Return(fn, if_);
     });
 
     EXPECT_WGSL(R"(
@@ -948,7 +949,7 @@
 )");
 }
 
-TEST_F(IRToProgramTest, ShortCircuit_And_Param_3_ab_c) {
+TEST_F(IRToProgramTest, ShortCircuit_And_3_ab_c) {
     auto* fn = b.Function("f", ty.bool_());
     auto* pa = b.FunctionParam("a", ty.bool_());
     auto* pb = b.FunctionParam("b", ty.bool_());
@@ -961,12 +962,12 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, pb); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, false); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, pc); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -976,7 +977,7 @@
 )");
 }
 
-TEST_F(IRToProgramTest, ShortCircuit_And_Param_3_a_bc) {
+TEST_F(IRToProgramTest, ShortCircuit_And_3_a_bc) {
     auto* fn = b.Function("f", ty.bool_());
     auto* pa = b.FunctionParam("a", ty.bool_());
     auto* pb = b.FunctionParam("b", ty.bool_());
@@ -992,10 +993,10 @@
             b.Append(if2->True(), [&] { b.ExitIf(if2, pc); });
             b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-            b.ExitIf(if1, if2->Result(0));
+            b.ExitIf(if1, if2);
         });
         b.Append(if1->False(), [&] { b.ExitIf(if1, false); });
-        b.Return(fn, if1->Result(0));
+        b.Return(fn, if1);
     });
 
     EXPECT_WGSL(R"(
@@ -1017,8 +1018,8 @@
         b.Append(if_->True(), [&] { b.ExitIf(if_, pb); });
         b.Append(if_->False(), [&] { b.ExitIf(if_, false); });
 
-        mod.SetName(if_->Result(0), "l");
-        b.Return(fn, if_->Result(0));
+        mod.SetName(if_, "l");
+        b.Return(fn, if_);
     });
 
     EXPECT_WGSL(R"(
@@ -1042,13 +1043,13 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, pb); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, false); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, pc); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-        mod.SetName(if2->Result(0), "l");
-        b.Return(fn, if2->Result(0));
+        mod.SetName(if2, "l");
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1075,12 +1076,12 @@
             b.Append(if2->True(), [&] { b.ExitIf(if2, pc); });
             b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-            b.ExitIf(if1, if2->Result(0));
+            b.ExitIf(if1, if2);
         });
         b.Append(if1->False(), [&] { b.ExitIf(if1, false); });
 
-        mod.SetName(if1->Result(0), "l");
-        b.Return(fn, if1->Result(0));
+        mod.SetName(if1, "l");
+        b.Return(fn, if1);
     });
 
     EXPECT_WGSL(R"(
@@ -1106,7 +1107,7 @@
         b.Append(if_->True(), [&] { b.ExitIf(if_, b.Call(ty.bool_(), fn_b)); });
         b.Append(if_->False(), [&] { b.ExitIf(if_, false); });
 
-        b.Return(fn, if_->Result(0));
+        b.Return(fn, if_);
     });
 
     EXPECT_WGSL(R"(
@@ -1142,12 +1143,12 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, b.Call(ty.bool_(), fn_b)); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, false); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, b.Call(ty.bool_(), fn_c)); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1190,11 +1191,11 @@
             b.Append(if2->True(), [&] { b.ExitIf(if2, b.Call(ty.bool_(), fn_c)); });
             b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-            b.ExitIf(if1, if2->Result(0));
+            b.ExitIf(if1, if2);
         });
         b.Append(if1->False(), [&] { b.ExitIf(if1, false); });
 
-        b.Return(fn, if1->Result(0));
+        b.Return(fn, if1);
     });
 
     EXPECT_WGSL(R"(
@@ -1228,7 +1229,7 @@
         b.Append(if_->True(), [&] { b.ExitIf(if_, true); });
         b.Append(if_->False(), [&] { b.ExitIf(if_, pb); });
 
-        b.Return(fn, if_->Result(0));
+        b.Return(fn, if_);
     });
 
     EXPECT_WGSL(R"(
@@ -1251,12 +1252,12 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, true); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, pb); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, pc); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1283,10 +1284,10 @@
             b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
             b.Append(if2->False(), [&] { b.ExitIf(if2, pc); });
 
-            b.ExitIf(if1, if2->Result(0));
+            b.ExitIf(if1, if2);
         });
 
-        b.Return(fn, if1->Result(0));
+        b.Return(fn, if1);
     });
 
     EXPECT_WGSL(R"(
@@ -1308,8 +1309,8 @@
         b.Append(if_->True(), [&] { b.ExitIf(if_, true); });
         b.Append(if_->False(), [&] { b.ExitIf(if_, pb); });
 
-        mod.SetName(if_->Result(0), "l");
-        b.Return(fn, if_->Result(0));
+        mod.SetName(if_, "l");
+        b.Return(fn, if_);
     });
 
     EXPECT_WGSL(R"(
@@ -1333,13 +1334,13 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, true); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, pb); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, pc); });
 
-        mod.SetName(if2->Result(0), "l");
-        b.Return(fn, if2->Result(0));
+        mod.SetName(if2, "l");
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1367,11 +1368,11 @@
             b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
             b.Append(if2->False(), [&] { b.ExitIf(if2, pc); });
 
-            b.ExitIf(if1, if2->Result(0));
+            b.ExitIf(if1, if2);
         });
 
-        mod.SetName(if1->Result(0), "l");
-        b.Return(fn, if1->Result(0));
+        mod.SetName(if1, "l");
+        b.Return(fn, if1);
     });
 
     EXPECT_WGSL(R"(
@@ -1397,7 +1398,7 @@
         b.Append(if_->True(), [&] { b.ExitIf(if_, true); });
         b.Append(if_->False(), [&] { b.ExitIf(if_, b.Call(ty.bool_(), fn_b)); });
 
-        b.Return(fn, if_->Result(0));
+        b.Return(fn, if_);
     });
 
     EXPECT_WGSL(R"(
@@ -1433,12 +1434,12 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, true); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, b.Call(ty.bool_(), fn_b)); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, b.Call(ty.bool_(), fn_c)); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1482,10 +1483,10 @@
             b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
             b.Append(if2->False(), [&] { b.ExitIf(if2, b.Call(ty.bool_(), fn_c)); });
 
-            b.ExitIf(if1, if2->Result(0));
+            b.ExitIf(if1, if2);
         });
 
-        b.Return(fn, if1->Result(0));
+        b.Return(fn, if1);
     });
 
     EXPECT_WGSL(R"(
@@ -1525,7 +1526,7 @@
         b.Append(if1->True(), [&] { b.ExitIf(if1, true); });
         b.Append(if1->False(), [&] { b.ExitIf(if1, b.Call(ty.bool_(), fn_b)); });
 
-        auto* if2 = b.If(if1->Result(0));
+        auto* if2 = b.If(if1);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] {
             auto* if3 = b.If(pc);
@@ -1533,12 +1534,12 @@
             b.Append(if3->True(), [&] { b.ExitIf(if3, true); });
             b.Append(if3->False(), [&] { b.ExitIf(if3, b.Call(ty.bool_(), fn_d)); });
 
-            b.ExitIf(if2, if3->Result(0));
+            b.ExitIf(if2, if3);
         });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-        mod.SetName(if2->Result(0), "l");
-        b.Return(fn, if2->Result(0));
+        mod.SetName(if2, "l");
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1582,9 +1583,9 @@
 
         auto* if2 = b.If(pa);
         if2->SetResults(b.InstructionResult(ty.bool_()));
-        b.Append(if2->True(), [&] { b.ExitIf(if2, if1->Result(0)); });
+        b.Append(if2->True(), [&] { b.ExitIf(if2, if1); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1623,10 +1624,10 @@
 
         auto* if2 = b.If(b.Call(ty.bool_(), fn_a));
         if2->SetResults(b.InstructionResult(ty.bool_()));
-        b.Append(if2->True(), [&] { b.ExitIf(if2, if1->Result(0)); });
+        b.Append(if2->True(), [&] { b.ExitIf(if2, if1); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1669,10 +1670,10 @@
 
         auto* if2 = b.If(pa);
         if2->SetResults(b.InstructionResult(ty.bool_()));
-        b.Append(if2->True(), [&] { b.ExitIf(if2, if1->Result(0)); });
+        b.Append(if2->True(), [&] { b.ExitIf(if2, if1); });
         b.Append(if2->False(), [&] { b.ExitIf(if2, false); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1704,10 +1705,10 @@
         auto* if2 = b.If(pa);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
-        b.Append(if2->False(), [&] { b.ExitIf(if2, if1->Result(0)); });
+        b.Append(if2->False(), [&] { b.ExitIf(if2, if1); });
 
-        mod.SetName(if2->Result(0), "l");
-        b.Return(fn, if2->Result(0));
+        mod.SetName(if2, "l");
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1747,9 +1748,9 @@
         auto* if2 = b.If(b.Call(ty.bool_(), fn_a));
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
-        b.Append(if2->False(), [&] { b.ExitIf(if2, if1->Result(0)); });
+        b.Append(if2->False(), [&] { b.ExitIf(if2, if1); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1793,9 +1794,9 @@
         auto* if2 = b.If(pa);
         if2->SetResults(b.InstructionResult(ty.bool_()));
         b.Append(if2->True(), [&] { b.ExitIf(if2, true); });
-        b.Append(if2->False(), [&] { b.ExitIf(if2, if1->Result(0)); });
+        b.Append(if2->False(), [&] { b.ExitIf(if2, if1); });
 
-        b.Return(fn, if2->Result(0));
+        b.Return(fn, if2);
     });
 
     EXPECT_WGSL(R"(
@@ -1813,7 +1814,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Add(ty.i32(), b.Load(v), 1_i));
 
         b.Return(fn);
@@ -1831,7 +1832,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Subtract(ty.i32(), b.Load(v), 1_i));
 
         b.Return(fn);
@@ -1849,7 +1850,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Add(ty.i32(), b.Load(v), 8_i));
 
         b.Return(fn);
@@ -1867,7 +1868,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Subtract(ty.i32(), b.Load(v), 8_i));
 
         b.Return(fn);
@@ -1885,7 +1886,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Multiply(ty.i32(), b.Load(v), 8_i));
 
         b.Return(fn);
@@ -1903,7 +1904,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Divide(ty.i32(), b.Load(v), 8_i));
 
         b.Return(fn);
@@ -1921,7 +1922,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
+        auto* v = b.Var<function, i32>("v");
         b.Store(v, b.Xor(ty.i32(), b.Load(v), 8_i));
 
         b.Return(fn);
@@ -1944,9 +1945,8 @@
     fn->SetParams({i});
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Complement(ty.u32(), i);
+        auto* v = b.Let("v", b.Complement(ty.u32(), i));
         b.Return(fn, v);
-        mod.SetName(v, "v");
     });
 
     EXPECT_WGSL(R"(
@@ -1963,9 +1963,8 @@
     fn->SetParams({i});
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Multiply(ty.i32(), i, 2_i);
+        auto* v = b.Let("v", b.Multiply(ty.i32(), i, 2_i));
         b.Return(fn, b.Add(ty.i32(), v, v));
-        mod.SetName(v, "v");
     });
 
     EXPECT_WGSL(R"(
@@ -1983,7 +1982,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {  //
-        b.Var("i", ty.ptr<function, i32>());
+        b.Var<function, i32>("i");
 
         b.Return(fn);
     });
@@ -1999,8 +1998,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* i = b.Var("i", ty.ptr<function, i32>());
-        i->SetInitializer(b.Constant(42_i));
+        b.Var("i", 42_i);
 
         b.Return(fn);
     });
@@ -2016,16 +2014,11 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* va = b.Var("a", ty.ptr<function, i32>());
-        va->SetInitializer(b.Constant(42_i));
+        auto* va = b.Var("a", 42_i);
 
-        auto* la = b.Load(va)->Result(0);
-        auto* vb = b.Var("b", ty.ptr<function, i32>());
-        vb->SetInitializer(la);
+        auto* vb = b.Var("b", b.Load(va));
 
-        auto* lb = b.Load(vb)->Result(0);
-        auto* vc = b.Var("c", ty.ptr<function, i32>());
-        vc->SetInitializer(lb);
+        b.Var("c", b.Load(vb));
 
         b.Return(fn);
     });
@@ -2097,8 +2090,7 @@
     auto* fn = b.Function("f", ty.i32());
 
     b.Append(fn->Block(), [&] {
-        auto* cond = b.Var("cond", ty.ptr<function, bool>());
-        cond->SetInitializer(b.Constant(true));
+        auto* cond = b.Var("cond", true);
         auto if_ = b.If(b.Load(cond));
         b.Append(if_->True(), [&] { b.Return(fn, 42_i); });
 
@@ -2162,8 +2154,7 @@
     auto* fn = b.Function("f", ty.f32());
 
     b.Append(fn->Block(), [&] {
-        auto* cond = b.Var("cond", ty.ptr<function, bool>());
-        cond->SetInitializer(b.Constant(true));
+        auto* cond = b.Var("cond", true);
         auto if_ = b.If(b.Load(cond));
         b.Append(if_->True(), [&] { b.Return(fn, 1.0_f); });
         b.Append(if_->False(), [&] { b.Return(fn, 2.0_f); });
@@ -2193,8 +2184,7 @@
     auto* fn = b.Function("f", ty.u32());
 
     b.Append(fn->Block(), [&] {
-        auto* cond = b.Var("cond", ty.ptr<function, bool>());
-        cond->SetInitializer(b.Constant(true));
+        auto* cond = b.Var("cond", true);
         auto if_ = b.If(b.Load(cond));
         b.Append(if_->True(), [&] { b.Return(fn, 1_u); });
         b.Append(if_->False(), [&] {
@@ -2238,8 +2228,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* cond = b.Var("cond", ty.ptr<function, bool>());
-        cond->SetInitializer(b.Constant(true));
+        auto* cond = b.Var("cond", true);
         auto if1 = b.If(b.Load(cond));
         b.Append(if1->True(), [&] {
             b.Call(ty.void_(), fn_a);
@@ -2351,8 +2340,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
-        v->SetInitializer(b.Constant(42_i));
+        auto* v = b.Var("v", 42_i);
 
         auto s = b.Switch(b.Load(v));
         b.Append(b.DefaultCase(s), [&] {
@@ -2391,8 +2379,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
-        v->SetInitializer(b.Constant(42_i));
+        auto* v = b.Var("v", 42_i);
 
         auto s = b.Switch(b.Load(v));
         b.Append(b.Case(s, {b.Constant(0_i)}), [&] {
@@ -2445,8 +2432,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* v = b.Var("v", ty.ptr<function, i32>());
-        v->SetInitializer(b.Constant(42_i));
+        auto* v = b.Var("v", 42_i);
 
         auto s = b.Switch(b.Load(v));
         b.Append(b.Case(s, {b.Constant(0_i)}), [&] { b.Return(fn); });
@@ -2491,11 +2477,9 @@
 
     auto* fn = b.Function("f", ty.void_());
     b.Append(fn->Block(), [&] {
-        auto* v1 = b.Var("v1", ty.ptr<function, i32>());
-        v1->SetInitializer(b.Constant(42_i));
+        auto* v1 = b.Var("v1", 42_i);
 
-        auto* v2 = b.Var("v2", ty.ptr<function, i32>());
-        v2->SetInitializer(b.Constant(24_i));
+        auto* v2 = b.Var("v2", 24_i);
 
         auto s1 = b.Switch(b.Load(v1));
         b.Append(b.Case(s1, {b.Constant(0_i)}), [&] {
@@ -2560,8 +2544,7 @@
         auto* loop = b.Loop();
 
         b.Append(loop->Initializer(), [&] {
-            auto* i = b.Var("i", ty.ptr<function, i32>());
-            i->SetInitializer(b.Constant(0_i));
+            auto* i = b.Var("i", 0_i);
             b.NextIteration(loop);
 
             b.Append(loop->Body(), [&] {
@@ -2592,8 +2575,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* i = b.Var("i", ty.ptr<function, i32>());
-        i->SetInitializer(b.Constant(0_i));
+        auto* i = b.Var("i", 0_i);
 
         auto* loop = b.Loop();
 
@@ -2628,8 +2610,7 @@
         auto* loop = b.Loop();
 
         b.Append(loop->Initializer(), [&] {
-            auto* i = b.Var("i", ty.ptr<function, i32>());
-            i->SetInitializer(b.Constant(0_i));
+            auto* i = b.Var("i", 0_i);
             b.NextIteration(loop);
 
             b.Append(loop->Body(), [&] {
@@ -2663,8 +2644,7 @@
         auto* loop = b.Loop();
 
         b.Append(loop->Initializer(), [&] {
-            auto* i = b.Var("i", ty.ptr<function, i32>());
-            i->SetInitializer(b.Constant(0_i));
+            auto* i = b.Var("i", 0_i);
             b.NextIteration(loop);
 
             b.Append(loop->Body(), [&] {
@@ -2714,8 +2694,7 @@
     auto* fn = b.Function("f", ty.i32());
 
     b.Append(fn->Block(), [&] {
-        auto* i = b.Var("i", ty.ptr<function, i32>());
-        i->SetInitializer(b.Constant(0_i));
+        auto* i = b.Var("i", 0_i);
 
         auto* loop = b.Loop();
 
@@ -2770,8 +2749,7 @@
         auto* loop = b.Loop();
 
         b.Append(loop->Initializer(), [&] {
-            auto* i = b.Var("i", ty.ptr<function, i32>());
-            i->SetInitializer(b.Constant(0_i));
+            auto* i = b.Var("i", 0_i);
             b.NextIteration(loop);
 
             b.Append(loop->Body(), [&] {
@@ -2820,9 +2798,7 @@
         auto* loop = b.Loop();
 
         b.Append(loop->Initializer(), [&] {
-            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);
+            auto* i = b.Var("i", b.Call(ty.i32(), fn_n, 0_i));
             b.NextIteration(loop);
 
             b.Append(loop->Body(), [&] {
@@ -2889,7 +2865,7 @@
     // }
 
     b.Append(mod.root_block, [&] {
-        auto* i = b.Var(ty.ptr<storage, u32, read_write>());
+        auto* i = b.Var<storage, u32, read_write>();
         i->SetBindingPoint(0, 0);
 
         auto* fn_f = b.Function("f", ty.void_());
@@ -3156,8 +3132,7 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* cond = b.Var("cond", ty.ptr<function, bool>());
-        cond->SetInitializer(b.Constant(false));
+        auto* cond = b.Var("cond", false);
 
         auto* loop = b.Loop();
 
@@ -3194,14 +3169,12 @@
     auto* fn = b.Function("f", ty.void_());
 
     b.Append(fn->Block(), [&] {
-        auto* var_b = b.Var("b", ty.ptr<function, i32>());
-        var_b->SetInitializer(b.Constant(1_i));
+        auto* var_b = b.Var("b", 1_i);
 
         auto* loop = b.Loop();
 
         b.Append(loop->Body(), [&] {
-            auto* var_a = b.Var("a", ty.ptr<function, i32>());
-            var_a->SetInitializer(b.Constant(2_i));
+            auto* var_a = b.Var("a", 2_i);
 
             auto* body_load_a = b.Load(var_a);
             auto* body_load_b = b.Load(var_b);
@@ -3343,8 +3316,9 @@
 TEST_F(IRToProgramTest, Enable_ChromiumExperimentalSubgroups_SubgroupBallot) {
     auto* fn = b.Function("f", ty.void_());
     b.Append(fn->Block(), [&] {
-        b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
+        auto* call = b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
             b.InstructionResult(ty.vec4<u32>()), wgsl::BuiltinFn::kSubgroupBallot, Empty));
+        b.Let("v", call);
         b.Return(fn);
     });
 
@@ -3352,7 +3326,7 @@
 enable chromium_experimental_subgroups;
 
 fn f() {
-  _ = subgroupBallot();
+  let v = subgroupBallot();
 }
 )");
 }
@@ -3361,8 +3335,9 @@
     auto* fn = b.Function("f", ty.void_());
     b.Append(fn->Block(), [&] {
         auto* one = b.Value(1_u);
-        b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
+        auto* call = b.Append(mod.instructions.Create<wgsl::ir::BuiltinCall>(
             b.InstructionResult(ty.u32()), wgsl::BuiltinFn::kSubgroupBroadcast, Vector{one, one}));
+        b.Let("v", call);
         b.Return(fn);
     });
 
@@ -3370,7 +3345,7 @@
 enable chromium_experimental_subgroups;
 
 fn f() {
-  _ = subgroupBroadcast(1u, 1u);
+  let v = subgroupBroadcast(1u, 1u);
 }
 )");
 }
diff --git a/src/tint/lang/wgsl/writer/raise/raise.cc b/src/tint/lang/wgsl/writer/raise/raise.cc
index 9d69c41..9a64c26 100644
--- a/src/tint/lang/wgsl/writer/raise/raise.cc
+++ b/src/tint/lang/wgsl/writer/raise/raise.cc
@@ -229,7 +229,8 @@
             }
         }
     }
-    if (auto result = RenameConflicts(&mod); result != Success) {
+
+    if (auto result = raise::RenameConflicts(mod); result != Success) {
         return result.Failure();
     }
 
diff --git a/src/tint/lang/wgsl/writer/raise/rename_conflicts.cc b/src/tint/lang/wgsl/writer/raise/rename_conflicts.cc
index 8ab6b7e..f54efd1 100644
--- a/src/tint/lang/wgsl/writer/raise/rename_conflicts.cc
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts.cc
@@ -50,7 +50,7 @@
 #include "src/tint/utils/rtti/switch.h"
 #include "src/tint/utils/text/string.h"
 
-namespace tint::wgsl::writer {
+namespace tint::wgsl::writer::raise {
 
 namespace {
 
@@ -58,7 +58,7 @@
 struct State {
     /// Constructor
     /// @param i the IR module
-    explicit State(core::ir::Module* i) : ir(i) {}
+    explicit State(core::ir::Module& i) : ir(i) {}
 
     /// Processes the module, renaming all declarations that would prevent an identifier resolving
     /// to the correct declaration.
@@ -69,17 +69,17 @@
         RegisterModuleScopeDecls();
 
         // Process the module-scope variable declarations
-        for (auto* inst : *ir->root_block) {
+        for (auto* inst : *ir.root_block) {
             Process(inst);
         }
 
         // Process the functions
-        for (core::ir::Function* fn : ir->functions) {
+        for (core::ir::Function* fn : ir.functions) {
             scopes.Push(Scope{});
             TINT_DEFER(scopes.Pop());
             for (auto* param : fn->Params()) {
                 EnsureResolvable(param->Type());
-                if (auto symbol = ir->NameOf(param); symbol.IsValid()) {
+                if (auto symbol = ir.NameOf(param); symbol.IsValid()) {
                     Declare(scopes.Back(), param, symbol.NameView());
                 }
             }
@@ -93,7 +93,7 @@
     using Scope = Hashmap<std::string_view, CastableBase*, 8>;
 
     /// The IR module.
-    core::ir::Module* ir = nullptr;
+    core::ir::Module& ir;
 
     /// Stack of scopes
     Vector<Scope, 8> scopes{};
@@ -102,7 +102,7 @@
     /// Duplicate declarations with the same name will renamed.
     void RegisterModuleScopeDecls() {
         // Declare all the user types
-        for (auto* ty : ir->Types()) {
+        for (auto* ty : ir.Types()) {
             if (auto* str = ty->As<core::type::Struct>()) {
                 auto name = str->Name().NameView();
                 if (!IsBuiltinStruct(str)) {
@@ -112,17 +112,17 @@
         }
 
         // Declare all the module-scope vars
-        for (auto* inst : *ir->root_block) {
+        for (auto* inst : *ir.root_block) {
             for (auto* result : inst->Results()) {
-                if (auto symbol = ir->NameOf(result)) {
+                if (auto symbol = ir.NameOf(result)) {
                     Declare(scopes.Front(), result, symbol.NameView());
                 }
             }
         }
 
         // Declare all the functions
-        for (core::ir::Function* fn : ir->functions) {
-            if (auto symbol = ir->NameOf(fn); symbol.IsValid()) {
+        for (core::ir::Function* fn : ir.functions) {
+            if (auto symbol = ir.NameOf(fn); symbol.IsValid()) {
                 Declare(scopes.Back(), fn, symbol.NameView());
             }
         }
@@ -142,7 +142,7 @@
         for (auto* operand : inst->Operands()) {
             if (operand) {
                 // Ensure that named operands can be resolved.
-                if (auto symbol = ir->NameOf(operand)) {
+                if (auto symbol = ir.NameOf(operand)) {
                     EnsureResolvesTo(symbol.NameView(), operand);
                 }
                 // If the operand is a constant, then ensure that type name can be resolved.
@@ -195,7 +195,7 @@
 
         // Register new operands and check their types can resolve
         for (auto* result : inst->Results()) {
-            if (auto symbol = ir->NameOf(result); symbol.IsValid()) {
+            if (auto symbol = ir.NameOf(result); symbol.IsValid()) {
                 Declare(scopes.Back(), result, symbol.NameView());
             }
         }
@@ -266,10 +266,10 @@
 
     /// Rename changes the name of @p thing with the old name of @p old_name
     void Rename(CastableBase* thing, std::string_view old_name) {
-        Symbol new_name = ir->symbols.New(old_name);
+        Symbol new_name = ir.symbols.New(old_name);
         Switch(
             thing,  //
-            [&](core::ir::Value* value) { ir->SetName(value, new_name); },
+            [&](core::ir::Value* value) { ir.SetName(value, new_name); },
             [&](core::type::Struct* str) { str->SetName(new_name); },  //
             TINT_ICE_ON_NO_MATCH);
     }
@@ -283,8 +283,8 @@
 
 }  // namespace
 
-Result<SuccessType> RenameConflicts(core::ir::Module* ir) {
-    auto result = ValidateAndDumpIfNeeded(*ir, "RenameConflicts transform");
+Result<SuccessType> RenameConflicts(core::ir::Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "RenameConflicts transform");
     if (result != Success) {
         return result;
     }
@@ -294,4 +294,4 @@
     return Success;
 }
 
-}  // namespace tint::wgsl::writer
+}  // namespace tint::wgsl::writer::raise
diff --git a/src/tint/lang/wgsl/writer/raise/rename_conflicts.h b/src/tint/lang/wgsl/writer/raise/rename_conflicts.h
index 9f5d989..e9c6816 100644
--- a/src/tint/lang/wgsl/writer/raise/rename_conflicts.h
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts.h
@@ -38,15 +38,15 @@
 class Module;
 }
 
-namespace tint::wgsl::writer {
+namespace tint::wgsl::writer::raise {
 
 /// 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);
+Result<SuccessType> RenameConflicts(core::ir::Module& module);
 
-}  // namespace tint::wgsl::writer
+}  // namespace tint::wgsl::writer::raise
 
 #endif  // SRC_TINT_LANG_WGSL_WRITER_RAISE_RENAME_CONFLICTS_H_
diff --git a/src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc b/src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc
index 6facee7..8b28870 100644
--- a/src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts_test.cc
@@ -36,7 +36,7 @@
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/type/matrix.h"
 
-namespace tint::wgsl::writer {
+namespace tint::wgsl::writer::raise {
 namespace {
 
 using namespace tint::core::fluent_types;     // NOLINT
@@ -56,7 +56,7 @@
         }
 
         // Run the transforms.
-        auto result = RenameConflicts(&mod);
+        auto result = RenameConflicts(mod);
         EXPECT_EQ(result, Success);
 
         // Validate the output IR.
@@ -1102,4 +1102,4 @@
 }
 
 }  // namespace
-}  // namespace tint::wgsl::writer
+}  // namespace tint::wgsl::writer::raise
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 96fdf82..6742227 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.bazel
@@ -61,6 +61,7 @@
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
     "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
     "//src/tint/utils/result",
     "//src/tint/utils/rtti",
     "//src/tint/utils/strconv",
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 859fa03..889fd35 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.cmake
@@ -60,6 +60,7 @@
   tint_utils_macros
   tint_utils_math
   tint_utils_memory
+  tint_utils_reflection
   tint_utils_result
   tint_utils_rtti
   tint_utils_strconv
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 71df0af..8a70111 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/BUILD.gn
@@ -60,6 +60,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",
diff --git a/src/tint/utils/bytes/decoder.h b/src/tint/utils/bytes/decoder.h
index 95e9f8f..6901f7c 100644
--- a/src/tint/utils/bytes/decoder.h
+++ b/src/tint/utils/bytes/decoder.h
@@ -35,6 +35,7 @@
 #include <tuple>
 #include <unordered_map>
 #include <utility>
+#include <vector>
 
 #include "src/tint/utils/bytes/reader.h"
 #include "src/tint/utils/reflection/reflection.h"
@@ -154,6 +155,34 @@
     }
 };
 
+/// Decoder specialization for std::vector
+template <typename V>
+struct Decoder<std::vector<V>, void> {
+    /// Decode decodes the vector from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded vector, or an error if the stream is too short.
+    static Result<std::vector<V>> Decode(Reader& reader) {
+        std::vector<V> out;
+
+        while (true) {
+            auto stop = bytes::Decode<bool>(reader);
+            if (stop != Success) {
+                return stop.Failure();
+            }
+            if (stop.Get()) {
+                break;
+            }
+            auto val = bytes::Decode<V>(reader);
+            if (val != Success) {
+                return val.Failure();
+            }
+            out.emplace_back(std::move(val.Get()));
+        }
+
+        return out;
+    }
+};
+
 /// Decoder specialization for std::optional
 template <typename T>
 struct Decoder<std::optional<T>, void> {
diff --git a/src/tint/utils/bytes/decoder_test.cc b/src/tint/utils/bytes/decoder_test.cc
index 9bae945..3535298 100644
--- a/src/tint/utils/bytes/decoder_test.cc
+++ b/src/tint/utils/bytes/decoder_test.cc
@@ -154,6 +154,24 @@
     EXPECT_NE(Decode<M>(reader), Success);
 }
 
+TEST(BytesDecoderTest, Vector) {
+    using M = std::vector<uint8_t>;
+    auto data = Data(0x00, 0x10,  //
+                     0x00, 0x30,  //
+                     0x00, 0x50,  //
+                     0x00, 0x70,  //
+                     0x01);
+    auto reader = BufferReader{Slice{data}};
+    auto got = Decode<M>(reader);
+    EXPECT_THAT(got.Get(), testing::ContainerEq(M{
+                               0x10u,
+                               0x30u,
+                               0x50u,
+                               0x70u,
+                           }));
+    EXPECT_NE(Decode<M>(reader), Success);
+}
+
 TEST(BytesDecoderTest, Optional) {
     auto data = Data(0x00,  //
                      0x01, 0x42);
diff --git a/src/tint/utils/socket/socket.cc b/src/tint/utils/socket/socket.cc
index bab68f2..064823c 100644
--- a/src/tint/utils/socket/socket.cc
+++ b/src/tint/utils/socket/socket.cc
@@ -320,7 +320,8 @@
 
             timeval tv;
             tv.tv_sec = timeout_us / 1000000;
-            tv.tv_usec = static_cast<int>(timeout_us - (tv.tv_sec * 1000000));
+            using USEC = decltype(tv.tv_usec);
+            tv.tv_usec = static_cast<USEC>(timeout_us) - (static_cast<USEC>(tv.tv_sec) * 1000000);
             res = select(static_cast<int>(socket + 1), nullptr, &fdset, nullptr, &tv);
             if (res > 0 && !Errored(socket) && SetBlocking(socket, true)) {
                 out = impl;
diff --git a/src/tint/utils/templates/enums.tmpl.inc b/src/tint/utils/templates/enums.tmpl.inc
index 23dc6d3..45f3945 100644
--- a/src/tint/utils/templates/enums.tmpl.inc
+++ b/src/tint/utils/templates/enums.tmpl.inc
@@ -36,6 +36,24 @@
 
 
 {{- /* ------------------------------------------------------------------ */ -}}
+{{-                         define "EnumFirst"                               -}}
+{{- /* Prints the name of the first enum entry                            */ -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+  kUndefined
+{{- end -}}
+
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                         define "EnumLast"                                -}}
+{{- /* Prints the name of the last enum entry                             */ -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{- $enum := $ -}}
+{{- $item := index $enum.Entries (Sum -1 (len $enum.Entries)) -}}
+  k{{PascalCase $item.Name}}
+{{- end -}}
+
+
+{{- /* ------------------------------------------------------------------ */ -}}
 {{-                         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: */ -}}