Import Tint changes from Dawn

Contains manual fixes for:
 * CMakeLists.txt
 * DEPS
 * scripts/tint_overrides_with_defaults.gni

And one new files:
 * third_party/protobuf.cmake

Also bumps Kokoro Clang version to 13.0.1, to match the Dawn bots.

Changes:
  - d86e9a765cb57b9e3c8476731a4de964e2dd25a3 [spirv-reader] Pass workgroup_id as argument by James Price <jrprice@google.com>
  - 3f11e1e7dff3c0a0a0ecc6d3ae23f9070f4c0aa8 [tint][ir] Serialize Swizzle instruction by Ben Clayton <bclayton@google.com>
  - 59bc89f8fe3967eee8eef4465eea31dc22fe1a79 Revert "clang-format" by Stephen White <senorblanco@google.com>
  - f91e88ad4a2306f6b65dd97849d33a2c515af6ac clang-format by Ben Clayton <bclayton@google.com>
  - 84a7cbf8ebc415970472d2b32eaef27a417b59bc [tint][ir] Serialize [Load|Store]VectorElement instructions by Ben Clayton <bclayton@google.com>
  - 53c0fa9a80310e69b5b1b6cae96f9469f20b654e Enable language extension `packed_4x8_integer_dot_product` by Jiawei Shao <jiawei.shao@intel.com>
  - c668c1690ed23db80238aabbe42002a15ee2eb0e [tint][ir] Serialize Unary and Binary instructions by Ben Clayton <bclayton@google.com>
  - dacc2637282217ac5d9a2c2a45a7ca2c54dbac61 [tint][ir] Serialize Var binding points by Ben Clayton <bclayton@google.com>
  - 7d9a66e3d2a562819b750d4aa1bdbb912de770b5 [tint][ir] Serialize the root block by Ben Clayton <bclayton@google.com>
  - cba30d2b886939bc7b84f7b5687236ff9ca83046 [tint][ir] Serialize Load and Store instructions by Ben Clayton <bclayton@google.com>
  - 112549bea523b3cfc1c2c0e0f37b2e2f442f2ca5 [tint][ir] Serialize UserCall by Ben Clayton <bclayton@google.com>
  - 0bc302670162c75ef9417f4e1c3d775bc96cdaa0 [tint][ir] Serialize fixed-size array types by Ben Clayton <bclayton@google.com>
  - 7f9721daadcf26d752f56847ff319644d1c5913e [tint][ir] Serialize var instructions and pointer types. by Ben Clayton <bclayton@google.com>
  - 3f620ab5d809ad4c2b90a165a1ec4241dbbb7913 [tint][ir] Serialize Access instructions by Ben Clayton <bclayton@google.com>
  - fc5664ab75a62d969cdc4d3870a073bf4ba5e0c4 [tint][ir] Serialize vectors and matrices by Ben Clayton <bclayton@google.com>
  - 069238f0d8b1eee5daaff62a7f5978f69dcab954 [tint][ir][validator] 'source' -> 'instruction' by Ben Clayton <bclayton@google.com>
  - 4eff8bec72c2900d4a8b9219535433016ceef279 [tint][ir] Serialize constant scalars and lets by Ben Clayton <bclayton@google.com>
  - 85b673578773a7104615183fce70b0dbf6462d58 [tint][ir] Serialize Discard instructions by Ben Clayton <bclayton@google.com>
  - 2388287c0ecacdb37a09ab2b73f10a7c66eff7bc [tint][fuzz] Don't attempt to memcpy from nullptr by Ben Clayton <bclayton@google.com>
  - 3ee81db189e8e5cebbec8c6f33f9d15e9124ed67 Fix license: it's BSD-3 not Apache 2 by David Neto <dneto@google.com>
  - 0a8dc81b6184478c1b4c83bda1ecb71da660bb04 [tint][utils] Make bytes::Reader an interface by Ben Clayton <bclayton@google.com>
  - 44a29b5e3743ad38a8d5147119471d28fabd44e5 [tint][gn] Fix include_dirs in tint_common_config by Ben Clayton <bclayton@google.com>
  - ae7240f3642136f636a49d07dfc1c87cd29cf0b9 Vulkan: Use the polyfill of dot4{I|U}8Packed() when neces... by Jiawei Shao <jiawei.shao@intel.com>
  - 648a2b32c94e2bbda7f12076ff57ace9699e602e OpenGL: Support dot4I8Packed() and dot4U8Packed() by Jiawei Shao <jiawei.shao@intel.com>
  - 90c7cc2304a6b629e5c5fdef18b4e1a1b3a2fbaa [tint][ir] Begin building proto-based serializer by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: d86e9a765cb57b9e3c8476731a4de964e2dd25a3
Change-Id: Ib656c7a3768d7a278d640f35ce2534b8c50f73d2
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/165140
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fc95d64..0deacd3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -26,6 +26,8 @@
   set(CMAKE_BUILD_TYPE "Debug")
 endif()
 
+set(DAWN_BUILD_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen")
+
 # TINT_IS_SUBPROJECT is 1 if added via add_subdirectory() from another project.
 get_directory_property(TINT_IS_SUBPROJECT PARENT_DIRECTORY)
 if(TINT_IS_SUBPROJECT)
@@ -74,6 +76,7 @@
 set_if_not_defined(TINT_MARKUPSAFE_DIR "${TINT_THIRD_PARTY_DIR}/markupsafe" "Directory in which to find MarkupSafe")
 set_if_not_defined(TINT_KHRONOS_DIR "${TINT_THIRD_PARTY_DIR}/khronos" "Directory in which to find Khronos GL headers")
 set_if_not_defined(TINT_SWIFTSHADER_DIR "${TINT_THIRD_PARTY_DIR}/swiftshader" "Directory in which to find swiftshader")
+set_if_not_defined(TINT_PROTOBUF_DIR "${TINT_THIRD_PARTY_DIR}/protobuf" "Directory in which to find protobuf")
 
 set_if_not_defined(TINT_SPIRV_TOOLS_DIR "${TINT_VULKAN_DEPS_DIR}/spirv-tools/src" "Directory in which to find SPIRV-Tools")
 set_if_not_defined(TINT_SPIRV_HEADERS_DIR "${TINT_VULKAN_DEPS_DIR}/spirv-headers/src" "Directory in which to find SPIRV-Headers")
@@ -392,6 +395,11 @@
   endif(TINT_EMIT_COVERAGE)
 endfunction()
 
+if (EXISTS "${TINT_PROTOBUF_DIR}/cmake")
+  # Needs to come before SPIR-V Tools
+  include("third_party/protobuf.cmake")
+endif()
+
 add_subdirectory(third_party)
 add_subdirectory(src/tint)
 
diff --git a/DEPS b/DEPS
index 4138b73..55f27cc 100644
--- a/DEPS
+++ b/DEPS
@@ -129,7 +129,7 @@
   },
 
   'third_party/protobuf': {
-    'url': '{chromium_git}/external/github.com/protocolbuffers/protobuf.git@2b673bbb57e34fe1bd4570f726fc86b769a3a3d2',
+    'url': '{chromium_git}/chromium/src/third_party/protobuf@41759e11ec427e29e1a72b9401d2af3f6e02d839',
   },
 }
 
diff --git a/kokoro/linux/docker.sh b/kokoro/linux/docker.sh
index a15ff37..eb0f1e9 100755
--- a/kokoro/linux/docker.sh
+++ b/kokoro/linux/docker.sh
@@ -99,7 +99,7 @@
     COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_BENCHMARKS=1"
 
     if [ "$BUILD_TOOLCHAIN" == "clang" ]; then
-        using clang-10.0.0
+        using clang-13.0.1
         COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_FUZZERS=1"
         COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_SPIRV_TOOLS_FUZZER=1"
         COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_AST_FUZZER=1"
diff --git a/scripts/tint_overrides_with_defaults.gni b/scripts/tint_overrides_with_defaults.gni
index 0edfb87..4e33a50 100644
--- a/scripts/tint_overrides_with_defaults.gni
+++ b/scripts/tint_overrides_with_defaults.gni
@@ -101,6 +101,11 @@
     tint_build_syntax_tree_writer = false
   }
 
+  # Build the IR binary serializer
+  if (!defined(tint_build_ir_binary)) {
+    tint_build_ir_binary = tint_has_protobuf
+  }
+
   # Build unittests
   if (!defined(tint_build_unittests)) {
     tint_build_unittests = true
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 6f78959..f8fe3f2 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -42,13 +42,13 @@
 # Common - Configs, etc. shared across targets
 ###############################################################################
 
+# tint_gen_dir is the relative path of tint_root_dir to '//', under root_gen_dir
+tint_gen_dir = "${root_gen_dir}/" + rebase_path("${tint_root_dir}", "//")
+
 config("tint_common_config") {
   include_dirs = [
-    "${target_gen_dir}",
-    "${tint_root_dir}/",
-    "${tint_spirv_headers_dir}/include",
-    "${tint_spirv_tools_dir}/",
-    "${tint_spirv_tools_dir}/include",
+    "${tint_root_dir}",
+    "${tint_gen_dir}",
   ]
 }
 
@@ -129,7 +129,6 @@
   include_dirs = [
     "${tint_root_dir}/",
     "${tint_root_dir}/include/",
-    "${tint_spirv_headers_dir}/include",
   ]
 }
 
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 208a231..9a4eba5 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -41,6 +41,12 @@
   endif()
 endif()
 
+if (TARGET libprotobuf)
+  set(TINT_BUILD_IR_BINARY 1)
+else()
+  set(TINT_BUILD_IR_BINARY 0)
+endif()
+
 ################################################################################
 # Helper functions
 ################################################################################
@@ -50,6 +56,7 @@
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_VALIDATOR=$<BOOL:${TINT_BUILD_GLSL_VALIDATOR}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_HLSL_WRITER=$<BOOL:${TINT_BUILD_HLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IR_BINARY=$<BOOL:${TINT_BUILD_IR_BINARY}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IS_LINUX=$<BOOL:${TINT_BUILD_IS_LINUX}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IS_MAC=$<BOOL:${TINT_BUILD_IS_MAC}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IS_WIN=$<BOOL:${TINT_BUILD_IS_WIN}>)
@@ -60,7 +67,6 @@
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
   target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
 
-
   if(TINT_BUILD_FUZZERS)
     target_compile_options(${TARGET} PRIVATE "-fsanitize=fuzzer")
   endif()
