Import Tint changes from Dawn

Changes:
  - 48360eaf7a0aa3c275654ba88858c72003208eda [tint][ir] Use CheckOperandsMatchTarget() for exit instru... by Ben Clayton <bclayton@google.com>
  - 6654d1f4064770aec50ba2ff8de1bde4b295e237 [tint][ir] Validate next_iteration value types match body... by Ben Clayton <bclayton@google.com>
  - fac459af919a4e6c2add45724ade2b83ccb0c854 [tint][ir] Validate continue value types match continuing... by Ben Clayton <bclayton@google.com>
  - c951b86b055943d20ea686bb16dbf05aae691db1 [tint][ir] Validate break_if value types match body block... by Ben Clayton <bclayton@google.com>
  - c7d4205172c8ae635e86d328873f2f67c9b6e8d4 Add generating Tint IR fuzzer corpus by Ryan Harrison <rharrison@chromium.org>
  - 47366120df82fe6d7c3874afe21e4a956ac8bc9d Tint/Inspector: Add blend_src to entry points by Jiawei Shao <jiawei.shao@intel.com>
  - f46acc5b7e8d6a899952e636be4c778b3f1b7675 [tint][ir] Move terminator validation to CheckTerminator() by Ben Clayton <bclayton@google.com>
  - 94ef15d00f29e8a241fb88648dca3121ee74d5fe [tint][ast][fuzz] Skip MultiplanarExternalTexture fuzzer ... by Ben Clayton <bclayton@google.com>
  - b6bff7f647a1615ebb73a6a435029465205d24d9 [tint][ast][fuzz] Replace Options DI with Context by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 48360eaf7a0aa3c275654ba88858c72003208eda
Change-Id: Ic74746d2708a0f776bad3479818acf0f01183c59
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/189300
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index af3d7a5..2544db7 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -275,7 +275,7 @@
 if (tint_has_fuzzers) {
   action("tint_generate_wgsl_corpus") {
     script = "${tint_src_dir}/cmd/fuzz/wgsl/generate_wgsl_corpus.py"
-    sources = [ "${tint_src_dir}/cmd/fuzz/wgsl/generate_wgsl_corpus.py" ]
+    sources = [ "${script}" ]
     args = [
       "--stamp=" + rebase_path(fuzzer_corpus_wgsl_stamp, root_build_dir),
       rebase_path("${tint_root_dir}/test", root_build_dir),
@@ -283,6 +283,23 @@
     ]
     outputs = [ fuzzer_corpus_wgsl_stamp ]
   }
+
+  if (tint_build_cmds) {
+    if (tint_build_wgsl_reader && tint_build_ir_binary) {
+      action("tint_generate_ir_corpus") {
+        script = "${tint_src_dir}/cmd/fuzz/wgsl/generate_ir_corpus.py"
+        sources = [ "${script}" ]
+        deps = [ "${tint_src_dir}/cmd/tint" ]
+        args = [
+          "--stamp=" + rebase_path(fuzzer_corpus_ir_stamp, root_build_dir),
+          rebase_path("${root_build_dir}/tint", root_build_dir),
+          rebase_path("${tint_root_dir}/test", root_build_dir),
+          rebase_path(fuzzer_corpus_ir_dir, root_build_dir),
+        ]
+        outputs = [ fuzzer_corpus_ir_stamp ]
+      }
+    }
+  }
 }
 
 ###############################################################################
