[tint][ir] Validate break_if value types match body block parameters / loop results

Change-Id: I6108d84cfe043f8bf7170a26495523e959a9f8a7
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/188981
Reviewed-by: James Price <jrprice@google.com>
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 504d50e..0f3010f 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";
@@ -1160,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); },  //
@@ -1174,6 +1282,28 @@
     }
 }
 
+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) {
     auto* loop = c->Loop();
     if (loop == nullptr) {
@@ -1395,6 +1525,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 3fedeef..c3e5c25 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -2914,6 +2914,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));
diff --git a/src/tint/lang/spirv/writer/loop_test.cc b/src/tint/lang/spirv/writer/loop_test.cc
index 1fb4f9e..ffa15ae 100644
--- a/src/tint/lang/spirv/writer/loop_test.cc
+++ b/src/tint/lang/spirv/writer/loop_test.cc
@@ -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);