diff --git a/src/tint/cmd/fuzz/ir/fuzz.cc b/src/tint/cmd/fuzz/ir/fuzz.cc
index fc96dcc..8934bf2 100644
--- a/src/tint/cmd/fuzz/ir/fuzz.cc
+++ b/src/tint/cmd/fuzz/ir/fuzz.cc
@@ -46,7 +46,6 @@
 bool IsUnsupported(const ast::Enable* enable) {
     for (auto ext : enable->extensions) {
         switch (ext->name) {
-            case tint::wgsl::Extension::kChromiumExperimentalDp4A:
             case tint::wgsl::Extension::kChromiumExperimentalFullPtrParameters:
             case tint::wgsl::Extension::kChromiumExperimentalPixelLocal:
             case tint::wgsl::Extension::kChromiumExperimentalPushConstant:
diff --git a/src/tint/cmd/fuzz/ir/fuzz.h b/src/tint/cmd/fuzz/ir/fuzz.h
index 897f6e1..36d8c02 100644
--- a/src/tint/cmd/fuzz/ir/fuzz.h
+++ b/src/tint/cmd/fuzz/ir/fuzz.h
@@ -53,7 +53,10 @@
     static IRFuzzer Create(std::string_view name, void (*fn)(core::ir::Module&, ARGS...)) {
         if constexpr (sizeof...(ARGS) > 0) {
             auto fn_with_decode = [fn](core::ir::Module& module, Slice<const std::byte> data) {
-                bytes::Reader reader{data};
+                if (!data.data) {
+                    return;
+                }
+                bytes::BufferReader reader{data};
                 if (auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader)) {
                     auto all_args =
                         std::tuple_cat(std::tuple<core::ir::Module&>{module}, data_args.Get());
diff --git a/src/tint/cmd/fuzz/wgsl/fuzz.h b/src/tint/cmd/fuzz/wgsl/fuzz.h
index 744ceb5..da2efe4 100644
--- a/src/tint/cmd/fuzz/wgsl/fuzz.h
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.h
@@ -50,7 +50,10 @@
     static ProgramFuzzer Create(std::string_view name, void (*fn)(const Program&, ARGS...)) {
         if constexpr (sizeof...(ARGS) > 0) {
             auto fn_with_decode = [fn](const Program& program, Slice<const std::byte> data) {
-                bytes::Reader reader{data};
+                if (!data.data) {
+                    return;
+                }
+                bytes::BufferReader reader{data};
                 if (auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader)) {
                     auto all_args =
                         std::tuple_cat(std::tuple<const Program&>{program}, data_args.Get());
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index 080bcb9..e0e9b73 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -103,6 +103,11 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_ir_binary": [
+      "//src/tint/lang/core/ir/binary:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_msl_writer": [
       "//src/tint/lang/msl/writer/ast_printer:test",
       "//src/tint/lang/msl/writer/common:test",
@@ -176,6 +181,11 @@
 )
 
 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",
 )
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index 3709145..909b3e8 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -112,6 +112,12 @@
   )
 endif(TINT_BUILD_HLSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 
+if(TINT_BUILD_IR_BINARY)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_core_ir_binary_test
+  )
+endif(TINT_BUILD_IR_BINARY)
+
 if(TINT_BUILD_MSL_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_msl_writer_ast_printer_test
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index e307f93..622cf0f 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -109,6 +109,10 @@
       deps += [ "${tint_src_dir}/lang/hlsl/writer/ast_raise:unittests" ]
     }
 
+    if (tint_build_ir_binary) {
+      deps += [ "${tint_src_dir}/lang/core/ir/binary:unittests" ]
+    }
+
     if (tint_build_msl_writer) {
       deps += [
         "${tint_src_dir}/lang/msl/writer/ast_printer:unittests",
diff --git a/src/tint/lang/core/builtin_fn.cc b/src/tint/lang/core/builtin_fn.cc
index 97c1baf..0109b7e 100644
--- a/src/tint/lang/core/builtin_fn.cc
+++ b/src/tint/lang/core/builtin_fn.cc
@@ -689,7 +689,7 @@
            f == BuiltinFn::kAtomicCompareExchangeWeak;
 }
 
-bool IsDP4a(BuiltinFn f) {
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f) {
     return f == BuiltinFn::kDot4I8Packed || f == BuiltinFn::kDot4U8Packed;
 }
 
diff --git a/src/tint/lang/core/builtin_fn.cc.tmpl b/src/tint/lang/core/builtin_fn.cc.tmpl
index 376b33f..0a39072 100644
--- a/src/tint/lang/core/builtin_fn.cc.tmpl
+++ b/src/tint/lang/core/builtin_fn.cc.tmpl
@@ -100,7 +100,7 @@
            f == BuiltinFn::kAtomicCompareExchangeWeak;
 }
 
-bool IsDP4a(BuiltinFn f) {
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f) {
     return f == BuiltinFn::kDot4I8Packed || f == BuiltinFn::kDot4U8Packed;
 }
 
diff --git a/src/tint/lang/core/builtin_fn.h b/src/tint/lang/core/builtin_fn.h
index 78ba924..badcd4c 100644
--- a/src/tint/lang/core/builtin_fn.h
+++ b/src/tint/lang/core/builtin_fn.h
@@ -468,10 +468,12 @@
 /// @returns true if the given `f` is an atomic builtin
 bool IsAtomic(BuiltinFn f);
 
-/// Determines if the given `f` is a DP4a builtin.
+/// Determines if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
 /// @param f the builtin type
-/// @returns true if the given `f` is a DP4a builtin
-bool IsDP4a(BuiltinFn f);
+/// @returns true if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f);
 
 /// Determines if the given `f` is a subgroup builtin.
 /// @param f the builtin type
diff --git a/src/tint/lang/core/builtin_fn.h.tmpl b/src/tint/lang/core/builtin_fn.h.tmpl
index d5198fa..bb08fff 100644
--- a/src/tint/lang/core/builtin_fn.h.tmpl
+++ b/src/tint/lang/core/builtin_fn.h.tmpl
@@ -108,10 +108,12 @@
 /// @returns true if the given `f` is an atomic builtin
 bool IsAtomic(BuiltinFn f);
 
-/// Determines if the given `f` is a DP4a builtin.
+/// Determines if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
 /// @param f the builtin type
-/// @returns true if the given `f` is a DP4a builtin
-bool IsDP4a(BuiltinFn f);
+/// @returns true if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f);
 
 /// Determines if the given `f` is a subgroup builtin.
 /// @param f the builtin type
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index 19c6a8a..c901636 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -34,6 +34,7 @@
 #                       Do not modify this file directly
 ################################################################################
 
+include(lang/core/ir/binary/BUILD.cmake)
 include(lang/core/ir/transform/BUILD.cmake)
 
 ################################################################################
diff --git a/src/tint/lang/core/ir/access.cc b/src/tint/lang/core/ir/access.cc
index c469a5d..b4434e6 100644
--- a/src/tint/lang/core/ir/access.cc
+++ b/src/tint/lang/core/ir/access.cc
@@ -37,6 +37,8 @@
 namespace tint::core::ir {
 
 //! @cond Doxygen_Suppress
+Access::Access() = default;
+
 Access::Access(InstructionResult* result, Value* object, VectorRef<Value*> indices) {
     AddOperand(Access::kObjectOperandOffset, object);
     AddOperands(Access::kIndicesOperandOffset, std::move(indices));
diff --git a/src/tint/lang/core/ir/access.h b/src/tint/lang/core/ir/access.h
index 0bf2caa..6ee0eb6 100644
--- a/src/tint/lang/core/ir/access.h
+++ b/src/tint/lang/core/ir/access.h
@@ -44,6 +44,9 @@
     /// The base offset in Operands() for the access indices
     static constexpr size_t kIndicesOperandOffset = 1;
 
+    /// Constructor (no results, no operands)
+    Access();
+
     /// Constructor
     /// @param result the result value
     /// @param object the accessor object
diff --git a/src/tint/lang/core/ir/binary.cc b/src/tint/lang/core/ir/binary.cc
index 41c9972..732f029 100644
--- a/src/tint/lang/core/ir/binary.cc
+++ b/src/tint/lang/core/ir/binary.cc
@@ -34,6 +34,8 @@
 
 namespace tint::core::ir {
 
+Binary::Binary() = default;
+
 Binary::Binary(InstructionResult* result, BinaryOp op, Value* lhs, Value* rhs) : op_(op) {
     AddOperand(Binary::kLhsOperandOffset, lhs);
     AddOperand(Binary::kRhsOperandOffset, rhs);
diff --git a/src/tint/lang/core/ir/binary.h b/src/tint/lang/core/ir/binary.h
index 6490be9..5f8c26a 100644
--- a/src/tint/lang/core/ir/binary.h
+++ b/src/tint/lang/core/ir/binary.h
@@ -67,6 +67,9 @@
     /// The offset in Operands() for the RHS
     static constexpr size_t kRhsOperandOffset = 1;
 
+    /// Constructor (no results, no operands)
+    Binary();
+
     /// Constructor
     /// @param result the result value
     /// @param op the binary operator
@@ -81,6 +84,9 @@
     /// @returns the binary operator
     BinaryOp Op() const { return op_; }
 
+    /// @param op the new binary operator
+    void SetOp(BinaryOp op) { op_ = op; }
+
     /// @returns the left-hand-side value for the instruction
     Value* LHS() { return operands_[kLhsOperandOffset]; }
 
@@ -97,7 +103,7 @@
     std::string FriendlyName() const override { return "binary"; }
 
   private:
-    BinaryOp op_;
+    BinaryOp op_ = BinaryOp::kAdd;
 };
 
 /// @param kind the enum value
diff --git a/src/tint/lang/core/ir/binary/BUILD.bazel b/src/tint/lang/core/ir/binary/BUILD.bazel
new file mode 100644
index 0000000..e781ef7
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/BUILD.bazel
@@ -0,0 +1,120 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "binary",
+  srcs = [
+    "decode.cc",
+    "encode.cc",
+  ],
+  hdrs = [
+    "decode.h",
+    "encode.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_ir_binary": [
+      "",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "roundtrip_test.cc",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/ir:test",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_ir_binary": [
+      "//src/tint/lang/core/ir/binary",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_ir_binary",
+  actual = "//src/tint:tint_build_ir_binary_true",
+)
+
diff --git a/src/tint/lang/core/ir/binary/BUILD.cfg b/src/tint/lang/core/ir/binary/BUILD.cfg
new file mode 100644
index 0000000..deb7ad2
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_ir_binary"
+}
diff --git a/src/tint/lang/core/ir/binary/BUILD.cmake b/src/tint/lang/core/ir/binary/BUILD.cmake
new file mode 100644
index 0000000..61650da
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/BUILD.cmake
@@ -0,0 +1,133 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+if(TINT_BUILD_IR_BINARY)
+################################################################################
+# Target:    tint_lang_core_ir_binary
+# Kind:      lib
+# Condition: TINT_BUILD_IR_BINARY
+################################################################################
+tint_add_target(tint_lang_core_ir_binary lib
+  lang/core/ir/binary/decode.cc
+  lang/core/ir/binary/decode.h
+  lang/core/ir/binary/encode.cc
+  lang/core/ir/binary/encode.h
+)
+
+tint_target_add_dependencies(tint_lang_core_ir_binary lib
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_IR_BINARY)
+  tint_target_add_dependencies(tint_lang_core_ir_binary lib
+    tint_lang_core_ir_binary_proto
+  )
+endif(TINT_BUILD_IR_BINARY)
+
+endif(TINT_BUILD_IR_BINARY)
+if(TINT_BUILD_IR_BINARY)
+################################################################################
+# Target:    tint_lang_core_ir_binary_proto
+# Kind:      proto
+# Condition: TINT_BUILD_IR_BINARY
+################################################################################
+tint_add_target(tint_lang_core_ir_binary_proto proto
+  lang/core/ir/binary/ir.proto
+)
+
+endif(TINT_BUILD_IR_BINARY)
+if(TINT_BUILD_IR_BINARY)
+################################################################################
+# Target:    tint_lang_core_ir_binary_test
+# Kind:      test
+# Condition: TINT_BUILD_IR_BINARY
+################################################################################
+tint_add_target(tint_lang_core_ir_binary_test test
+  lang/core/ir/binary/roundtrip_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_core_ir_binary_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_intrinsic
+  tint_lang_core_ir
+  tint_lang_core_ir_test
+  tint_lang_core_type
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_core_ir_binary_test test
+  "gtest"
+)
+
+if(TINT_BUILD_IR_BINARY)
+  tint_target_add_dependencies(tint_lang_core_ir_binary_test test
+    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
new file mode 100644
index 0000000..d0333c8
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/BUILD.gn
@@ -0,0 +1,118 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_ir_binary) {
+  libtint_source_set("binary") {
+    sources = [
+      "decode.cc",
+      "decode.h",
+      "encode.cc",
+      "encode.h",
+    ]
+    deps = [
+      "${tint_src_dir}/api/common",
+      "${tint_src_dir}/lang/core",
+      "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
+      "${tint_src_dir}/lang/core/ir",
+      "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/id",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/reflection",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/symbol",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_ir_binary) {
+      deps += [ "${tint_src_dir}/lang/core/ir/binary:proto" ]
+    }
+  }
+}
+if (tint_build_ir_binary) {
+  tint_proto_library("proto") {
+    sources = [ "ir.proto" ]
+    deps = []
+  }
+}
+if (tint_build_unittests) {
+  if (tint_build_ir_binary) {
+    tint_unittests_source_set("unittests") {
+      sources = [ "roundtrip_test.cc" ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/intrinsic",
+        "${tint_src_dir}/lang/core/ir",
+        "${tint_src_dir}/lang/core/ir:unittests",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/id",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/reflection",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/symbol",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_ir_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
new file mode 100644
index 0000000..6d23588
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/decode.cc
@@ -0,0 +1,597 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/binary/decode.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/utils/containers/transform.h"
+#include "src/tint/utils/macros/compiler.h"
+
+TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS();
+#include "src/tint/lang/core/ir/binary/ir.pb.h"
+TINT_END_DISABLE_PROTOBUF_WARNINGS();
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+namespace tint::core::ir::binary {
+namespace {
+
+struct Decoder {
+    pb::Module& mod_in_;
+    Module& mod_out_;
+    Vector<ir::Block*, 32> blocks_{};
+    Vector<const type::Type*, 32> types_{};
+    Vector<const core::constant::Value*, 32> constant_values_{};
+    Vector<ir::Value*, 32> values_{};
+    Builder b{mod_out_};
+
+    void Decode() {
+        {
+            const size_t n = static_cast<size_t>(mod_in_.types().size());
+            types_.Reserve(n);
+            for (auto& type_in : mod_in_.types()) {
+                types_.Push(CreateType(type_in));
+            }
+        }
+        {
+            const size_t n = static_cast<size_t>(mod_in_.functions().size());
+            mod_out_.functions.Reserve(n);
+            for (auto& fn_in : mod_in_.functions()) {
+                mod_out_.functions.Push(CreateFunction(fn_in));
+            }
+        }
+        {
+            const size_t n = static_cast<size_t>(mod_in_.blocks().size());
+            blocks_.Reserve(n);
+            for (size_t i = 0; i < n; i++) {
+                auto id = static_cast<uint32_t>(i + 1);
+                if (id == mod_in_.root_block()) {
+                    blocks_.Push(mod_out_.root_block);
+                } else {
+                    auto& block_in = mod_in_.blocks()[static_cast<int>(i)];
+                    blocks_.Push(CreateBlock(block_in));
+                }
+            }
+        }
+        {
+            const size_t n = static_cast<size_t>(mod_in_.constant_values().size());
+            constant_values_.Reserve(n);
+            for (auto& value_in : mod_in_.constant_values()) {
+                constant_values_.Push(CreateConstantValue(value_in));
+            }
+        }
+        {
+            const size_t n = static_cast<size_t>(mod_in_.values().size());
+            values_.Reserve(n);
+            for (auto& value_in : mod_in_.values()) {
+                values_.Push(CreateValue(value_in));
+            }
+        }
+        for (size_t i = 0, n = static_cast<size_t>(mod_in_.functions().size()); i < n; i++) {
+            PopulateFunction(mod_out_.functions[i], mod_in_.functions()[static_cast<int>(i)]);
+        }
+        for (size_t i = 0, n = static_cast<size_t>(mod_in_.blocks().size()); i < n; i++) {
+            PopulateBlock(blocks_[i], mod_in_.blocks()[static_cast<int>(i)]);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Functions
+    ////////////////////////////////////////////////////////////////////////////
+    ir::Function* CreateFunction(const pb::Function&) {
+        return mod_out_.values.Create<ir::Function>();
+    }
+
+    void PopulateFunction(ir::Function* fn_out, const pb::Function& fn_in) {
+        if (!fn_in.name().empty()) {
+            mod_out_.SetName(fn_out, fn_in.name());
+        }
+        fn_out->SetReturnType(Type(fn_in.return_type()));
+        if (fn_in.has_pipeline_stage()) {
+            fn_out->SetStage(PipelineStage(fn_in.pipeline_stage()));
+        }
+        if (fn_in.has_workgroup_size()) {
+            auto& wg_size_in = fn_in.workgroup_size();
+            fn_out->SetWorkgroupSize(wg_size_in.x(), wg_size_in.y(), wg_size_in.z());
+        }
+
+        Vector<FunctionParam*, 8> params_out;
+        for (auto param_in : fn_in.parameters()) {
+            params_out.Push(ValueAs<ir::FunctionParam>(param_in));
+        }
+        fn_out->SetParams(std::move(params_out));
+        fn_out->SetBlock(Block(fn_in.block()));
+    }
+
+    ir::Function* Function(uint32_t id) { return id > 0 ? mod_out_.functions[id - 1] : nullptr; }
+
+    Function::PipelineStage PipelineStage(pb::PipelineStage stage) {
+        switch (stage) {
+            case pb::PipelineStage::Compute:
+                return Function::PipelineStage::kCompute;
+            case pb::PipelineStage::Fragment:
+                return Function::PipelineStage::kFragment;
+            case pb::PipelineStage::Vertex:
+                return Function::PipelineStage::kVertex;
+            default:
+                TINT_ICE() << "unhandled PipelineStage: " << stage;
+                return Function::PipelineStage::kCompute;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Blocks
+    ////////////////////////////////////////////////////////////////////////////
+    ir::Block* CreateBlock(const pb::Block&) { return b.Block(); }
+
+    ir::Block* PopulateBlock(ir::Block* block_out, const pb::Block& block_in) {
+        for (auto& inst : block_in.instructions()) {
+            block_out->Append(Instruction(inst));
+        }
+        return block_out;
+    }
+
+    ir::Block* Block(uint32_t id) { return id > 0 ? blocks_[id - 1] : nullptr; }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Instructions
+    ////////////////////////////////////////////////////////////////////////////
+    ir::Instruction* Instruction(const pb::Instruction& inst_in) {
+        ir::Instruction* inst_out = nullptr;
+        switch (inst_in.kind_case()) {
+            case pb::Instruction::KindCase::kAccess:
+                inst_out = CreateInstructionAccess(inst_in.access());
+                break;
+            case pb::Instruction::KindCase::kBinary:
+                inst_out = CreateInstructionBinary(inst_in.binary());
+                break;
+            case pb::Instruction::KindCase::kConstruct:
+                inst_out = CreateInstructionConstruct(inst_in.construct());
+                break;
+            case pb::Instruction::KindCase::kDiscard:
+                inst_out = CreateInstructionDiscard(inst_in.discard());
+                break;
+            case pb::Instruction::KindCase::kLet:
+                inst_out = CreateInstructionLet(inst_in.let());
+                break;
+            case pb::Instruction::KindCase::kLoad:
+                inst_out = CreateInstructionLoad(inst_in.load());
+                break;
+            case pb::Instruction::KindCase::kLoadVectorElement:
+                inst_out = CreateInstructionLoadVectorElement(inst_in.load_vector_element());
+                break;
+            case pb::Instruction::KindCase::kReturn:
+                inst_out = CreateInstructionReturn(inst_in.return_());
+                break;
+            case pb::Instruction::KindCase::kStore:
+                inst_out = CreateInstructionStore(inst_in.store());
+                break;
+            case pb::Instruction::KindCase::kStoreVectorElement:
+                inst_out = CreateInstructionStoreVectorElement(inst_in.store_vector_element());
+                break;
+            case pb::Instruction::KindCase::kSwizzle:
+                inst_out = CreateInstructionSwizzle(inst_in.swizzle());
+                break;
+            case pb::Instruction::KindCase::kUnary:
+                inst_out = CreateInstructionUnary(inst_in.unary());
+                break;
+            case pb::Instruction::KindCase::kUserCall:
+                inst_out = CreateInstructionUserCall(inst_in.user_call());
+                break;
+            case pb::Instruction::KindCase::kVar:
+                inst_out = CreateInstructionVar(inst_in.var());
+                break;
+            default:
+                TINT_UNIMPLEMENTED() << inst_in.kind_case();
+                break;
+        }
+        TINT_ASSERT_OR_RETURN_VALUE(inst_out, nullptr);
+
+        Vector<ir::Value*, 4> operands;
+        for (auto id : inst_in.operands()) {
+            operands.Push(Value(id));
+        }
+        inst_out->SetOperands(std::move(operands));
+
+        Vector<ir::InstructionResult*, 4> results;
+        for (auto id : inst_in.results()) {
+            results.Push(ValueAs<ir::InstructionResult>(id));
+        }
+        inst_out->SetResults(std::move(results));
+
+        return inst_out;
+    }
+
+    ir::Access* CreateInstructionAccess(const pb::InstructionAccess&) {
+        return mod_out_.instructions.Create<ir::Access>();
+    }
+
+    ir::Binary* CreateInstructionBinary(const pb::InstructionBinary& binary_in) {
+        auto* binary_out = mod_out_.instructions.Create<ir::Binary>();
+        binary_out->SetOp(BinaryOp(binary_in.op()));
+        return binary_out;
+    }
+
+    ir::Construct* CreateInstructionConstruct(const pb::InstructionConstruct&) {
+        return mod_out_.instructions.Create<ir::Construct>();
+    }
+
+    ir::Discard* CreateInstructionDiscard(const pb::InstructionDiscard&) {
+        return mod_out_.instructions.Create<ir::Discard>();
+    }
+
+    ir::Let* CreateInstructionLet(const pb::InstructionLet&) {
+        return mod_out_.instructions.Create<ir::Let>();
+    }
+
+    ir::Load* CreateInstructionLoad(const pb::InstructionLoad&) {
+        return mod_out_.instructions.Create<ir::Load>();
+    }
+
+    ir::LoadVectorElement* CreateInstructionLoadVectorElement(
+        const pb::InstructionLoadVectorElement&) {
+        return mod_out_.instructions.Create<ir::LoadVectorElement>();
+    }
+
+    ir::Return* CreateInstructionReturn(const pb::InstructionReturn&) {
+        return mod_out_.instructions.Create<ir::Return>();
+    }
+
+    ir::Store* CreateInstructionStore(const pb::InstructionStore&) {
+        return mod_out_.instructions.Create<ir::Store>();
+    }
+
+    ir::StoreVectorElement* CreateInstructionStoreVectorElement(
+        const pb::InstructionStoreVectorElement&) {
+        return mod_out_.instructions.Create<ir::StoreVectorElement>();
+    }
+
+    ir::Swizzle* CreateInstructionSwizzle(const pb::InstructionSwizzle& swizzle_in) {
+        auto* swizzle_out = mod_out_.instructions.Create<ir::Swizzle>();
+        Vector<uint32_t, 4> indices;
+        for (auto idx : swizzle_in.indices()) {
+            indices.Push(idx);
+        }
+        swizzle_out->SetIndices(indices);
+        return swizzle_out;
+    }
+
+    ir::Unary* CreateInstructionUnary(const pb::InstructionUnary& unary_in) {
+        auto* unary_out = mod_out_.instructions.Create<ir::Unary>();
+        unary_out->SetOp(UnaryOp(unary_in.op()));
+        return unary_out;
+    }
+
+    ir::UserCall* CreateInstructionUserCall(const pb::InstructionUserCall&) {
+        return mod_out_.instructions.Create<ir::UserCall>();
+    }
+
+    ir::Var* CreateInstructionVar(const pb::InstructionVar& var_in) {
+        auto* var_out = mod_out_.instructions.Create<ir::Var>();
+        if (var_in.has_binding_point()) {
+            auto& bp_in = var_in.binding_point();
+            var_out->SetBindingPoint(bp_in.group(), bp_in.binding());
+        }
+        return var_out;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Types
+    ////////////////////////////////////////////////////////////////////////////
+    const type::Type* CreateType(const pb::Type type_in) {
+        switch (type_in.kind_case()) {
+            case pb::Type::KindCase::kBasic:
+                return CreateTypeBasic(type_in.basic());
+            case pb::Type::KindCase::kVector:
+                return CreateTypeVector(type_in.vector());
+            case pb::Type::KindCase::kMatrix:
+                return CreateTypeMatrix(type_in.matrix());
+            case pb::Type::KindCase::kPointer:
+                return CreateTypePointer(type_in.pointer());
+            case pb::Type::KindCase::kArray:
+                return CreateTypeArray(type_in.array());
+            case pb::Type::KindCase::kAtomic:
+                TINT_UNIMPLEMENTED() << type_in.kind_case();
+                return nullptr;
+
+            case pb::Type::KindCase::KIND_NOT_SET:
+                break;
+        }
+        TINT_ICE() << "invalid TypeDecl.kind";
+        return nullptr;
+    }
+
+    const type::Type* CreateTypeBasic(pb::BasicType basic_in) {
+        switch (basic_in) {
+            case pb::BasicType::void_:
+                return mod_out_.Types().Get<void>();
+            case pb::BasicType::bool_:
+                return mod_out_.Types().Get<bool>();
+            case pb::BasicType::i32:
+                return mod_out_.Types().Get<i32>();
+            case pb::BasicType::u32:
+                return mod_out_.Types().Get<u32>();
+            case pb::BasicType::f32:
+                return mod_out_.Types().Get<f32>();
+            case pb::BasicType::f16:
+                return mod_out_.Types().Get<f16>();
+            default:
+                TINT_ICE() << "invalid BasicType: " << basic_in;
+                return nullptr;
+        }
+    }
+
+    const type::Vector* CreateTypeVector(const pb::VectorType& vector_in) {
+        auto* el_ty = Type(vector_in.element_type());
+        return mod_out_.Types().vec(el_ty, vector_in.width());
+    }
+
+    const type::Matrix* CreateTypeMatrix(const pb::MatrixType& matrix_in) {
+        auto* el_ty = Type(matrix_in.element_type());
+        auto* column_ty = mod_out_.Types().vec(el_ty, matrix_in.num_rows());
+        return mod_out_.Types().mat(column_ty, matrix_in.num_columns());
+    }
+
+    const type::Pointer* CreateTypePointer(const pb::PointerType& pointer_in) {
+        auto address_space = AddressSpace(pointer_in.address_space());
+        auto* store_ty = Type(pointer_in.store_type());
+        auto access = Access(pointer_in.access());
+        return mod_out_.Types().ptr(address_space, store_ty, access);
+    }
+
+    const type::Array* CreateTypeArray(const pb::ArrayType& array_in) {
+        auto* element = Type(array_in.element());
+        uint32_t stride = static_cast<uint32_t>(array_in.stride());
+        uint32_t count = static_cast<uint32_t>(array_in.count());
+        return mod_out_.Types().array(element, count, stride);
+    }
+
+    const type::Type* Type(size_t id) { return id > 0 ? types_[id - 1] : nullptr; }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Values
+    ////////////////////////////////////////////////////////////////////////////
+    ir::Value* CreateValue(const pb::Value& value_in) {
+        ir::Value* value_out = nullptr;
+        switch (value_in.kind_case()) {
+            case pb::Value::KindCase::kFunction: {
+                value_out = Function(value_in.function());
+                break;
+            }
+            case pb::Value::KindCase::kInstructionResult: {
+                auto& res_in = value_in.instruction_result();
+                auto* type = Type(res_in.type());
+                value_out = b.InstructionResult(type);
+                if (res_in.has_name()) {
+                    mod_out_.SetName(value_out, res_in.name());
+                }
+                break;
+            }
+            case pb::Value::KindCase::kFunctionParameter: {
+                auto& param_in = value_in.function_parameter();
+                auto* type = Type(param_in.type());
+                value_out = b.FunctionParam(type);
+                if (param_in.has_name()) {
+                    mod_out_.SetName(value_out, param_in.name());
+                }
+                break;
+            }
+            case pb::Value::KindCase::kConstant: {
+                value_out = b.Constant(ConstantValue(value_in.constant()));
+                break;
+            }
+            default:
+                TINT_ICE() << "invalid TypeDecl.kind: " << value_in.kind_case();
+                return nullptr;
+        }
+        return value_out;
+    }
+
+    ir::Value* Value(uint32_t id) { return id > 0 ? values_[id - 1] : nullptr; }
+
+    template <typename T>
+    T* ValueAs(uint32_t id) {
+        auto* value = Value(id);
+        if (auto cast = value->As<T>(); TINT_LIKELY(cast)) {
+            return cast;
+        }
+        TINT_ICE() << "Value " << id << " is " << (value ? value->TypeInfo().name : "<null>")
+                   << " expected " << TypeInfo::Of<T>().name;
+        return nullptr;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // ConstantValues
+    ////////////////////////////////////////////////////////////////////////////
+    const core::constant::Value* CreateConstantValue(const pb::ConstantValue& value_in) {
+        switch (value_in.kind_case()) {
+            case pb::ConstantValue::KindCase::kScalar:
+                return CreateConstantScalar(value_in.scalar());
+            case pb::ConstantValue::KindCase::kComposite:
+                return CreateConstantComposite(value_in.composite());
+            case pb::ConstantValue::KindCase::kSplat:
+                return CreateConstantSplat(value_in.splat());
+            default:
+                TINT_ICE() << "invalid ConstantValue.kind: " << value_in.kind_case();
+                return nullptr;
+        }
+    }
+
+    const core::constant::Value* CreateConstantScalar(const pb::ConstantValueScalar& value_in) {
+        switch (value_in.kind_case()) {
+            case pb::ConstantValueScalar::KindCase::kBool:
+                return b.ConstantValue(value_in.bool_());
+            case pb::ConstantValueScalar::KindCase::kI32:
+                return b.ConstantValue(i32(value_in.i32()));
+            case pb::ConstantValueScalar::KindCase::kU32:
+                return b.ConstantValue(u32(value_in.u32()));
+            case pb::ConstantValueScalar::KindCase::kF32:
+                return b.ConstantValue(f32(value_in.f32()));
+            case pb::ConstantValueScalar::KindCase::kF16:
+                return b.ConstantValue(f16(value_in.f16()));
+            default:
+                TINT_ICE() << "invalid ConstantValueScalar.kind: " << value_in.kind_case();
+                return nullptr;
+        }
+    }
+
+    const core::constant::Value* CreateConstantComposite(
+        const pb::ConstantValueComposite& composite_in) {
+        auto* type = Type(composite_in.type());
+        Vector<const core::constant::Value*, 8> elements_out;
+        for (auto element_id : composite_in.elements()) {
+            elements_out.Push(ConstantValue(element_id));
+        }
+        return mod_out_.constant_values.Composite(type, std::move(elements_out));
+    }
+
+    const core::constant::Value* CreateConstantSplat(const pb::ConstantValueSplat& splat_in) {
+        auto* type = Type(splat_in.type());
+        auto* elem = ConstantValue(splat_in.elements());
+        return mod_out_.constant_values.Splat(type, elem, splat_in.count());
+    }
+
+    const core::constant::Value* ConstantValue(uint32_t id) {
+        return id > 0 ? constant_values_[id - 1] : nullptr;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Enums
+    ////////////////////////////////////////////////////////////////////////////
+    core::AddressSpace AddressSpace(pb::AddressSpace in) {
+        switch (in) {
+            case pb::AddressSpace::function:
+                return core::AddressSpace::kFunction;
+            case pb::AddressSpace::handle:
+                return core::AddressSpace::kHandle;
+            case pb::AddressSpace::pixel_local:
+                return core::AddressSpace::kPixelLocal;
+            case pb::AddressSpace::private_:
+                return core::AddressSpace::kPrivate;
+            case pb::AddressSpace::push_constant:
+                return core::AddressSpace::kPushConstant;
+            case pb::AddressSpace::storage:
+                return core::AddressSpace::kStorage;
+            case pb::AddressSpace::uniform:
+                return core::AddressSpace::kUniform;
+            case pb::AddressSpace::workgroup:
+                return core::AddressSpace::kWorkgroup;
+            default:
+                TINT_ICE() << "invalid AddressSpace: " << in;
+                return core::AddressSpace::kUndefined;
+        }
+    }
+
+    core::Access Access(pb::AccessControl in) {
+        switch (in) {
+            case pb::AccessControl::read:
+                return core::Access::kRead;
+            case pb::AccessControl::write:
+                return core::Access::kWrite;
+            case pb::AccessControl::read_write:
+                return core::Access::kReadWrite;
+            default:
+                TINT_ICE() << "invalid Access: " << in;
+                return core::Access::kUndefined;
+        }
+    }
+
+    core::ir::UnaryOp UnaryOp(pb::UnaryOp in) {
+        switch (in) {
+            case pb::UnaryOp::complement:
+                return core::ir::UnaryOp::kComplement;
+            case pb::UnaryOp::negation:
+                return core::ir::UnaryOp::kNegation;
+
+            default:
+                TINT_ICE() << "invalid UnaryOp: " << in;
+                return core::ir::UnaryOp::kComplement;
+        }
+    }
+
+    core::ir::BinaryOp BinaryOp(pb::BinaryOp in) {
+        switch (in) {
+            case pb::BinaryOp::add_:
+                return core::ir::BinaryOp::kAdd;
+            case pb::BinaryOp::subtract:
+                return core::ir::BinaryOp::kSubtract;
+            case pb::BinaryOp::multiply:
+                return core::ir::BinaryOp::kMultiply;
+            case pb::BinaryOp::divide:
+                return core::ir::BinaryOp::kDivide;
+            case pb::BinaryOp::modulo:
+                return core::ir::BinaryOp::kModulo;
+            case pb::BinaryOp::and_:
+                return core::ir::BinaryOp::kAnd;
+            case pb::BinaryOp::or_:
+                return core::ir::BinaryOp::kOr;
+            case pb::BinaryOp::xor_:
+                return core::ir::BinaryOp::kXor;
+            case pb::BinaryOp::equal:
+                return core::ir::BinaryOp::kEqual;
+            case pb::BinaryOp::not_equal:
+                return core::ir::BinaryOp::kNotEqual;
+            case pb::BinaryOp::less_than:
+                return core::ir::BinaryOp::kLessThan;
+            case pb::BinaryOp::greater_than:
+                return core::ir::BinaryOp::kGreaterThan;
+            case pb::BinaryOp::less_than_equal:
+                return core::ir::BinaryOp::kLessThanEqual;
+            case pb::BinaryOp::greater_than_equal:
+                return core::ir::BinaryOp::kGreaterThanEqual;
+            case pb::BinaryOp::shift_left:
+                return core::ir::BinaryOp::kShiftLeft;
+            case pb::BinaryOp::shift_right:
+                return core::ir::BinaryOp::kShiftRight;
+
+            default:
+                TINT_ICE() << "invalid BinaryOp: " << in;
+                return core::ir::BinaryOp::kAdd;
+        }
+    }
+};
+
+}  // namespace
+
+Result<Module> Decode(Slice<const std::byte> encoded) {
+    GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+    pb::Module mod_in;
+    if (!mod_in.ParseFromArray(encoded.data, static_cast<int>(encoded.len))) {
+        return Failure{"failed to deserialize protobuf"};
+    }
+
+    Module mod_out;
+    Decoder{mod_in, mod_out}.Decode();
+
+    return mod_out;
+}
+
+}  // namespace tint::core::ir::binary
diff --git a/src/tint/lang/core/ir/binary/decode.h b/src/tint/lang/core/ir/binary/decode.h
new file mode 100644
index 0000000..d4810b3
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/decode.h
@@ -0,0 +1,44 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_BINARY_DECODE_H_
+#define SRC_TINT_LANG_CORE_IR_BINARY_DECODE_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarartion
+namespace tint::core::ir {
+class Module;
+}  // namespace tint::core::ir
+
+namespace tint::core::ir::binary {
+
+Result<Module> Decode(Slice<const std::byte> encoded);
+
+}  // namespace tint::core::ir::binary
+
+#endif  // SRC_TINT_LANG_CORE_IR_BINARY_DECODE_H_
diff --git a/src/tint/lang/core/ir/binary/encode.cc b/src/tint/lang/core/ir/binary/encode.cc
new file mode 100644
index 0000000..7846a4a
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/encode.cc
@@ -0,0 +1,482 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/binary/encode.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/constant/composite.h"
+#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/construct.h"
+#include "src/tint/lang/core/ir/discard.h"
+#include "src/tint/lang/core/ir/function_param.h"
+#include "src/tint/lang/core/ir/let.h"
+#include "src/tint/lang/core/ir/load.h"
+#include "src/tint/lang/core/ir/load_vector_element.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/return.h"
+#include "src/tint/lang/core/ir/store.h"
+#include "src/tint/lang/core/ir/store_vector_element.h"
+#include "src/tint/lang/core/ir/swizzle.h"
+#include "src/tint/lang/core/ir/unary.h"
+#include "src/tint/lang/core/ir/user_call.h"
+#include "src/tint/lang/core/ir/var.h"
+#include "src/tint/lang/core/type/array.h"
+#include "src/tint/lang/core/type/bool.h"
+#include "src/tint/lang/core/type/f16.h"
+#include "src/tint/lang/core/type/f32.h"
+#include "src/tint/lang/core/type/i32.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/u32.h"
+#include "src/tint/lang/core/type/void.h"
+#include "src/tint/utils/macros/compiler.h"
+#include "src/tint/utils/rtti/switch.h"
+
+TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS();
+#include "src/tint/lang/core/ir/binary/ir.pb.h"
+TINT_END_DISABLE_PROTOBUF_WARNINGS();
+
+namespace tint::core::ir::binary {
+namespace {
+struct Encoder {
+    const Module& mod_in_;
+    pb::Module& mod_out_;
+    Hashmap<const core::ir::Function*, uint32_t, 32> functions_{};
+    Hashmap<const core::ir::Block*, uint32_t, 32> blocks_{};
+    Hashmap<const core::type::Type*, uint32_t, 32> types_{};
+    Hashmap<const core::ir::Value*, uint32_t, 32> values_{};
+    Hashmap<const core::constant::Value*, uint32_t, 32> constant_values_{};
+
+    void Encode() {
+        Vector<pb::Function*, 8> fns_out;
+        for (auto& fn_in : mod_in_.functions) {
+            uint32_t id = static_cast<uint32_t>(fns_out.Length() + 1);
+            fns_out.Push(mod_out_.add_functions());
+            functions_.Add(fn_in, id);
+        }
+        for (size_t i = 0, n = mod_in_.functions.Length(); i < n; i++) {
+            PopulateFunction(fns_out[i], mod_in_.functions[i]);
+        }
+        mod_out_.set_root_block(Block(mod_in_.root_block));
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Functions
+    ////////////////////////////////////////////////////////////////////////////
+    void PopulateFunction(pb::Function* fn_out, const ir::Function* fn_in) {
+        if (auto name = mod_in_.NameOf(fn_in)) {
+            fn_out->set_name(name.Name());
+        }
+        fn_out->set_return_type(Type(fn_in->ReturnType()));
+        if (fn_in->Stage() != Function::PipelineStage::kUndefined) {
+            fn_out->set_pipeline_stage(PipelineStage(fn_in->Stage()));
+        }
+        if (auto wg_size_in = fn_in->WorkgroupSize()) {
+            auto& wg_size_out = *fn_out->mutable_workgroup_size();
+            wg_size_out.set_x((*wg_size_in)[0]);
+            wg_size_out.set_y((*wg_size_in)[1]);
+            wg_size_out.set_z((*wg_size_in)[2]);
+        }
+        for (auto* param_in : fn_in->Params()) {
+            fn_out->add_parameters(Value(param_in));
+        }
+        fn_out->set_block(Block(fn_in->Block()));
+    }
+
+    uint32_t Function(const ir::Function* fn_in) { return fn_in ? *functions_.Get(fn_in) : 0; }
+
+    pb::PipelineStage PipelineStage(Function::PipelineStage stage) {
+        switch (stage) {
+            case Function::PipelineStage::kCompute:
+                return pb::PipelineStage::Compute;
+            case Function::PipelineStage::kFragment:
+                return pb::PipelineStage::Fragment;
+            case Function::PipelineStage::kVertex:
+                return pb::PipelineStage::Vertex;
+            default:
+                TINT_ICE() << "unhandled PipelineStage: " << stage;
+                return pb::PipelineStage::Compute;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Blocks
+    ////////////////////////////////////////////////////////////////////////////
+    uint32_t Block(const ir::Block* block_in) {
+        if (block_in == nullptr) {
+            return 0;
+        }
+        return blocks_.GetOrCreate(block_in, [&]() -> uint32_t {
+            auto& block_out = *mod_out_.add_blocks();
+            for (auto* inst : *block_in) {
+                Instruction(*block_out.add_instructions(), inst);
+            }
+            return static_cast<uint32_t>(blocks_.Count());
+        });
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Instructions
+    ////////////////////////////////////////////////////////////////////////////
+    void Instruction(pb::Instruction& inst_out, const ir::Instruction* inst_in) {
+        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::Construct* i) { InstructionConstruct(*inst_out.mutable_construct(), i); },
+            [&](const ir::Discard* i) { InstructionDiscard(*inst_out.mutable_discard(), i); },
+            [&](const ir::Let* i) { InstructionLet(*inst_out.mutable_let(), i); },
+            [&](const ir::Load* i) { InstructionLoad(*inst_out.mutable_load(), i); },
+            [&](const ir::LoadVectorElement* i) {
+                InstructionLoadVectorElement(*inst_out.mutable_load_vector_element(), i);
+            },
+            [&](const ir::Return* i) { InstructionReturn(*inst_out.mutable_return_(), i); },
+            [&](const ir::Store* i) { InstructionStore(*inst_out.mutable_store(), i); },
+            [&](const ir::StoreVectorElement* i) {
+                InstructionStoreVectorElement(*inst_out.mutable_store_vector_element(), 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); },
+            TINT_ICE_ON_NO_MATCH);
+        for (auto* operand : inst_in->Operands()) {
+            inst_out.add_operands(Value(operand));
+        }
+        for (auto* result : inst_in->Results()) {
+            inst_out.add_results(Value(result));
+        }
+    }
+
+    void InstructionAccess(pb::InstructionAccess&, const ir::Access*) {}
+
+    void InstructionBinary(pb::InstructionBinary& binary_out, const ir::Binary* binary_in) {
+        binary_out.set_op(BinaryOp(binary_in->Op()));
+    }
+
+    void InstructionConstruct(pb::InstructionConstruct&, const ir::Construct*) {}
+
+    void InstructionDiscard(pb::InstructionDiscard&, const ir::Discard*) {}
+
+    void InstructionLet(pb::InstructionLet&, const ir::Let*) {}
+
+    void InstructionLoad(pb::InstructionLoad&, const ir::Load*) {}
+
+    void InstructionLoadVectorElement(pb::InstructionLoadVectorElement&,
+                                      const ir::LoadVectorElement*) {}
+
+    void InstructionReturn(pb::InstructionReturn&, const ir::Return*) {}
+
+    void InstructionStore(pb::InstructionStore&, const ir::Store*) {}
+
+    void InstructionStoreVectorElement(pb::InstructionStoreVectorElement&,
+                                       const ir::StoreVectorElement*) {}
+
+    void InstructionSwizzle(pb::InstructionSwizzle& swizzle_out, const ir::Swizzle* swizzle_in) {
+        for (auto idx : swizzle_in->Indices()) {
+            swizzle_out.add_indices(idx);
+        }
+    }
+
+    void InstructionUnary(pb::InstructionUnary& unary_out, const ir::Unary* unary_in) {
+        unary_out.set_op(UnaryOp(unary_in->Op()));
+    }
+
+    void InstructionUserCall(pb::InstructionUserCall&, const ir::UserCall*) {}
+
+    void InstructionVar(pb::InstructionVar& var_out, const ir::Var* var_in) {
+        if (auto bp_in = var_in->BindingPoint()) {
+            auto& bp_out = *var_out.mutable_binding_point();
+            bp_out.set_group(bp_in->group);
+            bp_out.set_binding(bp_in->binding);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Types
+    ////////////////////////////////////////////////////////////////////////////
+    uint32_t Type(const core::type::Type* type_in) {
+        if (type_in == nullptr) {
+            return 0;
+        }
+        return types_.GetOrCreate(type_in, [&]() -> uint32_t {
+            pb::Type type_out;
+            Switch(
+                type_in,  //
+                [&](const core::type::Void*) { type_out.set_basic(pb::BasicType::void_); },
+                [&](const core::type::Bool*) { type_out.set_basic(pb::BasicType::bool_); },
+                [&](const core::type::I32*) { type_out.set_basic(pb::BasicType::i32); },
+                [&](const core::type::U32*) { type_out.set_basic(pb::BasicType::u32); },
+                [&](const core::type::F32*) { type_out.set_basic(pb::BasicType::f32); },
+                [&](const core::type::F16*) { type_out.set_basic(pb::BasicType::f16); },
+                [&](const core::type::Vector* v) { VectorType(*type_out.mutable_vector(), v); },
+                [&](const core::type::Matrix* m) { MatrixType(*type_out.mutable_matrix(), m); },
+                [&](const core::type::Pointer* m) { PointerType(*type_out.mutable_pointer(), m); },
+                [&](const core::type::Array* m) { ArrayType(*type_out.mutable_array(), m); },
+                TINT_ICE_ON_NO_MATCH);
+
+            mod_out_.mutable_types()->Add(std::move(type_out));
+            return static_cast<uint32_t>(mod_out_.types().size());
+        });
+    }
+
+    void VectorType(pb::VectorType& vector_out, const core::type::Vector* vector_in) {
+        vector_out.set_width(vector_in->Width());
+        vector_out.set_element_type(Type(vector_in->type()));
+    }
+
+    void MatrixType(pb::MatrixType& matrix_out, const core::type::Matrix* matrix_in) {
+        matrix_out.set_num_columns(matrix_in->columns());
+        matrix_out.set_num_rows(matrix_in->rows());
+        matrix_out.set_element_type(Type(matrix_in->type()));
+    }
+
+    void PointerType(pb::PointerType& pointer_out, const core::type::Pointer* pointer_in) {
+        pointer_out.set_address_space(AddressSpace(pointer_in->AddressSpace()));
+        pointer_out.set_store_type(Type(pointer_in->StoreType()));
+        pointer_out.set_access(Access(pointer_in->Access()));
+    }
+
+    void ArrayType(pb::ArrayType& array_out, const core::type::Array* array_in) {
+        array_out.set_element(Type(array_in->ElemType()));
+        array_out.set_stride(array_in->Stride());
+        Switch(
+            array_in->Count(),  //
+            [&](const core::type::ConstantArrayCount* c) { array_out.set_count(c->value); },
+            TINT_ICE_ON_NO_MATCH);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Values
+    ////////////////////////////////////////////////////////////////////////////
+    uint32_t Value(const ir::Value* value_in) {
+        if (!value_in) {
+            return 0;
+        }
+        return values_.GetOrCreate(value_in, [&] {
+            auto& value_out = *mod_out_.add_values();
+            Switch(
+                value_in,
+                [&](const ir::InstructionResult* v) {
+                    InstructionResult(*value_out.mutable_instruction_result(), v);
+                },
+                [&](const ir::FunctionParam* v) {
+                    FunctionParameter(*value_out.mutable_function_parameter(), v);
+                },
+                [&](const ir::Function* v) { value_out.set_function(Function(v)); },
+                [&](const ir::Constant* v) { value_out.set_constant(ConstantValue(v->Value())); },
+                TINT_ICE_ON_NO_MATCH);
+
+            return static_cast<uint32_t>(mod_out_.values().size());
+        });
+    }
+
+    void InstructionResult(pb::InstructionResult& res_out, const ir::InstructionResult* res_in) {
+        res_out.set_type(Type(res_in->Type()));
+        if (auto name = mod_in_.NameOf(res_in); name.IsValid()) {
+            res_out.set_name(name.Name());
+        }
+    }
+
+    void FunctionParameter(pb::FunctionParameter& param_out, const ir::FunctionParam* param_in) {
+        param_out.set_type(Type(param_in->Type()));
+        if (auto name = mod_in_.NameOf(param_in); name.IsValid()) {
+            param_out.set_name(name.Name());
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // ConstantValues
+    ////////////////////////////////////////////////////////////////////////////
+    uint32_t ConstantValue(const core::constant::Value* constant_in) {
+        if (!constant_in) {
+            return 0;
+        }
+        return constant_values_.GetOrCreate(constant_in, [&] {
+            pb::ConstantValue constant_out;
+            Switch(
+                constant_in,  //
+                [&](const core::constant::Scalar<bool>* b) {
+                    constant_out.mutable_scalar()->set_bool_(b->value);
+                },
+                [&](const core::constant::Scalar<core::i32>* i32) {
+                    constant_out.mutable_scalar()->set_i32(i32->value);
+                },
+                [&](const core::constant::Scalar<core::u32>* u32) {
+                    constant_out.mutable_scalar()->set_u32(u32->value);
+                },
+                [&](const core::constant::Scalar<core::f32>* f32) {
+                    constant_out.mutable_scalar()->set_f32(f32->value);
+                },
+                [&](const core::constant::Scalar<core::f16>* f16) {
+                    constant_out.mutable_scalar()->set_f16(f16->value);
+                },
+                [&](const core::constant::Composite* composite) {
+                    ConstantValueComposite(*constant_out.mutable_composite(), composite);
+                },
+                [&](const core::constant::Splat* splat) {
+                    ConstantValueSplat(*constant_out.mutable_splat(), splat);
+                },
+                TINT_ICE_ON_NO_MATCH);
+
+            mod_out_.mutable_constant_values()->Add(std::move(constant_out));
+            return static_cast<uint32_t>(mod_out_.constant_values().size());
+        });
+    }
+
+    void ConstantValueComposite(pb::ConstantValueComposite& composite_out,
+                                const core::constant::Composite* composite_in) {
+        composite_out.set_type(Type(composite_in->type));
+        for (auto* el : composite_in->elements) {
+            composite_out.add_elements(ConstantValue(el));
+        }
+    }
+
+    void ConstantValueSplat(pb::ConstantValueSplat& splat_out,
+                            const core::constant::Splat* splat_in) {
+        splat_out.set_type(Type(splat_in->type));
+        splat_out.set_elements(ConstantValue(splat_in->el));
+        splat_out.set_count(static_cast<uint32_t>(splat_in->count));
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // Enums
+    ////////////////////////////////////////////////////////////////////////////
+    pb::AddressSpace AddressSpace(core::AddressSpace in) {
+        switch (in) {
+            case core::AddressSpace::kFunction:
+                return pb::AddressSpace::function;
+            case core::AddressSpace::kHandle:
+                return pb::AddressSpace::handle;
+            case core::AddressSpace::kPixelLocal:
+                return pb::AddressSpace::pixel_local;
+            case core::AddressSpace::kPrivate:
+                return pb::AddressSpace::private_;
+            case core::AddressSpace::kPushConstant:
+                return pb::AddressSpace::push_constant;
+            case core::AddressSpace::kStorage:
+                return pb::AddressSpace::storage;
+            case core::AddressSpace::kUniform:
+                return pb::AddressSpace::uniform;
+            case core::AddressSpace::kWorkgroup:
+                return pb::AddressSpace::workgroup;
+            default:
+                TINT_ICE() << "invalid AddressSpace: " << in;
+                return pb::AddressSpace::function;
+        }
+    }
+
+    pb::AccessControl Access(core::Access in) {
+        switch (in) {
+            case core::Access::kRead:
+                return pb::AccessControl::read;
+            case core::Access::kWrite:
+                return pb::AccessControl::write;
+            case core::Access::kReadWrite:
+                return pb::AccessControl::read_write;
+            default:
+                TINT_ICE() << "invalid Access: " << in;
+                return pb::AccessControl::read;
+        }
+    }
+
+    pb::UnaryOp UnaryOp(core::ir::UnaryOp in) {
+        switch (in) {
+            case core::ir::UnaryOp::kComplement:
+                return pb::UnaryOp::complement;
+            case core::ir::UnaryOp::kNegation:
+                return pb::UnaryOp::negation;
+        }
+        TINT_ICE() << "invalid UnaryOp: " << in;
+        return pb::UnaryOp::complement;
+    }
+
+    pb::BinaryOp BinaryOp(core::ir::BinaryOp in) {
+        switch (in) {
+            case core::ir::BinaryOp::kAdd:
+                return pb::BinaryOp::add_;
+            case core::ir::BinaryOp::kSubtract:
+                return pb::BinaryOp::subtract;
+            case core::ir::BinaryOp::kMultiply:
+                return pb::BinaryOp::multiply;
+            case core::ir::BinaryOp::kDivide:
+                return pb::BinaryOp::divide;
+            case core::ir::BinaryOp::kModulo:
+                return pb::BinaryOp::modulo;
+            case core::ir::BinaryOp::kAnd:
+                return pb::BinaryOp::and_;
+            case core::ir::BinaryOp::kOr:
+                return pb::BinaryOp::or_;
+            case core::ir::BinaryOp::kXor:
+                return pb::BinaryOp::xor_;
+            case core::ir::BinaryOp::kEqual:
+                return pb::BinaryOp::equal;
+            case core::ir::BinaryOp::kNotEqual:
+                return pb::BinaryOp::not_equal;
+            case core::ir::BinaryOp::kLessThan:
+                return pb::BinaryOp::less_than;
+            case core::ir::BinaryOp::kGreaterThan:
+                return pb::BinaryOp::greater_than;
+            case core::ir::BinaryOp::kLessThanEqual:
+                return pb::BinaryOp::less_than_equal;
+            case core::ir::BinaryOp::kGreaterThanEqual:
+                return pb::BinaryOp::greater_than_equal;
+            case core::ir::BinaryOp::kShiftLeft:
+                return pb::BinaryOp::shift_left;
+            case core::ir::BinaryOp::kShiftRight:
+                return pb::BinaryOp::shift_right;
+        }
+
+        TINT_ICE() << "invalid BinaryOp: " << in;
+        return pb::BinaryOp::add_;
+    }
+};
+
+}  // namespace
+
+Result<Vector<std::byte, 0>> Encode(const Module& mod_in) {
+    GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+    pb::Module mod_out;
+    Encoder{mod_in, mod_out}.Encode();
+
+    Vector<std::byte, 0> buffer;
+    size_t len = mod_out.ByteSizeLong();
+    buffer.Resize(len);
+    if (len > 0) {
+        if (!mod_out.SerializeToArray(&buffer[0], static_cast<int>(len))) {
+            return Failure{"failed to serialize protobuf"};
+        }
+    }
+    return buffer;
+}
+
+}  // namespace tint::core::ir::binary
diff --git a/src/tint/lang/core/ir/binary/encode.h b/src/tint/lang/core/ir/binary/encode.h
new file mode 100644
index 0000000..3cdd520
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/encode.h
@@ -0,0 +1,45 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_CORE_IR_BINARY_ENCODE_H_
+#define SRC_TINT_LANG_CORE_IR_BINARY_ENCODE_H_
+
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/result/result.h"
+
+// Forward declarartion
+namespace tint::core::ir {
+class Module;
+}  // namespace tint::core::ir
+
+namespace tint::core::ir::binary {
+
+Result<Vector<std::byte, 0>> Encode(const Module& module);
+
+}  // namespace tint::core::ir::binary
+
+#endif  // SRC_TINT_LANG_CORE_IR_BINARY_ENCODE_H_
diff --git a/src/tint/lang/core/ir/binary/ir.proto b/src/tint/lang/core/ir/binary/ir.proto
new file mode 100644
index 0000000..3db27a5
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/ir.proto
@@ -0,0 +1,288 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+package tint.core.ir.binary.pb;
+
+message Module {
+    repeated Type types = 1;
+    repeated Value values = 2;
+    repeated ConstantValue constant_values = 3;
+    repeated Function functions = 4;
+    repeated Block blocks = 5;
+    uint32 root_block = 6;  // Module.blocks
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Types
+////////////////////////////////////////////////////////////////////////////////
+message Type {
+    oneof kind {
+        BasicType basic = 1;
+        VectorType vector = 2;
+        MatrixType matrix = 3;
+        ArrayType array = 4;
+        PointerType pointer = 5;
+        uint32 atomic = 6;  // Module.types
+        // TODO: textures, samplers
+    }
+}
+
+// Non-compound types
+enum BasicType {
+    void = 0;
+    bool = 1;
+    i32 = 2;
+    u32 = 3;
+    f32 = 4;
+    f16 = 5;
+}
+
+message VectorType {
+    uint32 width = 1;
+    uint32 element_type = 2;  // Module.types
+}
+
+message MatrixType {
+    uint32 num_columns = 1;
+    uint32 num_rows = 2;
+    uint32 element_type = 3;  // Module.types
+}
+
+message ArrayType {
+    uint32 element = 1;  // Module.types
+    uint32 stride = 2;
+    uint32 count = 3;
+}
+
+message PointerType {
+    AddressSpace address_space = 1;
+    uint32 store_type = 2;  // Module.types
+    AccessControl access = 3;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Values
+////////////////////////////////////////////////////////////////////////////////
+message Value {
+    oneof kind {
+        uint32 function = 1;  // Module.functions
+        InstructionResult instruction_result = 2;
+        FunctionParameter function_parameter = 3;
+        uint32 constant = 4;  // Module.constant_values
+    }
+}
+
+message InstructionResult {
+    uint32 type = 1;  // Module.types
+    optional string name = 2;
+}
+
+message FunctionParameter {
+    uint32 type = 1;  // Module.types
+    optional string name = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ConstantValues
+////////////////////////////////////////////////////////////////////////////////
+message ConstantValue {
+    oneof kind {
+        ConstantValueScalar scalar = 1;
+        ConstantValueComposite composite = 2;
+        ConstantValueSplat splat = 3;
+    }
+}
+
+message ConstantValueScalar {
+    oneof kind {
+        bool bool = 1;
+        int32 i32 = 2;
+        uint32 u32 = 3;
+        float f32 = 4;
+        float f16 = 5;
+    }
+}
+
+message ConstantValueComposite {
+    uint32 type = 1;               // Module.types
+    repeated uint32 elements = 2;  // Module.constant_values
+}
+
+message ConstantValueSplat {
+    uint32 type = 1;      // Module.types
+    uint32 elements = 2;  // Module.constant_values
+    uint32 count = 3;     // splat count
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Functions
+////////////////////////////////////////////////////////////////////////////////
+message Function {
+    uint32 return_type = 1;  // Module.types
+    uint32 block = 2;        // Module.blocks
+    optional string name = 3;
+    optional PipelineStage pipeline_stage = 4;
+    optional WorkgroupSize workgroup_size = 5;
+    repeated uint32 parameters = 6;  // Module.values
+}
+
+enum PipelineStage {
+    Compute = 0;
+    Fragment = 1;
+    Vertex = 2;
+}
+
+message WorkgroupSize {
+    uint32 x = 1;
+    uint32 y = 2;
+    uint32 z = 3;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Blocks
+////////////////////////////////////////////////////////////////////////////////
+message Block {
+    repeated uint32 parameters = 1;  // Module.values
+    repeated Instruction instructions = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Instructions
+////////////////////////////////////////////////////////////////////////////////
+message Instruction {
+    repeated uint32 operands = 1;  // Module.values
+    repeated uint32 results = 2;   // Module.values
+    oneof kind {
+        InstructionReturn return = 3;
+        InstructionUnary unary = 4;
+        InstructionBinary binary = 5;
+        InstructionBuiltin builtin = 6;
+        InstructionConstructor constructor = 7;
+        InstructionDiscard discard = 8;
+        InstructionLet let = 9;
+        InstructionVar var = 10;
+        InstructionConstruct construct = 11;
+        InstructionAccess access = 12;
+        InstructionUserCall user_call = 13;
+        InstructionLoad load = 14;
+        InstructionStore store = 15;
+        InstructionLoadVectorElement load_vector_element = 16;
+        InstructionStoreVectorElement store_vector_element = 17;
+        InstructionSwizzle swizzle = 18;
+    }
+}
+
+message InstructionReturn {}
+
+message InstructionUnary {
+    UnaryOp op = 1;
+}
+
+message InstructionBinary {
+    BinaryOp op = 1;
+}
+
+message InstructionBuiltin {}
+
+message InstructionConstructor {}
+
+message InstructionDiscard {}
+
+message InstructionLet {}
+
+message InstructionVar {
+    optional BindingPoint binding_point = 1;
+}
+
+message InstructionConstruct {}
+
+message InstructionAccess {}
+
+message InstructionUserCall {}
+
+message InstructionLoad {}
+
+message InstructionStore {}
+
+message InstructionLoadVectorElement {}
+
+message InstructionStoreVectorElement {}
+
+message InstructionSwizzle {
+    repeated uint32 indices = 1;
+}
+
+message BindingPoint {
+    uint32 group = 1;
+    uint32 binding = 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Enums
+////////////////////////////////////////////////////////////////////////////////
+enum AddressSpace {
+    function = 0;
+    handle = 1;
+    pixel_local = 2;
+    private = 3;
+    push_constant = 4;
+    storage = 5;
+    uniform = 6;
+    workgroup = 7;
+}
+
+enum AccessControl {
+    read = 0;
+    write = 1;
+    read_write = 2;
+}
+
+enum UnaryOp {
+    complement = 0;
+    negation = 1;
+}
+
+enum BinaryOp {
+    add_ = 0;
+    subtract = 1;
+    multiply = 2;
+    divide = 3;
+    modulo = 4;
+    and = 5;
+    or_ = 6;
+    xor_ = 7;
+    equal = 8;
+    not_equal = 9;
+    less_than = 10;
+    greater_than = 11;
+    less_than_equal = 12;
+    greater_than_equal = 13;
+    shift_left = 14;
+    shift_right = 15;
+}
diff --git a/src/tint/lang/core/ir/binary/roundtrip_test.cc b/src/tint/lang/core/ir/binary/roundtrip_test.cc
new file mode 100644
index 0000000..57ade4e
--- /dev/null
+++ b/src/tint/lang/core/ir/binary/roundtrip_test.cc
@@ -0,0 +1,318 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/core/ir/ir_helper_test.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 {
+
+using namespace tint::core::number_suffixes;  // NOLINT
+using namespace tint::core::fluent_types;     // NOLINT
+
+template <typename T = testing::Test>
+class IRBinaryRoundtripTestBase : public IRTestParamHelper<T> {
+  public:
+    std::pair<std::string, std::string> Roundtrip() {
+        auto pre = Disassemble(this->mod);
+        auto encoded = Encode(this->mod);
+        if (!encoded) {
+            return {pre, encoded.Failure().reason.str()};
+        }
+        auto decoded = Decode(encoded->Slice());
+        if (!decoded) {
+            return {pre, decoded.Failure().reason.str()};
+        }
+        auto post = Disassemble(decoded.Get());
+        return {pre, post};
+    }
+};
+
+#define RUN_TEST()                      \
+    {                                   \
+        auto [pre, post] = Roundtrip(); \
+        EXPECT_EQ(pre, post);           \
+    }                                   \
+    TINT_REQUIRE_SEMICOLON
+
+using IRBinaryRoundtripTest = IRBinaryRoundtripTestBase<>;
+TEST_F(IRBinaryRoundtripTest, EmptyModule) {
+    RUN_TEST();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Root block
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(IRBinaryRoundtripTest, RootBlock_Var_private_i32_Unnamed) {
+    b.Append(b.ir.root_block, [&] { b.Var<private_, i32>(); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, RootBlock_Var_workgroup_f32_Named) {
+    b.Append(b.ir.root_block, [&] { b.Var<workgroup, f32>("WG"); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, RootBlock_Var_storage_binding) {
+    b.Append(b.ir.root_block, [&] {
+        auto* v = b.Var<storage, f32>();
+        v->SetBindingPoint(10, 20);
+    });
+    RUN_TEST();
+}
+////////////////////////////////////////////////////////////////////////////////
+// Functions
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(IRBinaryRoundtripTest, Fn_i32_ret) {
+    b.Function("Function", ty.i32());
+    RUN_TEST();
+}
+
+using IRBinaryRoundtripTest_FnPipelineStage = IRBinaryRoundtripTestBase<Function::PipelineStage>;
+TEST_P(IRBinaryRoundtripTest_FnPipelineStage, Test) {
+    b.Function("Function", ty.i32(), GetParam());
+    RUN_TEST();
+}
+INSTANTIATE_TEST_SUITE_P(,
+                         IRBinaryRoundtripTest_FnPipelineStage,
+                         testing::Values(Function::PipelineStage::kCompute,
+                                         Function::PipelineStage::kFragment,
+                                         Function::PipelineStage::kVertex));
+
+TEST_F(IRBinaryRoundtripTest, Fn_WorkgroupSize) {
+    b.Function("Function", ty.i32(), Function::PipelineStage::kCompute,
+               std::array<uint32_t, 3>{1, 2, 3});
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Fn_Parameters) {
+    auto* fn = b.Function("Function", ty.void_());
+    auto* p0 = b.FunctionParam(ty.i32());
+    auto* p1 = b.FunctionParam(ty.u32());
+    auto* p2 = b.FunctionParam(ty.f32());
+    b.ir.SetName(p1, "p1");
+    fn->SetParams({p0, p1, p2});
+    RUN_TEST();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Instructions
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(IRBinaryRoundtripTest, Return) {
+    auto* fn = b.Function("Function", ty.void_());
+    b.Append(fn->Block(), [&] { b.Return(fn); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_bool) {
+    auto* fn = b.Function("Function", ty.bool_());
+    b.Append(fn->Block(), [&] { b.Return(fn, true); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_i32) {
+    auto* fn = b.Function("Function", ty.i32());
+    b.Append(fn->Block(), [&] { b.Return(fn, 42_i); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_u32) {
+    auto* fn = b.Function("Function", ty.u32());
+    b.Append(fn->Block(), [&] { b.Return(fn, 42_u); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_f32) {
+    auto* fn = b.Function("Function", ty.f32());
+    b.Append(fn->Block(), [&] { b.Return(fn, 42_f); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_f16) {
+    auto* fn = b.Function("Function", ty.f16());
+    b.Append(fn->Block(), [&] { b.Return(fn, 42_h); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_vec3f_Composite) {
+    auto* fn = b.Function("Function", ty.vec3<f32>());
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Composite<vec3<f32>>(1_f, 2_f, 3_f)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_vec3f_Splat) {
+    auto* fn = b.Function("Function", ty.vec3<f32>());
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Splat<vec3<f32>>(1_f, 3)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_mat2x3f_Composite) {
+    auto* fn = b.Function("Function", ty.mat2x3<f32>());
+    b.Append(fn->Block(),
+             [&] { b.Return(fn, b.Composite<mat2x3<f32>>(1_f, 2_f, 3_f, 4_f, 5_f, 6_f)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_mat2x3f_Splat) {
+    auto* fn = b.Function("Function", ty.mat2x3<f32>());
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Splat<mat2x3<f32>>(1_f, 6)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_array_f32_Composite) {
+    auto* fn = b.Function("Function", ty.array<f32, 3>());
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Composite<array<f32, 3>>(1_i, 2_i, 3_i)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Return_array_f32_Splat) {
+    auto* fn = b.Function("Function", ty.array<f32, 3>());
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Splat<array<f32, 3>>(1_i, 3)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Construct) {
+    auto* fn = b.Function("Function", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Construct<vec3<f32>>(1_f, 2_f, 3_f);
+        b.Return(fn);
+    });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Discard) {
+    auto* fn = b.Function("Function", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Discard();
+        b.Return(fn);
+    });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Let) {
+    auto* fn = b.Function("Function", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Let("Let", b.Constant(42_i));
+        b.Return(fn);
+    });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Var) {
+    auto* fn = b.Function("Function", ty.void_());
+    b.Append(fn->Block(), [&] {
+        b.Var<function>("Var", b.Constant(42_i));
+        b.Return(fn);
+    });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Access) {
+    auto* fn = b.Function("Function", ty.f32());
+    b.Append(fn->Block(),
+             [&] { b.Return(fn, b.Access<f32>(b.Construct<mat4x4<f32>>(), 1_u, 2_u)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, UserCall) {
+    auto* fn_a = b.Function("A", ty.f32());
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, 42_f); });
+    auto* fn_b = b.Function("B", ty.f32());
+    b.Append(fn_b->Block(), [&] { b.Return(fn_b, b.Call(fn_a)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Load) {
+    auto p = b.FunctionParam<ptr<function, f32, read_write>>("p");
+    auto* fn = b.Function("Function", ty.f32());
+    fn->SetParams({p});
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Load(p)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Store) {
+    auto p = b.FunctionParam<ptr<function, f32, read_write>>("p");
+    auto* fn = b.Function("Function", ty.void_());
+    fn->SetParams({p});
+    b.Append(fn->Block(), [&] {
+        b.Store(p, 42_f);
+        b.Return(fn);
+    });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, LoadVectorElement) {
+    auto p = b.FunctionParam<ptr<function, vec3<f32>, read_write>>("p");
+    auto* fn = b.Function("Function", ty.f32());
+    fn->SetParams({p});
+    b.Append(fn->Block(), [&] { b.Return(fn, b.LoadVectorElement(p, 1_i)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, StoreVectorElement) {
+    auto p = b.FunctionParam<ptr<function, vec3<f32>, read_write>>("p");
+    auto* fn = b.Function("Function", ty.void_());
+    fn->SetParams({p});
+    b.Append(fn->Block(), [&] {
+        b.StoreVectorElement(p, 1_u, 42_f);
+        b.Return(fn);
+    });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, UnaryOp) {
+    auto x = b.FunctionParam<bool>("x");
+    auto* fn = b.Function("Function", ty.bool_());
+    fn->SetParams({x});
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Not<bool>(x)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, BinaryOp) {
+    auto x = b.FunctionParam<f32>("x");
+    auto y = b.FunctionParam<f32>("y");
+    auto* fn = b.Function("Function", ty.f32());
+    fn->SetParams({x, y});
+    b.Append(fn->Block(), [&] { b.Return(fn, b.Add<f32>(x, y)); });
+    RUN_TEST();
+}
+
+TEST_F(IRBinaryRoundtripTest, Swizzle) {
+    auto x = b.FunctionParam<vec4<f32>>("x");
+    auto* fn = b.Function("Function", ty.vec3<f32>());
+    fn->SetParams({x});
+    b.Append(fn->Block(), [&] {
+        b.Return(fn, b.Swizzle<vec3<f32>>(x, Vector<uint32_t, 3>{1, 0, 2}));
+    });
+    RUN_TEST();
+}
+
+}  // namespace
+}  // namespace tint::core::ir::binary
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 2a89fa9..bdbd507 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -365,6 +365,17 @@
     }
 
     /// Creates a new ir::Constant
+    /// @tparam TYPE the splat type
+    /// @param value the splat value
+    /// @param size the number of items
+    /// @returns the new constant
+    template <typename TYPE, typename ARG>
+    ir::Constant* Splat(ARG&& value, size_t size) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Splat(type, std::forward<ARG>(value), size);
+    }
+
+    /// Creates a new ir::Constant
     /// @param ty the constant type
     /// @param values the composite values
     /// @returns the new constant
@@ -374,6 +385,16 @@
             ir.constant_values.Composite(ty, Vector{ConstantValue(std::forward<ARGS>(values))...}));
     }
 
+    /// Creates a new ir::Constant
+    /// @tparam TYPE the constant type
+    /// @param values the composite values
+    /// @returns the new constant
+    template <typename TYPE, typename... ARGS, typename = DisableIfVectorLike<ARGS...>>
+    ir::Constant* Composite(ARGS&&... values) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Composite(type, std::forward<ARGS>(values)...);
+    }
+
     /// Creates a new zero-value ir::Constant
     /// @param ty the constant type
     /// @returns the new constant
@@ -1270,6 +1291,17 @@
                                                          Values(std::forward<ARGS>(indices)...)));
     }
 
+    /// Creates a new `Access`
+    /// @tparam TYPE the return type
+    /// @param object the object being accessed
+    /// @param indices the access indices
+    /// @returns the instruction
+    template <typename TYPE, typename OBJ, typename... ARGS>
+    ir::Access* Access(OBJ&& object, ARGS&&... indices) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Access(type, std::forward<OBJ>(object), std::forward<ARGS>(indices)...);
+    }
+
     /// Creates a new `Swizzle`
     /// @param type the return type
     /// @param object the object being swizzled
@@ -1283,6 +1315,17 @@
     }
 
     /// Creates a new `Swizzle`
+    /// @tparam TYPE the return type
+    /// @param object the object being swizzled
+    /// @param indices the swizzle indices
+    /// @returns the instruction
+    template <typename TYPE, typename OBJ>
+    ir::Swizzle* Swizzle(OBJ&& object, VectorRef<uint32_t> indices) {
+        auto* type = ir.Types().Get<TYPE>();
+        return Swizzle(type, std::forward<OBJ>(object), std::move(indices));
+    }
+
+    /// Creates a new `Swizzle`
     /// @param type the return type
     /// @param object the object being swizzled
     /// @param indices the swizzle indices
diff --git a/src/tint/lang/core/ir/construct.cc b/src/tint/lang/core/ir/construct.cc
index fd67aee..8840b1f 100644
--- a/src/tint/lang/core/ir/construct.cc
+++ b/src/tint/lang/core/ir/construct.cc
@@ -36,6 +36,8 @@
 
 namespace tint::core::ir {
 
+Construct::Construct() = default;
+
 Construct::Construct(InstructionResult* result, VectorRef<Value*> arguments) {
     AddOperands(Construct::kArgsOperandOffset, std::move(arguments));
     AddResult(result);
diff --git a/src/tint/lang/core/ir/construct.h b/src/tint/lang/core/ir/construct.h
index ff9ca22..d0bbeb8 100644
--- a/src/tint/lang/core/ir/construct.h
+++ b/src/tint/lang/core/ir/construct.h
@@ -41,6 +41,9 @@
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
 
+    /// Constructor (no result, no operands)
+    Construct();
+
     /// Constructor
     /// @param result the result value
     /// @param args the constructor arguments
diff --git a/src/tint/lang/core/ir/control_instruction.h b/src/tint/lang/core/ir/control_instruction.h
index cb172db..0f98763 100644
--- a/src/tint/lang/core/ir/control_instruction.h
+++ b/src/tint/lang/core/ir/control_instruction.h
@@ -54,31 +54,6 @@
     /// @param cb the function to call once for each block
     virtual void ForeachBlock(const std::function<void(ir::Block*)>& cb) = 0;
 
-    /// Sets the results of the control instruction
-    /// @param values the new result values
-    void SetResults(VectorRef<InstructionResult*> values) {
-        for (auto* value : results_) {
-            if (value) {
-                value->SetInstruction(nullptr);
-            }
-        }
-        results_ = std::move(values);
-        for (auto* value : results_) {
-            if (value) {
-                value->SetInstruction(this);
-            }
-        }
-    }
-
-    /// Sets the results of the control instruction
-    /// @param values the new result values
-    template <typename... ARGS,
-              typename = std::enable_if_t<!tint::IsVectorLike<
-                  tint::traits::Decay<tint::traits::NthTypeOf<0, ARGS..., void>>>>>
-    void SetResults(ARGS&&... values) {
-        SetResults(Vector{std::forward<ARGS>(values)...});
-    }
-
     /// @return All the exits for the flow control instruction
     const Hashset<Exit*, 2>& Exits() const { return exits_; }
 
diff --git a/src/tint/lang/core/ir/function.cc b/src/tint/lang/core/ir/function.cc
index d3edbf1..8c095e0 100644
--- a/src/tint/lang/core/ir/function.cc
+++ b/src/tint/lang/core/ir/function.cc
@@ -36,6 +36,8 @@
 
 namespace tint::core::ir {
 
+Function::Function() = default;
+
 Function::Function(const core::type::Type* rt,
                    PipelineStage stage,
                    std::optional<std::array<uint32_t, 3>> wg_size)
diff --git a/src/tint/lang/core/ir/function.h b/src/tint/lang/core/ir/function.h
index 1937673..0193884 100644
--- a/src/tint/lang/core/ir/function.h
+++ b/src/tint/lang/core/ir/function.h
@@ -73,6 +73,9 @@
     };
 
     /// Constructor
+    Function();
+
+    /// Constructor
     /// @param rt the function return type
     /// @param stage the function stage
     /// @param wg_size the workgroup_size
@@ -103,6 +106,9 @@
     /// @returns the workgroup size information
     std::optional<std::array<uint32_t, 3>> WorkgroupSize() const { return workgroup_size_; }
 
+    /// @param type the return type for the function
+    void SetReturnType(const core::type::Type* type) { return_.type = type; }
+
     /// @returns the return type for the function
     const core::type::Type* ReturnType() const { return return_.type; }
 
@@ -165,7 +171,7 @@
     void Destroy() override;
 
   private:
-    PipelineStage pipeline_stage_;
+    PipelineStage pipeline_stage_ = PipelineStage::kUndefined;
     std::optional<std::array<uint32_t, 3>> workgroup_size_;
 
     struct {
diff --git a/src/tint/lang/core/ir/instruction.h b/src/tint/lang/core/ir/instruction.h
index 886df26..f8c72d5 100644
--- a/src/tint/lang/core/ir/instruction.h
+++ b/src/tint/lang/core/ir/instruction.h
@@ -61,6 +61,14 @@
     /// @returns the operands of the instruction
     virtual VectorRef<const ir::Value*> Operands() const = 0;
 
+    /// Replaces the operands of the instruction
+    /// @param operands the new operands of the instruction
+    virtual void SetOperands(VectorRef<ir::Value*> operands) = 0;
+
+    /// Replaces the results of the instruction
+    /// @param results the new results of the instruction
+    virtual void SetResults(VectorRef<ir::InstructionResult*> results) = 0;
+
     /// @returns the result values for this instruction
     virtual VectorRef<InstructionResult*> Results() = 0;
 
diff --git a/src/tint/lang/core/ir/let.cc b/src/tint/lang/core/ir/let.cc
index ddbf9a3..aeb185e 100644
--- a/src/tint/lang/core/ir/let.cc
+++ b/src/tint/lang/core/ir/let.cc
@@ -35,6 +35,8 @@
 
 namespace tint::core::ir {
 
+Let::Let() = default;
+
 Let::Let(InstructionResult* result, ir::Value* value) {
     AddOperand(Let::kValueOperandOffset, value);
     AddResult(result);
diff --git a/src/tint/lang/core/ir/let.h b/src/tint/lang/core/ir/let.h
index a2fe222..96a318d 100644
--- a/src/tint/lang/core/ir/let.h
+++ b/src/tint/lang/core/ir/let.h
@@ -40,10 +40,14 @@
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
 
+    /// Constructor (no result, no operands)
+    Let();
+
     /// Constructor
     /// @param result the result value
     /// @param value the let's value
     Let(InstructionResult* result, Value* value);
+
     ~Let() override;
 
     /// @copydoc Instruction::Clone()
diff --git a/src/tint/lang/core/ir/load.cc b/src/tint/lang/core/ir/load.cc
index ba9f6d3..6563ea1 100644
--- a/src/tint/lang/core/ir/load.cc
+++ b/src/tint/lang/core/ir/load.cc
@@ -36,6 +36,10 @@
 
 namespace tint::core::ir {
 
+Load::Load() {
+    flags_.Add(Flag::kSequenced);
+}
+
 Load::Load(InstructionResult* result, Value* from) {
     flags_.Add(Flag::kSequenced);
 
diff --git a/src/tint/lang/core/ir/load.h b/src/tint/lang/core/ir/load.h
index 167fa87..987b4b1 100644
--- a/src/tint/lang/core/ir/load.h
+++ b/src/tint/lang/core/ir/load.h
@@ -41,6 +41,9 @@
     /// The offset in Operands() for the from value
     static constexpr size_t kFromOperandOffset = 0;
 
+    /// Constructor (no results, no operands)
+    Load();
+
     /// Constructor (infers type)
     /// @param result the result value
     /// @param from the value being loaded from
diff --git a/src/tint/lang/core/ir/load_vector_element.cc b/src/tint/lang/core/ir/load_vector_element.cc
index 4db0a53..750a72e 100644
--- a/src/tint/lang/core/ir/load_vector_element.cc
+++ b/src/tint/lang/core/ir/load_vector_element.cc
@@ -34,6 +34,10 @@
 
 namespace tint::core::ir {
 
+LoadVectorElement::LoadVectorElement() {
+    flags_.Add(Flag::kSequenced);
+}
+
 LoadVectorElement::LoadVectorElement(InstructionResult* result, ir::Value* from, ir::Value* index) {
     flags_.Add(Flag::kSequenced);
 
diff --git a/src/tint/lang/core/ir/load_vector_element.h b/src/tint/lang/core/ir/load_vector_element.h
index f8cbb37..f94ddbf 100644
--- a/src/tint/lang/core/ir/load_vector_element.h
+++ b/src/tint/lang/core/ir/load_vector_element.h
@@ -44,6 +44,9 @@
     /// The offset in Operands() for the `index` value
     static constexpr size_t kIndexOperandOffset = 1;
 
+    /// Constructor (no results, no operands)
+    LoadVectorElement();
+
     /// Constructor
     /// @param result the result value
     /// @param from the vector pointer
diff --git a/src/tint/lang/core/ir/operand_instruction.h b/src/tint/lang/core/ir/operand_instruction.h
index 2b3f83a..cab7667 100644
--- a/src/tint/lang/core/ir/operand_instruction.h
+++ b/src/tint/lang/core/ir/operand_instruction.h
@@ -65,9 +65,9 @@
         }
     }
 
-    /// Sets the operands to @p operands
-    /// @param operands the new operands for the instruction
-    void SetOperands(VectorRef<ir::Value*> operands) {
+    /// Replaces the operands of the instruction
+    /// @param operands the new operands of the instruction
+    void SetOperands(VectorRef<ir::Value*> operands) override {
         ClearOperands();
         operands_ = std::move(operands);
         for (size_t i = 0; i < operands_.Length(); i++) {
@@ -88,8 +88,36 @@
         operands_.Clear();
     }
 
+    /// Replaces the results of the instruction
+    /// @param results the new results of the instruction
+    void SetResults(VectorRef<ir::InstructionResult*> results) override {
+        ClearResults();
+        results_ = std::move(results);
+        for (auto* result : results_) {
+            if (result) {
+                result->SetInstruction(this);
+            }
+        }
+    }
+
+    /// Sets the results of the instruction
+    /// @param values the new result values
+    template <typename... ARGS,
+              typename = std::enable_if_t<!tint::IsVectorLike<
+                  tint::traits::Decay<tint::traits::NthTypeOf<0, ARGS..., void>>>>>
+    void SetResults(ARGS&&... values) {
+        SetResults(Vector{std::forward<ARGS>(values)...});
+    }
+
     /// Removes all results from the instruction.
-    void ClearResults() { results_.Clear(); }
+    void ClearResults() {
+        for (auto* result : results_) {
+            if (result && result->Instruction() == this) {
+                result->SetInstruction(nullptr);
+            }
+        }
+        results_.Clear();
+    }
 
     /// @returns the operands of the instruction
     VectorRef<ir::Value*> Operands() override { return operands_; }
diff --git a/src/tint/lang/core/ir/return.cc b/src/tint/lang/core/ir/return.cc
index f105afc..fb6cd8a 100644
--- a/src/tint/lang/core/ir/return.cc
+++ b/src/tint/lang/core/ir/return.cc
@@ -37,6 +37,8 @@
 
 namespace tint::core::ir {
 
+Return::Return() = default;
+
 Return::Return(Function* func) {
     AddOperand(Return::kFunctionOperandOffset, func);
 }
diff --git a/src/tint/lang/core/ir/return.h b/src/tint/lang/core/ir/return.h
index 4fcdc86..4e8827b 100644
--- a/src/tint/lang/core/ir/return.h
+++ b/src/tint/lang/core/ir/return.h
@@ -49,6 +49,9 @@
     /// The offset in Operands() for the return argument
     static constexpr size_t kArgsOperandOffset = 1;
 
+    /// Constructor (no operands)
+    Return();
+
     /// Constructor (no return value)
     /// @param func the function being returned
     explicit Return(Function* func);
diff --git a/src/tint/lang/core/ir/store.cc b/src/tint/lang/core/ir/store.cc
index 7acfd51..2f6ab39 100644
--- a/src/tint/lang/core/ir/store.cc
+++ b/src/tint/lang/core/ir/store.cc
@@ -34,6 +34,10 @@
 
 namespace tint::core::ir {
 
+Store::Store() {
+    flags_.Add(Flag::kSequenced);
+}
+
 Store::Store(Value* to, Value* from) {
     flags_.Add(Flag::kSequenced);
 
diff --git a/src/tint/lang/core/ir/store.h b/src/tint/lang/core/ir/store.h
index 9ed7cc5..2ba7936 100644
--- a/src/tint/lang/core/ir/store.h
+++ b/src/tint/lang/core/ir/store.h
@@ -44,6 +44,9 @@
     /// The offset in Operands() for the `from` value
     static constexpr size_t kFromOperandOffset = 1;
 
+    /// Constructor (no results, no operands)
+    Store();
+
     /// Constructor
     /// @param to the value to store too
     /// @param from the value being stored from
diff --git a/src/tint/lang/core/ir/store_vector_element.cc b/src/tint/lang/core/ir/store_vector_element.cc
index c429c81..947c28e 100644
--- a/src/tint/lang/core/ir/store_vector_element.cc
+++ b/src/tint/lang/core/ir/store_vector_element.cc
@@ -34,6 +34,10 @@
 
 namespace tint::core::ir {
 
+StoreVectorElement::StoreVectorElement() {
+    flags_.Add(Flag::kSequenced);
+}
+
 StoreVectorElement::StoreVectorElement(ir::Value* to, ir::Value* index, ir::Value* value) {
     flags_.Add(Flag::kSequenced);
 
diff --git a/src/tint/lang/core/ir/store_vector_element.h b/src/tint/lang/core/ir/store_vector_element.h
index 2ef3895..91e8697 100644
--- a/src/tint/lang/core/ir/store_vector_element.h
+++ b/src/tint/lang/core/ir/store_vector_element.h
@@ -47,6 +47,9 @@
     /// The offset in Operands() for the `value` value
     static constexpr size_t kValueOperandOffset = 2;
 
+    /// Constructor (no operands)
+    StoreVectorElement();
+
     /// Constructor
     /// @param to the vector pointer
     /// @param index the new vector element index
diff --git a/src/tint/lang/core/ir/swizzle.cc b/src/tint/lang/core/ir/swizzle.cc
index 1a1e6dc..184ddf6 100644
--- a/src/tint/lang/core/ir/swizzle.cc
+++ b/src/tint/lang/core/ir/swizzle.cc
@@ -37,6 +37,8 @@
 
 namespace tint::core::ir {
 
+Swizzle::Swizzle() = default;
+
 Swizzle::Swizzle(InstructionResult* result, Value* object, VectorRef<uint32_t> indices)
     : indices_(std::move(indices)) {
     TINT_ASSERT(!indices.IsEmpty());
diff --git a/src/tint/lang/core/ir/swizzle.h b/src/tint/lang/core/ir/swizzle.h
index 1e5b0fc..88740d0 100644
--- a/src/tint/lang/core/ir/swizzle.h
+++ b/src/tint/lang/core/ir/swizzle.h
@@ -29,6 +29,7 @@
 #define SRC_TINT_LANG_CORE_IR_SWIZZLE_H_
 
 #include <string>
+#include <utility>
 
 #include "src/tint/lang/core/ir/operand_instruction.h"
 #include "src/tint/utils/rtti/castable.h"
@@ -41,6 +42,9 @@
     /// The offset in Operands() for the object being swizzled
     static constexpr size_t kObjectOperandOffset = 0;
 
+    /// Constructor (no results, no operands)
+    Swizzle();
+
     /// Constructor
     /// @param result the result value
     /// @param object the object being swizzled
@@ -60,6 +64,9 @@
     /// @returns the swizzle indices
     VectorRef<uint32_t> Indices() const { return indices_; }
 
+    /// @param indices the new swizzle indices
+    void SetIndices(VectorRef<uint32_t> indices) { indices_ = std::move(indices); }
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() const override { return "swizzle"; }
 
diff --git a/src/tint/lang/core/ir/unary.cc b/src/tint/lang/core/ir/unary.cc
index b79a23c..d4de4e1 100644
--- a/src/tint/lang/core/ir/unary.cc
+++ b/src/tint/lang/core/ir/unary.cc
@@ -34,6 +34,8 @@
 
 namespace tint::core::ir {
 
+Unary::Unary() = default;
+
 Unary::Unary(InstructionResult* result, UnaryOp op, Value* val) : op_(op) {
     AddOperand(Unary::kValueOperandOffset, val);
     AddResult(result);
@@ -47,4 +49,13 @@
     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 af03473..41a4875 100644
--- a/src/tint/lang/core/ir/unary.h
+++ b/src/tint/lang/core/ir/unary.h
@@ -47,6 +47,9 @@
     /// 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
@@ -66,13 +69,26 @@
     /// @returns the unary operator
     UnaryOp Op() const { return op_; }
 
+    /// @param op the new unary operator
+    void SetOp(UnaryOp op) { op_ = op; }
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() const override { return "unary"; }
 
   private:
-    UnaryOp op_;
+    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/user_call.cc b/src/tint/lang/core/ir/user_call.cc
index 698cb62..6cadb32 100644
--- a/src/tint/lang/core/ir/user_call.cc
+++ b/src/tint/lang/core/ir/user_call.cc
@@ -36,6 +36,10 @@
 
 namespace tint::core::ir {
 
+UserCall::UserCall() {
+    flags_.Add(Flag::kSequenced);
+}
+
 UserCall::UserCall(InstructionResult* result, Function* func, VectorRef<Value*> arguments) {
     flags_.Add(Flag::kSequenced);
     AddOperand(UserCall::kFunctionOperandOffset, func);
diff --git a/src/tint/lang/core/ir/user_call.h b/src/tint/lang/core/ir/user_call.h
index 4036208..a7a9b97 100644
--- a/src/tint/lang/core/ir/user_call.h
+++ b/src/tint/lang/core/ir/user_call.h
@@ -45,6 +45,9 @@
     /// The base offset in Operands() for the call arguments
     static constexpr size_t kArgsOperandOffset = 1;
 
+    /// Constructor (no results, no operands)
+    UserCall();
+
     /// Constructor
     /// @param result the result value
     /// @param func the function being called
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index a3a83df..9c24c34 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -484,15 +484,15 @@
     for (size_t i = 0; i < results.Length(); ++i) {
         auto* res = results[i];
         if (!res) {
-            AddResultError(inst, i, InstError(inst, "instruction result is undefined"));
+            AddResultError(inst, i, InstError(inst, "result is undefined"));
             continue;
         }
 
         if (res->Instruction() == nullptr) {
-            AddResultError(inst, i, InstError(inst, "instruction result source is undefined"));
+            AddResultError(inst, i, InstError(inst, "instruction of result is undefined"));
         } else if (res->Instruction() != inst) {
             AddResultError(inst, i,
-                           InstError(inst, "instruction result source has wrong instruction"));
+                           InstError(inst, "instruction of result is a different instruction"));
         }
     }
 
@@ -506,14 +506,11 @@
         // Note, a `nullptr` is a valid operand in some cases, like `var` so we can't just check
         // for `nullptr` here.
         if (!op->Alive()) {
-            AddError(inst, i,
-                     InstError(inst, "instruction operand " + std::to_string(i) + " is not alive"));
+            AddError(inst, i, InstError(inst, "operand is not alive"));
         }
 
         if (!op->HasUsage(inst, i)) {
-            AddError(
-                inst, i,
-                InstError(inst, "instruction operand " + std::to_string(i) + " missing usage"));
+            AddError(inst, i, InstError(inst, "operand missing usage"));
         }
     }
 
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 37cbbcc..bb43fe3 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -833,7 +833,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: if: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: if: result is undefined
     undef = if true [t: %b2, f: %b3] {  # if_1
     ^^^^^
 
@@ -905,7 +905,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:2:3 error: var: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:2:3 error: var: result is undefined
   undef = var
   ^^^^^
 
@@ -932,7 +932,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: var: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: var: result is undefined
     undef = var
     ^^^^^
 
@@ -991,7 +991,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: let: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: let: result is undefined
     undef = let 1i
     ^^^^^
 
@@ -1105,7 +1105,7 @@
     EXPECT_EQ(res.Failure().reason.str(), expected);
 }
 
-TEST_F(IR_ValidatorTest, Instruction_NullSource) {
+TEST_F(IR_ValidatorTest, Instruction_NullInstruction) {
     auto* f = b.Function("my_func", ty.void_());
 
     auto sb = b.Append(f->Block());
@@ -1116,7 +1116,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: var: instruction result source is undefined
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:3:5 error: var: instruction of result is undefined
     %2:ptr<function, f32, read_write> = var
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -1147,7 +1148,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:46 error: var: instruction operand 0 is not alive
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:46 error: var: operand is not alive
     %2:ptr<function, f32, read_write> = var, %3
                                              ^^
 
@@ -1178,7 +1179,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:46 error: var: instruction operand 0 missing usage
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:46 error: var: operand missing usage
     %2:ptr<function, f32, read_write> = var, %3
                                              ^^
 
@@ -1285,7 +1286,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: binary: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: binary: result is undefined
     undef = add 3i, 2i
     ^^^^^
 
@@ -1342,7 +1343,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: unary: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: unary: result is undefined
     undef = negation 2i
     ^^^^^
 
@@ -2985,7 +2986,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(res.Failure().reason.str(),
-              R"(:4:5 error: load_vector_element: instruction result is undefined
+              R"(:4:5 error: load_vector_element: result is undefined
     undef = load_vector_element %2, 1i
     ^^^^^
 
diff --git a/src/tint/lang/core/ir/var.cc b/src/tint/lang/core/ir/var.cc
index b9a8f91..32b1c83 100644
--- a/src/tint/lang/core/ir/var.cc
+++ b/src/tint/lang/core/ir/var.cc
@@ -37,6 +37,8 @@
 
 namespace tint::core::ir {
 
+Var::Var() = default;
+
 Var::Var(InstructionResult* result) {
     if (result && result->Type()) {
         TINT_ASSERT(result->Type()->Is<core::type::Pointer>());
diff --git a/src/tint/lang/core/ir/var.h b/src/tint/lang/core/ir/var.h
index cd72271..4220a4c 100644
--- a/src/tint/lang/core/ir/var.h
+++ b/src/tint/lang/core/ir/var.h
@@ -58,6 +58,9 @@
     /// The offset in Operands() for the initializer
     static constexpr size_t kInitializerOperandOffset = 0;
 
+    /// Constructor (no results, no operands)
+    Var();
+
     /// Constructor
     /// @param result the result value
     explicit Var(InstructionResult* result);
diff --git a/src/tint/lang/core/type/manager.h b/src/tint/lang/core/type/manager.h
index ada8c7e..8f361da 100644
--- a/src/tint/lang/core/type/manager.h
+++ b/src/tint/lang/core/type/manager.h
@@ -121,6 +121,8 @@
             return Get<core::type::F16>(std::forward<ARGS>(args)...);
         } else if constexpr (std::is_same_v<T, bool>) {
             return Get<core::type::Bool>(std::forward<ARGS>(args)...);
+        } else if constexpr (std::is_same_v<T, void>) {
+            return Get<core::type::Void>(std::forward<ARGS>(args)...);
         } else if constexpr (core::fluent_types::IsVector<T>) {
             return vec<typename T::type, T::width>(std::forward<ARGS>(args)...);
         } else if constexpr (core::fluent_types::IsMatrix<T>) {
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 9694635..fbc7d34 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -189,6 +189,7 @@
         polyfills.saturate = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.workgroup_uniform_load = true;
+        polyfills.dot_4x8_packed = true;
         data.Add<ast::transform::BuiltinPolyfill::Config>(polyfills);
         manager.Add<ast::transform::BuiltinPolyfill>();  // Must come before DirectVariableAccess
     }
@@ -264,7 +265,6 @@
             "GLSL", builder_.AST(), diagnostics_,
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
-                wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumInternalDualSourceBlending,
                 wgsl::Extension::kChromiumExperimentalPushConstant,
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 c668179..0ffbb1d 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -380,7 +380,6 @@
             "HLSL", builder_.AST(), diagnostics_,
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
-                wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumExperimentalPushConstant,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
@@ -1246,8 +1245,8 @@
     if (builtin->IsAtomic()) {
         return EmitWorkgroupAtomicCall(out, expr, builtin);
     }
-    if (builtin->IsDP4a()) {
-        return EmitDP4aCall(out, expr, builtin);
+    if (builtin->IsPacked4x8IntegerDotProductBuiltin()) {
+        return EmitPacked4x8IntegerDotProductBuiltinCall(out, expr, builtin);
     }
     if (builtin->IsSubgroup()) {
         if (builtin->Fn() == wgsl::BuiltinFn::kSubgroupBroadcast) {
@@ -2521,10 +2520,9 @@
         });
 }
 
-bool ASTPrinter::EmitDP4aCall(StringStream& out,
-                              const ast::CallExpression* expr,
-                              const sem::BuiltinFn* builtin) {
-    // TODO(crbug.com/tint/1497): support the polyfill version of DP4a functions.
+bool ASTPrinter::EmitPacked4x8IntegerDotProductBuiltinCall(StringStream& out,
+                                                           const ast::CallExpression* expr,
+                                                           const sem::BuiltinFn* builtin) {
     return CallBuiltinHelper(
         out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
             std::string functionName;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
index 0c449b2..633180c 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
@@ -286,14 +286,15 @@
     bool EmitTruncCall(StringStream& out,
                        const ast::CallExpression* expr,
                        const sem::BuiltinFn* builtin);
-    /// Handles generating a call to DP4a builtins (dot4I8Packed and dot4U8Packed)
+    /// Handles generating a call to the builtins defined in the language extension
+    /// `packed_4x8_integer_dot_product`.
     /// @param out the output stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDP4aCall(StringStream& out,
-                      const ast::CallExpression* expr,
-                      const sem::BuiltinFn* builtin);
+    bool EmitPacked4x8IntegerDotProductBuiltinCall(StringStream& out,
+                                                   const ast::CallExpression* expr,
+                                                   const sem::BuiltinFn* builtin);
     /// Handles generating a call to subgroup builtins.
     /// @param out the output stream
     /// @param expr the call expression
diff --git a/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc b/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
index 0c5f91e..d9d6d89 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
@@ -1449,7 +1449,7 @@
 }
 
 TEST_F(HlslASTPrinterTest_Builtin, Dot4I8Packed) {
-    Enable(wgsl::Extension::kChromiumExperimentalDp4A);
+    Require(wgsl::LanguageFeature::kPacked4X8IntegerDotProduct);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
@@ -1475,7 +1475,7 @@
 }
 
 TEST_F(HlslASTPrinterTest_Builtin, Dot4U8Packed) {
-    Enable(wgsl::Extension::kChromiumExperimentalDp4A);
+    Require(wgsl::LanguageFeature::kPacked4X8IntegerDotProduct);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
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 712eddb..a662d59 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -275,7 +275,6 @@
             "MSL", builder_.AST(), diagnostics_,
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
-                wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumExperimentalPixelLocal,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
index 4bb7e6a..0fdbcd1 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.bazel
@@ -43,12 +43,14 @@
     "decompose_strided_array.cc",
     "decompose_strided_matrix.cc",
     "fold_trivial_lets.cc",
+    "pass_workgroup_id_as_argument.cc",
   ],
   hdrs = [
     "atomics.h",
     "decompose_strided_array.h",
     "decompose_strided_matrix.h",
     "fold_trivial_lets.h",
+    "pass_workgroup_id_as_argument.h",
   ],
   deps = [
     "//src/tint/api/common",
@@ -88,6 +90,7 @@
     "decompose_strided_array_test.cc",
     "decompose_strided_matrix_test.cc",
     "fold_trivial_lets_test.cc",
+    "pass_workgroup_id_as_argument_test.cc",
   ],
   deps = [
     "//src/tint/api/common",
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
index 6d3870b..1f1a04c 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.cmake
@@ -49,6 +49,8 @@
   lang/spirv/reader/ast_lower/decompose_strided_matrix.h
   lang/spirv/reader/ast_lower/fold_trivial_lets.cc
   lang/spirv/reader/ast_lower/fold_trivial_lets.h
+  lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.cc
+  lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h
 )
 
 tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower lib
@@ -91,6 +93,7 @@
   lang/spirv/reader/ast_lower/decompose_strided_array_test.cc
   lang/spirv/reader/ast_lower/decompose_strided_matrix_test.cc
   lang/spirv/reader/ast_lower/fold_trivial_lets_test.cc
+  lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument_test.cc
 )
 
 tint_target_add_dependencies(tint_lang_spirv_reader_ast_lower_test test
diff --git a/src/tint/lang/spirv/reader/ast_lower/BUILD.gn b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
index c2ded34..9381efc 100644
--- a/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
+++ b/src/tint/lang/spirv/reader/ast_lower/BUILD.gn
@@ -52,6 +52,8 @@
       "decompose_strided_matrix.h",
       "fold_trivial_lets.cc",
       "fold_trivial_lets.h",
+      "pass_workgroup_id_as_argument.cc",
+      "pass_workgroup_id_as_argument.h",
     ]
     deps = [
       "${tint_src_dir}/api/common",
@@ -91,6 +93,7 @@
         "decompose_strided_array_test.cc",
         "decompose_strided_matrix_test.cc",
         "fold_trivial_lets_test.cc",
+        "pass_workgroup_id_as_argument_test.cc",
       ]
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.cc b/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.cc
new file mode 100644
index 0000000..cfaf300
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.cc
@@ -0,0 +1,163 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h"
+
+#include <utility>
+
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/statement.h"
+#include "src/tint/utils/containers/hashmap.h"
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::PassWorkgroupIdAsArgument);
+
+namespace tint::spirv::reader {
+
+/// PIMPL state for the transform.
+struct PassWorkgroupIdAsArgument::State {
+    /// The source program
+    const Program& src;
+    /// The target program builder
+    ProgramBuilder b;
+    /// The clone context
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
+    /// The semantic info.
+    const sem::Info& sem = src.Sem();
+
+    /// Map from function to the name of its workgroup_id parameter.
+    Hashmap<const ast::Function*, Symbol, 8> func_to_param;
+
+    /// Constructor
+    /// @param program the source program
+    explicit State(const Program& program) : src(program) {}
+
+    /// Runs the transform.
+    /// @returns the new program
+    ApplyResult Run() {
+        // Process all entry points in the module, looking for workgroup_id builtin parameters.
+        bool made_changes = false;
+        for (auto* func : src.AST().Functions()) {
+            if (func->IsEntryPoint()) {
+                for (auto* param : func->params) {
+                    if (auto* builtin =
+                            ast::GetAttribute<ast::BuiltinAttribute>(param->attributes)) {
+                        if (sem.Get(builtin)->Value() == core::BuiltinValue::kWorkgroupId) {
+                            ProcessBuiltin(func, param);
+                            made_changes = true;
+                        }
+                    }
+                }
+            }
+        }
+        if (!made_changes) {
+            return SkipTransform;
+        }
+
+        ctx.Clone();
+        return resolver::Resolve(b);
+    }
+
+    /// Process a workgroup_id builtin.
+    /// @param ep the entry point function
+    /// @param builtin the builtin parameter
+    void ProcessBuiltin(const ast::Function* ep, const ast::Parameter* builtin) {
+        // Record the name of the parameter for the entry point function.
+        func_to_param.Add(ep, ctx.Clone(builtin->name->symbol));
+
+        // The reader should only produce a single use of the parameter which assigns to a global.
+        const auto& users = sem.Get(builtin)->Users();
+        TINT_ASSERT_OR_RETURN(users.Length() == 1u);
+        auto* assign = users[0]->Stmt()->Declaration()->As<ast::AssignmentStatement>();
+        auto& stmts =
+            sem.Get(assign)->Parent()->Declaration()->As<ast::BlockStatement>()->statements;
+        auto* rhs = assign->rhs;
+        if (auto* bitcast = rhs->As<ast::BitcastExpression>()) {
+            // The RHS may be bitcast to a signed integer, so we capture that bitcast.
+            auto let = b.Symbols().New("tint_wgid_bitcast");
+            ctx.InsertBefore(stmts, assign, b.Decl(b.Let(let, ctx.Clone(bitcast))));
+            func_to_param.Replace(ep, let);
+            rhs = bitcast->expr;
+        }
+        TINT_ASSERT_OR_RETURN(assign && rhs == users[0]->Declaration());
+        auto* lhs = sem.GetVal(assign->lhs)->As<sem::VariableUser>();
+        TINT_ASSERT_OR_RETURN(lhs &&
+                              lhs->Variable()->AddressSpace() == core::AddressSpace::kPrivate);
+
+        // Replace all references to the global variable with a function parameter.
+        for (auto* user : lhs->Variable()->Users()) {
+            if (user == lhs) {
+                // Skip the assignment, which will be removed.
+                continue;
+            }
+            auto param = GetParameter(user->Stmt()->Function()->Declaration(),
+                                      lhs->Variable()->Declaration()->type);
+            ctx.Replace(user->Declaration(), b.Expr(param));
+        }
+
+        // Remove the global variable and the assignment to it.
+        ctx.Remove(src.AST().GlobalDeclarations(), lhs->Variable()->Declaration());
+        ctx.Remove(stmts, assign);
+    }
+
+    /// Get the workgroup_id parameter for a function, creating it and updating callsites if needed.
+    /// @param func the function
+    /// @param type the type of the parameter
+    /// @returns the name of the parameter
+    Symbol GetParameter(const ast::Function* func, const ast::Type& type) {
+        return func_to_param.GetOrCreate(func, [&] {
+            // Append a new parameter to the function.
+            auto name = b.Symbols().New("tint_wgid");
+            ctx.InsertBack(func->params, b.Param(name, ctx.Clone(type)));
+
+            // Recursively update all callsites to pass the workgroup_id as an argument.
+            for (auto* callsite : sem.Get(func)->CallSites()) {
+                auto param = GetParameter(callsite->Stmt()->Function()->Declaration(), type);
+                ctx.InsertBack(callsite->Declaration()->args, b.Expr(param));
+            }
+
+            return name;
+        });
+    }
+};
+
+PassWorkgroupIdAsArgument::PassWorkgroupIdAsArgument() = default;
+
+PassWorkgroupIdAsArgument::~PassWorkgroupIdAsArgument() = default;
+
+ast::transform::Transform::ApplyResult PassWorkgroupIdAsArgument::Apply(
+    const Program& src,
+    const ast::transform::DataMap&,
+    ast::transform::DataMap&) const {
+    return State(src).Run();
+}
+
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h b/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h
new file mode 100644
index 0000000..e680352
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h
@@ -0,0 +1,59 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_READER_AST_LOWER_PASS_WORKGROUP_ID_AS_ARGUMENT_H_
+#define SRC_TINT_LANG_SPIRV_READER_AST_LOWER_PASS_WORKGROUP_ID_AS_ARGUMENT_H_
+
+#include "src/tint/lang/wgsl/ast/transform/transform.h"
+
+namespace tint::spirv::reader {
+
+/// PassWorkgroupIdAsArgument is a transform that passes the workgroup_id builtin as an argument to
+/// functions that need it, instead of using a module-scope private variable. This allows the
+/// uniformity analysis to see that it is uniform, enabling shaders that use barriers in control
+/// flow guarded by this builtin.
+class PassWorkgroupIdAsArgument final
+    : public Castable<PassWorkgroupIdAsArgument, ast::transform::Transform> {
+  public:
+    /// Constructor
+    PassWorkgroupIdAsArgument();
+
+    /// Destructor
+    ~PassWorkgroupIdAsArgument() override;
+
+    /// @copydoc ast::transform::Transform::Apply
+    ApplyResult Apply(const Program& program,
+                      const ast::transform::DataMap& inputs,
+                      ast::transform::DataMap& outputs) const override;
+
+  private:
+    struct State;
+};
+
+}  // namespace tint::spirv::reader
+
+#endif  // SRC_TINT_LANG_SPIRV_READER_AST_LOWER_PASS_WORKGROUP_ID_AS_ARGUMENT_H_
diff --git a/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument_test.cc b/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument_test.cc
new file mode 100644
index 0000000..3c1bbcd
--- /dev/null
+++ b/src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument_test.cc
@@ -0,0 +1,408 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h"
+
+#include "src/tint/lang/wgsl/ast/transform/helper_test.h"
+
+namespace tint::spirv::reader {
+namespace {
+
+using PassWorkgroupIdAsArgumentTest = ast::transform::TransformTest;
+
+TEST_F(PassWorkgroupIdAsArgumentTest, Basic) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3u;
+
+fn inner() {
+  if (wgid.x == 0) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = wgid_param;
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner(tint_wgid : vec3u) {
+  if ((tint_wgid.x == 0)) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  inner(wgid_param);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PassWorkgroupIdAsArgumentTest, MultipleUses) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3u;
+
+fn inner() {
+  if (wgid.x == 0) {
+    workgroupBarrier();
+  }
+  if (wgid.y == 0) {
+    workgroupBarrier();
+  }
+  if (wgid.z == 0) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = wgid_param;
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner(tint_wgid : vec3u) {
+  if ((tint_wgid.x == 0)) {
+    workgroupBarrier();
+  }
+  if ((tint_wgid.y == 0)) {
+    workgroupBarrier();
+  }
+  if ((tint_wgid.z == 0)) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  inner(wgid_param);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PassWorkgroupIdAsArgumentTest, NestedCall) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3u;
+
+fn inner_2() {
+  if (wgid.x == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1() {
+  inner_2();
+}
+
+fn inner() {
+  inner_1();
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = wgid_param;
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner_2(tint_wgid : vec3u) {
+  if ((tint_wgid.x == 0)) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1(tint_wgid_1 : vec3u) {
+  inner_2(tint_wgid_1);
+}
+
+fn inner(tint_wgid_2 : vec3u) {
+  inner_1(tint_wgid_2);
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  inner(wgid_param);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PassWorkgroupIdAsArgumentTest, NestedCall_UsesAtEachLevel) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3u;
+
+fn inner_2() {
+  if (wgid.x == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1() {
+  inner_2();
+  if (wgid.y == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn inner() {
+  inner_1();
+  if (wgid.z == 0) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = wgid_param;
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner_2(tint_wgid : vec3u) {
+  if ((tint_wgid.x == 0)) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1(tint_wgid_1 : vec3u) {
+  inner_2(tint_wgid_1);
+  if ((tint_wgid_1.y == 0)) {
+    workgroupBarrier();
+  }
+}
+
+fn inner(tint_wgid_2 : vec3u) {
+  inner_1(tint_wgid_2);
+  if ((tint_wgid_2.z == 0)) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  inner(wgid_param);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PassWorkgroupIdAsArgumentTest, NestedCall_MultipleCallsites) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3u;
+
+fn inner_2() {
+  if (wgid.x == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1() {
+  inner_2();
+  inner_2();
+  inner_2();
+}
+
+fn inner() {
+  inner_1();
+  inner_2();
+  inner_1();
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = wgid_param;
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner_2(tint_wgid : vec3u) {
+  if ((tint_wgid.x == 0)) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1(tint_wgid_1 : vec3u) {
+  inner_2(tint_wgid_1);
+  inner_2(tint_wgid_1);
+  inner_2(tint_wgid_1);
+}
+
+fn inner(tint_wgid_2 : vec3u) {
+  inner_1(tint_wgid_2);
+  inner_2(tint_wgid_2);
+  inner_1(tint_wgid_2);
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  inner(wgid_param);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PassWorkgroupIdAsArgumentTest, NestedCall_OtherParameters) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3u;
+
+fn inner_2(a : u32, b : u32) {
+  if (wgid.x + a == b) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1(a : u32) {
+  inner_2(a, 1);
+}
+
+fn inner() {
+  inner_1(2);
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = wgid_param;
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner_2(a : u32, b : u32, tint_wgid : vec3u) {
+  if (((tint_wgid.x + a) == b)) {
+    workgroupBarrier();
+  }
+}
+
+fn inner_1(a : u32, tint_wgid_1 : vec3u) {
+  inner_2(a, 1, tint_wgid_1);
+}
+
+fn inner(tint_wgid_2 : vec3u) {
+  inner_1(2, tint_wgid_2);
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  inner(wgid_param);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PassWorkgroupIdAsArgumentTest, BitcastToI32) {
+    auto* src = R"(
+enable chromium_disable_uniformity_analysis;
+
+var<private> wgid : vec3i;
+
+fn inner() {
+  if (wgid.x == 0i) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  wgid = bitcast<vec3i>(wgid_param);
+  inner();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_disable_uniformity_analysis;
+
+fn inner(tint_wgid : vec3i) {
+  if ((tint_wgid.x == 0i)) {
+    workgroupBarrier();
+  }
+}
+
+@compute @workgroup_size(64)
+fn main(@builtin(workgroup_id) wgid_param : vec3u) {
+  let tint_wgid_bitcast = bitcast<vec3i>(wgid_param);
+  inner(tint_wgid_bitcast);
+}
+)";
+
+    auto got = Run<PassWorkgroupIdAsArgument>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/ast_parser/parse.cc b/src/tint/lang/spirv/reader/ast_parser/parse.cc
index e3f8cf6..c1b6af7 100644
--- a/src/tint/lang/spirv/reader/ast_parser/parse.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/parse.cc
@@ -33,16 +33,49 @@
 #include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h"
 #include "src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h"
 #include "src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h"
+#include "src/tint/lang/spirv/reader/ast_lower/pass_workgroup_id_as_argument.h"
 #include "src/tint/lang/spirv/reader/ast_parser/ast_parser.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
+#include "src/tint/lang/wgsl/extension.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::spirv::reader::ast_parser {
 
+namespace {
+
+/// Trivial transform that removes the enable directive that disables the uniformity analysis.
+class ReenableUniformityAnalysis final
+    : public Castable<ReenableUniformityAnalysis, ast::transform::Transform> {
+  public:
+    ReenableUniformityAnalysis() {}
+    ~ReenableUniformityAnalysis() override {}
+
+    /// @copydoc ast::transform::Transform::Apply
+    ApplyResult Apply(const Program& src,
+                      const ast::transform::DataMap&,
+                      ast::transform::DataMap&) const override {
+        ProgramBuilder b;
+        program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
+
+        // Remove the extension that disables the uniformity analysis.
+        for (auto* enable : src.AST().Enables()) {
+            if (enable->HasExtension(wgsl::Extension::kChromiumDisableUniformityAnalysis) &&
+                enable->extensions.Length() == 1u) {
+                ctx.Remove(src.AST().GlobalDeclarations(), enable);
+            }
+        }
+
+        ctx.Clone();
+        return resolver::Resolve(b);
+    }
+};
+
+}  // namespace
+
 Program Parse(const std::vector<uint32_t>& input, const Options& options) {
     ASTParser parser(input);
     bool parsed = parser.Parse();
@@ -60,13 +93,19 @@
         builder.DiagnosticDirective(wgsl::DiagnosticSeverity::kOff, "derivative_uniformity");
     }
 
+    // Disable the uniformity analysis temporarily.
+    // We will run transforms that attempt to change the AST to satisfy the analysis.
+    auto allowed_features = options.allowed_features;
+    allowed_features.extensions.insert(wgsl::Extension::kChromiumDisableUniformityAnalysis);
+    builder.Enable(wgsl::Extension::kChromiumDisableUniformityAnalysis);
+
     // The SPIR-V parser can construct disjoint AST nodes, which is invalid for
     // the Resolver. Clone the Program to clean these up.
     Program program_with_disjoint_ast(std::move(builder));
 
     ProgramBuilder output;
     program::CloneContext(&output, &program_with_disjoint_ast, false).Clone();
-    auto program = Program(resolver::Resolve(output, options.allowed_features));
+    auto program = Program(resolver::Resolve(output, allowed_features));
     if (!program.IsValid()) {
         return program;
     }
@@ -76,11 +115,15 @@
     manager.Add<ast::transform::Unshadow>();
     manager.Add<ast::transform::SimplifyPointers>();
     manager.Add<FoldTrivialLets>();
+    manager.Add<PassWorkgroupIdAsArgument>();
     manager.Add<DecomposeStridedMatrix>();
     manager.Add<DecomposeStridedArray>();
     manager.Add<ast::transform::RemoveUnreachableStatements>();
     manager.Add<Atomics>();
+    manager.Add<ReenableUniformityAnalysis>();
     return manager.Run(program, {}, outputs);
 }
 
 }  // namespace tint::spirv::reader::ast_parser
+
+TINT_INSTANTIATE_TYPEINFO(tint::spirv::reader::ast_parser::ReenableUniformityAnalysis);
diff --git a/src/tint/lang/spirv/reader/ast_parser/parser_test.cc b/src/tint/lang/spirv/reader/ast_parser/parser_test.cc
index 7375186..404e925 100644
--- a/src/tint/lang/spirv/reader/ast_parser/parser_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/parser_test.cc
@@ -91,6 +91,43 @@
     EXPECT_EQ(program.Diagnostics().count(), 0u) << errs;
 }
 
+TEST_F(ParserTest, WorkgroupIdGuardingBarrier) {
+    auto spv = test::Assemble(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %foo "foo" %wgid
+               OpExecutionMode %foo LocalSize 1 1 1
+               OpDecorate %wgid BuiltIn WorkgroupId
+       %uint = OpTypeInt 32 0
+      %vec3u = OpTypeVector %uint 3
+%_ptr_Input_vec3u = OpTypePointer Input %vec3u
+     %uint_0 = OpConstant %uint 0
+     %uint_2 = OpConstant %uint 2
+     %uint_8 = OpConstant %uint 8
+       %wgid = OpVariable %_ptr_Input_vec3u Input
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+  %func_type = OpTypeFunction %void
+        %foo = OpFunction %void None %func_type
+  %foo_start = OpLabel
+ %wgid_value = OpLoad %vec3u %wgid
+     %wgid_x = OpCompositeExtract %uint %wgid_value 0
+  %condition = OpIEqual %bool %wgid_x %uint_0
+               OpSelectionMerge %merge None
+               OpBranchConditional %condition %true_branch %merge
+%true_branch = OpLabel
+               OpControlBarrier %uint_2 %uint_2 %uint_8
+               OpBranch %merge
+      %merge = OpLabel
+               OpReturn
+               OpFunctionEnd
+)");
+    auto program = Parse(spv, {});
+    auto errs = program.Diagnostics().str();
+    EXPECT_TRUE(program.IsValid()) << errs;
+    EXPECT_EQ(program.Diagnostics().count(), 0u) << errs;
+}
+
 // TODO(dneto): uint32 vec, valid SPIR-V
 // TODO(dneto): uint32 vec, invalid SPIR-V
 
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc
index 6d11ca9..d9eee77 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_builtin_test.cc
@@ -4168,11 +4168,11 @@
 
 }  // namespace synchronization_builtin_tests
 
-// Tests for DP4A builtins, tint:1497
-namespace DP4A_builtin_tests {
+// Tests for `packed_4x8_integer_dot_product` builtins, tint:1497
+namespace Packed_4x8_integer_dot_product_builtin_tests {
 
 TEST_F(BuiltinSpirvASTPrinterTest, Call_Dot4I8Packed) {
-    Enable(wgsl::Extension::kChromiumExperimentalDp4A);
+    Require(wgsl::LanguageFeature::kPacked4X8IntegerDotProduct);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
@@ -4184,7 +4184,10 @@
     ASSERT_TRUE(b.GenerateFunction(func)) << b.Diagnostics();
 
     auto got = DumpModule(b.Module());
-    auto expect = R"(OpEntryPoint GLCompute %3 "test_function"
+    auto expect = R"(OpCapability DotProduct
+OpCapability DotProductInput4x8BitPacked
+OpExtension "SPV_KHR_integer_dot_product"
+OpEntryPoint GLCompute %3 "test_function"
 OpExecutionMode %3 LocalSize 1 1 1
 OpName %3 "test_function"
 OpName %5 "val1"
@@ -4209,7 +4212,7 @@
 }
 
 TEST_F(BuiltinSpirvASTPrinterTest, Call_Dot4U8Packed) {
-    Enable(wgsl::Extension::kChromiumExperimentalDp4A);
+    Require(wgsl::LanguageFeature::kPacked4X8IntegerDotProduct);
 
     auto* val1 = Var("val1", ty.u32());
     auto* val2 = Var("val2", ty.u32());
@@ -4221,7 +4224,10 @@
     ASSERT_TRUE(b.GenerateFunction(func)) << b.Diagnostics();
 
     auto got = DumpModule(b.Module());
-    auto expect = R"(OpEntryPoint GLCompute %3 "test_function"
+    auto expect = R"(OpCapability DotProduct
+OpCapability DotProductInput4x8BitPacked
+OpExtension "SPV_KHR_integer_dot_product"
+OpEntryPoint GLCompute %3 "test_function"
 OpExecutionMode %3 LocalSize 1 1 1
 OpName %3 "test_function"
 OpName %5 "val1"
@@ -4244,7 +4250,7 @@
     EXPECT_EQ(got, expect);
 }
 
-}  // namespace DP4A_builtin_tests
+}  // namespace Packed_4x8_integer_dot_product_builtin_tests
 
 }  // namespace
 }  // namespace tint::spirv::writer
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 61ceab1..2fe2ab2 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -145,6 +145,7 @@
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.quantize_to_vec_f16 = true;  // crbug.com/tint/1741
         polyfills.workgroup_uniform_load = true;
+        polyfills.dot_4x8_packed = options.polyfill_dot_4x8_packed;
         data.Add<ast::transform::BuiltinPolyfill::Config>(polyfills);
         manager.Add<ast::transform::BuiltinPolyfill>();  // Must come before DirectVariableAccess
     }
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index 2e1271e..26c4d3a 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -277,7 +277,6 @@
             "SPIR-V", builder_.AST(), builder_.Diagnostics(),
             Vector{
                 wgsl::Extension::kChromiumDisableUniformityAnalysis,
-                wgsl::Extension::kChromiumExperimentalDp4A,
                 wgsl::Extension::kChromiumExperimentalFullPtrParameters,
                 wgsl::Extension::kChromiumExperimentalPushConstant,
                 wgsl::Extension::kChromiumExperimentalSubgroups,
@@ -348,11 +347,6 @@
 
 bool Builder::GenerateExtension(wgsl::Extension extension) {
     switch (extension) {
-        case wgsl::Extension::kChromiumExperimentalDp4A:
-            module_.PushExtension("SPV_KHR_integer_dot_product");
-            module_.PushCapability(SpvCapabilityDotProductKHR);
-            module_.PushCapability(SpvCapabilityDotProductInput4x8BitPackedKHR);
-            break;
         case wgsl::Extension::kF16:
             module_.PushCapability(SpvCapabilityFloat16);
             module_.PushCapability(SpvCapabilityUniformAndStorageBuffer16BitAccess);
@@ -2534,6 +2528,7 @@
             }
             break;
         case wgsl::BuiltinFn::kDot4I8Packed: {
+            DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions();
             auto first_param_id = get_arg_as_value_id(0);
             auto second_param_id = get_arg_as_value_id(1);
             if (!push_function_inst(spv::Op::OpSDotKHR,
@@ -2546,6 +2541,7 @@
             return result_id;
         }
         case wgsl::BuiltinFn::kDot4U8Packed: {
+            DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions();
             auto first_param_id = get_arg_as_value_id(0);
             auto second_param_id = get_arg_as_value_id(1);
             if (!push_function_inst(spv::Op::OpUDotKHR,
@@ -4180,6 +4176,12 @@
     return true;
 }
 
+void Builder::DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions() {
+    module_.PushExtension("SPV_KHR_integer_dot_product");
+    module_.PushCapability(SpvCapabilityDotProductKHR);
+    module_.PushCapability(SpvCapabilityDotProductInput4x8BitPackedKHR);
+}
+
 Builder::ContinuingInfo::ContinuingInfo(const ast::Statement* the_last_statement,
                                         uint32_t loop_id,
                                         uint32_t break_id)
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.h b/src/tint/lang/spirv/writer/ast_printer/builder.h
index 6737973..21df95e 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.h
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.h
@@ -528,6 +528,10 @@
     /// Pops the top-most scope
     void PopScope();
 
+    /// Declare all the extensions and capabilities required by `OpSDot` and `OpUDot` using 4x8
+    // packed integer vectors as input.
+    void DeclarePacked4x8IntegerDotProductCapabilitiesAndExtensions();
+
     ProgramBuilder builder_;
     writer::Module module_;
     Function current_function_;
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index f5c7897..2c8a117 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -143,6 +143,9 @@
     /// SPIRV module. Issue: dawn:464
     bool experimental_require_subgroup_uniform_control_flow = false;
 
+    /// Set to `true` to generate polyfill for `dot4I8Packed` and `dot4U8Packed` builtins
+    bool polyfill_dot_4x8_packed = false;
+
     /// The bindings
     Bindings bindings;
 
@@ -155,6 +158,7 @@
                  emit_vertex_point_size,
                  clamp_frag_depth,
                  experimental_require_subgroup_uniform_control_flow,
+                 polyfill_dot_4x8_packed,
                  bindings);
 };
 
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc b/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
index 7045762..b33c31a 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill_test.cc
@@ -4067,8 +4067,6 @@
 
 TEST_F(BuiltinPolyfillTest, Dot4I8Packed) {
     auto* src = R"(
-enable chromium_experimental_dp4a;
-
 fn f() {
   let v1 = 0x01020304u;
   let v2 = 0xF1F2F3F4u;
@@ -4077,8 +4075,6 @@
 )";
 
     auto* expect = R"(
-enable chromium_experimental_dp4a;
-
 fn tint_dot4_i8_packed(a : u32, b : u32) -> i32 {
   const n = vec4<u32>(24, 16, 8, 0);
   let a_i8 = (bitcast<vec4<i32>>((vec4<u32>(a) << n)) >> vec4<u32>(24));
@@ -4100,8 +4096,6 @@
 
 TEST_F(BuiltinPolyfillTest, Dot4U8Packed) {
     auto* src = R"(
-enable chromium_experimental_dp4a;
-
 fn f() {
   let v1 = 0x01020304u;
   let v2 = 0xF1F2F3F4u;
@@ -4110,8 +4104,6 @@
 )";
 
     auto* expect = R"(
-enable chromium_experimental_dp4a;
-
 fn tint_dot4_u8_packed(a : u32, b : u32) -> u32 {
   const n = vec4<u32>(24, 16, 8, 0);
   let a_u8 = ((vec4<u32>(a) >> n) & vec4<u32>(255));
diff --git a/src/tint/lang/wgsl/builtin_fn.cc b/src/tint/lang/wgsl/builtin_fn.cc
index d392fcb..c43c067 100644
--- a/src/tint/lang/wgsl/builtin_fn.cc
+++ b/src/tint/lang/wgsl/builtin_fn.cc
@@ -694,7 +694,7 @@
            f == BuiltinFn::kAtomicCompareExchangeWeak;
 }
 
-bool IsDP4a(BuiltinFn f) {
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f) {
     return f == BuiltinFn::kDot4I8Packed || f == BuiltinFn::kDot4U8Packed;
 }
 
diff --git a/src/tint/lang/wgsl/builtin_fn.cc.tmpl b/src/tint/lang/wgsl/builtin_fn.cc.tmpl
index cf79bee..a255971 100644
--- a/src/tint/lang/wgsl/builtin_fn.cc.tmpl
+++ b/src/tint/lang/wgsl/builtin_fn.cc.tmpl
@@ -100,7 +100,7 @@
            f == BuiltinFn::kAtomicCompareExchangeWeak;
 }
 
-bool IsDP4a(BuiltinFn f) {
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f) {
     return f == BuiltinFn::kDot4I8Packed || f == BuiltinFn::kDot4U8Packed;
 }
 
diff --git a/src/tint/lang/wgsl/builtin_fn.h b/src/tint/lang/wgsl/builtin_fn.h
index 6829f67..d9c68da 100644
--- a/src/tint/lang/wgsl/builtin_fn.h
+++ b/src/tint/lang/wgsl/builtin_fn.h
@@ -471,10 +471,12 @@
 /// @returns true if the given `f` is an atomic builtin
 bool IsAtomic(BuiltinFn f);
 
-/// Determines if the given `f` is a DP4a builtin.
+/// Determines if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
 /// @param f the builtin type
-/// @returns true if the given `f` is a DP4a builtin
-bool IsDP4a(BuiltinFn f);
+/// @returns true if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f);
 
 /// Determines if the given `f` is a subgroup builtin.
 /// @param f the builtin type
diff --git a/src/tint/lang/wgsl/builtin_fn.h.tmpl b/src/tint/lang/wgsl/builtin_fn.h.tmpl
index e69eda2..57d0fe8 100644
--- a/src/tint/lang/wgsl/builtin_fn.h.tmpl
+++ b/src/tint/lang/wgsl/builtin_fn.h.tmpl
@@ -108,10 +108,12 @@
 /// @returns true if the given `f` is an atomic builtin
 bool IsAtomic(BuiltinFn f);
 
-/// Determines if the given `f` is a DP4a builtin.
+/// Determines if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
 /// @param f the builtin type
-/// @returns true if the given `f` is a DP4a builtin
-bool IsDP4a(BuiltinFn f);
+/// @returns true if the given `f` is a builtin defined in the language extension
+/// `packed_4x8_integer_dot_product`.
+bool IsPacked4x8IntegerDotProductBuiltin(BuiltinFn f);
 
 /// Determines if the given `f` is a subgroup builtin.
 /// @param f the builtin type
diff --git a/src/tint/lang/wgsl/extension.cc b/src/tint/lang/wgsl/extension.cc
index 6b001ef..c2f1762 100644
--- a/src/tint/lang/wgsl/extension.cc
+++ b/src/tint/lang/wgsl/extension.cc
@@ -45,9 +45,6 @@
     if (str == "chromium_disable_uniformity_analysis") {
         return Extension::kChromiumDisableUniformityAnalysis;
     }
-    if (str == "chromium_experimental_dp4a") {
-        return Extension::kChromiumExperimentalDp4A;
-    }
     if (str == "chromium_experimental_framebuffer_fetch") {
         return Extension::kChromiumExperimentalFramebufferFetch;
     }
@@ -81,8 +78,6 @@
             return "undefined";
         case Extension::kChromiumDisableUniformityAnalysis:
             return "chromium_disable_uniformity_analysis";
-        case Extension::kChromiumExperimentalDp4A:
-            return "chromium_experimental_dp4a";
         case Extension::kChromiumExperimentalFramebufferFetch:
             return "chromium_experimental_framebuffer_fetch";
         case Extension::kChromiumExperimentalFullPtrParameters:
diff --git a/src/tint/lang/wgsl/extension.h b/src/tint/lang/wgsl/extension.h
index 259c925..41edcba 100644
--- a/src/tint/lang/wgsl/extension.h
+++ b/src/tint/lang/wgsl/extension.h
@@ -47,7 +47,6 @@
 enum class Extension : uint8_t {
     kUndefined,
     kChromiumDisableUniformityAnalysis,
-    kChromiumExperimentalDp4A,
     kChromiumExperimentalFramebufferFetch,
     kChromiumExperimentalFullPtrParameters,
     kChromiumExperimentalPixelLocal,
@@ -76,17 +75,20 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr std::string_view kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",     "chromium_experimental_dp4a",
-    "chromium_experimental_framebuffer_fetch",  "chromium_experimental_full_ptr_parameters",
-    "chromium_experimental_pixel_local",        "chromium_experimental_push_constant",
-    "chromium_experimental_subgroups",          "chromium_internal_dual_source_blending",
-    "chromium_internal_relaxed_uniform_layout", "f16",
+    "chromium_disable_uniformity_analysis",
+    "chromium_experimental_framebuffer_fetch",
+    "chromium_experimental_full_ptr_parameters",
+    "chromium_experimental_pixel_local",
+    "chromium_experimental_push_constant",
+    "chromium_experimental_subgroups",
+    "chromium_internal_dual_source_blending",
+    "chromium_internal_relaxed_uniform_layout",
+    "f16",
 };
 
 /// All extensions
 static constexpr Extension kAllExtensions[] = {
     Extension::kChromiumDisableUniformityAnalysis,
-    Extension::kChromiumExperimentalDp4A,
     Extension::kChromiumExperimentalFramebufferFetch,
     Extension::kChromiumExperimentalFullPtrParameters,
     Extension::kChromiumExperimentalPixelLocal,
diff --git a/src/tint/lang/wgsl/extension_bench.cc b/src/tint/lang/wgsl/extension_bench.cc
index 63c8636..c2e6bd2 100644
--- a/src/tint/lang/wgsl/extension_bench.cc
+++ b/src/tint/lang/wgsl/extension_bench.cc
@@ -52,69 +52,62 @@
         "chromium_dis1ble_uniformity_analysis",
         "chromium_qqisable_unifomity_anaJysis",
         "chrollium_disable_uniformity_analysi77",
-        "chromippHm_experqqmetal_dp4a",
-        "chrmium_expecimntal_dp4",
-        "chrmiumGexpebimental_dp4a",
-        "chromium_experimental_dp4a",
-        "chromium_exverimentiil_dp4a",
-        "chro8ium_experimenWWal_dp4a",
-        "chromiMm_eperimxxntal_dp4a",
-        "cXromium_expermental_framggbuffer_fetch",
-        "chVomiu_experimntal_fXauebuffer_fetch",
-        "chromium_experimental_fr3mebuffer_fetch",
+        "cqqromium_eppperimental_framebuffe_fetcHH",
+        "chrmium_experimvntal_frcmebufer_ftch",
+        "chromium_expebimental_framGbufer_fetch",
         "chromium_experimental_framebuffer_fetch",
-        "chromium_experimental_fraEebuffer_fetch",
-        "chromTTum_experimental_fraebuffePP_fetch",
-        "chromum_experiddental_framebxxffer_fetch",
-        "chromium_experimental_full_ptr_p44rameters",
-        "SShromium_experimental_full_ptr_parameVVers",
-        "chroRium_expRrimental_fu22l_ptr_prameters",
+        "chromium_experimental_vramebuffeii_fetch",
+        "chro8WWum_experimental_framebuffer_fetch",
+        "chromium_eperimenxxMl_framebuffer_fetch",
+        "chromium_expeggimeXtal_full_ptr_paraeters",
+        "chromium_expVrimental_full_ptr_puraXeer",
+        "chromium_experimental_full_ptr3parameters",
         "chromium_experimental_full_ptr_parameters",
-        "chromium_experimFntal_full_ptr_paramet9r",
-        "chromium_experimentl_full_ptr_parameters",
-        "chromiuHexperimental_fulOO_pVr_paramRRters",
-        "chromium_experimenya_pixel_local",
-        "chrromium_exp77rimentnnl_pixellllocGl",
-        "chromium_exper4mental_pixel_lo00al",
+        "chromium_experimentalEfull_ptr_parameters",
+        "chromium_experimentalfull_ptr_PPaTTameters",
+        "chromium_ddxperimental_fullptrxxparameters",
+        "chromium_experi44ental_pixel_local",
+        "chromium_experimental_VVSixel_local",
+        "chroRium_experimental_pix22Rlocal",
         "chromium_experimental_pixel_local",
-        "chooomium_eperimenal_pxel_local",
-        "chromium_experzzental_pixel_ocal",
-        "chiiomiu_epperimental_pix11l_local",
-        "chromXXum_experimental_push_constant",
-        "chromII9um_experinnental_push_c55nstant",
-        "chSSomium_experiHHental_pusrraaconstaYt",
+        "chromiuF_experiment9lpixel_local",
+        "chromium_experimental_pixel_loca",
+        "Vhromium_expeOOimentalHpixRRl_lcal",
+        "chromiym_experimental_push_contant",
+        "nnhro77ium_experimenGal_push_conrrllant",
+        "chromium_experimental_push_c4nstan00",
         "chromium_experimental_push_constant",
-        "chromium_epHrimentkk_psh_constant",
-        "chromium_expegimenja_puRRh_costant",
-        "chromium_bxperimental_push_contan",
-        "chromium_experimental_sjbgroups",
-        "chromium_experimental_sbgroups",
-        "cromum_experimentalqsubgroups",
+        "chooomum_experimental_ush_constat",
+        "chromium_xperimntal_zzush_constant",
+        "chromi11m_experimepptal_psh_ciistant",
+        "chromium_experimental_subgroXXps",
+        "chromium55eIIperimental_subgnno99ps",
+        "chraamiuSS_experimentaHHr_subgrouYs",
         "chromium_experimental_subgroups",
-        "chromium_expNNrimental_subgoups",
-        "chromium_experimetal_svvbgrous",
-        "chromium_experiQental_subgroups",
-        "chrorum_internal_dal_source_bleffding",
-        "chromium_internal_dual_source_jlending",
-        "chromiNNm_internal_dua8_sourwwe_blening",
+        "chkkomium_eperimntal_subgroup",
+        "jhromium_experRmental_subgogps",
+        "chromiubexperiental_subgroups",
+        "chromium_internal_dujl_source_blending",
+        "chromium_intenal_dual_source_blending",
+        "chqomium_internal_dual_source_beding",
         "chromium_internal_dual_source_blending",
-        "chromium_internal_dual_soure_blending",
-        "chromium_irrternal_dual_source_blending",
-        "chromium_internal_duaG_source_blending",
-        "chromium_internalFFrelaxed_uniform_layout",
-        "chromEum_internal_relaxed_unifrmlyout",
-        "chromium_internalrrrelaxd_uniform_layout",
+        "chroium_NNnternal_dual_source_blending",
+        "chovvium_internal_dual_source_lending",
+        "chromium_internQQl_dual_sorce_blending",
+        "chromirm_intenal_rfflaxed_unifrm_layout",
+        "chromium_internal_jelaxed_uniform_layout",
+        "chromium_interna_relNNxed_uwwiform_lay82t",
         "chromium_internal_relaxed_uniform_layout",
-        "chromiuminternal_relaxed_uniform_layut",
-        "cXroDium_internal_rJJlaed_uniform_layout",
-        "chromium_int8nal_relaed_uniform_layut",
-        "k",
-        "16",
-        "J1",
+        "chromium_internal_relaxed_uniform_layut",
+        "chromium_internal_relaxed_rrniform_layout",
+        "chromium_internal_relaxedGuniform_layout",
+        "FF16",
+        "",
+        "rr1",
         "f16",
-        "c16",
-        "fO6",
-        "_KKttvv",
+        "1",
+        "DJ1",
+        "",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/wgsl/extension_test.cc b/src/tint/lang/wgsl/extension_test.cc
index c40bacf..08cda9b 100644
--- a/src/tint/lang/wgsl/extension_test.cc
+++ b/src/tint/lang/wgsl/extension_test.cc
@@ -58,7 +58,6 @@
 
 static constexpr Case kValidCases[] = {
     {"chromium_disable_uniformity_analysis", Extension::kChromiumDisableUniformityAnalysis},
-    {"chromium_experimental_dp4a", Extension::kChromiumExperimentalDp4A},
     {"chromium_experimental_framebuffer_fetch", Extension::kChromiumExperimentalFramebufferFetch},
     {"chromium_experimental_full_ptr_parameters",
      Extension::kChromiumExperimentalFullPtrParameters},
@@ -74,33 +73,30 @@
     {"chromium_disableuniformiccy_analysis", Extension::kUndefined},
     {"chromil3_disable_unifority_analss", Extension::kUndefined},
     {"chromium_disable_Vniformity_analysis", Extension::kUndefined},
-    {"chro1ium_experimental_dp4a", Extension::kUndefined},
-    {"chrJmium_experiqqetal_dp4a", Extension::kUndefined},
-    {"chromium_experimenll77l_dp4a", Extension::kUndefined},
-    {"cqqromium_eppperimental_framebuffe_fetcHH", Extension::kUndefined},
-    {"chrmium_experimvntal_frcmebufer_ftch", Extension::kUndefined},
-    {"chromium_expebimental_framGbufer_fetch", Extension::kUndefined},
-    {"chvomium_exiierimental_full_ptr_parameters", Extension::kUndefined},
-    {"chromium_WWxperimental_full8ptr_parameters", Extension::kUndefined},
-    {"chromxxum_Mperimental_full_ptr_parameters", Extension::kUndefined},
-    {"chromum_experimental_pixeX_loggal", Extension::kUndefined},
-    {"chromium_expVrXmntal_ixel_local", Extension::kUndefined},
-    {"3hromium_experimental_pixel_local", Extension::kUndefined},
-    {"chromium_experEmental_push_constant", Extension::kUndefined},
-    {"chPPomiumexperimental_push_conTTtant", Extension::kUndefined},
-    {"chromixxm_experimentddl_push_constnt", Extension::kUndefined},
-    {"chromium_experimental_44ubgroups", Extension::kUndefined},
-    {"cSSromVVum_experimental_subgroups", Extension::kUndefined},
-    {"chrmium_e22perimental_suRgrRups", Extension::kUndefined},
-    {"chroFium_internal_dual_source_bl9ndig", Extension::kUndefined},
-    {"chrmium_internal_dual_source_blending", Extension::kUndefined},
-    {"cVromium_interHal_dualOOsouRRce_blening", Extension::kUndefined},
-    {"chromium_internl_relaxyd_uniform_layout", Extension::kUndefined},
-    {"chromnnum_internrr77_Gelaxell_uniform_layout", Extension::kUndefined},
-    {"chromium_intern4l_relaxe00_uniform_layout", Extension::kUndefined},
-    {"5", Extension::kUndefined},
-    {"u16", Extension::kUndefined},
-    {"f", Extension::kUndefined},
+    {"chromium_experimental_framebuf1er_fetch", Extension::kUndefined},
+    {"chromium_experiqqntal_framebuffer_fetch", Extension::kUndefined},
+    {"chromium_experimental_framebuffll77_fetch", Extension::kUndefined},
+    {"chroium_experimental_full_ptr_paqqppmetHHrs", Extension::kUndefined},
+    {"chrium_evperiental_full_ptr_paraceters", Extension::kUndefined},
+    {"chromium_expGimental_fullbptr_parameters", Extension::kUndefined},
+    {"vhromium_experimental_pixel_liical", Extension::kUndefined},
+    {"chromium_experiment8l_pixel_lWWcal", Extension::kUndefined},
+    {"chromium_expeimentMl_xxixel_local", Extension::kUndefined},
+    {"chrXmium_experimeggtal_ush_constant", Extension::kUndefined},
+    {"chromiu_experVmentalpusX_constant", Extension::kUndefined},
+    {"chro3ium_experimental_push_constant", Extension::kUndefined},
+    {"cEromium_experimental_subgroups", Extension::kUndefined},
+    {"TThromium_experiPPental_sugroups", Extension::kUndefined},
+    {"chddomium_experimental_subgroxxs", Extension::kUndefined},
+    {"chromium_internal_44ual_source_blending", Extension::kUndefined},
+    {"chromium_inteSSnal_dual_source_blendinVV", Extension::kUndefined},
+    {"chromiuR_interna22dual_source_blenRing", Extension::kUndefined},
+    {"chromium_int9rnal_relaxed_Fnifor_layout", Extension::kUndefined},
+    {"chrmium_internal_relaxed_uniform_layout", Extension::kUndefined},
+    {"VRhHomium_internal_relaxd_uniform_OOayout", Extension::kUndefined},
+    {"y1", Extension::kUndefined},
+    {"l77rrn6", Extension::kUndefined},
+    {"4016", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/wgsl/features/language_feature.cc b/src/tint/lang/wgsl/features/language_feature.cc
index 708697b..0dc4b3a 100644
--- a/src/tint/lang/wgsl/features/language_feature.cc
+++ b/src/tint/lang/wgsl/features/language_feature.cc
@@ -57,6 +57,9 @@
     if (str == "chromium_testing_unsafe_experimental") {
         return LanguageFeature::kChromiumTestingUnsafeExperimental;
     }
+    if (str == "packed_4x8_integer_dot_product") {
+        return LanguageFeature::kPacked4X8IntegerDotProduct;
+    }
     if (str == "readonly_and_readwrite_storage_textures") {
         return LanguageFeature::kReadonlyAndReadwriteStorageTextures;
     }
@@ -77,6 +80,8 @@
             return "chromium_testing_unimplemented";
         case LanguageFeature::kChromiumTestingUnsafeExperimental:
             return "chromium_testing_unsafe_experimental";
+        case LanguageFeature::kPacked4X8IntegerDotProduct:
+            return "packed_4x8_integer_dot_product";
         case LanguageFeature::kReadonlyAndReadwriteStorageTextures:
             return "readonly_and_readwrite_storage_textures";
     }
diff --git a/src/tint/lang/wgsl/features/language_feature.h b/src/tint/lang/wgsl/features/language_feature.h
index 4788b2d..0f1d2c9 100644
--- a/src/tint/lang/wgsl/features/language_feature.h
+++ b/src/tint/lang/wgsl/features/language_feature.h
@@ -51,6 +51,7 @@
     kChromiumTestingShippedWithKillswitch,
     kChromiumTestingUnimplemented,
     kChromiumTestingUnsafeExperimental,
+    kPacked4X8IntegerDotProduct,
     kReadonlyAndReadwriteStorageTextures,
 };
 
@@ -69,6 +70,7 @@
     "chromium_testing_shipped_with_killswitch",
     "chromium_testing_unimplemented",
     "chromium_testing_unsafe_experimental",
+    "packed_4x8_integer_dot_product",
     "readonly_and_readwrite_storage_textures",
 };
 
@@ -79,6 +81,7 @@
     LanguageFeature::kChromiumTestingShippedWithKillswitch,
     LanguageFeature::kChromiumTestingUnimplemented,
     LanguageFeature::kChromiumTestingUnsafeExperimental,
+    LanguageFeature::kPacked4X8IntegerDotProduct,
     LanguageFeature::kReadonlyAndReadwriteStorageTextures,
 };
 
diff --git a/src/tint/lang/wgsl/features/status.cc b/src/tint/lang/wgsl/features/status.cc
index 1520bbf..b0c4bb3 100644
--- a/src/tint/lang/wgsl/features/status.cc
+++ b/src/tint/lang/wgsl/features/status.cc
@@ -34,6 +34,7 @@
 FeatureStatus GetLanguageFeatureStatus(LanguageFeature f) {
     switch (f) {
         case LanguageFeature::kReadonlyAndReadwriteStorageTextures:
+        case LanguageFeature::kPacked4X8IntegerDotProduct:
             return FeatureStatus::kExperimental;
         case LanguageFeature::kUndefined:
             return FeatureStatus::kUnknown;
diff --git a/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc b/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc
index ed40d5a..415f32c 100644
--- a/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc
+++ b/src/tint/lang/wgsl/helpers/check_supported_extensions_test.cc
@@ -42,7 +42,7 @@
     ASSERT_TRUE(CheckSupportedExtensions("writer", AST(), Diagnostics(),
                                          Vector{
                                              wgsl::Extension::kF16,
-                                             wgsl::Extension::kChromiumExperimentalDp4A,
+                                             wgsl::Extension::kChromiumExperimentalSubgroups,
                                          }));
 }
 
@@ -51,7 +51,7 @@
 
     ASSERT_FALSE(CheckSupportedExtensions("writer", AST(), Diagnostics(),
                                           Vector{
-                                              wgsl::Extension::kChromiumExperimentalDp4A,
+                                              wgsl::Extension::kChromiumExperimentalSubgroups,
                                           }));
     EXPECT_EQ(Diagnostics().str(), "12:34 error: writer backend does not support extension 'f16'");
 }
diff --git a/src/tint/lang/wgsl/language_feature_test.cc b/src/tint/lang/wgsl/language_feature_test.cc
index 045f462..6a103ad 100644
--- a/src/tint/lang/wgsl/language_feature_test.cc
+++ b/src/tint/lang/wgsl/language_feature_test.cc
@@ -63,6 +63,7 @@
      LanguageFeature::kChromiumTestingShippedWithKillswitch},
     {"chromium_testing_unimplemented", LanguageFeature::kChromiumTestingUnimplemented},
     {"chromium_testing_unsafe_experimental", LanguageFeature::kChromiumTestingUnsafeExperimental},
+    {"packed_4x8_integer_dot_product", LanguageFeature::kPacked4X8IntegerDotProduct},
     {"readonly_and_readwrite_storage_textures",
      LanguageFeature::kReadonlyAndReadwriteStorageTextures},
 };
@@ -83,9 +84,12 @@
     {"chXggmium_testing_unsafe_expermental", LanguageFeature::kUndefined},
     {"Xhomiuu_testng_unsafe_experimental", LanguageFeature::kUndefined},
     {"chromium_3esting_unsafe_experimental", LanguageFeature::kUndefined},
-    {"readonly_and_readwrite_stErage_textures", LanguageFeature::kUndefined},
-    {"readoTTly_and_readwrite_strage_tPPxtures", LanguageFeature::kUndefined},
-    {"readoly_and_redddwrite_storaxxe_textures", LanguageFeature::kUndefined},
+    {"packed_4x8_integer_Eot_product", LanguageFeature::kUndefined},
+    {"paked_4x8_integePP_dTTt_product", LanguageFeature::kUndefined},
+    {"packed_4x8_integxxrdot_pddoduct", LanguageFeature::kUndefined},
+    {"readon44y_and_readwrite_storage_textures", LanguageFeature::kUndefined},
+    {"readonly_and_readwrite_storageVVSSextures", LanguageFeature::kUndefined},
+    {"rRadonly_an_rea22write_storRge_textures", LanguageFeature::kUndefined},
 };
 
 using LanguageFeatureParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
index 6536271..0af6015 100644
--- a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
@@ -82,8 +82,8 @@
 
 // Test a valid enable directive with multiple extensions.
 TEST_F(EnableDirectiveTest, Multiple) {
-    auto p =
-        parser("enable f16, chromium_disable_uniformity_analysis, chromium_experimental_dp4a;");
+    auto p = parser(
+        "enable f16, chromium_disable_uniformity_analysis, chromium_experimental_subgroups;");
     p->enable_directive();
     EXPECT_FALSE(p->has_error()) << p->error();
     auto program = p->program();
@@ -101,19 +101,19 @@
     EXPECT_EQ(enable->extensions[1]->source.range.begin.column, 13u);
     EXPECT_EQ(enable->extensions[1]->source.range.end.line, 1u);
     EXPECT_EQ(enable->extensions[1]->source.range.end.column, 49u);
-    EXPECT_EQ(enable->extensions[2]->name, wgsl::Extension::kChromiumExperimentalDp4A);
+    EXPECT_EQ(enable->extensions[2]->name, wgsl::Extension::kChromiumExperimentalSubgroups);
     EXPECT_EQ(enable->extensions[2]->source.range.begin.line, 1u);
     EXPECT_EQ(enable->extensions[2]->source.range.begin.column, 51u);
     EXPECT_EQ(enable->extensions[2]->source.range.end.line, 1u);
-    EXPECT_EQ(enable->extensions[2]->source.range.end.column, 77u);
+    EXPECT_EQ(enable->extensions[2]->source.range.end.column, 82u);
     ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
     EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
 }
 
 // Test a valid enable directive with multiple extensions.
 TEST_F(EnableDirectiveTest, MultipleTrailingComma) {
-    auto p =
-        parser("enable f16, chromium_disable_uniformity_analysis, chromium_experimental_dp4a,;");
+    auto p = parser(
+        "enable f16, chromium_disable_uniformity_analysis, chromium_experimental_subgroups,;");
     p->enable_directive();
     EXPECT_FALSE(p->has_error()) << p->error();
     auto program = p->program();
@@ -131,11 +131,11 @@
     EXPECT_EQ(enable->extensions[1]->source.range.begin.column, 13u);
     EXPECT_EQ(enable->extensions[1]->source.range.end.line, 1u);
     EXPECT_EQ(enable->extensions[1]->source.range.end.column, 49u);
-    EXPECT_EQ(enable->extensions[2]->name, wgsl::Extension::kChromiumExperimentalDp4A);
+    EXPECT_EQ(enable->extensions[2]->name, wgsl::Extension::kChromiumExperimentalSubgroups);
     EXPECT_EQ(enable->extensions[2]->source.range.begin.line, 1u);
     EXPECT_EQ(enable->extensions[2]->source.range.begin.column, 51u);
     EXPECT_EQ(enable->extensions[2]->source.range.end.line, 1u);
-    EXPECT_EQ(enable->extensions[2]->source.range.end.column, 77u);
+    EXPECT_EQ(enable->extensions[2]->source.range.end.column, 82u);
     ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
     EXPECT_EQ(ast.GlobalDeclarations()[0], enable);
 }
@@ -205,7 +205,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_framebuffer_fetch', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_framebuffer_fetch', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/lang/wgsl/resolver/builtin_validation_test.cc b/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
index 60898ce..7a062b5 100644
--- a/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
@@ -545,14 +545,11 @@
 
 }  // namespace texture_constexpr_args
 
-// TODO(crbug.com/tint/1497): Update or remove ResolverDP4aExtensionValidationTest when the
-// experimental extension chromium_experimental_dp4a is not needed.
-using ResolverDP4aExtensionValidationTest = ResolverTest;
+using ResolverPacked4x8IntegerDotProductExtensionValidationTest = ResolverTest;
 
-TEST_F(ResolverDP4aExtensionValidationTest, Dot4I8PackedWithExtension) {
-    // enable chromium_experimental_dp4a;
+TEST_F(ResolverPacked4x8IntegerDotProductExtensionValidationTest, Dot4I8Packed) {
     // fn func { return dot4I8Packed(1u, 2u); }
-    Enable(wgsl::Extension::kChromiumExperimentalDp4A);
+    Require(wgsl::LanguageFeature::kPacked4X8IntegerDotProduct);
 
     Func("func", tint::Empty, ty.i32(),
          Vector{
@@ -563,7 +560,7 @@
     EXPECT_TRUE(r()->Resolve());
 }
 
-TEST_F(ResolverDP4aExtensionValidationTest, Dot4I8PackedWithoutExtension) {
+TEST_F(ResolverPacked4x8IntegerDotProductExtensionValidationTest, Dot4I8Packed_FeatureDisallowed) {
     // fn func { return dot4I8Packed(1u, 2u); }
     Func("func", tint::Empty, ty.i32(),
          Vector{
@@ -571,16 +568,17 @@
                          Vector{Expr(1_u), Expr(2_u)})),
          });
 
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(
-        r()->error(),
-        R"(12:34 error: cannot call built-in function 'dot4I8Packed' without extension chromium_experimental_dp4a)");
+    auto resolver = Resolver(this, {});
+    EXPECT_FALSE(resolver.Resolve());
+    EXPECT_EQ(resolver.error(),
+              "12:34 error: built-in function 'dot4I8Packed' requires the "
+              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "current environment");
 }
 
-TEST_F(ResolverDP4aExtensionValidationTest, Dot4U8PackedWithExtension) {
-    // enable chromium_experimental_dp4a;
+TEST_F(ResolverPacked4x8IntegerDotProductExtensionValidationTest, Dot4U8Packed) {
     // fn func { return dot4U8Packed(1u, 2u); }
-    Enable(wgsl::Extension::kChromiumExperimentalDp4A);
+    Require(wgsl::LanguageFeature::kPacked4X8IntegerDotProduct);
 
     Func("func", tint::Empty, ty.u32(),
          Vector{
@@ -591,7 +589,7 @@
     EXPECT_TRUE(r()->Resolve());
 }
 
-TEST_F(ResolverDP4aExtensionValidationTest, Dot4U8PackedWithoutExtension) {
+TEST_F(ResolverPacked4x8IntegerDotProductExtensionValidationTest, Dot4U8Packed_FeatureDisallowed) {
     // fn func { return dot4U8Packed(1u, 2u); }
     Func("func", tint::Empty, ty.u32(),
          Vector{
@@ -599,10 +597,12 @@
                          Vector{Expr(1_u), Expr(2_u)})),
          });
 
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(
-        r()->error(),
-        R"(12:34 error: cannot call built-in function 'dot4U8Packed' without extension chromium_experimental_dp4a)");
+    auto resolver = Resolver(this, {});
+    EXPECT_FALSE(resolver.Resolve());
+    EXPECT_EQ(resolver.error(),
+              "12:34 error: built-in function 'dot4U8Packed' requires the "
+              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "current environment");
 }
 
 TEST_F(ResolverBuiltinValidationTest, WorkgroupUniformLoad_WrongAddressSpace) {
diff --git a/src/tint/lang/wgsl/resolver/side_effects_test.cc b/src/tint/lang/wgsl/resolver/side_effects_test.cc
index 23cd725..b9fa38b 100644
--- a/src/tint/lang/wgsl/resolver/side_effects_test.cc
+++ b/src/tint/lang/wgsl/resolver/side_effects_test.cc
@@ -180,7 +180,6 @@
 using SideEffectsBuiltinTest = resolver::ResolverTestWithParam<Case>;
 
 TEST_P(SideEffectsBuiltinTest, Test) {
-    Enable(tint::wgsl::Extension::kChromiumExperimentalDp4A);
     auto& c = GetParam();
 
     uint32_t next_binding = 0;
diff --git a/src/tint/lang/wgsl/sem/builtin_fn.cc b/src/tint/lang/wgsl/sem/builtin_fn.cc
index e180378..0e48dfc 100644
--- a/src/tint/lang/wgsl/sem/builtin_fn.cc
+++ b/src/tint/lang/wgsl/sem/builtin_fn.cc
@@ -93,8 +93,8 @@
     return wgsl::IsAtomic(fn_);
 }
 
-bool BuiltinFn::IsDP4a() const {
-    return wgsl::IsDP4a(fn_);
+bool BuiltinFn::IsPacked4x8IntegerDotProductBuiltin() const {
+    return wgsl::IsPacked4x8IntegerDotProductBuiltin(fn_);
 }
 
 bool BuiltinFn::IsSubgroup() const {
@@ -106,9 +106,6 @@
 }
 
 wgsl::Extension BuiltinFn::RequiredExtension() const {
-    if (IsDP4a()) {
-        return wgsl::Extension::kChromiumExperimentalDp4A;
-    }
     if (IsSubgroup()) {
         return wgsl::Extension::kChromiumExperimentalSubgroups;
     }
@@ -119,6 +116,9 @@
     if (fn_ == wgsl::BuiltinFn::kTextureBarrier) {
         return wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures;
     }
+    if (IsPacked4x8IntegerDotProductBuiltin()) {
+        return wgsl::LanguageFeature::kPacked4X8IntegerDotProduct;
+    }
     return wgsl::LanguageFeature::kUndefined;
 }
 
diff --git a/src/tint/lang/wgsl/sem/builtin_fn.h b/src/tint/lang/wgsl/sem/builtin_fn.h
index 83d62bf..7c5d12c 100644
--- a/src/tint/lang/wgsl/sem/builtin_fn.h
+++ b/src/tint/lang/wgsl/sem/builtin_fn.h
@@ -102,9 +102,9 @@
     /// @returns true if builtin is a atomic builtin
     bool IsAtomic() const;
 
-    /// @returns true if builtin is a DP4a builtin (defined in the extension
-    /// chromium_experimental_DP4a)
-    bool IsDP4a() const;
+    /// @returns true if builtin is a builtin defined in the language extension
+    /// `packed_4x8_integer_dot_product`.
+    bool IsPacked4x8IntegerDotProductBuiltin() const;
 
     /// @returns true if builtin is a subgroup builtin (defined in the extension
     /// chromium_experimental_subgroups)
diff --git a/src/tint/lang/wgsl/wgsl.def b/src/tint/lang/wgsl/wgsl.def
index 7969b9f..1a3bbee 100644
--- a/src/tint/lang/wgsl/wgsl.def
+++ b/src/tint/lang/wgsl/wgsl.def
@@ -68,9 +68,6 @@
 enum extension {
   // WGSL Extension "f16"
   f16
-  // An extension for the experimental feature "chromium_experimental_dp4a".
-  // See crbug.com/tint/1497 for more details
-  chromium_experimental_dp4a
   // A Chromium-specific extension for disabling uniformity analysis.
   chromium_disable_uniformity_analysis
   // A Chromium-specific extension for push constants
@@ -95,6 +92,7 @@
 // https://gpuweb.github.io/gpuweb/wgsl/#language-extensions-sec
 enum language_feature {
   readonly_and_readwrite_storage_textures
+  packed_4x8_integer_dot_product
 
   // Language features used only for testing whose status will never change.
   chromium_testing_unimplemented
diff --git a/src/tint/utils/bytes/BUILD.bazel b/src/tint/utils/bytes/BUILD.bazel
index 1b60b1d..2bfa708 100644
--- a/src/tint/utils/bytes/BUILD.bazel
+++ b/src/tint/utils/bytes/BUILD.bazel
@@ -40,6 +40,7 @@
   name = "bytes",
   srcs = [
     "bytes.cc",
+    "reader.cc",
   ],
   hdrs = [
     "decoder.h",
diff --git a/src/tint/utils/bytes/BUILD.cmake b/src/tint/utils/bytes/BUILD.cmake
index 1012db6..a3997fd 100644
--- a/src/tint/utils/bytes/BUILD.cmake
+++ b/src/tint/utils/bytes/BUILD.cmake
@@ -42,6 +42,7 @@
   utils/bytes/bytes.cc
   utils/bytes/decoder.h
   utils/bytes/endianness.h
+  utils/bytes/reader.cc
   utils/bytes/reader.h
   utils/bytes/swap.h
 )
diff --git a/src/tint/utils/bytes/BUILD.gn b/src/tint/utils/bytes/BUILD.gn
index 97931f1..c10512f 100644
--- a/src/tint/utils/bytes/BUILD.gn
+++ b/src/tint/utils/bytes/BUILD.gn
@@ -47,6 +47,7 @@
     "bytes.cc",
     "decoder.h",
     "endianness.h",
+    "reader.cc",
     "reader.h",
     "swap.h",
   ]
diff --git a/src/tint/utils/bytes/decoder.h b/src/tint/utils/bytes/decoder.h
index 591f255..4496ad9 100644
--- a/src/tint/utils/bytes/decoder.h
+++ b/src/tint/utils/bytes/decoder.h
@@ -34,7 +34,6 @@
 #include <utility>
 
 #include "src/tint/utils/bytes/reader.h"
-#include "src/tint/utils/result/result.h"
 
 namespace tint::bytes {
 
@@ -43,10 +42,11 @@
 
 /// Decodes T from @p reader.
 /// @param reader the byte reader
+/// @param args additional arguments used by Decoder<T>::Decode()
 /// @returns the decoded object
-template <typename T>
-Result<T> Decode(Reader& reader) {
-    return Decoder<T>::Decode(reader);
+template <typename T, typename... ARGS>
+Result<T> Decode(Reader& reader, ARGS&&... args) {
+    return Decoder<T>::Decode(reader, std::forward<ARGS>(args)...);
 }
 
 /// Decoder specialization for integer types
@@ -54,12 +54,10 @@
 struct Decoder<T, std::enable_if_t<std::is_integral_v<T>>> {
     /// Decode decodes the integer type from @p reader.
     /// @param reader the reader to decode from
+    /// @param endianness the endianness of the integer
     /// @returns the decoded integer type, or an error if the stream is too short.
-    static Result<T> Decode(Reader& reader) {
-        if (reader.BytesRemaining() < sizeof(T)) {
-            return Failure{"EOF"};
-        }
-        return reader.Int<T>();
+    static Result<T> Decode(Reader& reader, Endianness endianness = Endianness::kLittle) {
+        return reader.Int<T>(endianness);
     }
 };
 
@@ -69,44 +67,31 @@
     /// Decode decodes the floating point type from @p reader.
     /// @param reader the reader to decode from
     /// @returns the decoded floating point type, or an error if the stream is too short.
-    static Result<T> Decode(Reader& reader) {
-        if (reader.BytesRemaining() < sizeof(T)) {
-            return Failure{"EOF"};
-        }
-        return reader.Float<T>();
-    }
+    static Result<T> Decode(Reader& reader) { return reader.Float<T>(); }
 };
 
 /// Decoder specialization for a uint16_t length prefixed string.
-template <typename T>
-struct Decoder<T, std::enable_if_t<std::is_same_v<T, std::string>>> {
+template <>
+struct Decoder<std::string, void> {
     /// Decode decodes the string from @p reader.
     /// @param reader the reader to decode from
     /// @returns the decoded string, or an error if the stream is too short.
-    static Result<T> Decode(Reader& reader) {
-        if (reader.BytesRemaining() < sizeof(uint16_t)) {
-            return Failure{"EOF"};
-        }
+    static Result<std::string> Decode(Reader& reader) {
         auto len = reader.Int<uint16_t>();
-        if (reader.BytesRemaining() < len) {
-            return Failure{"EOF"};
+        if (!len) {
+            return len.Failure();
         }
-        return reader.String(len);
+        return reader.String(len.Get());
     }
 };
 
 /// Decoder specialization for bool types
 template <>
 struct Decoder<bool, void> {
-    static Result<bool> Decode(Reader& reader) {
-        /// Decode decodes the boolean from @p reader.
-        /// @param reader the reader to decode from
-        /// @returns the decoded boolean, or an error if the stream is too short.
-        if (reader.IsEOF()) {
-            return Failure{"EOF"};
-        }
-        return reader.Bool();
-    }
+    /// Decode decodes the boolean from @p reader.
+    /// @param reader the reader to decode from
+    /// @returns the decoded boolean, or an error if the stream is too short.
+    static Result<bool> Decode(Reader& reader) { return reader.Bool(); }
 };
 
 /// Decoder specialization for types that use TINT_REFLECT
@@ -143,10 +128,11 @@
         std::unordered_map<K, V> out;
 
         while (true) {
-            if (reader.IsEOF()) {
-                return Failure{"EOF"};
+            auto stop = bytes::Decode<bool>(reader);
+            if (!stop) {
+                return stop.Failure();
             }
-            if (reader.Bool()) {
+            if (stop.Get()) {
                 break;
             }
             auto key = bytes::Decode<K>(reader);
diff --git a/src/tint/utils/bytes/decoder_test.cc b/src/tint/utils/bytes/decoder_test.cc
index cce8e5a..12640b1 100644
--- a/src/tint/utils/bytes/decoder_test.cc
+++ b/src/tint/utils/bytes/decoder_test.cc
@@ -44,7 +44,7 @@
 
 TEST(BytesDecoderTest, Uint8) {
     auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
-    auto reader = Reader{Slice{data}, 0, Endianness::kLittle};
+    auto reader = BufferReader{Slice{data}};
     EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x10u);
     EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x20u);
     EXPECT_EQ(Decode<uint8_t>(reader).Get(), 0x30u);
@@ -58,7 +58,7 @@
 
 TEST(BytesDecoderTest, Uint16) {
     auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
-    auto reader = Reader{Slice{data}, 0, Endianness::kLittle};
+    auto reader = BufferReader{Slice{data}};
     EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x2010u);
     EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x4030u);
     EXPECT_EQ(Decode<uint16_t>(reader).Get(), 0x6050u);
@@ -68,22 +68,22 @@
 
 TEST(BytesDecoderTest, Uint32) {
     auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
-    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
-    EXPECT_EQ(Decode<uint32_t>(reader).Get(), 0x10203040u);
-    EXPECT_EQ(Decode<uint32_t>(reader).Get(), 0x50607080u);
+    auto reader = BufferReader{Slice{data}};
+    EXPECT_EQ(Decode<uint32_t>(reader, Endianness::kBig).Get(), 0x10203040u);
+    EXPECT_EQ(Decode<uint32_t>(reader, Endianness::kBig).Get(), 0x50607080u);
     EXPECT_FALSE(Decode<uint32_t>(reader));
 }
 
 TEST(BytesDecoderTest, Float) {
     auto data = Data(0x00, 0x00, 0x08, 0x41);
-    auto reader = Reader{Slice{data}};
+    auto reader = BufferReader{Slice{data}};
     EXPECT_EQ(Decode<float>(reader).Get(), 8.5f);
     EXPECT_FALSE(Decode<float>(reader));
 }
 
 TEST(BytesDecoderTest, Bool) {
     auto data = Data(0x0, 0x1, 0x2, 0x1, 0x0);
-    auto reader = Reader{Slice{data}};
+    auto reader = BufferReader{Slice{data}};
     EXPECT_EQ(Decode<bool>(reader).Get(), false);
     EXPECT_EQ(Decode<bool>(reader).Get(), true);
     EXPECT_EQ(Decode<bool>(reader).Get(), true);
@@ -93,8 +93,8 @@
 }
 
 TEST(BytesDecoderTest, String) {
-    auto data = Data(0x0, 0x5, 'h', 'e', 'l', 'l', 'o', 0x0, 0x5, 'w', 'o', 'r', 'l', 'd');
-    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    auto data = Data(0x5, 0x0, 'h', 'e', 'l', 'l', 'o', 0x5, 0x0, 'w', 'o', 'r', 'l', 'd');
+    auto reader = BufferReader{Slice{data}};
     EXPECT_EQ(Decode<std::string>(reader).Get(), "hello");
     EXPECT_EQ(Decode<std::string>(reader).Get(), "world");
     EXPECT_FALSE(Decode<std::string>(reader));
@@ -109,11 +109,11 @@
 
 TEST(BytesDecoderTest, ReflectedObject) {
     auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80);
-    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    auto reader = BufferReader{Slice{data}};
     auto got = Decode<S>(reader);
     EXPECT_EQ(got->a, 0x10u);
-    EXPECT_EQ(got->b, 0x2030u);
-    EXPECT_EQ(got->c, 0x40506070u);
+    EXPECT_EQ(got->b, 0x3020u);
+    EXPECT_EQ(got->c, 0x70605040u);
     EXPECT_FALSE(Decode<S>(reader));
 }
 
@@ -124,13 +124,13 @@
                      0x00, 0x50, 0x06, 0x60,  //
                      0x00, 0x70, 0x08, 0x80,  //
                      0x01);
-    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
+    auto reader = BufferReader{Slice{data}};
     auto got = Decode<M>(reader);
     EXPECT_THAT(got.Get(), testing::ContainerEq(M{
-                               std::pair<uint8_t, uint32_t>(0x10u, 0x0220u),
-                               std::pair<uint8_t, uint32_t>(0x30u, 0x0440u),
-                               std::pair<uint8_t, uint32_t>(0x50u, 0x0660u),
-                               std::pair<uint8_t, uint32_t>(0x70u, 0x0880u),
+                               std::pair<uint8_t, uint32_t>(0x10u, 0x2002u),
+                               std::pair<uint8_t, uint32_t>(0x30u, 0x4004u),
+                               std::pair<uint8_t, uint32_t>(0x50u, 0x6006u),
+                               std::pair<uint8_t, uint32_t>(0x70u, 0x8008u),
                            }));
     EXPECT_FALSE(Decode<M>(reader));
 }
@@ -141,8 +141,8 @@
                      0x20, 0x30,              //
                      0x40, 0x50, 0x60, 0x70,  //
                      0x80);
-    auto reader = Reader{Slice{data}, 0, Endianness::kBig};
-    EXPECT_THAT(Decode<T>(reader).Get(), (T{0x10u, 0x2030u, 0x40506070u}));
+    auto reader = BufferReader{Slice{data}};
+    EXPECT_THAT(Decode<T>(reader).Get(), (T{0x10u, 0x3020u, 0x70605040u}));
     EXPECT_FALSE(Decode<T>(reader));
 }
 
diff --git a/src/tint/utils/bytes/reader.cc b/src/tint/utils/bytes/reader.cc
new file mode 100644
index 0000000..42e0176
--- /dev/null
+++ b/src/tint/utils/bytes/reader.cc
@@ -0,0 +1,35 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/bytes/reader.h"
+
+namespace tint::bytes {
+
+Reader::~Reader() = default;
+BufferReader::~BufferReader() = default;
+
+}  // namespace tint::bytes
diff --git a/src/tint/utils/bytes/reader.h b/src/tint/utils/bytes/reader.h
index 38bbf4e..9e58c15 100644
--- a/src/tint/utils/bytes/reader.h
+++ b/src/tint/utils/bytes/reader.h
@@ -36,104 +36,118 @@
 #include "src/tint/utils/bytes/swap.h"
 #include "src/tint/utils/containers/slice.h"
 #include "src/tint/utils/reflection/reflection.h"
+#include "src/tint/utils/result/result.h"
 
 namespace tint::bytes {
 
-/// A binary stream reader.
-struct Reader {
-    /// @returns true if there are no more bytes remaining
-    bool IsEOF() const { return offset >= bytes.len; }
+/// A binary stream reader interface
+class Reader {
+  public:
+    /// Read reads bytes from the stream, blocking until there are @p count bytes available, or the
+    /// end of the stream has been reached.
+    /// @param out a pointer to the byte buffer that will be filled with the read data. Must be at
+    /// least @p count size.
+    /// @param count the number of bytes to read. Must be greater than 0.
+    /// @returns the number of bytes read from the stream. If Read() returns less than @p count,
+    /// then the end of the stream has been reached.
+    virtual size_t Read(std::byte* out, size_t count) = 0;
 
-    /// @returns the number of bytes remaining in the stream
-    size_t BytesRemaining() const { return IsEOF() ? 0 : bytes.len - offset; }
+    // Destructor
+    virtual ~Reader();
 
     /// Reads an integer from the stream, performing byte swapping if the stream's endianness
-    /// differs from the native endianness. If there are too few bytes remaining in the stream, then
-    /// the missing data will be substituted with zeros.
+    /// differs from the native endianness.
+    /// If there are too few bytes remaining in the stream, then a failure is returned.
+    /// @param endianness the encoded endianness of the integer
     /// @return the deserialized integer
     template <typename T>
-    T Int() {
+    Result<T> Int(Endianness endianness = Endianness::kLittle) {
         static_assert(std::is_integral_v<T>);
         T out = 0;
-        if (!IsEOF()) {
-            size_t n = std::min(sizeof(T), BytesRemaining());
-            memcpy(&out, &bytes[offset], n);
-            offset += n;
-            if (NativeEndianness() != endianness) {
-                out = Swap(out);
-            }
+        if (size_t n = Read(reinterpret_cast<std::byte*>(&out), sizeof(T)); n != sizeof(T)) {
+            return Failure{"EOF"};
+        }
+        if (NativeEndianness() != endianness) {
+            out = Swap(out);
         }
         return out;
     }
 
-    /// Reads a float from the stream. If there are too few bytes remaining in the stream, then
-    /// the missing data will be substituted with zeros.
+    /// Reads a float from the stream.
+    /// If there are too few bytes remaining in the stream, then a failure is returned.
     /// @return the deserialized floating point number
     template <typename T>
-    T Float() {
+    Result<T> Float() {
         static_assert(std::is_floating_point_v<T>);
         T out = 0;
-        if (!IsEOF()) {
-            size_t n = std::min(sizeof(T), BytesRemaining());
-            memcpy(&out, &bytes[offset], n);
-            offset += n;
+        if (size_t n = Read(reinterpret_cast<std::byte*>(&out), sizeof(T)); n != sizeof(T)) {
+            return Failure{"EOF"};
         }
         return out;
     }
 
     /// Reads a boolean from the stream
+    /// If there are too few bytes remaining in the stream, then a failure is returned.
     /// @returns true if the next byte is non-zero
-    bool Bool() {
-        if (IsEOF()) {
-            return false;
+    Result<bool> Bool() {
+        std::byte b{0};
+        if (size_t n = Read(&b, 1); n != 1) {
+            return Failure{"EOF"};
         }
-        return bytes[offset++] != std::byte{0};
+        return b != std::byte{0};
     }
 
-    /// Reads a string of @p len bytes from the stream. If there are too few bytes remaining in the
-    /// stream, then the returned string will be truncated.
+    /// Reads a string of @p len bytes from the stream.
+    /// If there are too few bytes remaining in the stream, then a failure is returned.
     /// @param len the length of the returned string in bytes
     /// @return the deserialized string
-    std::string String(size_t len) {
-        if (IsEOF()) {
-            return "";
+    Result<std::string> String(size_t len) {
+        std::string out;
+        out.resize(len);
+        if (size_t n = Read(reinterpret_cast<std::byte*>(out.data()), sizeof(char) * len);
+            n != len) {
+            return Failure{"EOF"};
         }
-        size_t n = std::min(len, BytesRemaining());
-        std::string out(reinterpret_cast<const char*>(&bytes[offset]), n);
-        offset += n;
         return out;
     }
-
-    /// The data to read from
-    Slice<const std::byte> bytes;
-
-    /// The current byte offset
-    size_t offset = 0;
-
-    /// The endianness of integers serialized in the stream
-    Endianness endianness = Endianness::kLittle;
 };
 
-/// Reads the templated type from the reader and assigns it to @p out
-/// @note This function does not
-template <typename T>
-Reader& operator>>(Reader& reader, T& out) {
-    constexpr bool is_numeric = std::is_integral_v<T> || std::is_floating_point_v<T>;
-    static_assert(is_numeric);
+/// BufferReader is an implementation of the Reader interface backed by a buffer.
+class BufferReader final : public Reader {
+  public:
+    // Destructor
+    ~BufferReader() override;
 
-    if constexpr (std::is_integral_v<T>) {
-        out = reader.Int<T>();
-        return reader;
+    /// Constructor
+    /// @param data the data to read from
+    /// @param size the number of bytes in the buffer
+    BufferReader(const std::byte* data, size_t size) : data_(data), bytes_remaining_(size) {
+        TINT_ASSERT(data);
     }
 
-    if constexpr (std::is_floating_point_v<T>) {
-        out = reader.Float<T>();
-        return reader;
+    /// Constructor
+    /// @param slice the byte slice to read from
+    explicit BufferReader(Slice<const std::byte> slice)
+        : data_(slice.data), bytes_remaining_(slice.len) {
+        TINT_ASSERT(slice.data);
     }
 
-    // Unreachable
-    return reader;
-}
+    /// @copydoc Reader::Read
+    size_t Read(std::byte* out, size_t count) override {
+        size_t n = std::min(count, bytes_remaining_);
+        memcpy(out, data_, n);
+        data_ += n;
+        bytes_remaining_ -= n;
+        return n;
+    }
+
+  private:
+    /// The data to read from
+    const std::byte* data_ = nullptr;
+
+    /// The number of bytes remaining
+    size_t bytes_remaining_ = 0;
+};
 
 }  // namespace tint::bytes
 
diff --git a/src/tint/utils/bytes/reader_test.cc b/src/tint/utils/bytes/reader_test.cc
index e8af39d..6b76e22 100644
--- a/src/tint/utils/bytes/reader_test.cc
+++ b/src/tint/utils/bytes/reader_test.cc
@@ -37,70 +37,49 @@
     return std::array{std::byte{static_cast<uint8_t>(args)}...};
 }
 
-TEST(BytesReaderTest, IntegerBigEndian) {
+TEST(BufferReaderTest, IntegerBigEndian) {
     auto data = Data(0x10, 0x20, 0x30, 0x40);
-    auto u32 = Reader{Slice{data}, 0, Endianness::kBig}.Int<uint32_t>();
+    auto u32 = BufferReader{Slice{data}}.Int<uint32_t>(Endianness::kBig);
     EXPECT_EQ(u32, 0x10203040u);
-    auto i32 = Reader{Slice{data}, 0, Endianness::kBig}.Int<int32_t>();
+    auto i32 = BufferReader{Slice{data}}.Int<int32_t>(Endianness::kBig);
     EXPECT_EQ(i32, 0x10203040);
 }
 
-TEST(BytesReaderTest, IntegerBigEndian_Offset) {
-    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60);
-    auto u32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<uint32_t>();
-    EXPECT_EQ(u32, 0x30405060u);
-    auto i32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<int32_t>();
-    EXPECT_EQ(i32, 0x30405060);
+TEST(BufferReaderTest, IntegerBigEndian_TooShort) {
+    auto data = Data(0x10, 0x20);
+    auto u32 = BufferReader{Slice{data}}.Int<uint32_t>(Endianness::kBig);
+    EXPECT_FALSE(u32);
+    auto i32 = BufferReader{Slice{data}}.Int<int32_t>(Endianness::kBig);
+    EXPECT_FALSE(i32);
 }
 
-TEST(BytesReaderTest, IntegerBigEndian_Clipped) {
+TEST(BufferReaderTest, IntegerLittleEndian) {
     auto data = Data(0x10, 0x20, 0x30, 0x40);
-    auto u32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<uint32_t>();
-    EXPECT_EQ(u32, 0x30400000u);
-    auto i32 = Reader{Slice{data}, 2, Endianness::kBig}.Int<int32_t>();
-    EXPECT_EQ(i32, 0x30400000);
-}
-
-TEST(BytesReaderTest, IntegerLittleEndian) {
-    auto data = Data(0x10, 0x20, 0x30, 0x40);
-    auto u32 = Reader{Slice{data}, 0, Endianness::kLittle}.Int<uint32_t>();
+    auto u32 = BufferReader{Slice{data}}.Int<uint32_t>(Endianness::kLittle);
     EXPECT_EQ(u32, 0x40302010u);
-    auto i32 = Reader{Slice{data}, 0, Endianness::kLittle}.Int<int32_t>();
+    auto i32 = BufferReader{Slice{data}}.Int<int32_t>(Endianness::kLittle);
     EXPECT_EQ(i32, 0x40302010);
 }
 
-TEST(BytesReaderTest, IntegerLittleEndian_Offset) {
-    auto data = Data(0x10, 0x20, 0x30, 0x40, 0x50, 0x60);
-    auto u32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<uint32_t>();
-    EXPECT_EQ(u32, 0x60504030u);
-    auto i32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<int32_t>();
-    EXPECT_EQ(i32, 0x60504030);
+TEST(BufferReaderTest, IntegerLittleEndian_TooShort) {
+    auto data = Data(0x30, 0x40);
+    auto u32 = BufferReader{Slice{data}}.Int<uint32_t>(Endianness::kLittle);
+    EXPECT_FALSE(u32);
+    auto i32 = BufferReader{Slice{data}}.Int<int32_t>(Endianness::kLittle);
+    EXPECT_FALSE(i32);
 }
 
-TEST(BytesReaderTest, IntegerLittleEndian_Clipped) {
-    auto data = Data(0x10, 0x20, 0x30, 0x40);
-    auto u32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<uint32_t>();
-    EXPECT_EQ(u32, 0x00004030u);
-    auto i32 = Reader{Slice{data}, 2, Endianness::kLittle}.Int<int32_t>();
-    EXPECT_EQ(i32, 0x00004030);
-}
-
-TEST(BytesReaderTest, Float) {
+TEST(BufferReaderTest, Float) {
     auto data = Data(0x00, 0x00, 0x08, 0x41);
-    float f32 = Reader{Slice{data}}.Float<float>();
-    EXPECT_EQ(f32, 8.5f);
+    auto f32 = BufferReader{Slice{data}}.Float<float>();
+    ASSERT_TRUE(f32);
+    EXPECT_EQ(f32.Get(), 8.5f);
 }
 
-TEST(BytesReaderTest, Float_Offset) {
-    auto data = Data(0x00, 0x00, 0x08, 0x41, 0x80, 0x3e);
-    float f32 = Reader{Slice{data}, 2}.Float<float>();
-    EXPECT_EQ(f32, 0.25049614f);
-}
-
-TEST(BytesReaderTest, Float_Clipped) {
-    auto data = Data(0x00, 0x00, 0x08, 0x41);
-    float f32 = Reader{Slice{data}, 2}.Float<float>();
-    EXPECT_EQ(f32, 2.3329e-41f);
+TEST(BufferReaderTest, Float_TooShort) {
+    auto data = Data(0x08, 0x41);
+    auto f32 = BufferReader{Slice{data}}.Float<float>();
+    EXPECT_FALSE(f32);
 }
 
 }  // namespace
diff --git a/src/tint/utils/macros/compiler.h b/src/tint/utils/macros/compiler.h
index b1fc791..623e4c4 100644
--- a/src/tint/utils/macros/compiler.h
+++ b/src/tint/utils/macros/compiler.h
@@ -45,8 +45,14 @@
 #define TINT_DISABLE_WARNING_WEAK_VTABLES /* currently no-op */
 #define TINT_DISABLE_WARNING_FLOAT_EQUAL  /* currently no-op */
 #define TINT_DISABLE_WARNING_DEPRECATED __pragma(warning(disable : 4996))
-#define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER /* currently no-op */
-#define TINT_DISABLE_WARNING_UNUSED_VALUE        /* currently no-op */
+#define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER       /* currently no-op */
+#define TINT_DISABLE_WARNING_RESERVED_MACRO_IDENTIFIER /* currently no-op */
+#define TINT_DISABLE_WARNING_UNUSED_VALUE              /* currently no-op */
+#define TINT_DISABLE_WARNING_UNUSED_PARAMETER __pragma(warning(disable : 4100))
+#define TINT_DISABLE_WARNING_SHADOW_FIELD_IN_CONSTRUCTOR /* currently no-op */
+#define TINT_DISABLE_WARNING_EXTRA_SEMICOLON             /* currently no-op */
+#define TINT_DISABLE_WARNING_ZERO_AS_NULLPTR             /* currently no-op */
+#define TINT_DISABLE_WARNING_MISSING_DESTRUCTOR_OVERRIDE /* currently no-op */
 
 // clang-format off
 #define TINT_BEGIN_DISABLE_WARNING(name)     \
@@ -58,14 +64,20 @@
     TINT_REQUIRE_SEMICOLON
 // clang-format on
 
+#define TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS() \
+    __pragma(warning(push)) TINT_DISABLE_WARNING_UNUSED_PARAMETER TINT_REQUIRE_SEMICOLON
+#define TINT_END_DISABLE_PROTOBUF_WARNINGS() __pragma(warning(pop)) TINT_REQUIRE_SEMICOLON
+
 #define TINT_UNLIKELY(x) x /* currently no-op */
 #define TINT_LIKELY(x) x   /* currently no-op */
+
 #elif defined(__clang__)
 ////////////////////////////////////////////////////////////////////////////////
 // Clang
 ////////////////////////////////////////////////////////////////////////////////
-#define TINT_DISABLE_WARNING_CONSTANT_OVERFLOW   /* currently no-op */
-#define TINT_DISABLE_WARNING_MAYBE_UNINITIALIZED /* currently no-op */
+#define TINT_DISABLE_WARNING_CONSTANT_OVERFLOW /* currently no-op */
+#define TINT_DISABLE_WARNING_MAYBE_UNINITIALIZED \
+    _Pragma("clang diagnostic ignored \"-Wconditional-uninitialized\"")
 #define TINT_DISABLE_WARNING_NEWLINE_EOF _Pragma("clang diagnostic ignored \"-Wnewline-eof\"")
 #define TINT_DISABLE_WARNING_OLD_STYLE_CAST _Pragma("clang diagnostic ignored \"-Wold-style-cast\"")
 #define TINT_DISABLE_WARNING_SIGN_CONVERSION \
@@ -76,13 +88,45 @@
 #define TINT_DISABLE_WARNING_DEPRECATED /* currently no-op */
 #define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER \
     _Pragma("clang diagnostic ignored \"-Wreserved-identifier\"")
+#define TINT_DISABLE_WARNING_RESERVED_MACRO_IDENTIFIER \
+    _Pragma("clang diagnostic ignored \"-Wreserved-macro-identifier\"")
 #define TINT_DISABLE_WARNING_UNUSED_VALUE _Pragma("clang diagnostic ignored \"-Wunused-value\"")
+#define TINT_DISABLE_WARNING_UNUSED_PARAMETER \
+    _Pragma("clang diagnostic ignored \"-Wunused-parameter\"")
+#define TINT_DISABLE_WARNING_SHADOW_FIELD_IN_CONSTRUCTOR \
+    _Pragma("clang diagnostic ignored \"-Wshadow-field-in-constructor\"")
+#define TINT_DISABLE_WARNING_EXTRA_SEMICOLON \
+    _Pragma("clang diagnostic ignored \"-Wextra-semi-stmt\"")
+#define TINT_DISABLE_WARNING_ZERO_AS_NULLPTR \
+    _Pragma("clang diagnostic ignored \"-Wzero-as-null-pointer-constant\"")
+#define TINT_DISABLE_WARNING_MISSING_DESTRUCTOR_OVERRIDE                                   \
+    _Pragma("clang diagnostic ignored \"-Wsuggest-destructor-override\"")                  \
+        _Pragma("clang diagnostic ignored \"-Winconsistent-missing-destructor-override\"")
 
 // clang-format off
+#define TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS()        \
+    _Pragma("clang diagnostic push")                  \
+    TINT_DISABLE_WARNING_EXTRA_SEMICOLON              \
+    TINT_DISABLE_WARNING_MAYBE_UNINITIALIZED          \
+    TINT_DISABLE_WARNING_MISSING_DESTRUCTOR_OVERRIDE  \
+    TINT_DISABLE_WARNING_RESERVED_IDENTIFIER          \
+    TINT_DISABLE_WARNING_RESERVED_MACRO_IDENTIFIER    \
+    TINT_DISABLE_WARNING_SHADOW_FIELD_IN_CONSTRUCTOR  \
+    TINT_DISABLE_WARNING_SIGN_CONVERSION              \
+    TINT_DISABLE_WARNING_UNUSED_PARAMETER             \
+    TINT_DISABLE_WARNING_WEAK_VTABLES                 \
+    TINT_DISABLE_WARNING_ZERO_AS_NULLPTR              \
+    TINT_REQUIRE_SEMICOLON
+
+#define TINT_END_DISABLE_PROTOBUF_WARNINGS() \
+    _Pragma("clang diagnostic pop")          \
+    TINT_REQUIRE_SEMICOLON
+
 #define TINT_BEGIN_DISABLE_WARNING(name)     \
     _Pragma("clang diagnostic push")         \
     TINT_CONCAT(TINT_DISABLE_WARNING_, name) \
     TINT_REQUIRE_SEMICOLON
+
 #define TINT_END_DISABLE_WARNING(name)       \
     _Pragma("clang diagnostic pop")          \
     TINT_REQUIRE_SEMICOLON
@@ -97,15 +141,26 @@
 #define TINT_DISABLE_WARNING_CONSTANT_OVERFLOW /* currently no-op */
 #define TINT_DISABLE_WARNING_MAYBE_UNINITIALIZED \
     _Pragma("GCC diagnostic ignored \"-Wmaybe-uninitialized\"")
-#define TINT_DISABLE_WARNING_NEWLINE_EOF         /* currently no-op */
-#define TINT_DISABLE_WARNING_OLD_STYLE_CAST      /* currently no-op */
-#define TINT_DISABLE_WARNING_SIGN_CONVERSION     /* currently no-op */
-#define TINT_DISABLE_WARNING_UNREACHABLE_CODE    /* currently no-op */
-#define TINT_DISABLE_WARNING_WEAK_VTABLES        /* currently no-op */
-#define TINT_DISABLE_WARNING_FLOAT_EQUAL         /* currently no-op */
-#define TINT_DISABLE_WARNING_DEPRECATED          /* currently no-op */
-#define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER /* currently no-op */
+#define TINT_DISABLE_WARNING_NEWLINE_EOF               /* currently no-op */
+#define TINT_DISABLE_WARNING_OLD_STYLE_CAST            /* currently no-op */
+#define TINT_DISABLE_WARNING_SIGN_CONVERSION           /* currently no-op */
+#define TINT_DISABLE_WARNING_UNREACHABLE_CODE          /* currently no-op */
+#define TINT_DISABLE_WARNING_WEAK_VTABLES              /* currently no-op */
+#define TINT_DISABLE_WARNING_FLOAT_EQUAL               /* currently no-op */
+#define TINT_DISABLE_WARNING_DEPRECATED                /* currently no-op */
+#define TINT_DISABLE_WARNING_RESERVED_IDENTIFIER       /* currently no-op */
+#define TINT_DISABLE_WARNING_RESERVED_MACRO_IDENTIFIER /* currently no-op */
 #define TINT_DISABLE_WARNING_UNUSED_VALUE _Pragma("GCC diagnostic ignored \"-Wunused-value\"")
+#define TINT_DISABLE_WARNING_UNUSED_PARAMETER \
+    _Pragma("GCC diagnostic ignored \"-Wunused-parameter\"")
+#define TINT_DISABLE_WARNING_SHADOW_FIELD_IN_CONSTRUCTOR /* currently no-op */
+#define TINT_DISABLE_WARNING_EXTRA_SEMICOLON             /* currently no-op */
+#define TINT_DISABLE_WARNING_ZERO_AS_NULLPTR             /* currently no-op */
+#define TINT_DISABLE_WARNING_MISSING_DESTRUCTOR_OVERRIDE /* currently no-op */
+
+#define TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS() \
+    _Pragma("GCC diagnostic push") TINT_DISABLE_WARNING_UNUSED_PARAMETER TINT_REQUIRE_SEMICOLON
+#define TINT_END_DISABLE_PROTOBUF_WARNINGS() _Pragma("GCC diagnostic pop") TINT_REQUIRE_SEMICOLON
 
 // clang-format off
 #define TINT_BEGIN_DISABLE_WARNING(name)     \
@@ -125,6 +180,8 @@
 ////////////////////////////////////////////////////////////////////////////////
 #define TINT_BEGIN_DISABLE_WARNING(name) TINT_REQUIRE_SEMICOLON
 #define TINT_END_DISABLE_WARNING(name) TINT_REQUIRE_SEMICOLON
+#define TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS() TINT_REQUIRE_SEMICOLON
+#define TINT_END_DISABLE_PROTOBUF_WARNINGS() TINT_REQUIRE_SEMICOLON
 #define TINT_UNLIKELY(x) x
 #define TINT_LIKELY(x) x
 
diff --git a/third_party/protobuf.cmake b/third_party/protobuf.cmake
new file mode 100644
index 0000000..2d060d3
--- /dev/null
+++ b/third_party/protobuf.cmake
@@ -0,0 +1,176 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+set(protobuf_INSTALL OFF CACHE BOOL "Install protobuf binaries and files" FORCE)
+set(protobuf_BUILD_CONFORMANCE OFF CACHE BOOL "Build conformance tests" FORCE)
+set(protobuf_BUILD_EXAMPLES OFF CACHE BOOL "Build examples" FORCE)
+set(protobuf_BUILD_LIBPROTOC OFF CACHE BOOL "Build libprotoc" FORCE)
+set(protobuf_BUILD_TESTS OFF CACHE BOOL "Controls whether protobuf tests are built" FORCE)
+set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "Controls whether a protobuf static runtime is built" FORCE)
+
+set(protobuf_BUILD_PROTOC_BINARIES ON CACHE BOOL "Build libprotoc and protoc compiler" FORCE)
+set(protobuf_DISABLE_RTTI ON CACHE BOOL "Remove runtime type information in the binaries" FORCE)
+
+add_subdirectory("${TINT_PROTOBUF_DIR}/cmake")
+
+target_compile_definitions(libprotobuf PUBLIC "-DGOOGLE_PROTOBUF_INTERNAL_DONATE_STEAL_INLINE=0")
+
+# A simplified version of protobuf_generate()
+function(generate_protos)
+  set(OPTIONS APPEND_PATH)
+  set(SINGLE_ARGS TARGET LANGUAGE EXPORT_MACRO PROTOC_OUT_DIR PLUGIN PLUGIN_OPTIONS)
+  set(MULTI_ARGS IMPORT_DIRS GENERATE_EXTENSIONS PROTOC_OPTIONS)
+  cmake_parse_arguments(ARGS "${OPTIONS}" "${SINGLE_ARGS}" "${MULTI_ARGS}" "${ARGN}")
+
+  if(NOT ARGS_TARGET)
+    message(FATAL_ERROR "generate_protos called without a target")
+  endif()
+
+  if(NOT ARGS_LANGUAGE)
+    set(ARGS_LANGUAGE cpp)
+  endif()
+  string(TOLOWER ${ARGS_LANGUAGE} ARGS_LANGUAGE)
+
+  if(NOT ARGS_PROTOC_OUT_DIR)
+    set(ARGS_PROTOC_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR})
+  endif()
+
+  foreach(OPTION ${ARGS_PLUGIN_OPTIONS})
+    # append comma - not using CMake lists and string replacement as users
+    # might have semicolons in options
+    if(PLUGIN_OPTIONS)
+      set( PLUGIN_OPTIONS "${PLUGIN_OPTIONS},")
+    endif()
+    set(PLUGIN_OPTIONS "${PLUGIN_OPTIONS}${OPTION}")
+  endforeach()
+
+  if(ARGS_PLUGIN)
+      set(_plugin "--plugin=${ARGS_PLUGIN}")
+  endif()
+
+  if(NOT ARGS_GENERATE_EXTENSIONS)
+    if(ARGS_LANGUAGE STREQUAL cpp)
+      set(ARGS_GENERATE_EXTENSIONS .pb.h .pb.cc)
+    elseif(ARGS_LANGUAGE STREQUAL python)
+      set(ARGS_GENERATE_EXTENSIONS _pb2.py)
+    else()
+      message(FATAL_ERROR "generate_protos given unknown Language ${LANGUAGE}, please provide a value for GENERATE_EXTENSIONS")
+    endif()
+  endif()
+
+  if(ARGS_TARGET)
+    get_target_property(SOURCE_LIST ${ARGS_TARGET} SOURCES)
+    foreach(FILE ${SOURCE_LIST})
+      if(FILE MATCHES ".proto$")
+        list(APPEND PROTO_FILES ${FILE})
+      endif()
+    endforeach()
+  endif()
+
+  if(NOT PROTO_FILES)
+    message(FATAL_ERROR "generate_protos could not find any .proto files")
+  endif()
+
+  if(ARGS_APPEND_PATH)
+    # Create an include path for each file specified
+    foreach(FILE ${PROTO_FILES})
+      get_filename_component(ABS_FILE ${FILE} ABSOLUTE)
+      get_filename_component(ABS_PATH ${ABS_FILE} PATH)
+      list(FIND PROTOBUF_INCLUDE_PATH ${ABS_PATH} FOUND)
+      if(${FOUND} EQUAL -1)
+          list(APPEND PROTOBUF_INCLUDE_PATH -I ${ABS_PATH})
+      endif()
+    endforeach()
+  endif()
+
+  foreach(DIR ${ARGS_IMPORT_DIRS})
+    get_filename_component(ABS_PATH ${DIR} ABSOLUTE)
+    list(FIND PROTOBUF_INCLUDE_PATH ${ABS_PATH} FOUND)
+    if(${FOUND} EQUAL -1)
+        list(APPEND PROTOBUF_INCLUDE_PATH -I ${ABS_PATH})
+    endif()
+  endforeach()
+
+  if(NOT PROTOBUF_INCLUDE_PATH)
+    set(PROTOBUF_INCLUDE_PATH -I ${CMAKE_CURRENT_SOURCE_DIR})
+  endif()
+
+  set(ALL_GENERATED_SRCS)
+  foreach(PROTO_FILE ${PROTO_FILES})
+    get_filename_component(ABS_FILE ${PROTO_FILE} ABSOLUTE)
+    get_filename_component(ABS_DIR ${ABS_FILE} DIRECTORY)
+
+    get_filename_component(FILE_FULL_NAME ${PROTO_FILE} NAME)
+    string(FIND "${FILE_FULL_NAME}" "." FILE_LAST_EXT_POS REVERSE)
+    string(SUBSTRING "${FILE_FULL_NAME}" 0 ${FILE_LAST_EXT_POS} BASENAME)
+
+    set(SUITABLE_INCLUDE_FOUND FALSE)
+    foreach(DIR ${PROTOBUF_INCLUDE_PATH})
+      if(NOT DIR STREQUAL "-I")
+        file(RELATIVE_PATH REL_DIR ${DIR} ${ABS_DIR})
+        string(FIND "${REL_DIR}" "../" IS_IN_PARENT_FOLDER)
+        if (NOT ${IS_IN_PARENT_FOLDER} EQUAL 0)
+          set(SUITABLE_INCLUDE_FOUND TRUE)
+          break()
+        endif()
+      endif()
+    endforeach()
+
+    if(NOT SUITABLE_INCLUDE_FOUND)
+      message(FATAL_ERROR "generate_protos could not find any correct proto include directory.")
+    endif()
+
+    set(GENERATED_SRCS)
+    foreach(EXT ${ARGS_GENERATE_EXTENSIONS})
+      list(APPEND GENERATED_SRCS "${ARGS_PROTOC_OUT_DIR}/${REL_DIR}/${BASENAME}${EXT}")
+    endforeach()
+    list(APPEND ALL_GENERATED_SRCS ${GENERATED_SRCS})
+
+    set(COMMENT "Running ${ARGS_LANGUAGE} protocol buffer compiler on ${PROTO_FILE}")
+    if(ARGS_PROTOC_OPTIONS)
+      set(COMMENT "${COMMENT}, protoc-options: ${ARGS_PROTOC_OPTIONS}")
+    endif()
+    if(PLUGIN_OPTIONS)
+      set(COMMENT "${COMMENT}, plugin-options: ${PLUGIN_OPTIONS}")
+    endif()
+
+    file(MAKE_DIRECTORY "${ARGS_PROTOC_OUT_DIR}/${REL_DIR}")
+
+    add_custom_command(
+      OUTPUT ${GENERATED_SRCS}
+      COMMAND protobuf::protoc
+      ARGS ${ARGS_PROTOC_OPTIONS} --${ARGS_LANGUAGE}_out ${_plugin_options}:${ARGS_PROTOC_OUT_DIR} ${_plugin} ${PROTOBUF_INCLUDE_PATH} ${ABS_FILE}
+      DEPENDS ${ABS_FILE} protobuf::protoc
+      COMMENT ${COMMENT}
+      VERBATIM)
+  endforeach()
+
+  set_source_files_properties(${ALL_GENERATED_SRCS} PROPERTIES GENERATED TRUE)
+  if(ARGS_TARGET)
+    target_sources(${ARGS_TARGET} PRIVATE ${ALL_GENERATED_SRCS})
+  endif()
+endfunction()