diff --git a/src/tint/cmd/fuzz/ir/fuzz.cc b/src/tint/cmd/fuzz/ir/fuzz.cc
index 771a37a..08f9568 100644
--- a/src/tint/cmd/fuzz/ir/fuzz.cc
+++ b/src/tint/cmd/fuzz/ir/fuzz.cc
@@ -41,32 +41,13 @@
 
 #if TINT_BUILD_WGSL_READER
 namespace tint::fuzz::ir {
-namespace {
-
-bool IsUnsupported(const ast::Enable* enable) {
-    for (auto ext : enable->extensions) {
-        switch (ext->name) {
-            case tint::wgsl::Extension::kChromiumExperimentalFramebufferFetch:
-            case tint::wgsl::Extension::kChromiumExperimentalPixelLocal:
-            case tint::wgsl::Extension::kChromiumExperimentalPushConstant:
-            case tint::wgsl::Extension::kChromiumInternalDualSourceBlending:
-            case tint::wgsl::Extension::kChromiumInternalRelaxedUniformLayout:
-                return true;
-            default:
-                break;
-        }
-    }
-    return false;
-}
-
-}  // namespace
 
 void Register(const IRFuzzer& fuzzer) {
     wgsl::Register({
         fuzzer.name,
-        [fn = fuzzer.fn](const Program& program, const fuzz::wgsl::Options& options,
+        [fn = fuzzer.fn](const Program& program, const fuzz::wgsl::Context& context,
                          Slice<const std::byte> data) {
-            if (program.AST().Enables().Any(IsUnsupported)) {
+            if (program.AST().Enables().Any(tint::wgsl::reader::IsUnsupportedByIR)) {
                 return;
             }
 
diff --git a/src/tint/cmd/fuzz/wgsl/fuzz.cc b/src/tint/cmd/fuzz/wgsl/fuzz.cc
index 17c2e1c..20de4f5 100644
--- a/src/tint/cmd/fuzz/wgsl/fuzz.cc
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.cc
@@ -28,14 +28,24 @@
 #include "src/tint/cmd/fuzz/wgsl/fuzz.h"
 
 #include <iostream>
+#include <string>
+#include <string_view>
 #include <thread>
 
+#include "src/tint/lang/core/builtin_type.h"
+#include "src/tint/lang/wgsl/ast/alias.h"
+#include "src/tint/lang/wgsl/ast/function.h"
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/struct.h"
+#include "src/tint/lang/wgsl/ast/variable.h"
+#include "src/tint/lang/wgsl/builtin_fn.h"
 #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"
 #include "src/tint/utils/macros/static_init.h"
+#include "src/tint/utils/rtti/switch.h"
 
 #if TINT_BUILD_WGSL_WRITER
 #include "src/tint/lang/wgsl/program/program.h"
@@ -61,6 +71,42 @@
     __builtin_trap();
 }
 
+bool IsBuiltinFn(std::string_view name) {
+    return tint::wgsl::ParseBuiltinFn(name) != tint::wgsl::BuiltinFn::kNone;
+}
+
+bool IsBuiltinType(std::string_view name) {
+    return tint::core::ParseBuiltinType(name) != tint::core::BuiltinType::kUndefined;
+}
+
+/// Scans @p program for patterns, returning a set of ProgramProperties.
+EnumSet<ProgramProperties> ScanProgramProperties(const Program& program) {
+    EnumSet<ProgramProperties> out;
+    auto check = [&](std::string_view name) {
+        if (IsBuiltinFn(name)) {
+            out.Add(ProgramProperties::kBuiltinFnsShadowed);
+        }
+        if (IsBuiltinType(name)) {
+            out.Add(ProgramProperties::kBuiltinTypesShadowed);
+        }
+    };
+
+    for (auto* node : program.ASTNodes().Objects()) {
+        tint::Switch(
+            node,  //
+            [&](const ast::Variable* variable) { check(variable->name->symbol.NameView()); },
+            [&](const ast::Function* fn) { check(fn->name->symbol.NameView()); },
+            [&](const ast::Struct* str) { check(str->name->symbol.NameView()); },
+            [&](const ast::Alias* alias) { check(alias->name->symbol.NameView()); });
+
+        if (out.Contains(ProgramProperties::kBuiltinFnsShadowed) &&
+            out.Contains(ProgramProperties::kBuiltinTypesShadowed)) {
+            break;  // Early exit - nothing more to find.
+        }
+    }
+    return out;
+}
+
 }  // namespace
 
 void Register(const ProgramFuzzer& fuzzer) {
@@ -100,6 +146,10 @@
         return;
     }
 
+    Context context;
+    context.options = options;
+    context.program_properties = ScanProgramProperties(program);
+
     // Run each of the program fuzzer functions
     if (options.run_concurrently) {
         size_t n = Fuzzers().Length();
@@ -110,13 +160,13 @@
                 Fuzzers()[i].name.find(options.filter) == std::string::npos) {
                 continue;
             }
-            threads.Push(std::thread([i, &program, &data, &options] {
+            threads.Push(std::thread([i, &program, &data, &context] {
                 auto& fuzzer = Fuzzers()[i];
                 currently_running = fuzzer.name;
-                if (options.verbose) {
+                if (context.options.verbose) {
                     std::cout << " • [" << i << "] Running: " << currently_running << std::endl;
                 }
-                fuzzer.fn(program, options, data);
+                fuzzer.fn(program, context, data);
             }));
         }
         for (auto& thread : threads) {
@@ -133,7 +183,7 @@
             if (options.verbose) {
                 std::cout << " • Running: " << currently_running << std::endl;
             }
-            fuzzer.fn(program, options, data);
+            fuzzer.fn(program, context, data);
         }
     }
 }
diff --git a/src/tint/cmd/fuzz/wgsl/fuzz.h b/src/tint/cmd/fuzz/wgsl/fuzz.h
index d128695..a24514b 100644
--- a/src/tint/cmd/fuzz/wgsl/fuzz.h
+++ b/src/tint/cmd/fuzz/wgsl/fuzz.h
@@ -35,6 +35,7 @@
 #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/enum_set.h"
 #include "src/tint/utils/containers/slice.h"
 #include "src/tint/utils/macros/static_init.h"
 
@@ -53,17 +54,34 @@
     std::string dxc;
 };
 
+/// ProgramProperties is an enumerator of flags used to describe characteristics of the input
+/// program.
+enum class ProgramProperties {
+    /// The program has builtin functions which have been shadowed
+    kBuiltinFnsShadowed,
+    /// The program has builtin types which have been shadowed
+    kBuiltinTypesShadowed,
+};
+
+/// Context holds information about the fuzzer options and the input program.
+struct Context {
+    /// The options used for Run()
+    Options options;
+    /// The properties of the input program
+    EnumSet<ProgramProperties> program_properties;
+};
+
 /// ProgramFuzzer describes a fuzzer function that takes a WGSL program as input
 struct ProgramFuzzer {
     /// @param name the name of the fuzzer
-    /// @param fn the fuzzer function with the signature `void(const Program&, const Options&, ...)`
-    /// @returns a ProgramFuzzer that invokes the function @p fn with the Program, Options, along
+    /// @param fn the fuzzer function with the signature `void(const Program&, const Context&, ...)`
+    /// @returns a ProgramFuzzer that invokes the function @p fn with the Program, Context, along
     /// with any additional arguments which are deserialized from the fuzzer input.
     template <typename... ARGS>
     static ProgramFuzzer Create(std::string_view name,
-                                void (*fn)(const Program&, const Options&, ARGS...)) {
+                                void (*fn)(const Program&, const Context&, ARGS...)) {
         if constexpr (sizeof...(ARGS) > 0) {
-            auto fn_with_decode = [fn](const Program& program, const Options& options,
+            auto fn_with_decode = [fn](const Program& program, const Context& context,
                                        Slice<const std::byte> data) {
                 if (!data.data) {
                     return;
@@ -72,7 +90,7 @@
                 auto data_args = bytes::Decode<std::tuple<std::decay_t<ARGS>...>>(reader);
                 if (data_args == Success) {
                     auto all_args =
-                        std::tuple_cat(std::tuple<const Program&, const Options&>{program, options},
+                        std::tuple_cat(std::tuple<const Program&, const Context&>{program, context},
                                        data_args.Get());
                     std::apply(*fn, all_args);
                 }
@@ -81,7 +99,7 @@
         } else {
             return ProgramFuzzer{
                 name,
-                [fn](const Program& program, const Options& options, Slice<const std::byte>) {
+                [fn](const Program& program, const Context& options, Slice<const std::byte>) {
                     fn(program, options);
                 },
             };
@@ -95,7 +113,7 @@
     template <typename... ARGS>
     static ProgramFuzzer Create(std::string_view name, void (*fn)(const Program&, ARGS...)) {
         if constexpr (sizeof...(ARGS) > 0) {
-            auto fn_with_decode = [fn](const Program& program, const Options&,
+            auto fn_with_decode = [fn](const Program& program, const Context&,
                                        Slice<const std::byte> data) {
                 if (!data.data) {
                     return;
@@ -112,7 +130,7 @@
         } else {
             return ProgramFuzzer{
                 name,
-                [fn](const Program& program, const Options&, Slice<const std::byte>) {
+                [fn](const Program& program, const Context&, Slice<const std::byte>) {
                     fn(program);
                 },
             };
@@ -122,7 +140,7 @@
     /// Name of the fuzzer function
     std::string_view name;
     /// The fuzzer function
-    std::function<void(const Program&, const Options&, Slice<const std::byte> data)> fn;
+    std::function<void(const Program&, const Context&, Slice<const std::byte> data)> fn;
 };
 
 /// Runs all the registered WGSL fuzzers with the supplied WGSL
@@ -142,9 +160,9 @@
 /// Where `...` is any number of deserializable parameters which are decoded from the base64
 /// content of the WGSL comments.
 /// @see bytes::Decode()
-#define TINT_WGSL_PROGRAM_FUZZER(FUNCTION)         \
+#define TINT_WGSL_PROGRAM_FUZZER(FUNCTION, ...)    \
     TINT_STATIC_INIT(::tint::fuzz::wgsl::Register( \
-        ::tint::fuzz::wgsl::ProgramFuzzer::Create(#FUNCTION, FUNCTION)))
+        ::tint::fuzz::wgsl::ProgramFuzzer::Create(#FUNCTION, FUNCTION, ##__VA_ARGS__)))
 
 }  // namespace tint::fuzz::wgsl
 
diff --git a/src/tint/cmd/fuzz/wgsl/generate_ir_corpus.py b/src/tint/cmd/fuzz/wgsl/generate_ir_corpus.py
new file mode 100644
index 0000000..c332259
--- /dev/null
+++ b/src/tint/cmd/fuzz/wgsl/generate_ir_corpus.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+# 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.
+
+# Collect all .wgsl files under a given directory and convert them to IR
+# protobuf in a given corpus directory, flattening their file names by replacing
+# path separators with underscores. If the output directory already exists, it
+# will be deleted and re-created. Files ending with ".expected.wgsl" are
+# skipped.
+#
+# The intended use of this script is to generate a  corpus of IR protobufs
+# for fuzzing.
+#
+# Based off of generate_wgsl_corpus.py
+#
+# Usage:
+#    generate_ir_corpus.py <path to tint cmd> <input_dir> <corpus_dir>
+
+import optparse
+import subprocess
+
+import os
+import pathlib
+import shutil
+import sys
+
+
+def list_wgsl_files(root_search_dir):
+    for root, folders, files in os.walk(root_search_dir):
+        for filename in folders + files:
+            if pathlib.Path(filename).suffix == '.wgsl':
+                yield os.path.join(root, filename)
+
+
+def main():
+    parser = optparse.OptionParser(
+        usage="usage: %prog [option] tint-cmd input-dir output-dir")
+    parser.add_option('--stamp', dest='stamp', help='stamp file')
+    options, args = parser.parse_args(sys.argv[1:])
+
+    if len(args) != 3:
+        parser.error("incorrect number of arguments")
+
+    tint_cmd: str = os.path.abspath(args[0])
+    if not os.path.isfile(tint_cmd) or not os.access(tint_cmd, os.X_OK):
+        parser.error("Unable to run tint-cmd '" + os.path.abspath(args[0]) +
+                     "' (" + tint_cmd + ")")
+
+    input_dir: str = os.path.abspath(args[1].rstrip(os.sep))
+    corpus_dir: str = os.path.abspath(args[2])
+
+    if os.path.exists(corpus_dir):
+        shutil.rmtree(corpus_dir)
+    os.makedirs(corpus_dir)
+
+    for in_file in list_wgsl_files(input_dir):
+        if in_file.endswith(".expected.wgsl"):
+            continue
+
+        out_file = os.path.splitext(in_file[len(input_dir) + 1:].replace(
+            os.sep, '_'))[0] + '.tirb'
+        subprocess.run([
+            tint_cmd, '--format=ir_bin',
+            '--output-name=' + corpus_dir + os.sep + out_file, in_file
+        ],
+                       stderr=subprocess.STDOUT)
+
+    if options.stamp:
+        pathlib.Path(options.stamp).touch(mode=0o644, exist_ok=True)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index ccfdcf1..5203fec 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -71,6 +71,8 @@
 
 #if TINT_BUILD_IR_BINARY
 #include "src/tint/lang/core/ir/binary/encode.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/wgsl/helpers/apply_substitute_overrides.h"
 #endif  // TINT_BUILD_IR_BINARY
 
 #endif  // TINT_BUILD_WGSL_READER
@@ -1259,6 +1261,37 @@
 #endif
 }
 
+/// Generate an IR module for a program, performs checking for unsupported
+/// enables, needed transforms, and validation.
+/// @param program the program to generate
+/// @param options the options that Tint was invoked with
+/// @returns generated module on success, tint::failure on failure
+#if WGSL_READER_AND_IR_BINARY
+tint::Result<tint::core::ir::Module> GenerateIrModule([[maybe_unused]] const tint::Program& program,
+                                                      [[maybe_unused]] const Options& options) {
+    if (program.AST().Enables().Any(tint::wgsl::reader::IsUnsupportedByIR)) {
+        return tint::Failure{"Unsupported enable used in shader"};
+    }
+
+    auto transformed = tint::wgsl::ApplySubstituteOverrides(program);
+    auto& src = transformed ? transformed.value() : program;
+    if (!src.IsValid()) {
+        return tint::Failure{src.Diagnostics()};
+    }
+
+    auto ir = tint::wgsl::reader::ProgramToLoweredIR(src);
+    if (ir != tint::Success) {
+        return ir.Failure();
+    }
+
+    if (auto val = tint::core::ir::Validate(ir.Get()); val != tint::Success) {
+        return val.Failure();
+    }
+
+    return ir;
+}
+#endif  // WGSL_READER_AND_IR_BINARY
+
 /// Generate IR binary protobuf for a program.
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
@@ -1272,11 +1305,12 @@
     std::cerr << "IR binary not enabled in tint build" << std::endl;
     return false;
 #else
-    auto module = tint::wgsl::reader::ProgramToLoweredIR(program);
+    auto module = GenerateIrModule(program, options);
     if (module != tint::Success) {
-        std::cerr << "Failed to build IR from program: " << module.Failure() << "\n";
+        std::cerr << "Failed to generate lowered IR from program: " << module.Failure() << "\n";
         return false;
     }
+
     auto pb = tint::core::ir::binary::Encode(module.Get());
     if (pb != tint::Success) {
         std::cerr << "Failed to encode IR module to protobuf: " << pb.Failure() << "\n";
@@ -1300,11 +1334,12 @@
 #if WGSL_READER_AND_IR_BINARY
 bool GenerateIrProtoDebug([[maybe_unused]] const tint::Program& program,
                           [[maybe_unused]] const Options& options) {
-    auto module = tint::wgsl::reader::ProgramToLoweredIR(program);
+    auto module = GenerateIrModule(program, options);
     if (module != tint::Success) {
-        std::cerr << "Failed to build IR from program: " << module.Failure() << "\n";
+        std::cerr << "Failed to generate lowered IR from program: " << module.Failure() << "\n";
         return false;
     }
+
     auto pb = tint::core::ir::binary::EncodeDebug(module.Get());
     if (pb != tint::Success) {
         std::cerr << "Failed to encode IR module to protobuf: " << pb.Failure() << "\n";
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 5fbbb14..240afc1 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -1406,6 +1406,15 @@
         return BlockParam(name, type);
     }
 
+    /// Creates a new `BlockParam`
+    /// @tparam TYPE the parameter type
+    /// @returns the value
+    template <typename TYPE>
+    ir::BlockParam* BlockParam() {
+        auto* type = ir.Types().Get<TYPE>();
+        return BlockParam(type);
+    }
+
     /// Creates a new `FunctionParam`
     /// @param type the parameter type
     /// @returns the value
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index 1d69625..b72475c 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -27,9 +27,11 @@
 
 #include "src/tint/lang/core/ir/validator.h"
 
+#include <algorithm>
 #include <cstdint>
 #include <memory>
 #include <string>
+#include <string_view>
 #include <utility>
 
 #include "src/tint/lang/core/intrinsic/table.h"
@@ -81,8 +83,11 @@
 #include "src/tint/utils/containers/predicates.h"
 #include "src/tint/utils/containers/reverse.h"
 #include "src/tint/utils/containers/transform.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/macros/defer.h"
+#include "src/tint/utils/result/result.h"
+#include "src/tint/utils/rtti/castable.h"
 #include "src/tint/utils/rtti/switch.h"
 #include "src/tint/utils/text/styled_text.h"
 #include "src/tint/utils/text/text_style.h"
@@ -240,15 +245,57 @@
     /// @param src the source lines to highlight
     diag::Diagnostic& AddNote(Source src = {});
 
-    /// Adds a note to the diagnostics highlighting where the value was declared, if it has a source
+    /// Adds a note to the diagnostics highlighting where the value instruction or block is
+    /// declared, if it has a source location.
+    /// @param decl the value instruction or block
+    void AddDeclarationNote(const CastableBase* decl);
+
+    /// Adds a note to the diagnostics highlighting where the block is declared, if it has a source
     /// location.
-    /// @param value the value
-    void AddDeclarationNote(const Value* value);
+    /// @param block the block
+    void AddDeclarationNote(const Block* block);
+
+    /// Adds a note to the diagnostics highlighting where the block parameter is declared, if it
+    /// has a source location.
+    /// @param param the block parameter
+    void AddDeclarationNote(const BlockParam* param);
+
+    /// Adds a note to the diagnostics highlighting where the function is declared, if it has a
+    /// source location.
+    /// @param fn the function
+    void AddDeclarationNote(const Function* fn);
+
+    /// Adds a note to the diagnostics highlighting where the function parameter is declared, if it
+    /// has a source location.
+    /// @param param the function parameter
+    void AddDeclarationNote(const FunctionParam* param);
+
+    /// Adds a note to the diagnostics highlighting where the instruction is declared, if it has a
+    /// source location.
+    /// @param inst the inst
+    void AddDeclarationNote(const Instruction* inst);
+
+    /// Adds a note to the diagnostics highlighting where instruction result was declared, if it has
+    /// a source location.
+    /// @param res the res
+    void AddDeclarationNote(const InstructionResult* res);
+
+    /// @param decl the value, instruction or block to get the name for
+    /// @returns the styled name for the given value, instruction or block
+    StyledText NameOf(const CastableBase* decl);
 
     /// @param v the value to get the name for
-    /// @returns the name for the given value
+    /// @returns the styled name for the given value
     StyledText NameOf(const Value* v);
 
+    /// @param inst the instruction to get the name for
+    /// @returns the styled  name for the given instruction
+    StyledText NameOf(const Instruction* inst);
+
+    /// @param block the block to get the name for
+    /// @returns the styled  name for the given block
+    StyledText NameOf(const Block* block);
+
     /// Checks the given operand is not null
     /// @param inst the instruction
     /// @param operand the operand
@@ -327,6 +374,10 @@
     /// @param b the terminator to validate
     void CheckTerminator(const Terminator* b);
 
+    /// Validates the break if instruction
+    /// @param b the break if to validate
+    void CheckBreakIf(const BreakIf* b);
+
     /// Validates the continue instruction
     /// @param c the continue to validate
     void CheckContinue(const Continue* c);
@@ -377,6 +428,19 @@
     /// @param s the store vector element to validate
     void CheckStoreVectorElement(const StoreVectorElement* s);
 
+    /// Validates that the number and types of the source instruction operands match the target's
+    /// values.
+    /// @param source_inst the source instruction
+    /// @param source_operand_offset the index of the first operand of the source instruction
+    /// @param source_operand_count the number of operands of the source instruction
+    /// @param target the receiver of the operand values
+    /// @param target_values the receiver of the operand values
+    void CheckOperandsMatchTarget(const Instruction* source_inst,
+                                  size_t source_operand_offset,
+                                  size_t source_operand_count,
+                                  const CastableBase* target,
+                                  VectorRef<const Value*> target_values);
+
     /// @param inst the instruction
     /// @param idx the operand index
     /// @returns the vector pointer type for the given instruction operand
@@ -574,39 +638,83 @@
     return diag;
 }
 
-void Validator::AddDeclarationNote(const Value* value) {
+void Validator::AddDeclarationNote(const CastableBase* decl) {
     tint::Switch(
-        value,  //
-        [&](const InstructionResult* res) {
-            if (auto* inst = res->Instruction()) {
-                auto results = inst->Results();
-                for (size_t i = 0; i < results.Length(); i++) {
-                    if (results[i] == value) {
-                        AddResultNote(res->Instruction(), i) << NameOf(value) << " declared here";
-                        return;
-                    }
-                }
+        decl,  //
+        [&](const Block* block) { AddDeclarationNote(block); },
+        [&](const BlockParam* param) { AddDeclarationNote(param); },
+        [&](const Function* fn) { AddDeclarationNote(fn); },
+        [&](const FunctionParam* param) { AddDeclarationNote(param); },
+        [&](const Instruction* inst) { AddDeclarationNote(inst); },
+        [&](const InstructionResult* res) { AddDeclarationNote(res); });
+}
+
+void Validator::AddDeclarationNote(const Block* block) {
+    auto src = Disassembly().BlockSource(block);
+    if (src.file) {
+        AddNote(src) << NameOf(block) << " declared here";
+    }
+}
+
+void Validator::AddDeclarationNote(const BlockParam* param) {
+    auto src = Disassembly().BlockParamSource(param);
+    if (src.file) {
+        AddNote(src) << NameOf(param) << " declared here";
+    }
+}
+
+void Validator::AddDeclarationNote(const Function* fn) {
+    AddNote(fn) << NameOf(fn) << " declared here";
+}
+
+void Validator::AddDeclarationNote(const FunctionParam* param) {
+    auto src = Disassembly().FunctionParamSource(param);
+    if (src.file) {
+        AddNote(src) << NameOf(param) << " declared here";
+    }
+}
+
+void Validator::AddDeclarationNote(const Instruction* inst) {
+    auto src = Disassembly().InstructionSource(inst);
+    if (src.file) {
+        AddNote(src) << NameOf(inst) << " declared here";
+    }
+}
+
+void Validator::AddDeclarationNote(const InstructionResult* res) {
+    if (auto* inst = res->Instruction()) {
+        auto results = inst->Results();
+        for (size_t i = 0; i < results.Length(); i++) {
+            if (results[i] == res) {
+                AddResultNote(res->Instruction(), i) << NameOf(res) << " declared here";
+                return;
             }
-        },
-        [&](const FunctionParam* param) {
-            auto src = Disassembly().FunctionParamSource(param);
-            if (src.file) {
-                AddNote(src) << NameOf(value) << " declared here";
-            }
-        },
-        [&](const BlockParam* param) {
-            auto src = Disassembly().BlockParamSource(param);
-            if (src.file) {
-                AddNote(src) << NameOf(value) << " declared here";
-            }
-        },
-        [&](const Function* fn) { AddNote(fn) << NameOf(value) << " declared here"; });
+        }
+    }
+}
+
+StyledText Validator::NameOf(const CastableBase* decl) {
+    return tint::Switch(
+        decl,  //
+        [&](const Value* value) { return NameOf(value); },
+        [&](const Instruction* inst) { return NameOf(inst); },
+        [&](const Block* block) { return NameOf(block); },  //
+        TINT_ICE_ON_NO_MATCH);
 }
 
 StyledText Validator::NameOf(const Value* value) {
     return Disassembly().NameOf(value);
 }
 
+StyledText Validator::NameOf(const Instruction* inst) {
+    return StyledText{} << style::Instruction(inst->FriendlyName());
+}
+
+StyledText Validator::NameOf(const Block* block) {
+    return StyledText{} << style::Instruction(block->Parent()->FriendlyName()) << " block "
+                        << Disassembly().NameOf(block);
+}
+
 void Validator::CheckOperandNotNull(const Instruction* inst, const ir::Value* operand, size_t idx) {
     if (operand == nullptr) {
         AddError(inst, idx) << "operand is undefined";
@@ -716,11 +824,6 @@
         if (inst->Block() != blk) {
             AddError(inst) << "block instruction does not have same block as parent";
             AddNote(blk) << "in block";
-            continue;
-        }
-        if (inst->Is<ir::Terminator>() && inst != blk->Terminator()) {
-            AddError(inst) << "block terminator which isn't the final instruction";
-            continue;
         }
     }
 
@@ -1165,7 +1268,7 @@
 
     tint::Switch(
         b,                                                           //
-        [&](const ir::BreakIf*) {},                                  //
+        [&](const ir::BreakIf* i) { CheckBreakIf(i); },              //
         [&](const ir::Continue* c) { CheckContinue(c); },            //
         [&](const ir::Exit* e) { CheckExit(e); },                    //
         [&](const ir::NextIteration* n) { CheckNextIteration(n); },  //
@@ -1173,6 +1276,32 @@
         [&](const ir::TerminateInvocation*) {},                      //
         [&](const ir::Unreachable*) {},                              //
         [&](Default) { AddError(b) << "missing validation"; });
+
+    if (b->next) {
+        AddError(b) << "must be the last instruction in the block";
+    }
+}
+
+void Validator::CheckBreakIf(const BreakIf* b) {
+    auto* loop = b->Loop();
+    if (loop == nullptr) {
+        AddError(b) << "has no associated loop";
+        return;
+    }
+
+    if (loop->Continuing() != b->Block()) {
+        AddError(b) << "must only be called directly from loop continuing";
+    }
+
+    auto next_iter_values = b->NextIterValues();
+    if (auto* body = loop->Body()) {
+        CheckOperandsMatchTarget(b, b->ArgsOperandOffset(), next_iter_values.Length(), body,
+                                 body->Params());
+    }
+
+    auto exit_values = b->ExitValues();
+    CheckOperandsMatchTarget(b, b->ArgsOperandOffset() + next_iter_values.Length(),
+                             exit_values.Length(), loop, loop->Results());
 }
 
 void Validator::CheckContinue(const Continue* c) {
@@ -1189,6 +1318,11 @@
         }
     }
 
+    if (auto* cont = loop->Continuing()) {
+        CheckOperandsMatchTarget(c, Continue::kArgsOperandOffset, c->Args().Length(), cont,
+                                 cont->Params());
+    }
+
     first_continues_.Add(loop, c);
 }
 
@@ -1203,24 +1337,9 @@
         return;
     }
 
-    auto results = e->ControlInstruction()->Results();
     auto args = e->Args();
-    if (results.Length() != args.Length()) {
-        AddError(e) << ("args count (") << args.Length()
-                    << ") does not match control instruction result count (" << results.Length()
-                    << ")";
-        AddNote(e->ControlInstruction()) << "control instruction";
-        return;
-    }
-
-    for (size_t i = 0; i < results.Length(); ++i) {
-        if (results[i] && args[i] && results[i]->Type() != args[i]->Type()) {
-            AddError(e, i) << "argument type " << style::Type(results[i]->Type()->FriendlyName())
-                           << " does not match control instruction type "
-                           << style::Type(args[i]->Type()->FriendlyName());
-            AddNote(e->ControlInstruction()) << "control instruction";
-        }
-    }
+    CheckOperandsMatchTarget(e, e->ArgsOperandOffset(), args.Length(), e->ControlInstruction(),
+                             e->ControlInstruction()->Results());
 
     tint::Switch(
         e,                                                     //
@@ -1243,6 +1362,11 @@
             AddError(n) << "called outside of associated loop";
         }
     }
+
+    if (auto* body = loop->Body()) {
+        CheckOperandsMatchTarget(n, NextIteration::kArgsOperandOffset, n->Args().Length(), body,
+                                 body->Params());
+    }
 }
 
 void Validator::CheckExitIf(const ExitIf* e) {
@@ -1396,6 +1520,37 @@
     }
 }
 
+void Validator::CheckOperandsMatchTarget(const Instruction* source_inst,
+                                         size_t source_operand_offset,
+                                         size_t source_operand_count,
+                                         const CastableBase* target,
+                                         VectorRef<const Value*> target_values) {
+    if (source_operand_count != target_values.Length()) {
+        auto values = [&](size_t n) { return n == 1 ? " value" : " values"; };
+        AddError(source_inst) << "provides " << source_operand_count << values(source_operand_count)
+                              << " but " << NameOf(target) << " expects " << target_values.Length()
+                              << values(target_values.Length());
+        AddDeclarationNote(target);
+    }
+    size_t count = std::min(source_operand_count, target_values.Length());
+    for (size_t i = 0; i < count; i++) {
+        auto* source_value = source_inst->Operand(source_operand_offset + i);
+        auto* target_value = target_values[i];
+        if (!source_value || !target_value) {
+            continue;  // Caller should be checking operands are not null
+        }
+        auto* source_type = source_value->Type();
+        auto* target_type = target_value->Type();
+        if (source_type != target_type) {
+            AddError(source_inst, source_operand_offset + i)
+                << "operand with type " << style::Type(source_type->FriendlyName())
+                << " does not match " << NameOf(target) << " target type "
+                << style::Type(target_type->FriendlyName());
+            AddDeclarationNote(target_value);
+        }
+    }
+}
+
 const core::type::Type* Validator::GetVectorPtrElementType(const Instruction* inst, size_t idx) {
     auto* operand = inst->Operands()[idx];
     if (TINT_UNLIKELY(!operand)) {
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index bc6b744..820182a 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -1144,7 +1144,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: return: block terminator which isn't the final instruction
+              R"(:3:5 error: return: must be the last instruction in the block
     ret
     ^^^
 
@@ -1914,9 +1914,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:9 error: exit_if: args count (1) does not match control instruction result count (2)
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_if: provides 1 value but 'if' expects 2 values
         exit_if 1i  # if_1
         ^^^^^^^^^^
 
@@ -1924,7 +1923,7 @@
       $B2: {  # true
       ^^^
 
-:3:5 note: control instruction
+:3:5 note: 'if' declared here
     %2:i32, %3:f32 = if true [t: $B2] {  # if_1
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -1958,9 +1957,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:9 error: exit_if: args count (3) does not match control instruction result count (2)
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_if: provides 3 values but 'if' expects 2 values
         exit_if 1i, 2.0f, 3i  # if_1
         ^^^^^^^^^^^^^^^^^^^^
 
@@ -1968,7 +1966,7 @@
       $B2: {  # true
       ^^^
 
-:3:5 note: control instruction
+:3:5 note: 'if' declared here
     %2:i32, %3:f32 = if true [t: $B2] {  # if_1
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -2019,9 +2017,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:21 error: exit_if: argument type 'f32' does not match control instruction type 'i32'
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:21 error: exit_if: operand with type 'i32' does not match 'if' target type 'f32'
         exit_if 1i, 2i  # if_1
                     ^^
 
@@ -2029,9 +2026,9 @@
       $B2: {  # true
       ^^^
 
-:3:5 note: control instruction
+:3:13 note: %3 declared here
     %2:i32, %3:f32 = if true [t: $B2] {  # if_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+            ^^^^^^
 
 note: # Disassembly
 %my_func = func():void {
@@ -2307,9 +2304,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:9 error: exit_switch: args count (1) does not match control instruction result count (2)
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_switch: provides 1 value but 'switch' expects 2 values
         exit_switch 1i  # switch_1
         ^^^^^^^^^^^^^^
 
@@ -2317,7 +2313,7 @@
       $B2: {  # case
       ^^^
 
-:3:5 note: control instruction
+:3:5 note: 'switch' declared here
     %2:i32, %3:f32 = switch true [c: (default, $B2)] {  # switch_1
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -2351,9 +2347,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:9 error: exit_switch: args count (3) does not match control instruction result count (2)
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_switch: provides 3 values but 'switch' expects 2 values
         exit_switch 1i, 2.0f, 3i  # switch_1
         ^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -2361,7 +2356,7 @@
       $B2: {  # case
       ^^^
 
-:3:5 note: control instruction
+:3:5 note: 'switch' declared here
     %2:i32, %3:f32 = switch true [c: (default, $B2)] {  # switch_1
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -2415,7 +2410,7 @@
     ASSERT_NE(res, Success);
     EXPECT_EQ(
         res.Failure().reason.Str(),
-        R"(:5:25 error: exit_switch: argument type 'f32' does not match control instruction type 'i32'
+        R"(:5:25 error: exit_switch: operand with type 'i32' does not match 'switch' target type 'f32'
         exit_switch 1i, 2i  # switch_1
                         ^^
 
@@ -2423,9 +2418,9 @@
       $B2: {  # case
       ^^^
 
-:3:5 note: control instruction
+:3:13 note: %3 declared here
     %2:i32, %3:f32 = switch true [c: (default, $B2)] {  # switch_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+            ^^^^^^
 
 note: # Disassembly
 %my_func = func():void {
@@ -2622,7 +2617,7 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, ContinueOutsideOfLoop) {
+TEST_F(IR_ValidatorTest, Continue_OutsideOfLoop) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2655,7 +2650,7 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, ContinueInLoopInit) {
+TEST_F(IR_ValidatorTest, Continue_InLoopInit) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2692,7 +2687,7 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, ContinueInLoopBody) {
+TEST_F(IR_ValidatorTest, Continue_InLoopBody) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2704,7 +2699,7 @@
     ASSERT_EQ(res, Success);
 }
 
-TEST_F(IR_ValidatorTest, ContinueInLoopContinuing) {
+TEST_F(IR_ValidatorTest, Continue_InLoopContinuing) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2741,7 +2736,161 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, NextIterationOutsideOfLoop) {
+TEST_F(IR_ValidatorTest, Continue_UnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_f); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: continue: provides 2 values but 'loop' block $B3 expects 0 values
+        continue 1i, 2.0f  # -> $B3
+        ^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3: {  # continuing
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue 1i, 2.0f  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_MissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: continue: provides 0 values but 'loop' block $B3 expects 2 values
+        continue  # -> $B3
+        ^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3 (%2:i32, %3:i32): {  # continuing
+      ^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3 (%2:i32, %3:i32): {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_MismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_i, 3_f, false); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:22 error: continue: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
+        continue 1i, 2i, 3.0f, false  # -> $B3
+                     ^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:20 note: %3 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
+                   ^^
+
+:5:26 error: continue: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
+        continue 1i, 2i, 3.0f, false  # -> $B3
+                         ^^^^
+
+:4:7 note: in block
+      $B2: {  # body
+      ^^^
+
+:7:28 note: %4 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
+                           ^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue 1i, 2i, 3.0f, false  # -> $B3
+      }
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, Continue_MatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Continuing()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop, 1_i, 2_f, 3_u, false); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_OutsideOfLoop) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2774,7 +2923,7 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, NextIterationInLoopInit) {
+TEST_F(IR_ValidatorTest, NextIteration_InLoopInit) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2787,7 +2936,7 @@
     ASSERT_EQ(res, Success);
 }
 
-TEST_F(IR_ValidatorTest, NextIterationInLoopBody) {
+TEST_F(IR_ValidatorTest, NextIteration_InLoopBody) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2820,7 +2969,7 @@
 )");
 }
 
-TEST_F(IR_ValidatorTest, NextIterationInLoopContinuing) {
+TEST_F(IR_ValidatorTest, NextIteration_InLoopContinuing) {
     auto* f = b.Function("my_func", ty.void_());
     b.Append(f->Block(), [&] {
         auto* loop = b.Loop();
@@ -2833,6 +2982,160 @@
     ASSERT_EQ(res, Success);
 }
 
+TEST_F(IR_ValidatorTest, NextIteration_UnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_f); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: next_iteration: provides 2 values but 'loop' block $B3 expects 0 values
+        next_iteration 1i, 2.0f  # -> $B3
+        ^^^^^^^^^^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        next_iteration 1i, 2.0f  # -> $B3
+      }
+      $B3: {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_MissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: next_iteration: provides 0 values but 'loop' block $B3 expects 2 values
+        next_iteration  # -> $B3
+        ^^^^^^^^^^^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:7 note: 'loop' block $B3 declared here
+      $B3 (%2:i32, %3:i32): {  # body
+      ^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        next_iteration  # -> $B3
+      }
+      $B3 (%2:i32, %3:i32): {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_MismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_i, 3_f, false); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:5:28 error: next_iteration: operand with type 'i32' does not match 'loop' block $B3 target type 'f32'
+        next_iteration 1i, 2i, 3.0f, false  # -> $B3
+                           ^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:20 note: %3 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                   ^^
+
+:5:32 error: next_iteration: operand with type 'f32' does not match 'loop' block $B3 target type 'u32'
+        next_iteration 1i, 2i, 3.0f, false  # -> $B3
+                               ^^^^
+
+:4:7 note: in block
+      $B2: {  # initializer
+      ^^^
+
+:7:28 note: %4 declared here
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                           ^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [i: $B2, b: $B3] {  # loop_1
+      $B2: {  # initializer
+        next_iteration 1i, 2i, 3.0f, false  # -> $B3
+      }
+      $B3 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, NextIteration_MatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Initializer(), [&] { b.NextIteration(loop, 1_i, 2_f, 3_u, false); });
+        b.Append(loop->Body(), [&] { b.ExitLoop(loop); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
 TEST_F(IR_ValidatorTest, ContinuingUseValueBeforeContinue) {
     auto* f = b.Function("my_func", ty.void_());
     auto* value = b.Let("value", 1_i);
@@ -2914,6 +3217,318 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, BreakIf_NextIterUnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, b.Values(1_i, 2_i), Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 2 values but 'loop' block $B2 expects 0 values
+        break_if true next_iteration: [ 1i ]  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:4:7 note: 'loop' block $B2 declared here
+      $B2: {  # body
+      ^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true next_iteration: [ 1i ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterMissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams({b.BlockParam<i32>(), b.BlockParam<i32>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 0 values but 'loop' block $B2 expects 2 values
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:4:7 note: 'loop' block $B2 declared here
+      $B2 (%2:i32, %3:i32): {  # body
+      ^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2 (%2:i32, %3:i32): {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterMismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, b.Values(1_i, 2_i, 3_f, false), Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:8:45 error: break_if: operand with type 'i32' does not match 'loop' block $B2 target type 'f32'
+        break_if true next_iteration: [ 1i, 2i, 3.0f ]  # -> [t: exit_loop loop_1, f: $B2]
+                                            ^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:4:20 note: %3 declared here
+      $B2 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                   ^^
+
+:8:49 error: break_if: operand with type 'f32' does not match 'loop' block $B2 target type 'u32'
+        break_if true next_iteration: [ 1i, 2i, 3.0f ]  # -> [t: exit_loop loop_1, f: $B2]
+                                                ^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:4:28 note: %4 declared here
+      $B2 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+                           ^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2 (%2:i32, %3:f32, %4:u32, %5:bool): {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true next_iteration: [ 1i, 2i, 3.0f ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_NextIterMatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->Body()->SetParams(
+            {b.BlockParam<i32>(), b.BlockParam<f32>(), b.BlockParam<u32>(), b.BlockParam<bool>()});
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, b.Values(1_i, 2_f, 3_u, false), Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitUnexpectedValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_i)); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 2 values but 'loop' expects 0 values
+        break_if true exit_loop: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:5 note: 'loop' declared here
+    loop [b: $B2, c: $B3] {  # loop_1
+    ^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true exit_loop: [ 1i, 2i ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitMissingValues) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<i32>());
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(), [&] { b.BreakIf(loop, true, Empty, Empty); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:8:9 error: break_if: provides 0 values but 'loop' expects 2 values
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+        ^^^^^^^^^^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:5 note: 'loop' declared here
+    %2:i32, %3:i32 = loop [b: $B2, c: $B3] {  # loop_1
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:i32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitMismatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<f32>(),
+                         b.InstructionResult<u32>(), b.InstructionResult<bool>());
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_i, 3_f, false)); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(
+        res.Failure().reason.Str(),
+        R"(:8:40 error: break_if: operand with type 'i32' does not match 'loop' target type 'f32'
+        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
+                                       ^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:13 note: %3 declared here
+    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
+            ^^^^^^
+
+:8:44 error: break_if: operand with type 'f32' does not match 'loop' target type 'u32'
+        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
+                                           ^^^^
+
+:7:7 note: in block
+      $B3: {  # continuing
+      ^^^
+
+:3:21 note: %4 declared here
+    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
+                    ^^^^^^
+
+note: # Disassembly
+%my_func = func():void {
+  $B1: {
+    %2:i32, %3:f32, %4:u32, %5:bool = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        break_if true exit_loop: [ 1i, 2i, 3.0f, false ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(IR_ValidatorTest, BreakIf_ExitMatchedTypes) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* loop = b.Loop();
+        loop->SetResults(b.InstructionResult<i32>(), b.InstructionResult<f32>(),
+                         b.InstructionResult<u32>(), b.InstructionResult<bool>());
+        b.Append(loop->Body(), [&] { b.Continue(loop); });
+        b.Append(loop->Continuing(),
+                 [&] { b.BreakIf(loop, true, Empty, b.Values(1_i, 2_f, 3_u, false)); });
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_EQ(res, Success);
+}
+
 TEST_F(IR_ValidatorTest, ExitLoop) {
     auto* loop = b.Loop();
     loop->Continuing()->Append(b.NextIteration(loop));
@@ -2981,9 +3596,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:9 error: exit_loop: args count (1) does not match control instruction result count (2)
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_loop: provides 1 value but 'loop' expects 2 values
         exit_loop 1i  # loop_1
         ^^^^^^^^^^^^
 
@@ -2991,7 +3605,7 @@
       $B2: {  # body
       ^^^
 
-:3:5 note: control instruction
+:3:5 note: 'loop' declared here
     %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -3028,9 +3642,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(
-        res.Failure().reason.Str(),
-        R"(:5:9 error: exit_loop: args count (3) does not match control instruction result count (2)
+    EXPECT_EQ(res.Failure().reason.Str(),
+              R"(:5:9 error: exit_loop: provides 3 values but 'loop' expects 2 values
         exit_loop 1i, 2.0f, 3i  # loop_1
         ^^^^^^^^^^^^^^^^^^^^^^
 
@@ -3038,7 +3651,7 @@
       $B2: {  # body
       ^^^
 
-:3:5 note: control instruction
+:3:5 note: 'loop' declared here
     %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -3095,7 +3708,7 @@
     ASSERT_NE(res, Success);
     EXPECT_EQ(
         res.Failure().reason.Str(),
-        R"(:5:23 error: exit_loop: argument type 'f32' does not match control instruction type 'i32'
+        R"(:5:23 error: exit_loop: operand with type 'i32' does not match 'loop' target type 'f32'
         exit_loop 1i, 2i  # loop_1
                       ^^
 
@@ -3103,9 +3716,9 @@
       $B2: {  # body
       ^^^
 
-:3:5 note: control instruction
+:3:13 note: %3 declared here
     %2:i32, %3:f32 = loop [b: $B2, c: $B3] {  # loop_1
-    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+            ^^^^^^
 
 note: # Disassembly
 %my_func = func():void {
diff --git a/src/tint/lang/hlsl/writer/writer_ast_fuzz.cc b/src/tint/lang/hlsl/writer/writer_ast_fuzz.cc
index 12497e6..568a636 100644
--- a/src/tint/lang/hlsl/writer/writer_ast_fuzz.cc
+++ b/src/tint/lang/hlsl/writer/writer_ast_fuzz.cc
@@ -39,9 +39,7 @@
 namespace tint::hlsl::writer {
 namespace {
 
-void ASTFuzzer(const tint::Program& program,
-               const fuzz::wgsl::Options& fuzz_options,
-               Options options) {
+void ASTFuzzer(const tint::Program& program, const fuzz::wgsl::Context& context, Options options) {
     if (program.AST().HasOverrides()) {
         return;
     }
@@ -50,9 +48,9 @@
     if (res == Success) {
         const char* dxc_path = validate::kDxcDLLName;
         bool must_validate = false;
-        if (!fuzz_options.dxc.empty()) {
+        if (!context.options.dxc.empty()) {
             must_validate = true;
-            dxc_path = fuzz_options.dxc.c_str();
+            dxc_path = context.options.dxc.c_str();
         }
 
         auto dxc = tint::Command::LookPath(dxc_path);
diff --git a/src/tint/lang/spirv/writer/loop_test.cc b/src/tint/lang/spirv/writer/loop_test.cc
index 1fb4f9e..f8ccb38 100644
--- a/src/tint/lang/spirv/writer/loop_test.cc
+++ b/src/tint/lang/spirv/writer/loop_test.cc
@@ -212,7 +212,7 @@
         auto* loop = b.Loop();
         b.Append(loop->Body(), [&] {
             auto* result = b.Equal(ty.bool_(), 1_i, 2_i);
-            b.Continue(loop, result);
+            b.Continue(loop);
 
             b.Append(loop->Continuing(), [&] {  //
                 b.BreakIf(loop, result);
@@ -410,7 +410,7 @@
         b.Append(loop->Continuing(), [&] {
             auto* cmp = b.GreaterThan(ty.bool_(), cont_param_a, 5_i);
             auto* not_b = b.Not(ty.bool_(), cont_param_b);
-            b.BreakIf(loop, cmp, cont_param_a, not_b);
+            b.BreakIf(loop, cmp, b.Values(cont_param_a, not_b), Empty);
         });
 
         b.Return(func);
diff --git a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture_fuzz.cc b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture_fuzz.cc
index 4c6efeb..78d6d9a 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture_fuzz.cc
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture_fuzz.cc
@@ -36,7 +36,13 @@
 namespace {
 
 bool CanRun(const Program& program,
+            const fuzz::wgsl::Context& context,
             const MultiplanarExternalTexture::NewBindingPoints& remappings) {
+    if (context.program_properties.Contains(fuzz::wgsl::ProgramProperties::kBuiltinFnsShadowed) ||
+        context.program_properties.Contains(fuzz::wgsl::ProgramProperties::kBuiltinTypesShadowed)) {
+        return false;  // MultiplanarExternalTexture assumes the Renamer transform has been run
+    }
+
     Hashset<BindingPoint, 8> all_binding_points;
     for (auto* global : program.AST().GlobalVariables()) {
         if (auto* sem = program.Sem().Get<sem::GlobalVariable>(global)) {
@@ -83,8 +89,9 @@
 
 void MultiplanarExternalTextureFuzzer(
     const Program& program,
+    const fuzz::wgsl::Context& context,
     const MultiplanarExternalTexture::NewBindingPoints& binding_points) {
-    if (!CanRun(program, binding_points)) {
+    if (!CanRun(program, context, binding_points)) {
         return;
     }
 
diff --git a/src/tint/lang/wgsl/inspector/entry_point.h b/src/tint/lang/wgsl/inspector/entry_point.h
index 3709ade..3946e63 100644
--- a/src/tint/lang/wgsl/inspector/entry_point.h
+++ b/src/tint/lang/wgsl/inspector/entry_point.h
@@ -92,6 +92,8 @@
         std::optional<uint32_t> location;
         /// Value of the color attribute, if set.
         std::optional<uint32_t> color;
+        /// Value of the blend_src attribute, if set.
+        std::optional<uint32_t> blend_src;
     } attributes;
     /// Scalar type that the variable is composed of.
     ComponentType component_type = ComponentType::kUnknown;
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index d98ada7..9857271 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -49,6 +49,7 @@
 #include "src/tint/lang/core/type/u32.h"
 #include "src/tint/lang/core/type/vector.h"
 #include "src/tint/lang/core/type/void.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/call_expression.h"
 #include "src/tint/lang/wgsl/ast/float_literal_expression.h"
@@ -613,6 +614,12 @@
     std::tie(stage_variable.component_type, stage_variable.composition_type) =
         CalculateComponentAndComposition(type);
 
+    if (auto* blend_src_attribute = ast::GetAttribute<ast::BlendSrcAttribute>(attributes)) {
+        TINT_ASSERT(blend_src_attribute->expr->Is<ast::IntLiteralExpression>());
+        stage_variable.attributes.blend_src = static_cast<uint32_t>(
+            blend_src_attribute->expr->As<ast::IntLiteralExpression>()->value);
+    }
+
     stage_variable.attributes.location = location;
     stage_variable.attributes.color = color;
 
diff --git a/src/tint/lang/wgsl/inspector/inspector_test.cc b/src/tint/lang/wgsl/inspector/inspector_test.cc
index 7468ccc..1a7c557 100644
--- a/src/tint/lang/wgsl/inspector/inspector_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_test.cc
@@ -143,6 +143,8 @@
 
 class InspectorGetEnableDirectivesTest : public InspectorRunner, public testing::Test {};
 
+class InspectorGetBlendSrcTest : public InspectorBuilder, public testing::Test {};
+
 // This is a catch all for shaders that have demonstrated regressions/crashes in
 // the wild.
 class InspectorRegressionTest : public InspectorRunner, public testing::Test {};
@@ -4046,6 +4048,34 @@
     }
 }
 
+TEST_F(InspectorGetBlendSrcTest, Basic) {
+    Enable(wgsl::Extension::kChromiumInternalDualSourceBlending);
+
+    Structure("out_struct",
+              Vector{
+                  Member("output_color", ty.vec4<f32>(), Vector{Location(0_u), BlendSrc(0_u)}),
+                  Member("output_blend", ty.vec4<f32>(), Vector{Location(0_u), BlendSrc(1_u)}),
+              });
+
+    Func("ep_func", tint::Empty, ty("out_struct"),
+         Vector{
+             Decl(Var("out_var", ty("out_struct"))),
+             Return("out_var"),
+         },
+         Vector{
+             Stage(ast::PipelineStage::kFragment),
+         });
+
+    Inspector& inspector = Build();
+
+    auto result = inspector.GetEntryPoints();
+
+    ASSERT_EQ(1u, result.size());
+    ASSERT_EQ(2u, result[0].output_variables.size());
+    EXPECT_EQ(0u, result[0].output_variables[0].attributes.blend_src);
+    EXPECT_EQ(1u, result[0].output_variables[1].attributes.blend_src);
+}
+
 }  // namespace
 
 static std::ostream& operator<<(std::ostream& out, const Inspector::TextureQueryType& ty) {
diff --git a/src/tint/lang/wgsl/reader/reader.cc b/src/tint/lang/wgsl/reader/reader.cc
index ed8009a..d446964 100644
--- a/src/tint/lang/wgsl/reader/reader.cc
+++ b/src/tint/lang/wgsl/reader/reader.cc
@@ -77,4 +77,20 @@
     return ir;
 }
 
+bool IsUnsupportedByIR(const ast::Enable* enable) {
+    for (auto ext : enable->extensions) {
+        switch (ext->name) {
+            case tint::wgsl::Extension::kChromiumExperimentalFramebufferFetch:
+            case tint::wgsl::Extension::kChromiumExperimentalPixelLocal:
+            case tint::wgsl::Extension::kChromiumExperimentalPushConstant:
+            case tint::wgsl::Extension::kChromiumInternalDualSourceBlending:
+            case tint::wgsl::Extension::kChromiumInternalRelaxedUniformLayout:
+                return true;
+            default:
+                break;
+        }
+    }
+    return false;
+}
+
 }  // namespace tint::wgsl::reader
diff --git a/src/tint/lang/wgsl/reader/reader.h b/src/tint/lang/wgsl/reader/reader.h
index 45edcd8..991e3e0 100644
--- a/src/tint/lang/wgsl/reader/reader.h
+++ b/src/tint/lang/wgsl/reader/reader.h
@@ -32,6 +32,10 @@
 #include "src/tint/lang/wgsl/program/program.h"
 #include "src/tint/lang/wgsl/reader/options.h"
 
+namespace tint::ast {
+class Enable;
+}  // namespace tint::ast
+
 namespace tint::wgsl::reader {
 
 /// Parses the WGSL source, returning the parsed program.
@@ -58,6 +62,10 @@
 /// concrete types.
 tint::Result<core::ir::Module> ProgramToLoweredIR(const Program& program);
 
+/// Allows for checking if an extension is currently supported/unsupported by IR
+/// before trying to convert to it.
+bool IsUnsupportedByIR(const ast::Enable* enable);
+
 }  // namespace tint::wgsl::reader
 
 #endif  // SRC_TINT_LANG_WGSL_READER_READER_H_
diff --git a/src/tint/tint.gni b/src/tint/tint.gni
index bf48aae..8f3d528 100644
--- a/src/tint/tint.gni
+++ b/src/tint/tint.gni
@@ -133,6 +133,8 @@
   import("//testing/libfuzzer/fuzzer_test.gni")
   fuzzer_corpus_wgsl_dir = "${root_gen_dir}/fuzzers/wgsl_corpus"
   fuzzer_corpus_wgsl_stamp = "${fuzzer_corpus_wgsl_dir}.stamp"
+  fuzzer_corpus_ir_dir = "${root_gen_dir}/fuzzers/ir_corpus"
+  fuzzer_corpus_ir_stamp = "${fuzzer_corpus_ir_dir}.stamp"
 
   template("tint_fuzz_source_set") {
     source_set(target_name) {