Import Tint changes from Dawn

Changes:
  - 6488e26e6b3742a78e3918d8000a82e2d325c94c [tint][fuzz][wgsl] Add Raise fuzzer by Ben Clayton <bclayton@google.com>
  - 50faf095a595281feb00d7cd05f225d46a9996fa [tint][fuzz][wgsl] Add PtrToRef fuzzer by Ben Clayton <bclayton@google.com>
  - 2a8c10e51d79ec119dca7475137c28b6f7d5b4b7 [tint][fuzz][wgsl] Add RenameConflicts fuzzer by Ben Clayton <bclayton@google.com>
  - 24aa34ee3fe4621a5be5cf0efeb3ced443f510f1 [tint][fuzz][wgsl] Add ValueToLet fuzzer by Ben Clayton <bclayton@google.com>
  - d07a2002494ccc6ece8bbeb13814d3d98e4ab75c [spirv] Fix dynamic indexes into constant arrays by James Price <jrprice@google.com>
  - 4f491bf8c370e4975672b3f5abd0f1de4a95f12e [ir] Validate block parameters by James Price <jrprice@google.com>
  - 750f71d3234ac9d49e175d5e7558b052e6b7519b [tint][ir] Track owning block in BlockParam by James Price <jrprice@google.com>
  - e5380b752f0f1f9d5950710af8cad0191fcd8122 [ir] Validate function parameters by James Price <jrprice@google.com>
  - 21517e4b5ff7944caf09200658e7444f5232cccb [ir] Add source map for a function and its params by James Price <jrprice@google.com>
  - e5fff2277474fbdc1b3d696c69346b088fc098aa [tint][ir] Track owning Function in FunctionParam by James Price <jrprice@google.com>
  - b0e445c0a103a80d530c802a223a395e8782146a [tint][IRToProgram] Create phony assignment if a value is... by Ben Clayton <bclayton@google.com>
  - 304e57a5b2863383ba037fb42bfd26162da8e37b Restore "[tint][fuzz] Enable AllowedFeatures::Everything()" by Ben Clayton <bclayton@google.com>
  - 83f6d0d2d3f6f1af440347fffd4f184c32ca6e47 [tint][hlsl] Don't ICE if the PixelLocal transform is mis... by Ben Clayton <bclayton@google.com>
  - a871f650869cecb5dadae4abb30fb28bc7ef5f76 [tint][ir] Correctly `enable chromium_internal_graphite;` by Ben Clayton <bclayton@google.com>
  - f7c7b93ba92915bd75805e67681dc6310d453b0d [tint][ir] Ensure all enumerators are serialized by Ben Clayton <bclayton@google.com>
  - ddd45561455cda0def30fcabf6748a89a44fd23c [tools][utils] Split BufferReader out to separate files by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 6488e26e6b3742a78e3918d8000a82e2d325c94c
Change-Id: I4b4c0b7c6d46e996f6ee675f665976346d713e39
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/185801
Reviewed-by: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/tint/cmd/fuzz/ir/fuzz.h b/src/tint/cmd/fuzz/ir/fuzz.h
index 533cac4..18ffdce 100644
--- a/src/tint/cmd/fuzz/ir/fuzz.h
+++ b/src/tint/cmd/fuzz/ir/fuzz.h
@@ -33,6 +33,7 @@
 #include <tuple>
 #include <utility>
 
+#include "src/tint/utils/bytes/buffer_reader.h"
 #include "src/tint/utils/bytes/decoder.h"
 #include "src/tint/utils/containers/slice.h"
 #include "src/tint/utils/macros/static_init.h"
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.cmake b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
index d51bd4d..350a170 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.cmake
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.cmake
@@ -55,6 +55,7 @@
   tint_lang_wgsl_program
   tint_lang_wgsl_program_fuzz
   tint_lang_wgsl_sem
+  tint_lang_wgsl_writer_raise_fuzz
   tint_lang_wgsl_fuzz
   tint_utils_bytes
   tint_utils_cli
diff --git a/src/tint/cmd/fuzz/wgsl/BUILD.gn b/src/tint/cmd/fuzz/wgsl/BUILD.gn
index 389a98e..89d7ea9 100644
--- a/src/tint/cmd/fuzz/wgsl/BUILD.gn
+++ b/src/tint/cmd/fuzz/wgsl/BUILD.gn
@@ -98,6 +98,7 @@
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/program:fuzz",
       "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/lang/wgsl/writer/raise:fuzz",
       "${tint_src_dir}/utils/bytes",
       "${tint_src_dir}/utils/cli",
       "${tint_src_dir}/utils/containers",
diff --git a/src/tint/cmd/fuzz/wgsl/fuzz.cc b/src/tint/cmd/fuzz/wgsl/fuzz.cc
index 6e4d9b8..23bbe0f 100644
--- a/src/tint/cmd/fuzz/wgsl/fuzz.cc
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.cc
@@ -30,6 +30,8 @@
 #include <iostream>
 #include <thread>
 
+#include "src/tint/lang/wgsl/common/allowed_features.h"
+#include "src/tint/lang/wgsl/reader/options.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/macros/defer.h"
@@ -87,8 +89,14 @@
     tint::Source::File file("test.wgsl", wgsl);
 
     // Parse the WGSL program.
-    auto program = tint::wgsl::reader::Parse(&file);
+    tint::wgsl::reader::Options parse_options;
+    parse_options.allowed_features = tint::wgsl::AllowedFeatures::Everything();
+    auto program = tint::wgsl::reader::Parse(&file, parse_options);
     if (!program.IsValid()) {
+        if (options.verbose) {
+            std::cerr << "invalid WGSL program: " << std::endl
+                      << program.Diagnostics() << std::endl;
+        }
         return;
     }
 
diff --git a/src/tint/cmd/fuzz/wgsl/fuzz.h b/src/tint/cmd/fuzz/wgsl/fuzz.h
index c01455b..e4bc7a0 100644
--- a/src/tint/cmd/fuzz/wgsl/fuzz.h
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.h
@@ -33,6 +33,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/utils/bytes/buffer_reader.h"
 #include "src/tint/utils/bytes/decoder.h"
 #include "src/tint/utils/containers/slice.h"
 #include "src/tint/utils/macros/static_init.h"
diff --git a/src/tint/lang/core/ir/binary/decode.cc b/src/tint/lang/core/ir/binary/decode.cc
index 825be15..5fbb84d 100644
--- a/src/tint/lang/core/ir/binary/decode.cc
+++ b/src/tint/lang/core/ir/binary/decode.cc
@@ -196,10 +196,13 @@
                 return Function::PipelineStage::kFragment;
             case pb::PipelineStage::Vertex:
                 return Function::PipelineStage::kVertex;
-            default:
-                TINT_ICE() << "unhandled PipelineStage: " << stage;
-                return Function::PipelineStage::kCompute;
+
+            case pb::PipelineStage::PipelineStage_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::PipelineStage::PipelineStage_INT_MAX_SENTINEL_DO_NOT_USE_:
+                break;
         }
+        TINT_ICE() << "unhandled PipelineStage: " << stage;
+        return Function::PipelineStage::kCompute;
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -322,8 +325,7 @@
             case pb::Instruction::KindCase::kUnreachable:
                 inst_out = CreateInstructionUnreachable(inst_in.unreachable());
                 break;
-            default:
-                TINT_UNIMPLEMENTED() << inst_in.kind_case();
+            case pb::Instruction::KindCase::KIND_NOT_SET:
                 break;
         }
         TINT_ASSERT_OR_RETURN_VALUE(inst_out, nullptr);
@@ -550,7 +552,7 @@
                 return CreateTypeExternalTexture(type_in.external_texture());
             case pb::Type::KindCase::kSampler:
                 return CreateTypeSampler(type_in.sampler());
-            default:
+            case pb::Type::KindCase::KIND_NOT_SET:
                 break;
         }
         TINT_ICE() << type_in.kind_case();
@@ -571,10 +573,13 @@
                 return mod_out_.Types().Get<f32>();
             case pb::TypeBasic::f16:
                 return mod_out_.Types().Get<f16>();
-            default:
-                TINT_ICE() << "invalid TypeBasic: " << basic_in;
-                return nullptr;
+
+            case pb::TypeBasic::TypeBasic_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::TypeBasic::TypeBasic_INT_MAX_SENTINEL_DO_NOT_USE_:
+                break;
         }
+        TINT_ICE() << "invalid TypeBasic: " << basic_in;
+        return nullptr;
     }
 
     const type::Vector* CreateTypeVector(const pb::TypeVector& vector_in) {
@@ -712,10 +717,15 @@
             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;
+            case pb::Value::KindCase::KIND_NOT_SET:
+                break;
         }
+
+        if (!value_out) {
+            TINT_ICE() << "invalid TypeDecl.kind: " << value_in.kind_case();
+            return nullptr;
+        }
+
         return value_out;
     }
 
@@ -788,10 +798,11 @@
                 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;
+            case pb::ConstantValue::KindCase::KIND_NOT_SET:
+                break;
         }
+        TINT_ICE() << "invalid ConstantValue.kind: " << value_in.kind_case();
+        return nullptr;
     }
 
     const core::constant::Value* CreateConstantScalar(const pb::ConstantValueScalar& value_in) {
@@ -806,10 +817,11 @@
                 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;
+            case pb::ConstantValueScalar::KindCase::KIND_NOT_SET:
+                break;
         }
+        TINT_ICE() << "invalid ConstantValueScalar.kind: " << value_in.kind_case();
+        return nullptr;
     }
 
     const core::constant::Value* CreateConstantComposite(
@@ -874,10 +886,13 @@
                 return core::AddressSpace::kUniform;
             case pb::AddressSpace::workgroup:
                 return core::AddressSpace::kWorkgroup;
-            default:
-                TINT_ICE() << "invalid AddressSpace: " << in;
-                return core::AddressSpace::kUndefined;
+
+            case pb::AddressSpace::AddressSpace_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::AddressSpace::AddressSpace_INT_MAX_SENTINEL_DO_NOT_USE_:
+                break;
         }
+        TINT_ICE() << "invalid AddressSpace: " << in;
+        return core::AddressSpace::kUndefined;
     }
 
     core::Access AccessControl(pb::AccessControl in) {
@@ -888,10 +903,13 @@
                 return core::Access::kWrite;
             case pb::AccessControl::read_write:
                 return core::Access::kReadWrite;
-            default:
-                TINT_ICE() << "invalid Access: " << in;
-                return core::Access::kUndefined;
+
+            case pb::AccessControl::AccessControl_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::AccessControl::AccessControl_INT_MAX_SENTINEL_DO_NOT_USE_:
+                break;
         }
+        TINT_ICE() << "invalid Access: " << in;
+        return core::Access::kUndefined;
     }
 
     core::UnaryOp UnaryOp(pb::UnaryOp in) {
@@ -907,10 +925,12 @@
             case pb::UnaryOp::not_:
                 return core::UnaryOp::kNot;
 
-            default:
-                TINT_ICE() << "invalid UnaryOp: " << in;
-                return core::UnaryOp::kComplement;
+            case pb::UnaryOp::UnaryOp_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::UnaryOp::UnaryOp_INT_MAX_SENTINEL_DO_NOT_USE_:
+                break;
         }
+        TINT_ICE() << "invalid UnaryOp: " << in;
+        return core::UnaryOp::kComplement;
     }
 
     core::BinaryOp BinaryOp(pb::BinaryOp in) {
@@ -947,11 +967,17 @@
                 return core::BinaryOp::kShiftLeft;
             case pb::BinaryOp::shift_right:
                 return core::BinaryOp::kShiftRight;
+            case pb::BinaryOp::logical_and:
+                return core::BinaryOp::kLogicalAnd;
+            case pb::BinaryOp::logical_or:
+                return core::BinaryOp::kLogicalOr;
 
-            default:
-                TINT_ICE() << "invalid BinaryOp: " << in;
-                return core::BinaryOp::kAdd;
+            case pb::BinaryOp::BinaryOp_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::BinaryOp::BinaryOp_INT_MAX_SENTINEL_DO_NOT_USE_:
+                break;
         }
+        TINT_ICE() << "invalid BinaryOp: " << in;
+        return core::BinaryOp::kAdd;
     }
 
     core::type::TextureDimension TextureDimension(pb::TextureDimension in) {
@@ -968,7 +994,9 @@
                 return core::type::TextureDimension::kCube;
             case pb::TextureDimension::cube_array:
                 return core::type::TextureDimension::kCubeArray;
-            default:
+
+            case pb::TextureDimension::TextureDimension_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::TextureDimension::TextureDimension_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
 
@@ -980,6 +1008,8 @@
         switch (in) {
             case pb::TexelFormat::bgra8_unorm:
                 return core::TexelFormat::kBgra8Unorm;
+            case pb::TexelFormat::r8_unorm:
+                return core::TexelFormat::kR8Unorm;
             case pb::TexelFormat::r32_float:
                 return core::TexelFormat::kR32Float;
             case pb::TexelFormat::r32_sint:
@@ -1012,7 +1042,9 @@
                 return core::TexelFormat::kRgba8Uint;
             case pb::TexelFormat::rgba8_unorm:
                 return core::TexelFormat::kRgba8Unorm;
-            default:
+
+            case pb::TexelFormat::TexelFormat_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::TexelFormat::TexelFormat_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
 
@@ -1026,7 +1058,9 @@
                 return core::type::SamplerKind::kSampler;
             case pb::SamplerKind::comparison:
                 return core::type::SamplerKind::kComparisonSampler;
-            default:
+
+            case pb::SamplerKind::SamplerKind_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::SamplerKind::SamplerKind_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
 
@@ -1042,7 +1076,9 @@
                 return core::InterpolationType::kLinear;
             case pb::InterpolationType::perspective:
                 return core::InterpolationType::kPerspective;
-            default:
+
+            case pb::InterpolationType::InterpolationType_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::InterpolationType::InterpolationType_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
         TINT_ICE() << "invalid InterpolationType: " << in;
@@ -1057,7 +1093,9 @@
                 return core::InterpolationSampling::kCentroid;
             case pb::InterpolationSampling::sample:
                 return core::InterpolationSampling::kSample;
-            default:
+
+            case pb::InterpolationSampling::InterpolationSampling_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::InterpolationSampling::InterpolationSampling_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
         TINT_ICE() << "invalid InterpolationSampling: " << in;
@@ -1096,7 +1134,9 @@
                 return core::BuiltinValue::kVertexIndex;
             case pb::BuiltinValue::workgroup_id:
                 return core::BuiltinValue::kWorkgroupId;
-            default:
+
+            case pb::BuiltinValue::BuiltinValue_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::BuiltinValue::BuiltinValue_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
         TINT_ICE() << "invalid BuiltinValue: " << in;
@@ -1225,6 +1265,14 @@
                 return core::BuiltinFn::kPack4X8Snorm;
             case pb::BuiltinFn::pack4x8_unorm:
                 return core::BuiltinFn::kPack4X8Unorm;
+            case pb::BuiltinFn::pack4xi8:
+                return core::BuiltinFn::kPack4XI8;
+            case pb::BuiltinFn::pack4xu8:
+                return core::BuiltinFn::kPack4XU8;
+            case pb::BuiltinFn::pack4xi8_clamp:
+                return core::BuiltinFn::kPack4XI8Clamp;
+            case pb::BuiltinFn::pack4xu8_clamp:
+                return core::BuiltinFn::kPack4XU8Clamp;
             case pb::BuiltinFn::pow:
                 return core::BuiltinFn::kPow;
             case pb::BuiltinFn::quantize_to_f16:
@@ -1275,6 +1323,10 @@
                 return core::BuiltinFn::kUnpack4X8Snorm;
             case pb::BuiltinFn::unpack4x8_unorm:
                 return core::BuiltinFn::kUnpack4X8Unorm;
+            case pb::BuiltinFn::unpack4xi8:
+                return core::BuiltinFn::kUnpack4XI8;
+            case pb::BuiltinFn::unpack4xu8:
+                return core::BuiltinFn::kUnpack4XU8;
             case pb::BuiltinFn::workgroup_barrier:
                 return core::BuiltinFn::kWorkgroupBarrier;
             case pb::BuiltinFn::texture_barrier:
@@ -1335,7 +1387,9 @@
                 return core::BuiltinFn::kSubgroupBallot;
             case pb::BuiltinFn::subgroup_broadcast:
                 return core::BuiltinFn::kSubgroupBroadcast;
-            default:
+
+            case pb::BuiltinFn::BuiltinFn_INT_MIN_SENTINEL_DO_NOT_USE_:
+            case pb::BuiltinFn::BuiltinFn_INT_MAX_SENTINEL_DO_NOT_USE_:
                 break;
         }
         TINT_ICE() << "invalid BuiltinFn: " << in;
diff --git a/src/tint/lang/core/ir/binary/encode.cc b/src/tint/lang/core/ir/binary/encode.cc
index ef364ce..562a568 100644
--- a/src/tint/lang/core/ir/binary/encode.cc
+++ b/src/tint/lang/core/ir/binary/encode.cc
@@ -162,10 +162,11 @@
                 return pb::PipelineStage::Fragment;
             case Function::PipelineStage::kVertex:
                 return pb::PipelineStage::Vertex;
-            default:
-                TINT_ICE() << "unhandled PipelineStage: " << stage;
-                return pb::PipelineStage::Compute;
+            case Function::PipelineStage::kUndefined:
+                break;
         }
+        TINT_ICE() << "unhandled PipelineStage: " << stage;
+        return pb::PipelineStage::Compute;
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -655,10 +656,14 @@
                 return pb::AddressSpace::uniform;
             case core::AddressSpace::kWorkgroup:
                 return pb::AddressSpace::workgroup;
-            default:
-                TINT_ICE() << "invalid AddressSpace: " << in;
-                return pb::AddressSpace::function;
+
+            case core::AddressSpace::kUndefined:
+            case core::AddressSpace::kIn:
+            case core::AddressSpace::kOut:
+                break;
         }
+        TINT_ICE() << "invalid AddressSpace: " << in;
+        return pb::AddressSpace::function;
     }
 
     pb::AccessControl AccessControl(core::Access in) {
@@ -669,10 +674,11 @@
                 return pb::AccessControl::write;
             case core::Access::kReadWrite:
                 return pb::AccessControl::read_write;
-            default:
-                TINT_ICE() << "invalid Access: " << in;
-                return pb::AccessControl::read;
+            case core::Access::kUndefined:
+                break;
         }
+        TINT_ICE() << "invalid Access: " << in;
+        return pb::AccessControl::read;
     }
 
     pb::UnaryOp UnaryOp(core::UnaryOp in) {
@@ -750,7 +756,7 @@
                 return pb::TextureDimension::cube;
             case core::type::TextureDimension::kCubeArray:
                 return pb::TextureDimension::cube_array;
-            default:
+            case core::type::TextureDimension::kNone:
                 break;
         }
 
@@ -768,6 +774,8 @@
                 return pb::TexelFormat::r32_sint;
             case core::TexelFormat::kR32Uint:
                 return pb::TexelFormat::r32_uint;
+            case core::TexelFormat::kR8Unorm:
+                return pb::TexelFormat::r8_unorm;
             case core::TexelFormat::kRg32Float:
                 return pb::TexelFormat::rg32_float;
             case core::TexelFormat::kRg32Sint:
@@ -794,7 +802,7 @@
                 return pb::TexelFormat::rgba8_uint;
             case core::TexelFormat::kRgba8Unorm:
                 return pb::TexelFormat::rgba8_unorm;
-            default:
+            case core::TexelFormat::kUndefined:
                 break;
         }
 
@@ -822,7 +830,7 @@
                 return pb::InterpolationType::linear;
             case core::InterpolationType::kPerspective:
                 return pb::InterpolationType::perspective;
-            default:
+            case core::InterpolationType::kUndefined:
                 break;
         }
         TINT_ICE() << "invalid InterpolationType: " << in;
@@ -837,7 +845,7 @@
                 return pb::InterpolationSampling::centroid;
             case core::InterpolationSampling::kSample:
                 return pb::InterpolationSampling::sample;
-            default:
+            case core::InterpolationSampling::kUndefined:
                 break;
         }
         TINT_ICE() << "invalid InterpolationSampling: " << in;
@@ -876,7 +884,7 @@
                 return pb::BuiltinValue::vertex_index;
             case core::BuiltinValue::kWorkgroupId:
                 return pb::BuiltinValue::workgroup_id;
-            default:
+            case core::BuiltinValue::kUndefined:
                 break;
         }
         TINT_ICE() << "invalid BuiltinValue: " << in;
@@ -1005,6 +1013,14 @@
                 return pb::BuiltinFn::pack4x8_snorm;
             case core::BuiltinFn::kPack4X8Unorm:
                 return pb::BuiltinFn::pack4x8_unorm;
+            case core::BuiltinFn::kPack4XI8:
+                return pb::BuiltinFn::pack4xi8;
+            case core::BuiltinFn::kPack4XU8:
+                return pb::BuiltinFn::pack4xu8;
+            case core::BuiltinFn::kPack4XI8Clamp:
+                return pb::BuiltinFn::pack4xi8_clamp;
+            case core::BuiltinFn::kPack4XU8Clamp:
+                return pb::BuiltinFn::pack4xu8_clamp;
             case core::BuiltinFn::kPow:
                 return pb::BuiltinFn::pow;
             case core::BuiltinFn::kQuantizeToF16:
@@ -1055,6 +1071,10 @@
                 return pb::BuiltinFn::unpack4x8_snorm;
             case core::BuiltinFn::kUnpack4X8Unorm:
                 return pb::BuiltinFn::unpack4x8_unorm;
+            case core::BuiltinFn::kUnpack4XI8:
+                return pb::BuiltinFn::unpack4xi8;
+            case core::BuiltinFn::kUnpack4XU8:
+                return pb::BuiltinFn::unpack4xu8;
             case core::BuiltinFn::kWorkgroupBarrier:
                 return pb::BuiltinFn::workgroup_barrier;
             case core::BuiltinFn::kTextureBarrier:
@@ -1115,7 +1135,7 @@
                 return pb::BuiltinFn::subgroup_ballot;
             case core::BuiltinFn::kSubgroupBroadcast:
                 return pb::BuiltinFn::subgroup_broadcast;
-            default:
+            case core::BuiltinFn::kNone:
                 break;
         }
         TINT_ICE() << "invalid BuiltinFn: " << in;
diff --git a/src/tint/lang/core/ir/binary/ir.proto b/src/tint/lang/core/ir/binary/ir.proto
index e59a9cb..19f3b0a 100644
--- a/src/tint/lang/core/ir/binary/ir.proto
+++ b/src/tint/lang/core/ir/binary/ir.proto
@@ -247,31 +247,30 @@
         InstructionReturn return = 3;
         InstructionUnary unary = 4;
         InstructionBinary binary = 5;
-        InstructionBuiltin builtin = 6;
-        InstructionDiscard discard = 7;
-        InstructionLet let = 8;
-        InstructionVar var = 9;
-        InstructionBitcast bitcast = 10;
-        InstructionConstruct construct = 11;
-        InstructionConvert convert = 12;
-        InstructionAccess access = 13;
-        InstructionUserCall user_call = 14;
-        InstructionBuiltinCall builtin_call = 15;
-        InstructionLoad load = 16;
-        InstructionStore store = 17;
-        InstructionLoadVectorElement load_vector_element = 18;
-        InstructionStoreVectorElement store_vector_element = 19;
-        InstructionSwizzle swizzle = 20;
-        InstructionIf if = 21;
-        InstructionSwitch switch = 22;
-        InstructionLoop loop = 23;
-        InstructionExitIf exit_if = 24;
-        InstructionExitSwitch exit_switch = 25;
-        InstructionExitLoop exit_loop = 26;
-        InstructionNextIteration next_iteration = 27;
-        InstructionContinue continue = 28;
-        InstructionBreakIf break_if = 29;
-        InstructionUnreachable unreachable = 30;
+        InstructionDiscard discard = 6;
+        InstructionLet let = 7;
+        InstructionVar var = 8;
+        InstructionBitcast bitcast = 9;
+        InstructionConstruct construct = 10;
+        InstructionConvert convert = 11;
+        InstructionAccess access = 12;
+        InstructionUserCall user_call = 13;
+        InstructionBuiltinCall builtin_call = 14;
+        InstructionLoad load = 15;
+        InstructionStore store = 16;
+        InstructionLoadVectorElement load_vector_element = 17;
+        InstructionStoreVectorElement store_vector_element = 18;
+        InstructionSwizzle swizzle = 19;
+        InstructionIf if = 20;
+        InstructionSwitch switch = 21;
+        InstructionLoop loop = 22;
+        InstructionExitIf exit_if = 23;
+        InstructionExitSwitch exit_switch = 24;
+        InstructionExitLoop exit_loop = 25;
+        InstructionNextIteration next_iteration = 26;
+        InstructionContinue continue = 27;
+        InstructionBreakIf break_if = 28;
+        InstructionUnreachable unreachable = 29;
     }
 }
 
@@ -285,8 +284,6 @@
     BinaryOp op = 1;
 }
 
-message InstructionBuiltin {}
-
 message InstructionBitcast {}
 
 message InstructionConstruct {}
@@ -450,22 +447,23 @@
 
 enum TexelFormat {
     bgra8_unorm = 0;
-    r32_float = 1;
-    r32_sint = 2;
-    r32_uint = 3;
-    rg32_float = 4;
-    rg32_sint = 5;
-    rg32_uint = 6;
-    rgba16_float = 7;
-    rgba16_sint = 8;
-    rgba16_uint = 9;
-    rgba32_float = 10;
-    rgba32_sint = 11;
-    rgba32_uint = 12;
-    rgba8_sint = 13;
-    rgba8_snorm = 14;
-    rgba8_uint = 15;
-    rgba8_unorm = 16;
+    r8_unorm = 1;
+    r32_float = 2;
+    r32_sint = 3;
+    r32_uint = 4;
+    rg32_float = 5;
+    rg32_sint = 6;
+    rg32_uint = 7;
+    rgba16_float = 8;
+    rgba16_sint = 9;
+    rgba16_uint = 10;
+    rgba32_float = 11;
+    rgba32_sint = 12;
+    rgba32_uint = 13;
+    rgba8_sint = 14;
+    rgba8_snorm = 15;
+    rgba8_uint = 16;
+    rgba8_unorm = 17;
 }
 
 enum SamplerKind {
@@ -564,59 +562,65 @@
     pack2x16_unorm = 57;
     pack4x8_snorm = 58;
     pack4x8_unorm = 59;
-    pow = 60;
-    quantize_to_f16 = 61;
-    radians = 62;
-    reflect = 63;
-    refract = 64;
-    reverse_bits = 65;
-    round = 66;
-    saturate = 67;
-    select = 68;
-    sign = 69;
-    sin = 70;
-    sinh = 71;
-    smoothstep = 72;
-    sqrt = 73;
-    step = 74;
-    storage_barrier = 75;
-    tan = 76;
-    tanh = 77;
-    transpose = 78;
-    trunc = 79;
-    unpack2x16_float = 80;
-    unpack2x16_snorm = 81;
-    unpack2x16_unorm = 82;
-    unpack4x8_snorm = 83;
-    unpack4x8_unorm = 84;
-    workgroup_barrier = 85;
-    texture_barrier = 86;
-    texture_dimensions = 87;
-    texture_gather = 88;
-    texture_gather_compare = 89;
-    texture_num_layers = 90;
-    texture_num_levels = 91;
-    texture_num_samples = 92;
-    texture_sample = 93;
-    texture_sample_bias = 94;
-    texture_sample_compare = 95;
-    texture_sample_compare_level = 96;
-    texture_sample_grad = 97;
-    texture_sample_level = 98;
-    texture_sample_base_clamp_to_edge = 99;
-    texture_store = 100;
-    texture_load = 101;
-    atomic_load = 102;
-    atomic_store = 103;
-    atomic_add = 104;
-    atomic_sub = 105;
-    atomic_max = 106;
-    atomic_min = 107;
-    atomic_and = 108;
-    atomic_or = 109;
-    atomic_xor = 110;
-    atomic_exchange = 111;
-    atomic_compare_exchange_weak = 112;
-    subgroup_ballot = 113;
-    subgroup_broadcast = 114;
+    pack4xi8 = 60;
+    pack4xu8 = 61;
+    pack4xi8_clamp = 62;
+    pack4xu8_clamp = 63;
+    pow = 64;
+    quantize_to_f16 = 65;
+    radians = 66;
+    reflect = 67;
+    refract = 68;
+    reverse_bits = 69;
+    round = 70;
+    saturate = 71;
+    select = 72;
+    sign = 73;
+    sin = 74;
+    sinh = 75;
+    smoothstep = 76;
+    sqrt = 77;
+    step = 78;
+    storage_barrier = 79;
+    tan = 80;
+    tanh = 81;
+    transpose = 82;
+    trunc = 83;
+    unpack2x16_float = 84;
+    unpack2x16_snorm = 85;
+    unpack2x16_unorm = 86;
+    unpack4x8_snorm = 87;
+    unpack4x8_unorm = 88;
+    unpack4xi8 = 89;
+    unpack4xu8 = 90;
+    workgroup_barrier = 91;
+    texture_barrier = 92;
+    texture_dimensions = 93;
+    texture_gather = 94;
+    texture_gather_compare = 95;
+    texture_num_layers = 96;
+    texture_num_levels = 97;
+    texture_num_samples = 98;
+    texture_sample = 99;
+    texture_sample_bias = 100;
+    texture_sample_compare = 101;
+    texture_sample_compare_level = 102;
+    texture_sample_grad = 103;
+    texture_sample_level = 104;
+    texture_sample_base_clamp_to_edge = 105;
+    texture_store = 106;
+    texture_load = 107;
+    atomic_load = 108;
+    atomic_store = 109;
+    atomic_add = 110;
+    atomic_sub = 111;
+    atomic_max = 112;
+    atomic_min = 113;
+    atomic_and = 114;
+    atomic_or = 115;
+    atomic_xor = 116;
+    atomic_exchange = 117;
+    atomic_compare_exchange_weak = 118;
+    subgroup_ballot = 119;
+    subgroup_broadcast = 120;
 }
diff --git a/src/tint/lang/core/ir/block_param.h b/src/tint/lang/core/ir/block_param.h
index 009e022..0b697a2 100644
--- a/src/tint/lang/core/ir/block_param.h
+++ b/src/tint/lang/core/ir/block_param.h
@@ -31,25 +31,42 @@
 #include "src/tint/lang/core/ir/value.h"
 #include "src/tint/utils/rtti/castable.h"
 
+// Forward declarations
+namespace tint::core::ir {
+class MultiInBlock;
+}  // namespace tint::core::ir
+
 namespace tint::core::ir {
 
-/// An instruction in the IR.
+/// A block parameter in the IR.
 class BlockParam : public Castable<BlockParam, Value> {
   public:
     /// Constructor
-    /// @param type the type of the var
+    /// @param type the type of the parameter
     explicit BlockParam(const core::type::Type* type);
     ~BlockParam() override;
 
-    /// @returns the type of the var
+    /// @returns the type of the parameter
     const core::type::Type* Type() const override { return type_; }
 
+    /// Sets the block that this parameter belongs to.
+    /// @param block the block
+    void SetBlock(MultiInBlock* block) { block_ = block; }
+
+    /// @returns the block that this parameter belongs to, or nullptr
+    MultiInBlock* Block() { return block_; }
+
+    /// @returns the block that this parameter belongs to, or nullptr
+    const MultiInBlock* Block() const { return block_; }
+
     /// @copydoc Instruction::Clone()
     BlockParam* Clone(CloneContext& ctx) override;
 
   private:
-    /// the result type of the instruction
+    /// the type of the parameter
     const core::type::Type* type_ = nullptr;
+    /// the block that the parameter belongs to
+    MultiInBlock* block_ = nullptr;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index c705d0c..99e94d1 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -219,6 +219,37 @@
         cb();
     }
 
+    /// Calls @p cb with the builder inserting after @p val
+    /// @param val the insertion point for new instructions
+    /// @param cb the function to call with the builder inserting new instructions after @p val
+    template <typename FUNCTION>
+    void InsertAfter(ir::Value* val, FUNCTION&& cb) {
+        tint::Switch(
+            val,
+            [&](core::ir::InstructionResult* result) {
+                const TINT_SCOPED_ASSIGNMENT(insertion_point_,
+                                             InsertionPoints::InsertAfter{result->Instruction()});
+                cb();
+            },
+            [&](core::ir::FunctionParam* param) {
+                auto* body = param->Function()->Block();
+                if (body->IsEmpty()) {
+                    Append(body, cb);
+                } else {
+                    InsertBefore(body->Front(), cb);
+                }
+            },
+            [&](core::ir::BlockParam* param) {
+                auto* block = param->Block();
+                if (block->IsEmpty()) {
+                    Append(block, cb);
+                } else {
+                    InsertBefore(block->Front(), cb);
+                }
+            },
+            TINT_ICE_ON_NO_MATCH);
+    }
+
     /// Adds and returns the instruction @p instruction to the current insertion point. If there
     /// is no current insertion point set, then @p instruction is just returned.
     /// @param instruction the instruction to append
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index 2e95c2c..281cb61 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -188,7 +188,14 @@
     if (auto* merge = blk->As<MultiInBlock>()) {
         if (!merge->Params().IsEmpty()) {
             out_ << " (";
-            EmitValueList(merge->Params().Slice());
+            for (auto* p : merge->Params()) {
+                if (p != merge->Params().Front()) {
+                    out_ << ", ";
+                }
+                SourceMarker psm(this);
+                EmitValue(p);
+                psm.Store(p);
+            }
             out_ << ")";
         }
     }
@@ -302,7 +309,12 @@
     in_function_ = true;
 
     std::string fn_id = IdOf(func);
-    Indent() << "%" << fn_id << " =";
+    {
+        SourceMarker sm(this);
+        Indent() << "%" << fn_id;
+        sm.Store(func);
+    }
+    out_ << " =";
 
     if (func->Stage() != Function::PipelineStage::kUndefined) {
         out_ << " @" << func->Stage();
@@ -318,7 +330,9 @@
         if (p != func->Params().Front()) {
             out_ << ", ";
         }
+        SourceMarker sm(this);
         out_ << "%" << IdOf(p) << ":" << p->Type()->FriendlyName();
+        sm.Store(p);
 
         EmitParamAttributes(p);
     }
@@ -790,15 +804,6 @@
     );
 }
 
-void Disassembler::EmitValueList(tint::Slice<const Value* const> values) {
-    for (size_t i = 0, n = values.Length(); i < n; i++) {
-        if (i > 0) {
-            out_ << ", ";
-        }
-        EmitValue(values[i]);
-    }
-}
-
 void Disassembler::EmitBinary(const Binary* b) {
     SourceMarker sm(this);
     EmitValueWithType(b);
diff --git a/src/tint/lang/core/ir/disassembler.h b/src/tint/lang/core/ir/disassembler.h
index d22b8a1..6b6f7c6 100644
--- a/src/tint/lang/core/ir/disassembler.h
+++ b/src/tint/lang/core/ir/disassembler.h
@@ -32,6 +32,7 @@
 
 #include "src/tint/lang/core/ir/binary.h"
 #include "src/tint/lang/core/ir/block.h"
+#include "src/tint/lang/core/ir/block_param.h"
 #include "src/tint/lang/core/ir/call.h"
 #include "src/tint/lang/core/ir/if.h"
 #include "src/tint/lang/core/ir/loop.h"
@@ -100,10 +101,26 @@
     /// @returns the source for the result
     Source ResultSource(IndexedValue result) { return result_to_src_.GetOr(result, Source{}); }
 
-    /// @param blk teh block to retrieve
+    /// @param blk the block to retrieve
     /// @returns the source for the block
     Source BlockSource(const Block* blk) { return block_to_src_.GetOr(blk, Source{}); }
 
+    /// @param param the block parameter to retrieve
+    /// @returns the source for the parameter
+    Source BlockParamSource(const BlockParam* param) {
+        return block_param_to_src_.GetOr(param, Source{});
+    }
+
+    /// @param func the function to retrieve
+    /// @returns the source for the function
+    Source FunctionSource(const Function* func) { return function_to_src_.GetOr(func, Source{}); }
+
+    /// @param param the function parameter to retrieve
+    /// @returns the source for the parameter
+    Source FunctionParamSource(const FunctionParam* param) {
+        return function_param_to_src_.GetOr(param, Source{});
+    }
+
     /// Stores the given @p src location for @p inst instruction
     /// @param inst the instruction to store
     /// @param src the source location
@@ -114,6 +131,23 @@
     /// @param src the source location
     void SetSource(const Block* blk, Source src) { block_to_src_.Add(blk, src); }
 
+    /// Stores the given @p src location for @p param block parameter
+    /// @param param the block parameter to store
+    /// @param src the source location
+    void SetSource(const BlockParam* param, Source src) { block_param_to_src_.Add(param, src); }
+
+    /// Stores the given @p src location for @p func function
+    /// @param func the function to store
+    /// @param src the source location
+    void SetSource(const Function* func, Source src) { function_to_src_.Add(func, src); }
+
+    /// Stores the given @p src location for @p param function parameter
+    /// @param param the function parameter to store
+    /// @param src the source location
+    void SetSource(const FunctionParam* param, Source src) {
+        function_param_to_src_.Add(param, src);
+    }
+
     /// Stores the given @p src location for @p op operand
     /// @param op the operand to store
     /// @param src the source location
@@ -137,6 +171,12 @@
 
         void Store(const Block* blk) { dis_->SetSource(blk, MakeSource()); }
 
+        void Store(const BlockParam* param) { dis_->SetSource(param, MakeSource()); }
+
+        void Store(const Function* func) { dis_->SetSource(func, MakeSource()); }
+
+        void Store(const FunctionParam* param) { dis_->SetSource(param, MakeSource()); }
+
         void Store(IndexedValue operand) { dis_->SetSource(operand, MakeSource()); }
 
         void StoreResult(IndexedValue result) { dis_->SetResultSource(result, MakeSource()); }
@@ -168,7 +208,6 @@
     void EmitValueWithType(const Instruction* val);
     void EmitValueWithType(const Value* val);
     void EmitValue(const Value* val);
-    void EmitValueList(tint::Slice<const ir::Value* const> values);
     void EmitBinary(const Binary* b);
     void EmitUnary(const Unary* b);
     void EmitTerminator(const Terminator* b);
@@ -193,9 +232,12 @@
     uint32_t current_output_start_pos_ = 0;
 
     Hashmap<const Block*, Source, 8> block_to_src_;
+    Hashmap<const BlockParam*, Source, 8> block_param_to_src_;
     Hashmap<const Instruction*, Source, 8> instruction_to_src_;
     Hashmap<IndexedValue, Source, 8> operand_to_src_;
     Hashmap<IndexedValue, Source, 8> result_to_src_;
+    Hashmap<const Function*, Source, 8> function_to_src_;
+    Hashmap<const FunctionParam*, Source, 8> function_param_to_src_;
     Hashmap<const If*, std::string, 8> if_names_;
     Hashmap<const Loop*, std::string, 8> loop_names_;
     Hashmap<const Switch*, std::string, 8> switch_names_;
diff --git a/src/tint/lang/core/ir/function.cc b/src/tint/lang/core/ir/function.cc
index 51f7928..5b669c2 100644
--- a/src/tint/lang/core/ir/function.cc
+++ b/src/tint/lang/core/ir/function.cc
@@ -53,7 +53,7 @@
     auto* new_func =
         ctx.ir.allocators.values.Create<Function>(return_.type, pipeline_stage_, workgroup_size_);
     new_func->block_ = ctx.ir.blocks.Create<ir::Block>();
-    new_func->params_ = ctx.Clone<1>(params_.Slice());
+    new_func->SetParams(ctx.Clone<1>(params_.Slice()));
     new_func->return_.builtin = return_.builtin;
     new_func->return_.location = return_.location;
     new_func->return_.invariant = return_.invariant;
@@ -66,13 +66,25 @@
 }
 
 void Function::SetParams(VectorRef<FunctionParam*> params) {
+    for (auto* param : params_) {
+        param->SetFunction(nullptr);
+    }
     params_ = std::move(params);
-    TINT_ASSERT(!params_.Any(IsNull));
+    TINT_ASSERT_OR_RETURN(!params_.Any(IsNull));
+    for (auto* param : params_) {
+        param->SetFunction(this);
+    }
 }
 
 void Function::SetParams(std::initializer_list<FunctionParam*> params) {
+    for (auto* param : params_) {
+        param->SetFunction(nullptr);
+    }
     params_ = params;
-    TINT_ASSERT(!params_.Any(IsNull));
+    TINT_ASSERT_OR_RETURN(!params_.Any(IsNull));
+    for (auto* param : params_) {
+        param->SetFunction(this);
+    }
 }
 
 void Function::Destroy() {
diff --git a/src/tint/lang/core/ir/function_param.h b/src/tint/lang/core/ir/function_param.h
index 6cefb10..f323914 100644
--- a/src/tint/lang/core/ir/function_param.h
+++ b/src/tint/lang/core/ir/function_param.h
@@ -38,6 +38,11 @@
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/rtti/castable.h"
 
+// Forward declarations
+namespace tint::core::ir {
+class Function;
+}  // namespace tint::core::ir
+
 namespace tint::core::ir {
 
 /// A function parameter in the IR.
@@ -48,6 +53,16 @@
     explicit FunctionParam(const core::type::Type* type);
     ~FunctionParam() override;
 
+    /// Sets the function that this parameter belongs to.
+    /// @param func the function
+    void SetFunction(ir::Function* func) { func_ = func; }
+
+    /// @returns the function that this parameter belongs to, or nullptr
+    ir::Function* Function() { return func_; }
+
+    /// @returns the function that this parameter belongs to, or nullptr
+    const ir::Function* Function() const { return func_; }
+
     /// @returns the type of the var
     const core::type::Type* Type() const override { return type_; }
 
@@ -99,6 +114,7 @@
     std::optional<struct BindingPoint> BindingPoint() const { return binding_point_; }
 
   private:
+    ir::Function* func_ = nullptr;
     const core::type::Type* type_ = nullptr;
     std::optional<core::BuiltinValue> builtin_;
     std::optional<struct Location> location_;
diff --git a/src/tint/lang/core/ir/function_test.cc b/src/tint/lang/core/ir/function_test.cc
index 8873648..27f332e 100644
--- a/src/tint/lang/core/ir/function_test.cc
+++ b/src/tint/lang/core/ir/function_test.cc
@@ -126,6 +126,12 @@
 
     // Cloned functions are not automatically added to the module.
     EXPECT_EQ(mod.functions.Length(), 1u);
+
+    // Check parameter ownership is correct.
+    EXPECT_EQ(param1->Function(), f);
+    EXPECT_EQ(param2->Function(), f);
+    EXPECT_EQ(new_param1->Function(), new_f);
+    EXPECT_EQ(new_param2->Function(), new_f);
 }
 
 TEST_F(IR_FunctionTest, CloneWithExits) {
@@ -138,5 +144,23 @@
     EXPECT_EQ(new_f, new_f->Block()->Front()->As<Return>()->Func());
 }
 
+TEST_F(IR_FunctionTest, Parameters) {
+    auto* f = b.Function("my_func", mod.Types().void_());
+
+    auto* param1 = b.FunctionParam("a", mod.Types().i32());
+    auto* param2 = b.FunctionParam("b", mod.Types().f32());
+    auto* param3 = b.FunctionParam("b", mod.Types().f32());
+
+    f->SetParams({param1, param2});
+    EXPECT_EQ(param1->Function(), f);
+    EXPECT_EQ(param2->Function(), f);
+    EXPECT_EQ(param3->Function(), nullptr);
+
+    f->SetParams({param1, param3});
+    EXPECT_EQ(param1->Function(), f);
+    EXPECT_EQ(param2->Function(), nullptr);
+    EXPECT_EQ(param3->Function(), f);
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/multi_in_block.cc b/src/tint/lang/core/ir/multi_in_block.cc
index c57c622..411d06c 100644
--- a/src/tint/lang/core/ir/multi_in_block.cc
+++ b/src/tint/lang/core/ir/multi_in_block.cc
@@ -55,11 +55,25 @@
 }
 
 void MultiInBlock::SetParams(VectorRef<BlockParam*> params) {
+    for (auto* param : params_) {
+        param->SetBlock(nullptr);
+    }
     params_ = std::move(params);
+    TINT_ASSERT_OR_RETURN(!params_.Any(IsNull));
+    for (auto* param : params_) {
+        param->SetBlock(this);
+    }
 }
 
 void MultiInBlock::SetParams(std::initializer_list<BlockParam*> params) {
+    for (auto* param : params_) {
+        param->SetBlock(nullptr);
+    }
     params_ = std::move(params);
+    TINT_ASSERT_OR_RETURN(!params_.Any(IsNull));
+    for (auto* param : params_) {
+        param->SetBlock(this);
+    }
 }
 
 void MultiInBlock::AddInboundSiblingBranch(ir::Terminator* node) {
diff --git a/src/tint/lang/core/ir/multi_in_block_test.cc b/src/tint/lang/core/ir/multi_in_block_test.cc
index 790a90e..a377c44 100644
--- a/src/tint/lang/core/ir/multi_in_block_test.cc
+++ b/src/tint/lang/core/ir/multi_in_block_test.cc
@@ -54,7 +54,9 @@
     auto* blk = b.MultiInBlock();
     auto* add = b.Add(mod.Types().i32(), 1_i, 2_i);
     blk->Append(add);
-    blk->SetParams({b.BlockParam(mod.Types().i32()), b.BlockParam(mod.Types().f32())});
+    auto* param1 = b.BlockParam(mod.Types().i32());
+    auto* param2 = b.BlockParam(mod.Types().f32());
+    blk->SetParams({param1, param2});
     blk->SetParent(loop);
 
     auto* terminate = b.TerminateInvocation();
@@ -75,6 +77,12 @@
     EXPECT_NE(add, new_blk->Front());
     EXPECT_TRUE(new_blk->Front()->Is<Binary>());
     EXPECT_EQ(BinaryOp::kAdd, new_blk->Front()->As<Binary>()->Op());
+
+    // Check parameter ownership is correct.
+    EXPECT_EQ(param1->Block(), blk);
+    EXPECT_EQ(param2->Block(), blk);
+    EXPECT_EQ(new_blk->Params()[0]->Block(), new_blk);
+    EXPECT_EQ(new_blk->Params()[1]->Block(), new_blk);
 }
 
 TEST_F(IR_MultiInBlockTest, CloneEmpty) {
@@ -86,5 +94,23 @@
     EXPECT_EQ(0u, new_blk->Params().Length());
 }
 
+TEST_F(IR_MultiInBlockTest, Parameters) {
+    auto* blk = b.MultiInBlock();
+
+    auto* param1 = b.BlockParam("a", mod.Types().i32());
+    auto* param2 = b.BlockParam("b", mod.Types().f32());
+    auto* param3 = b.BlockParam("b", mod.Types().f32());
+
+    blk->SetParams({param1, param2});
+    EXPECT_EQ(param1->Block(), blk);
+    EXPECT_EQ(param2->Block(), blk);
+    EXPECT_EQ(param3->Block(), nullptr);
+
+    blk->SetParams({param1, param3});
+    EXPECT_EQ(param1->Block(), blk);
+    EXPECT_EQ(param2->Block(), nullptr);
+    EXPECT_EQ(param3->Block(), blk);
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index 3f1a681..24d594d 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -158,11 +158,26 @@
     /// @returns the diagnostic
     diag::Diagnostic& AddResultError(const Instruction* inst, size_t idx);
 
-    /// Adds an error the @p block and highlights the block header in the disassembly
+    /// Adds an error for the @p block and highlights the block header in the disassembly
     /// @param blk the block
     /// @returns the diagnostic
     diag::Diagnostic& AddError(const Block* blk);
 
+    /// Adds an error for the @p param and highlights the parameter in the disassembly
+    /// @param param the parameter
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(const BlockParam* param);
+
+    /// Adds an error for the @p func and highlights the function in the disassembly
+    /// @param func the function
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(const Function* func);
+
+    /// Adds an error for the @p param and highlights the parameter in the disassembly
+    /// @param param the parameter
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(const FunctionParam* param);
+
     /// Adds an error the @p block and highlights the block header in the disassembly
     /// @param src the source lines to highlight
     /// @returns the diagnostic
@@ -172,6 +187,10 @@
     /// @param inst the instruction
     diag::Diagnostic& AddNote(const Instruction* inst);
 
+    /// Adds a note to @p func and highlights the function in the disassembly
+    /// @param func the function
+    diag::Diagnostic& AddNote(const Function* func);
+
     /// Adds a note to @p inst for operand @p idx and highlights the operand in the
     /// disassembly
     /// @param inst the instruction
@@ -346,8 +365,8 @@
 
     for (auto& func : mod_.functions) {
         if (!all_functions_.Add(func.Get())) {
-            AddError(Source{}) << "function " << style::Function(Name(func.Get()))
-                               << " added to module multiple times";
+            AddError(func) << "function " << style::Function(Name(func.Get()))
+                           << " added to module multiple times";
         }
     }
 
@@ -413,12 +432,36 @@
     return AddError(src);
 }
 
+diag::Diagnostic& Validator::AddError(const BlockParam* param) {
+    DisassembleIfNeeded();
+    auto src = dis_.BlockParamSource(param);
+    return AddError(src);
+}
+
+diag::Diagnostic& Validator::AddError(const Function* func) {
+    DisassembleIfNeeded();
+    auto src = dis_.FunctionSource(func);
+    return AddError(src);
+}
+
+diag::Diagnostic& Validator::AddError(const FunctionParam* param) {
+    DisassembleIfNeeded();
+    auto src = dis_.FunctionParamSource(param);
+    return AddError(src);
+}
+
 diag::Diagnostic& Validator::AddNote(const Instruction* inst) {
     DisassembleIfNeeded();
     auto src = dis_.InstructionSource(inst);
     return AddNote(src);
 }
 
+diag::Diagnostic& Validator::AddNote(const Function* func) {
+    DisassembleIfNeeded();
+    auto src = dis_.FunctionSource(func);
+    return AddNote(src);
+}
+
 diag::Diagnostic& Validator::AddNote(const Instruction* inst, size_t idx) {
     DisassembleIfNeeded();
     auto src = dis_.OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
@@ -488,22 +531,50 @@
 void Validator::CheckFunction(const Function* func) {
     CheckBlock(func->Block());
 
-    // References not allowed on function signatures even with Capability::kAllowRefTypes
     for (auto* param : func->Params()) {
+        if (!param->Alive()) {
+            AddError(param) << "destroyed parameter found in function parameter list";
+            return;
+        }
+        if (!param->Function()) {
+            AddError(param) << "function parameter has nullptr parent function";
+            return;
+        } else if (param->Function() != func) {
+            AddError(param) << "function parameter has incorrect parent function";
+            AddNote(param->Function()) << "parent function declared here";
+            return;
+        }
+
+        // References not allowed on function signatures even with Capability::kAllowRefTypes
         if (HoldsType<type::Reference>(param->Type())) {
-            // TODO(dsinclair): Parameters need a source mapping.
-            AddError(Source{}) << "references are not permitted as parameter types";
+            AddError(param) << "references are not permitted as parameter types";
         }
     }
     if (HoldsType<type::Reference>(func->ReturnType())) {
-        // TODO(dsinclair): Function need a source mapping.
-        AddError(Source{}) << "references are not permitted as return types";
+        AddError(func) << "references are not permitted as return types";
     }
 }
 
 void Validator::CheckBlock(const Block* blk) {
     TINT_SCOPED_ASSIGNMENT(current_block_, blk);
 
+    if (auto* mb = blk->As<MultiInBlock>()) {
+        for (auto* param : mb->Params()) {
+            if (!param->Alive()) {
+                AddError(param) << "destroyed parameter found in block parameter list";
+                return;
+            }
+            if (!param->Block()) {
+                AddError(param) << "block parameter has nullptr parent block";
+                return;
+            } else if (param->Block() != mb) {
+                AddError(param) << "block parameter has incorrect parent block";
+                AddNote(param->Block()) << "parent block declared here";
+                return;
+            }
+        }
+    }
+
     if (!blk->Terminator()) {
         AddError(blk) << "block: does not end in a terminator instruction";
     }
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 3e1a708..8742f8a 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -139,7 +139,10 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(error: function 'my_func' added to module multiple times
+              R"(:1:1 error: function 'my_func' added to module multiple times
+%my_func = func(%2:i32, %3:f32):void -> %b1 {
+^^^^^^^^
+
 note: # Disassembly
 %my_func = func(%2:i32, %3:f32):void -> %b1 {
   %b1 = block {
@@ -154,6 +157,88 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, Function_DeadParameter) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.f32());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    p->Destroy();
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: destroyed parameter found in function parameter list
+%my_func = func(%my_param:f32):void -> %b1 {
+                ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:f32):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterWithNullFunction) {
+    auto* f = b.Function("my_func", ty.void_());
+    auto* p = b.FunctionParam("my_param", ty.f32());
+    f->SetParams({p});
+    f->Block()->Append(b.Return(f));
+
+    p->SetFunction(nullptr);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:17 error: function parameter has nullptr parent function
+%my_func = func(%my_param:f32):void -> %b1 {
+                ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func(%my_param:f32):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Function_ParameterUsedInMultipleFunctions) {
+    auto* p = b.FunctionParam("my_param", ty.f32());
+    auto* f1 = b.Function("my_func1", ty.void_());
+    auto* f2 = b.Function("my_func2", ty.void_());
+    f1->SetParams({p});
+    f2->SetParams({p});
+    f1->Block()->Append(b.Return(f1));
+    f2->Block()->Append(b.Return(f2));
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:1:18 error: function parameter has incorrect parent function
+%my_func1 = func(%my_param:f32):void -> %b1 {
+                 ^^^^^^^^^^^^^
+
+:6:1 note: parent function declared here
+%my_func2 = func(%my_param:f32):void -> %b2 {
+^^^^^^^^^
+
+note: # Disassembly
+%my_func1 = func(%my_param:f32):void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+%my_func2 = func(%my_param:f32):void -> %b2 {
+  %b2 = block {
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, CallToFunctionOutsideModule) {
     auto* f = b.Function("f", ty.void_());
     auto* g = b.Function("g", ty.void_());
@@ -393,6 +478,115 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, Block_DeadParameter) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* p = b.BlockParam("my_param", ty.f32());
+    b.Append(f->Block(), [&] {
+        auto* l = b.Loop();
+        l->Body()->SetParams({p});
+        b.Append(l->Body(), [&] { b.ExitLoop(l); });
+        b.Return(f);
+    });
+
+    p->Destroy();
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: destroyed parameter found in block parameter list
+      %b2 = block (%my_param:f32) {  # body
+                   ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block (%my_param:f32) {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Block_ParameterWithNullBlock) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* p = b.BlockParam("my_param", ty.f32());
+    b.Append(f->Block(), [&] {
+        auto* l = b.Loop();
+        l->Body()->SetParams({p});
+        b.Append(l->Body(), [&] { b.ExitLoop(l); });
+        b.Return(f);
+    });
+
+    p->SetBlock(nullptr);
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: block parameter has nullptr parent block
+      %b2 = block (%my_param:f32) {  # body
+                   ^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block (%my_param:f32) {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Block_ParameterUsedInMultipleBlocks) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* p = b.BlockParam("my_param", ty.f32());
+    b.Append(f->Block(), [&] {
+        auto* l = b.Loop();
+        l->Body()->SetParams({p});
+        b.Append(l->Body(), [&] { b.Continue(l, p); });
+        l->Continuing()->SetParams({p});
+        b.Append(l->Continuing(), [&] { b.NextIteration(l, p); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:4:20 error: block parameter has incorrect parent block
+      %b2 = block (%my_param:f32) {  # body
+                   ^^^^^^^^^^^^^
+
+:7:7 note: parent block declared here
+      %b3 = block (%my_param:f32) {  # continuing
+      ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    loop [b: %b2, c: %b3] {  # loop_1
+      %b2 = block (%my_param:f32) {  # body
+        continue %b3 %my_param:f32
+      }
+      %b3 = block (%my_param:f32) {  # continuing
+        next_iteration %b2 %my_param:f32
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, Access_NegativeIndex) {
     auto* f = b.Function("my_func", ty.void_());
     auto* obj = b.FunctionParam(ty.vec3<f32>());
diff --git a/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc b/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc
index 04e7b14..fd59677 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc
@@ -39,7 +39,10 @@
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/utils/containers/transform.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/result/result.h"
 #include "src/tint/utils/rtti/switch.h"
+#include "src/tint/utils/text/text_style.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::PixelLocal);
 TINT_INSTANTIATE_TYPEINFO(tint::hlsl::writer::PixelLocal::RasterizerOrderedView);
@@ -120,7 +123,12 @@
                 // Obtain struct of the pixel local.
                 auto* pixel_local_str =
                     pixel_local_variable->Type()->UnwrapRef()->As<sem::Struct>();
-                TransformEntryPoint(entry_point, pixel_local_variable, pixel_local_str);
+                if (auto res =
+                        TransformEntryPoint(entry_point, pixel_local_variable, pixel_local_str);
+                    res != Success) {
+                    b.Diagnostics().Add(res.Failure().reason);
+                    made_changes = true;
+                }
 
                 break;  // Only a single `var<pixel_local>` can be used by an entry point.
             }
@@ -139,9 +147,9 @@
     /// @param entry_point the entry point
     /// @param pixel_local_var the `var<pixel_local>`
     /// @param pixel_local_str the struct type of the var
-    void TransformEntryPoint(const sem::Function* entry_point,
-                             const sem::GlobalVariable* pixel_local_var,
-                             const sem::Struct* pixel_local_str) {
+    Result<SuccessType> TransformEntryPoint(const sem::Function* entry_point,
+                                            const sem::GlobalVariable* pixel_local_var,
+                                            const sem::Struct* pixel_local_str) {
         // Wrap the old entry point "fn" into a new entry point where functions to load and store
         // ROV data are called.
         auto* original_entry_point_fn = entry_point->Declaration();
@@ -175,9 +183,12 @@
         // load data from and store data into the ROVs.
         auto load_rov_function_name = b.Symbols().New("load_from_pixel_local_storage");
         auto store_rov_function_name = b.Symbols().New("store_into_pixel_local_storage");
-        DeclareROVsAndLoadStoreFunctions(load_rov_function_name, store_rov_function_name,
-                                         pixel_local_var->Declaration()->name->symbol.Name(),
-                                         pixel_local_str);
+        if (auto res = DeclareROVsAndLoadStoreFunctions(
+                load_rov_function_name, store_rov_function_name,
+                pixel_local_var->Declaration()->name->symbol.Name(), pixel_local_str);
+            res != Success) {
+            return res.Failure();
+        }
 
         // Declare new entry point
         Vector<const ast::Statement*, 5> new_entry_point_function_body;
@@ -276,6 +287,7 @@
         // Declare the new entry point that calls the inner function
         b.Func(entry_point_name, std::move(new_entry_point_params), new_entry_point_return_type,
                new_entry_point_function_body, Vector{b.Stage(ast::PipelineStage::kFragment)});
+        return Success;
     }
 
     /// Add the declarations of all the ROVs as a special type of read-write storage texture that
@@ -285,10 +297,11 @@
     /// @param store_rov_function_name the name of the function that stores the data into the ROVs
     /// @param pixel_local_variable_name the name of the pixel local variable
     /// @param pixel_local_str the struct type of the pixel local variable
-    void DeclareROVsAndLoadStoreFunctions(const Symbol& load_rov_function_name,
-                                          const Symbol& store_rov_function_name,
-                                          const std::string& pixel_local_variable_name,
-                                          const sem::Struct* pixel_local_str) {
+    Result<SuccessType> DeclareROVsAndLoadStoreFunctions(
+        const Symbol& load_rov_function_name,
+        const Symbol& store_rov_function_name,
+        const std::string& pixel_local_variable_name,
+        const sem::Struct* pixel_local_str) {
         std::string_view load_store_input_name = "my_input";
         Vector load_parameters{b.Param(load_store_input_name, b.ty.vec4<f32>())};
         Vector store_parameters{b.Param(load_store_input_name, b.ty.vec4<f32>())};
@@ -314,8 +327,11 @@
                 [&](const core::type::F32*) { return core::TexelFormat::kR32Float; },
                 TINT_ICE_ON_NO_MATCH);
             auto rov_format = ROVTexelFormat(member->Index());
-            auto rov_type = b.ty.storage_texture(core::type::TextureDimension::k2d, rov_format,
-                                                 core::Access::kReadWrite);
+            if (TINT_UNLIKELY(rov_format != Success)) {
+                return rov_format.Failure();
+            }
+            auto rov_type = b.ty.storage_texture(core::type::TextureDimension::k2d,
+                                                 rov_format.Get(), core::Access::kReadWrite);
             auto rov_symbol_name = b.Symbols().New("pixel_local_" + member->Name().Name());
             b.GlobalVar(rov_symbol_name, rov_type,
                         tint::Vector{b.Binding(AInt(ROVRegisterIndex(member->Index()))),
@@ -352,7 +368,7 @@
             // textureStore(
             //     pixel_local_member, rov_texcoord, vec4u(bitcast(PLS_Private_Variable.member)));
             std::string rov_pixel_type;
-            switch (rov_format) {
+            switch (rov_format.Get()) {
                 case core::TexelFormat::kR32Uint:
                     rov_pixel_type = "vec4u";
                     break;
@@ -363,8 +379,7 @@
                     rov_pixel_type = "vec4f";
                     break;
                 default:
-                    TINT_ICE() << "Invalid ROV format (now only R32Uint, R32Sint and R32Float are "
-                                  "supported)";
+                    TINT_UNREACHABLE();
                     break;
             }
             auto pixel_local_var_member_access_in_store_call =
@@ -374,7 +389,7 @@
                 to_vec4_call = b.Call(rov_pixel_type, pixel_local_var_member_access_in_store_call);
             } else {
                 ast::Type rov_pixel_ast_type;
-                switch (rov_format) {
+                switch (rov_format.Get()) {
                     case core::TexelFormat::kR32Uint:
                         rov_pixel_ast_type = b.ty.u32();
                         break;
@@ -399,6 +414,7 @@
 
         b.Func(load_rov_function_name, std::move(load_parameters), b.ty.void_(), load_body);
         b.Func(store_rov_function_name, std::move(store_parameters), b.ty.void_(), store_body);
+        return Success;
     }
 
     /// Find and get `@builtin(position)` which is needed for loading and storing data with ROVs
@@ -462,12 +478,14 @@
 
     /// @returns the texel format for the pixel local field with the given index
     /// @param field_index the pixel local field index
-    core::TexelFormat ROVTexelFormat(uint32_t field_index) {
+    Result<core::TexelFormat> ROVTexelFormat(uint32_t field_index) {
         auto format = cfg.pls_member_to_rov_format.Get(field_index);
         if (TINT_UNLIKELY(!format)) {
-            b.Diagnostics().AddError(diag::System::Transform, Source{})
-                << "PixelLocal::Config::attachments missing entry for field " << field_index;
-            return core::TexelFormat::kUndefined;
+            diag::Diagnostic err;
+            err.severity = diag::Severity::Error;
+            err.message << "PixelLocal::Config::attachments missing entry for field "
+                        << field_index;
+            return Failure{std::move(err)};
         }
         return *format;
     }
diff --git a/src/tint/lang/hlsl/writer/ast_raise/pixel_local_test.cc b/src/tint/lang/hlsl/writer/ast_raise/pixel_local_test.cc
index 9f644a7..f34138a 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/pixel_local_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/pixel_local_test.cc
@@ -61,6 +61,31 @@
     EXPECT_FALSE(ShouldRun<PixelLocal>(src, Bindings({})));
 }
 
+TEST_F(HLSLPixelLocalTest, MissingBindings) {
+    auto* src = R"(
+enable chromium_experimental_pixel_local;
+
+struct PixelLocal {
+  a : u32,
+}
+
+var<pixel_local> P : PixelLocal;
+
+@fragment
+fn F() -> @location(0) vec4f {
+  P.a += 42;
+  return vec4f(1, 0, 0, 1);
+}
+)";
+
+    auto* expect = R"(error: PixelLocal::Config::attachments missing entry for field 0)";
+    ast::transform::DataMap data;
+    data.Add<PixelLocal::Config>();
+    auto got = Run<PixelLocal>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 TEST_F(HLSLPixelLocalTest, UseInEntryPoint_NoPosition) {
     auto* src = R"(
 enable chromium_experimental_pixel_local;
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
index 901b4bb..98b3067 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -134,7 +134,7 @@
     }
 
     // Replace each access instruction that we recorded.
-    Hashmap<core::ir::Value*, core::ir::Value*, 4> object_to_local;
+    Hashmap<core::ir::Value*, core::ir::Value*, 4> object_to_var;
     Hashmap<PartialAccess, core::ir::Value*, 4> source_object_to_value;
     for (const auto& to_replace : worklist) {
         auto* access = to_replace.access;
@@ -145,24 +145,58 @@
         if (to_replace.first_dynamic_index > 0) {
             PartialAccess partial_access = {
                 access->Object(), access->Indices().Truncate(to_replace.first_dynamic_index)};
-            source_object = source_object_to_value.GetOrAdd(partial_access, [&] {
-                auto* intermediate_source = builder.Access(to_replace.dynamic_index_source_type,
-                                                           source_object, partial_access.indices);
-                intermediate_source->InsertBefore(access);
-                return intermediate_source->Result(0);
-            });
+            source_object =
+                source_object_to_value.GetOrAdd(partial_access, [&]() -> core::ir::Value* {
+                    // If the source is a constant, then the partial access will also produce a
+                    // constant. Extract the constant::Value and use that as the new source object.
+                    if (source_object->Is<core::ir::Constant>()) {
+                        for (const auto& i : partial_access.indices) {
+                            auto idx = i->As<core::ir::Constant>()->Value()->ValueAs<uint32_t>();
+                            source_object = builder.Constant(
+                                source_object->As<core::ir::Constant>()->Value()->Index(idx));
+                        }
+                        return source_object;
+                    }
+
+                    // Extract a non-constant intermediate source using an access instruction that
+                    // we insert immediately after the definition of the root source object.
+                    auto* intermediate_source =
+                        builder.Access(to_replace.dynamic_index_source_type, source_object,
+                                       partial_access.indices);
+                    builder.InsertAfter(source_object,
+                                        [&] { builder.Append(intermediate_source); });
+                    return intermediate_source->Result(0);
+                });
         }
 
-        // Declare a local variable and copy the source object to it.
-        auto* local = object_to_local.GetOrAdd(source_object, [&] {
-            auto* decl = builder.Var(ir.Types().ptr(
-                core::AddressSpace::kFunction, source_object->Type(), core::Access::kReadWrite));
+        // Declare a variable and copy the source object to it.
+        auto* var = object_to_var.GetOrAdd(source_object, [&] {
+            // If the source object is a constant we use a module-scope variable, as it could be
+            // indexed by multiple functions. Otherwise, we declare a function-scope variable
+            // immediately after the definition of the source object.
+            core::ir::Var* decl = nullptr;
+            if (source_object->Is<core::ir::Constant>()) {
+                decl = builder.Var(ir.Types().ptr(core::AddressSpace::kPrivate,
+                                                  source_object->Type(), core::Access::kReadWrite));
+                ir.root_block->Append(decl);
+            } else {
+                builder.InsertAfter(source_object, [&] {
+                    decl = builder.Var(ir.Types().ptr(core::AddressSpace::kFunction,
+                                                      source_object->Type(),
+                                                      core::Access::kReadWrite));
+
+                    // If we ever support value declarations at module-scope, we will need to modify
+                    // the partial access logic above since `access` instructions cannot be used in
+                    // the root block.
+                    TINT_ASSERT(decl->Block() != ir.root_block);
+                });
+            }
+
             decl->SetInitializer(source_object);
-            decl->InsertBefore(access);
             return decl->Result(0);
         });
 
-        // Create a new access instruction using the local variable as the source.
+        // Create a new access instruction using the new variable as the source.
         Vector<core::ir::Value*, 4> indices{
             access->Indices().Offset(to_replace.first_dynamic_index)};
         const core::type::Type* access_type = access->Result(0)->Type();
@@ -178,9 +212,9 @@
             vector_index = indices.Pop();
         }
 
+        auto addrspace = var->Type()->As<core::type::Pointer>()->AddressSpace();
         core::ir::Instruction* new_access = builder.Access(
-            ir.Types().ptr(core::AddressSpace::kFunction, access_type, core::Access::kReadWrite),
-            local, indices);
+            ir.Types().ptr(addrspace, access_type, core::Access::kReadWrite), var, indices);
         new_access->InsertBefore(access);
 
         core::ir::Instruction* load = nullptr;
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index_test.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index_test.cc
index e4adae9..657488b 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index_test.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index_test.cc
@@ -430,5 +430,449 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvWriter_VarForDynamicIndexTest, MultipleAccessesToFuncParam_FromDifferentBlocks) {
+    auto* arr = b.FunctionParam(ty.array<i32, 4>());
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* idx_a = b.FunctionParam(ty.i32());
+    auto* idx_b = b.FunctionParam(ty.i32());
+    auto* func = b.Function("func", ty.i32());
+    func->SetParams({arr, cond, idx_a, idx_b});
+    b.Append(func->Block(), [&] {  //
+        auto* if_ = b.If(cond);
+        b.Append(if_->True(), [&] {  //
+            b.Return(func, b.Access(ty.i32(), arr, idx_a));
+        });
+        b.Append(if_->False(), [&] {  //
+            b.Return(func, b.Access(ty.i32(), arr, idx_b));
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%func = func(%2:array<i32, 4>, %3:bool, %4:i32, %5:i32):i32 -> %b1 {
+  %b1 = block {
+    if %3 [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+        %6:i32 = access %2, %4
+        ret %6
+      }
+      %b3 = block {  # false
+        %7:i32 = access %2, %5
+        ret %7
+      }
+    }
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%func = func(%2:array<i32, 4>, %3:bool, %4:i32, %5:i32):i32 -> %b1 {
+  %b1 = block {
+    %6:ptr<function, array<i32, 4>, read_write> = var, %2
+    if %3 [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+        %7:ptr<function, i32, read_write> = access %6, %4
+        %8:i32 = load %7
+        ret %8
+      }
+      %b3 = block {  # false
+        %9:ptr<function, i32, read_write> = access %6, %5
+        %10:i32 = load %9
+        ret %10
+      }
+    }
+    unreachable
+  }
+}
+)";
+
+    Run(VarForDynamicIndex);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_VarForDynamicIndexTest,
+       MultipleAccessesToFuncParam_FromDifferentBlocks_WithLeadingConstantIndex) {
+    auto* arr = b.FunctionParam(ty.array(ty.array<i32, 4>(), 4));
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* idx_a = b.FunctionParam(ty.i32());
+    auto* idx_b = b.FunctionParam(ty.i32());
+    auto* func = b.Function("func", ty.i32());
+    func->SetParams({arr, cond, idx_a, idx_b});
+    b.Append(func->Block(), [&] {  //
+        auto* if_ = b.If(cond);
+        b.Append(if_->True(), [&] {  //
+            b.Return(func, b.Access(ty.i32(), arr, 0_u, idx_a));
+        });
+        b.Append(if_->False(), [&] {  //
+            b.Return(func, b.Access(ty.i32(), arr, 0_u, idx_b));
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%func = func(%2:array<array<i32, 4>, 4>, %3:bool, %4:i32, %5:i32):i32 -> %b1 {
+  %b1 = block {
+    if %3 [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+        %6:i32 = access %2, 0u, %4
+        ret %6
+      }
+      %b3 = block {  # false
+        %7:i32 = access %2, 0u, %5
+        ret %7
+      }
+    }
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%func = func(%2:array<array<i32, 4>, 4>, %3:bool, %4:i32, %5:i32):i32 -> %b1 {
+  %b1 = block {
+    %6:array<i32, 4> = access %2, 0u
+    %7:ptr<function, array<i32, 4>, read_write> = var, %6
+    if %3 [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+        %8:ptr<function, i32, read_write> = access %7, %4
+        %9:i32 = load %8
+        ret %9
+      }
+      %b3 = block {  # false
+        %10:ptr<function, i32, read_write> = access %7, %5
+        %11:i32 = load %10
+        ret %11
+      }
+    }
+    unreachable
+  }
+}
+)";
+
+    Run(VarForDynamicIndex);
+
+    EXPECT_EQ(expect, str());
+}
+TEST_F(SpirvWriter_VarForDynamicIndexTest, MultipleAccessesToBlockParam_FromDifferentBlocks) {
+    auto* arr = b.BlockParam(ty.array<i32, 4>());
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* idx_a = b.FunctionParam(ty.i32());
+    auto* idx_b = b.FunctionParam(ty.i32());
+    auto* func = b.Function("func", ty.i32());
+    func->SetParams({cond, idx_a, idx_b});
+    b.Append(func->Block(), [&] {  //
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({arr});
+        b.Append(loop->Body(), [&] {
+            auto* if_ = b.If(cond);
+            b.Append(if_->True(), [&] {  //
+                b.Return(func, b.Access(ty.i32(), arr, idx_a));
+            });
+            b.Append(if_->False(), [&] {  //
+                b.Return(func, b.Access(ty.i32(), arr, idx_b));
+            });
+            b.Unreachable();
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%func = func(%2:bool, %3:i32, %4:i32):i32 -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block (%5:array<i32, 4>) {  # body
+        if %2 [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
+            %6:i32 = access %5:array<i32, 4>, %3
+            ret %6
+          }
+          %b4 = block {  # false
+            %7:i32 = access %5:array<i32, 4>, %4
+            ret %7
+          }
+        }
+        unreachable
+      }
+    }
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%func = func(%2:bool, %3:i32, %4:i32):i32 -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block (%5:array<i32, 4>) {  # body
+        %6:ptr<function, array<i32, 4>, read_write> = var, %5:array<i32, 4>
+        if %2 [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
+            %7:ptr<function, i32, read_write> = access %6, %3
+            %8:i32 = load %7
+            ret %8
+          }
+          %b4 = block {  # false
+            %9:ptr<function, i32, read_write> = access %6, %4
+            %10:i32 = load %9
+            ret %10
+          }
+        }
+        unreachable
+      }
+    }
+    unreachable
+  }
+}
+)";
+
+    Run(VarForDynamicIndex);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_VarForDynamicIndexTest,
+       MultipleAccessesToBlockParam_FromDifferentBlocks_WithLeadingConstantIndex) {
+    auto* arr = b.BlockParam(ty.array(ty.array<i32, 4>(), 4));
+    auto* cond = b.FunctionParam(ty.bool_());
+    auto* idx_a = b.FunctionParam(ty.i32());
+    auto* idx_b = b.FunctionParam(ty.i32());
+    auto* func = b.Function("func", ty.i32());
+    func->SetParams({cond, idx_a, idx_b});
+    b.Append(func->Block(), [&] {  //
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({arr});
+        b.Append(loop->Body(), [&] {
+            auto* if_ = b.If(cond);
+            b.Append(if_->True(), [&] {  //
+                b.Return(func, b.Access(ty.i32(), arr, 0_u, idx_a));
+            });
+            b.Append(if_->False(), [&] {  //
+                b.Return(func, b.Access(ty.i32(), arr, 0_u, idx_b));
+            });
+            b.Unreachable();
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%func = func(%2:bool, %3:i32, %4:i32):i32 -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block (%5:array<array<i32, 4>, 4>) {  # body
+        if %2 [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
+            %6:i32 = access %5:array<array<i32, 4>, 4>, 0u, %3
+            ret %6
+          }
+          %b4 = block {  # false
+            %7:i32 = access %5:array<array<i32, 4>, 4>, 0u, %4
+            ret %7
+          }
+        }
+        unreachable
+      }
+    }
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%func = func(%2:bool, %3:i32, %4:i32):i32 -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block (%5:array<array<i32, 4>, 4>) {  # body
+        %6:array<i32, 4> = access %5:array<array<i32, 4>, 4>, 0u
+        %7:ptr<function, array<i32, 4>, read_write> = var, %6
+        if %2 [t: %b3, f: %b4] {  # if_1
+          %b3 = block {  # true
+            %8:ptr<function, i32, read_write> = access %7, %3
+            %9:i32 = load %8
+            ret %9
+          }
+          %b4 = block {  # false
+            %10:ptr<function, i32, read_write> = access %7, %4
+            %11:i32 = load %10
+            ret %11
+          }
+        }
+        unreachable
+      }
+    }
+    unreachable
+  }
+}
+)";
+
+    Run(VarForDynamicIndex);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_VarForDynamicIndexTest, MultipleAccessesToConstant_FromDifferentFunctions) {
+    auto* arr = b.Constant(mod.constant_values.Zero(ty.array<i32, 4>()));
+
+    auto* idx_a = b.FunctionParam(ty.i32());
+    auto* func_a = b.Function("func_a", ty.i32());
+    func_a->SetParams({idx_a});
+    b.Append(func_a->Block(), [&] {  //
+        b.Return(func_a, b.Access(ty.i32(), arr, idx_a));
+    });
+
+    auto* idx_b = b.FunctionParam(ty.i32());
+    auto* func_b = b.Function("func_b", ty.i32());
+    func_b->SetParams({idx_b});
+    b.Append(func_b->Block(), [&] {  //
+        b.Return(func_b, b.Access(ty.i32(), arr, idx_b));
+    });
+
+    auto* idx_c = b.FunctionParam(ty.i32());
+    auto* func_c = b.Function("func_c", ty.i32());
+    func_c->SetParams({idx_c});
+    b.Append(func_c->Block(), [&] {  //
+        b.Return(func_c, b.Access(ty.i32(), arr, idx_c));
+    });
+
+    auto* src = R"(
+%func_a = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = access array<i32, 4>(0i), %2
+    ret %3
+  }
+}
+%func_b = func(%5:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = access array<i32, 4>(0i), %5
+    ret %6
+  }
+}
+%func_c = func(%8:i32):i32 -> %b3 {
+  %b3 = block {
+    %9:i32 = access array<i32, 4>(0i), %8
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %1:ptr<private, array<i32, 4>, read_write> = var, array<i32, 4>(0i)
+}
+
+%func_a = func(%3:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:ptr<private, i32, read_write> = access %1, %3
+    %5:i32 = load %4
+    ret %5
+  }
+}
+%func_b = func(%7:i32):i32 -> %b3 {
+  %b3 = block {
+    %8:ptr<private, i32, read_write> = access %1, %7
+    %9:i32 = load %8
+    ret %9
+  }
+}
+%func_c = func(%11:i32):i32 -> %b4 {
+  %b4 = block {
+    %12:ptr<private, i32, read_write> = access %1, %11
+    %13:i32 = load %12
+    ret %13
+  }
+}
+)";
+
+    Run(VarForDynamicIndex);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_VarForDynamicIndexTest,
+       MultipleAccessesToConstant_FromDifferentFunctions_WithLeadingConstantIndex) {
+    auto* arr = b.Constant(mod.constant_values.Zero(ty.array(ty.array<i32, 4>(), 4)));
+
+    auto* idx_a = b.FunctionParam(ty.i32());
+    auto* func_a = b.Function("func_a", ty.i32());
+    func_a->SetParams({idx_a});
+    b.Append(func_a->Block(), [&] {  //
+        b.Return(func_a, b.Access(ty.i32(), arr, 0_u, idx_a));
+    });
+
+    auto* idx_b = b.FunctionParam(ty.i32());
+    auto* func_b = b.Function("func_b", ty.i32());
+    func_b->SetParams({idx_b});
+    b.Append(func_b->Block(), [&] {  //
+        b.Return(func_b, b.Access(ty.i32(), arr, 0_u, idx_b));
+    });
+
+    auto* idx_c = b.FunctionParam(ty.i32());
+    auto* func_c = b.Function("func_c", ty.i32());
+    func_c->SetParams({idx_c});
+    b.Append(func_c->Block(), [&] {  //
+        b.Return(func_c, b.Access(ty.i32(), arr, 0_u, idx_c));
+    });
+
+    auto* src = R"(
+%func_a = func(%2:i32):i32 -> %b1 {
+  %b1 = block {
+    %3:i32 = access array<array<i32, 4>, 4>(array<i32, 4>(0i)), 0u, %2
+    ret %3
+  }
+}
+%func_b = func(%5:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = access array<array<i32, 4>, 4>(array<i32, 4>(0i)), 0u, %5
+    ret %6
+  }
+}
+%func_c = func(%8:i32):i32 -> %b3 {
+  %b3 = block {
+    %9:i32 = access array<array<i32, 4>, 4>(array<i32, 4>(0i)), 0u, %8
+    ret %9
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %1:ptr<private, array<i32, 4>, read_write> = var, array<i32, 4>(0i)
+}
+
+%func_a = func(%3:i32):i32 -> %b2 {
+  %b2 = block {
+    %4:ptr<private, i32, read_write> = access %1, %3
+    %5:i32 = load %4
+    ret %5
+  }
+}
+%func_b = func(%7:i32):i32 -> %b3 {
+  %b3 = block {
+    %8:ptr<private, i32, read_write> = access %1, %7
+    %9:i32 = load %8
+    ret %9
+  }
+}
+%func_c = func(%11:i32):i32 -> %b4 {
+  %b4 = block {
+    %12:ptr<private, i32, read_write> = access %1, %11
+    %13:i32 = load %12
+    ret %13
+  }
+}
+)";
+
+    Run(VarForDynamicIndex);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc b/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc
index b745f56..3b41233 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_fuzz.cc
@@ -46,7 +46,9 @@
         return;
     }
 
-    auto dst = tint::wgsl::writer::IRToProgram(ir);
+    writer::ProgramOptions program_options;
+    program_options.allowed_features = AllowedFeatures::Everything();
+    auto dst = tint::wgsl::writer::IRToProgram(ir, program_options);
     if (!dst.IsValid()) {
         std::cerr << "IR:\n" << core::ir::Disassemble(ir) << std::endl;
         if (auto result = tint::wgsl::writer::Generate(dst, {}); result == Success) {
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index 5cb3f99..6573c32 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -196,6 +196,14 @@
 )");
 }
 
+TEST_F(IRToProgramRoundtripTest, SingleFunction_UnrestrictedPointerParameters) {
+    RUN_TEST(R"(
+fn f(p : ptr<uniform, i32>) -> i32 {
+  return *(p);
+}
+)");
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Struct declaration
 ////////////////////////////////////////////////////////////////////////////////
@@ -1921,6 +1929,21 @@
 )");
 }
 
+TEST_F(IRToProgramRoundtripTest, PhonyAssign_Conversion) {
+    RUN_TEST(R"(
+fn f() {
+  let i = 42i;
+  _ = u32(i);
+}
+)",
+             R"(
+fn f() {
+  let i = 42i;
+  _ = u32(i);
+}
+)");
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // let
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/lang/wgsl/reader/lower/lower.cc b/src/tint/lang/wgsl/reader/lower/lower.cc
index 21782ee..94f2bac 100644
--- a/src/tint/lang/wgsl/reader/lower/lower.cc
+++ b/src/tint/lang/wgsl/reader/lower/lower.cc
@@ -35,6 +35,7 @@
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/wgsl/builtin_fn.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
+#include "src/tint/utils/ice/ice.h"
 
 namespace tint::wgsl::reader {
 namespace {
@@ -166,10 +167,15 @@
         CASE(kAtomicCompareExchangeWeak)
         CASE(kSubgroupBallot)
         CASE(kSubgroupBroadcast)
-        default:
-            TINT_ICE() << "unhandled builtin function: " << fn;
-            return core::BuiltinFn::kNone;
+
+        case tint::wgsl::BuiltinFn::kBitcast:               // should lower to ir::Bitcast
+        case tint::wgsl::BuiltinFn::kWorkgroupUniformLoad:  // should be handled in Lower()
+        case tint::wgsl::BuiltinFn::kTintMaterialize:
+        case tint::wgsl::BuiltinFn::kNone:
+            break;
     }
+    TINT_ICE() << "unhandled builtin function: " << fn;
+    return core::BuiltinFn::kNone;
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index 8bf72e7..8840d93 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -68,6 +68,7 @@
 #include "src/tint/lang/core/ir/user_call.h"
 #include "src/tint/lang/core/ir/validator.h"
 #include "src/tint/lang/core/ir/var.h"
+#include "src/tint/lang/core/texel_format.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/core/type/depth_multisampled_texture.h"
 #include "src/tint/lang/core/type/depth_texture.h"
@@ -75,7 +76,9 @@
 #include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/core/type/sampler.h"
+#include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture.h"
+#include "src/tint/lang/core/type/type.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 #include "src/tint/lang/wgsl/ir/unary.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
@@ -968,6 +971,10 @@
                 return b.ty.sampled_texture(t->dim(), el);
             },
             [&](const core::type::StorageTexture* t) {
+                if (RequiresChromiumInternalGraphite(t)) {
+                    Enable(wgsl::Extension::kChromiumInternalGraphite);
+                }
+
                 return b.ty.storage_texture(t->dim(), t->texel_format(), t->access());
             },
             [&](const core::type::Sampler* s) { return b.ty.sampler(s->kind()); },
@@ -1051,12 +1058,17 @@
         });
     }
 
-    /// Associates the IR value @p value with the AST expression @p expr.
+    /// Associates the IR value @p value with the AST expression @p expr if it is used, otherwise
+    /// creates a phony assignment with @p expr.
     void Bind(const core::ir::Value* value, const ast::Expression* expr) {
         TINT_ASSERT(value);
-        // Value will be inlined at its place of usage.
-        if (TINT_UNLIKELY(!bindings_.Add(value, InlinedValue{expr}))) {
-            TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
+        if (value->IsUsed()) {
+            // Value will be inlined at its place of usage.
+            if (TINT_UNLIKELY(!bindings_.Add(value, InlinedValue{expr}))) {
+                TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
+            }
+        } else {
+            Append(b.Assign(b.Phony(), expr));
         }
     }
 
@@ -1192,6 +1204,12 @@
                 return false;
         }
     }
+
+    /// @returns true if the storage texture type requires the kChromiumInternalGraphite extension
+    /// to be enabled.
+    bool RequiresChromiumInternalGraphite(const core::type::StorageTexture* tex) {
+        return tex->texel_format() == core::TexelFormat::kR8Unorm;
+    }
 };
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
index 10e88ca..30e6795 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
@@ -30,8 +30,12 @@
 #include <sstream>
 #include <string>
 
+#include "src/tint/lang/core/access.h"
+#include "src/tint/lang/core/address_space.h"
 #include "src/tint/lang/core/ir/disassembler.h"
+#include "src/tint/lang/core/texel_format.h"
 #include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/core/type/texture_dimension.h"
 #include "src/tint/lang/wgsl/ir/builtin_call.h"
 #include "src/tint/lang/wgsl/ir/unary.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
@@ -2629,5 +2633,23 @@
 )");
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// chromium_internal_graphite
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(IRToProgramTest, Enable_ChromiumInternalGraphite_SubgroupBallot) {
+    b.Append(b.ir.root_block, [&] {
+        auto t = b.Var("T", ty.ref<core::AddressSpace::kHandle>(ty.Get<core::type::StorageTexture>(
+                                core::type::TextureDimension::k2d, core::TexelFormat::kR8Unorm,
+                                core::Access::kRead, ty.f32())));
+        t->SetBindingPoint(0, 0);
+    });
+
+    EXPECT_WGSL(R"(
+enable chromium_internal_graphite;
+
+@group(0) @binding(0) var T : texture_storage_2d<r8unorm, read>;
+)");
+}
+
 }  // namespace
 }  // namespace tint::wgsl::writer
diff --git a/src/tint/lang/wgsl/writer/raise/BUILD.cmake b/src/tint/lang/wgsl/writer/raise/BUILD.cmake
index d2ae1d1..ff5b855 100644
--- a/src/tint/lang/wgsl/writer/raise/BUILD.cmake
+++ b/src/tint/lang/wgsl/writer/raise/BUILD.cmake
@@ -112,3 +112,38 @@
 tint_target_add_external_dependencies(tint_lang_wgsl_writer_raise_test test
   "gtest"
 )
+
+################################################################################
+# Target:    tint_lang_wgsl_writer_raise_fuzz
+# Kind:      fuzz
+################################################################################
+tint_add_target(tint_lang_wgsl_writer_raise_fuzz fuzz
+  lang/wgsl/writer/raise/ptr_to_ref_fuzz.cc
+  lang/wgsl/writer/raise/raise_fuzz.cc
+  lang/wgsl/writer/raise/rename_conflicts_fuzz.cc
+  lang/wgsl/writer/raise/value_to_let_fuzz.cc
+)
+
+tint_target_add_dependencies(tint_lang_wgsl_writer_raise_fuzz fuzz
+  tint_api_common
+  tint_cmd_fuzz_ir_fuzz
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_lang_wgsl_writer_raise
+  tint_utils_bytes
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
diff --git a/src/tint/lang/wgsl/writer/raise/BUILD.gn b/src/tint/lang/wgsl/writer/raise/BUILD.gn
index 095b41e..1f5cfa8 100644
--- a/src/tint/lang/wgsl/writer/raise/BUILD.gn
+++ b/src/tint/lang/wgsl/writer/raise/BUILD.gn
@@ -112,3 +112,35 @@
     ]
   }
 }
+
+tint_fuzz_source_set("fuzz") {
+  sources = [
+    "ptr_to_ref_fuzz.cc",
+    "raise_fuzz.cc",
+    "rename_conflicts_fuzz.cc",
+    "value_to_let_fuzz.cc",
+  ]
+  deps = [
+    "${tint_src_dir}/api/common",
+    "${tint_src_dir}/cmd/fuzz/ir:fuzz",
+    "${tint_src_dir}/lang/core",
+    "${tint_src_dir}/lang/core/constant",
+    "${tint_src_dir}/lang/core/ir",
+    "${tint_src_dir}/lang/core/type",
+    "${tint_src_dir}/lang/wgsl/writer/raise",
+    "${tint_src_dir}/utils/bytes",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/id",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/reflection",
+    "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
+    "${tint_src_dir}/utils/symbol",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+}
diff --git a/src/tint/lang/wgsl/writer/raise/ptr_to_ref_fuzz.cc b/src/tint/lang/wgsl/writer/raise/ptr_to_ref_fuzz.cc
new file mode 100644
index 0000000..b67fb22
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/raise/ptr_to_ref_fuzz.cc
@@ -0,0 +1,50 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT\ OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/writer/raise/ptr_to_ref.h"
+
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::wgsl::writer::raise {
+namespace {
+
+void PtrToRefFuzzer(core::ir::Module& module) {
+    if (auto res = PtrToRef(module); res != Success) {
+        return;
+    }
+
+    core::ir::Capabilities capabilities{core::ir::Capability::kAllowRefTypes};
+    if (auto res = Validate(module, capabilities); res != Success) {
+        TINT_ICE() << "result of PtrToRef failed IR validation\n" << res.Failure();
+    }
+}
+
+}  // namespace
+}  // namespace tint::wgsl::writer::raise
+
+TINT_IR_MODULE_FUZZER(tint::wgsl::writer::raise::PtrToRefFuzzer);
diff --git a/src/tint/lang/wgsl/writer/raise/raise.cc b/src/tint/lang/wgsl/writer/raise/raise.cc
index 46cfe7e..dcb3e4e 100644
--- a/src/tint/lang/wgsl/writer/raise/raise.cc
+++ b/src/tint/lang/wgsl/writer/raise/raise.cc
@@ -108,6 +108,10 @@
         CASE(kPack2X16Unorm)
         CASE(kPack4X8Snorm)
         CASE(kPack4X8Unorm)
+        CASE(kPack4XI8)
+        CASE(kPack4XU8)
+        CASE(kPack4XI8Clamp)
+        CASE(kPack4XU8Clamp)
         CASE(kPow)
         CASE(kQuantizeToF16)
         CASE(kRadians)
@@ -133,6 +137,8 @@
         CASE(kUnpack2X16Unorm)
         CASE(kUnpack4X8Snorm)
         CASE(kUnpack4X8Unorm)
+        CASE(kUnpack4XI8)
+        CASE(kUnpack4XU8)
         CASE(kWorkgroupBarrier)
         CASE(kTextureBarrier)
         CASE(kTextureDimensions)
@@ -163,10 +169,11 @@
         CASE(kAtomicCompareExchangeWeak)
         CASE(kSubgroupBallot)
         CASE(kSubgroupBroadcast)
-        default:
-            TINT_ICE() << "unhandled builtin function: " << fn;
-            return wgsl::BuiltinFn::kNone;
+        case core::BuiltinFn::kNone:
+            break;
     }
+    TINT_ICE() << "unhandled builtin function: " << fn;
+    return wgsl::BuiltinFn::kNone;
 }
 
 void ReplaceBuiltinFnCall(core::ir::Module& mod, core::ir::CoreBuiltinCall* call) {
diff --git a/src/tint/lang/wgsl/writer/raise/raise_fuzz.cc b/src/tint/lang/wgsl/writer/raise/raise_fuzz.cc
new file mode 100644
index 0000000..e63783c
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/raise/raise_fuzz.cc
@@ -0,0 +1,50 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT\ OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/writer/raise/raise.h"
+
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::wgsl::writer::raise {
+namespace {
+
+void RaiseFuzzer(core::ir::Module& module) {
+    if (auto res = Raise(module); res != Success) {
+        return;
+    }
+
+    core::ir::Capabilities capabilities{core::ir::Capability::kAllowRefTypes};
+    if (auto res = Validate(module, capabilities); res != Success) {
+        TINT_ICE() << "result of Raise failed IR validation\n" << res.Failure();
+    }
+}
+
+}  // namespace
+}  // namespace tint::wgsl::writer::raise
+
+TINT_IR_MODULE_FUZZER(tint::wgsl::writer::raise::RaiseFuzzer);
diff --git a/src/tint/lang/wgsl/writer/raise/rename_conflicts_fuzz.cc b/src/tint/lang/wgsl/writer/raise/rename_conflicts_fuzz.cc
new file mode 100644
index 0000000..8ac74e0
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/raise/rename_conflicts_fuzz.cc
@@ -0,0 +1,50 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT\ OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/writer/raise/rename_conflicts.h"
+
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::wgsl::writer::raise {
+namespace {
+
+void RenameConflictsFuzzer(core::ir::Module& module) {
+    if (auto res = RenameConflicts(module); res != Success) {
+        return;
+    }
+
+    core::ir::Capabilities capabilities;
+    if (auto res = Validate(module, capabilities); res != Success) {
+        TINT_ICE() << "result of RenameConflicts failed IR validation\n" << res.Failure();
+    }
+}
+
+}  // namespace
+}  // namespace tint::wgsl::writer::raise
+
+TINT_IR_MODULE_FUZZER(tint::wgsl::writer::raise::RenameConflictsFuzzer);
diff --git a/src/tint/lang/wgsl/writer/raise/value_to_let_fuzz.cc b/src/tint/lang/wgsl/writer/raise/value_to_let_fuzz.cc
new file mode 100644
index 0000000..84755d0
--- /dev/null
+++ b/src/tint/lang/wgsl/writer/raise/value_to_let_fuzz.cc
@@ -0,0 +1,50 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT\ OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/writer/raise/value_to_let.h"
+
+#include "src/tint/cmd/fuzz/ir/fuzz.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+namespace tint::wgsl::writer::raise {
+namespace {
+
+void ValueToLetFuzzer(core::ir::Module& module) {
+    if (auto res = ValueToLet(module); res != Success) {
+        return;
+    }
+
+    core::ir::Capabilities capabilities;
+    if (auto res = Validate(module, capabilities); res != Success) {
+        TINT_ICE() << "result of ValueToLet failed IR validation\n" << res.Failure();
+    }
+}
+
+}  // namespace
+}  // namespace tint::wgsl::writer::raise
+
+TINT_IR_MODULE_FUZZER(tint::wgsl::writer::raise::ValueToLetFuzzer);
diff --git a/src/tint/utils/bytes/BUILD.bazel b/src/tint/utils/bytes/BUILD.bazel
index 3b0bb76..3daeace 100644
--- a/src/tint/utils/bytes/BUILD.bazel
+++ b/src/tint/utils/bytes/BUILD.bazel
@@ -39,11 +39,13 @@
 cc_library(
   name = "bytes",
   srcs = [
+    "buffer_reader.cc",
     "bytes.cc",
     "reader.cc",
     "writer.cc",
   ],
   hdrs = [
+    "buffer_reader.h",
     "buffer_writer.h",
     "decoder.h",
     "endianness.h",
@@ -71,9 +73,9 @@
   name = "test",
   alwayslink = True,
   srcs = [
+    "buffer_reader_test.cc",
     "buffer_writer_test.cc",
     "decoder_test.cc",
-    "reader_test.cc",
     "swap_test.cc",
   ],
   deps = [
diff --git a/src/tint/utils/bytes/BUILD.cmake b/src/tint/utils/bytes/BUILD.cmake
index 6d919d3..3438bbb 100644
--- a/src/tint/utils/bytes/BUILD.cmake
+++ b/src/tint/utils/bytes/BUILD.cmake
@@ -39,6 +39,8 @@
 # Kind:      lib
 ################################################################################
 tint_add_target(tint_utils_bytes lib
+  utils/bytes/buffer_reader.cc
+  utils/bytes/buffer_reader.h
   utils/bytes/buffer_writer.h
   utils/bytes/bytes.cc
   utils/bytes/decoder.h
@@ -69,9 +71,9 @@
 # Kind:      test
 ################################################################################
 tint_add_target(tint_utils_bytes_test test
+  utils/bytes/buffer_reader_test.cc
   utils/bytes/buffer_writer_test.cc
   utils/bytes/decoder_test.cc
-  utils/bytes/reader_test.cc
   utils/bytes/swap_test.cc
 )
 
diff --git a/src/tint/utils/bytes/BUILD.gn b/src/tint/utils/bytes/BUILD.gn
index d3babe2..6618381 100644
--- a/src/tint/utils/bytes/BUILD.gn
+++ b/src/tint/utils/bytes/BUILD.gn
@@ -44,6 +44,8 @@
 
 libtint_source_set("bytes") {
   sources = [
+    "buffer_reader.cc",
+    "buffer_reader.h",
     "buffer_writer.h",
     "bytes.cc",
     "decoder.h",
@@ -71,9 +73,9 @@
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
     sources = [
+      "buffer_reader_test.cc",
       "buffer_writer_test.cc",
       "decoder_test.cc",
-      "reader_test.cc",
       "swap_test.cc",
     ]
     deps = [
diff --git a/src/tint/utils/bytes/buffer_reader.cc b/src/tint/utils/bytes/buffer_reader.cc
new file mode 100644
index 0000000..4294fef
--- /dev/null
+++ b/src/tint/utils/bytes/buffer_reader.cc
@@ -0,0 +1,46 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/bytes/buffer_reader.h"
+
+namespace tint::bytes {
+
+BufferReader::~BufferReader() = default;
+
+size_t BufferReader::Read(std::byte* out, size_t count) {
+    size_t n = std::min(count, bytes_remaining_);
+    memcpy(out, data_, n);
+    data_ += n;
+    bytes_remaining_ -= n;
+    return n;
+}
+
+bool BufferReader::IsEOF() const {
+    return bytes_remaining_ == 0;
+}
+
+}  // namespace tint::bytes
diff --git a/src/tint/utils/bytes/buffer_reader.h b/src/tint/utils/bytes/buffer_reader.h
new file mode 100644
index 0000000..0367bb0
--- /dev/null
+++ b/src/tint/utils/bytes/buffer_reader.h
@@ -0,0 +1,81 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_BYTES_BUFFER_READER_H_
+#define SRC_TINT_UTILS_BYTES_BUFFER_READER_H_
+
+#include <algorithm>
+#include <string>
+
+#include "src/tint/utils/bytes/reader.h"
+#include "src/tint/utils/ice/ice.h"
+
+namespace tint::bytes {
+
+/// BufferReader is an implementation of the Reader interface backed by a buffer.
+class BufferReader final : public Reader {
+  public:
+    // Destructor
+    ~BufferReader() override;
+
+    /// 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);
+    }
+
+    /// Constructor
+    /// @param string the string to read from
+    explicit BufferReader(std::string_view string)
+        : data_(reinterpret_cast<const std::byte*>(string.data())),
+          bytes_remaining_(string.length()) {}
+
+    /// 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);
+    }
+
+    /// @copydoc Reader::Read
+    size_t Read(std::byte* out, size_t count) override;
+
+    /// @copydoc Reader::IsEOF
+    bool IsEOF() const override;
+
+  private:
+    /// The data to read from
+    const std::byte* data_ = nullptr;
+
+    /// The number of bytes remaining
+    size_t bytes_remaining_ = 0;
+};
+
+}  // namespace tint::bytes
+
+#endif  // SRC_TINT_UTILS_BYTES_BUFFER_READER_H_
diff --git a/src/tint/utils/bytes/reader_test.cc b/src/tint/utils/bytes/buffer_reader_test.cc
similarity index 97%
rename from src/tint/utils/bytes/reader_test.cc
rename to src/tint/utils/bytes/buffer_reader_test.cc
index 2e90e88..b0b72d8 100644
--- a/src/tint/utils/bytes/reader_test.cc
+++ b/src/tint/utils/bytes/buffer_reader_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2023 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/utils/bytes/reader.h"
+#include "src/tint/utils/bytes/buffer_reader.h"
 
 #include "gtest/gtest.h"
 
@@ -33,7 +33,7 @@
 namespace {
 
 template <typename... ARGS>
-auto Data(ARGS&&... args) {
+auto Data(ARGS... args) {
     return std::array{std::byte{static_cast<uint8_t>(args)}...};
 }
 
diff --git a/src/tint/utils/bytes/decoder_test.cc b/src/tint/utils/bytes/decoder_test.cc
index 125d7e6..11f663a 100644
--- a/src/tint/utils/bytes/decoder_test.cc
+++ b/src/tint/utils/bytes/decoder_test.cc
@@ -34,6 +34,7 @@
 #include <utility>
 
 #include "gmock/gmock.h"
+#include "src/tint/utils/bytes/buffer_reader.h"
 #include "src/tint/utils/result/result.h"
 
 namespace tint {
diff --git a/src/tint/utils/bytes/reader.cc b/src/tint/utils/bytes/reader.cc
index fc2dfc2..e12a020 100644
--- a/src/tint/utils/bytes/reader.cc
+++ b/src/tint/utils/bytes/reader.cc
@@ -31,18 +31,4 @@
 
 Reader::~Reader() = default;
 
-BufferReader::~BufferReader() = default;
-
-size_t BufferReader::Read(std::byte* out, size_t count) {
-    size_t n = std::min(count, bytes_remaining_);
-    memcpy(out, data_, n);
-    data_ += n;
-    bytes_remaining_ -= n;
-    return n;
-}
-
-bool BufferReader::IsEOF() const {
-    return bytes_remaining_ == 0;
-}
-
 }  // namespace tint::bytes
diff --git a/src/tint/utils/bytes/reader.h b/src/tint/utils/bytes/reader.h
index 3b5f51a..f0d074a 100644
--- a/src/tint/utils/bytes/reader.h
+++ b/src/tint/utils/bytes/reader.h
@@ -28,15 +28,11 @@
 #ifndef SRC_TINT_UTILS_BYTES_READER_H_
 #define SRC_TINT_UTILS_BYTES_READER_H_
 
-#include <algorithm>
-#include <cstdint>
 #include <string>
 
 #include "src/tint/utils/bytes/endianness.h"
 #include "src/tint/utils/bytes/swap.h"
-#include "src/tint/utils/containers/slice.h"
 #include "src/tint/utils/result/result.h"
-
 namespace tint::bytes {
 
 /// A binary stream reader interface
@@ -114,46 +110,6 @@
     }
 };
 
-/// BufferReader is an implementation of the Reader interface backed by a buffer.
-class BufferReader final : public Reader {
-  public:
-    // Destructor
-    ~BufferReader() override;
-
-    /// 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);
-    }
-
-    /// Constructor
-    /// @param string the string to read from
-    explicit BufferReader(std::string_view string)
-        : data_(reinterpret_cast<const std::byte*>(string.data())),
-          bytes_remaining_(string.length()) {}
-
-    /// 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);
-    }
-
-    /// @copydoc Reader::Read
-    size_t Read(std::byte* out, size_t count) override;
-
-    /// @copydoc Reader::IsEOF
-    bool IsEOF() const override;
-
-  private:
-    /// The data to read from
-    const std::byte* data_ = nullptr;
-
-    /// The number of bytes remaining
-    size_t bytes_remaining_ = 0;
-};
-
 }  // namespace tint::bytes
 
 #endif  // SRC_TINT_UTILS_BYTES_READER_H_