[spirv] Fix unreachable in loop continuing blocks

SPIR-V requires that continue blocks are structurally post-dominated
by back-edge blocks, and the presence of OpUnreachable (a function
terminator) can trip up this validation.

Use a transform to replace unreachable instructions nested inside loop
continuing blocks with regular branches.

Fixed: 354627692
Change-Id: Idd6f58e755f761ba8b8c966ac01619df53974177
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/199896
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: David Neto <dneto@google.com>
diff --git a/src/tint/lang/spirv/writer/loop_test.cc b/src/tint/lang/spirv/writer/loop_test.cc
index a02873e..524d7de 100644
--- a/src/tint/lang/spirv/writer/loop_test.cc
+++ b/src/tint/lang/spirv/writer/loop_test.cc
@@ -336,6 +336,146 @@
 )");
 }
 
+// Test that we generate valid SPIR-V when there is an unreachable instruction in the body of a
+// loop nested inside another loop's continuing block. SPIR-V requires that continue blocks are
+// structurally post-dominated by back-edge blocks, and the presence of OpUnreachable (a function
+// terminator) can trip up this validation. See crbug.com/354627692.
+TEST_F(SpirvWriterTest, Loop_NestedLoopInContinuing_UnreachableInNestedBody) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* outer_loop = b.Loop();
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* inner_loop = b.Loop();
+                b.Append(inner_loop->Body(), [&] {
+                    auto* ifelse = b.If(true);
+                    b.Append(ifelse->True(), [&] {  //
+                        b.ExitLoop(inner_loop);
+                    });
+                    b.Append(ifelse->False(), [&] {  //
+                        b.ExitLoop(inner_loop);
+                    });
+                    b.Unreachable();
+
+                    b.Append(inner_loop->Continuing(), [&] {  //
+                        b.BreakIf(inner_loop, true);
+                    });
+                });
+                b.BreakIf(outer_loop, true);
+            });
+        });
+        b.Return(func);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+          %4 = OpLabel
+               OpBranch %7
+          %7 = OpLabel
+               OpLoopMerge %8 %6 None
+               OpBranch %5
+          %5 = OpLabel
+               OpBranch %6
+          %6 = OpLabel
+               OpBranch %11
+         %11 = OpLabel
+               OpLoopMerge %12 %10 None
+               OpBranch %9
+          %9 = OpLabel
+               OpSelectionMerge %13 None
+               OpBranchConditional %true %14 %15
+         %14 = OpLabel
+               OpBranch %12
+         %15 = OpLabel
+               OpBranch %12
+         %13 = OpLabel
+               OpBranch %12
+         %10 = OpLabel
+               OpBranchConditional %true %12 %11
+         %12 = OpLabel
+               OpBranchConditional %true %8 %7
+          %8 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
+TEST_F(SpirvWriterTest, Loop_NestedLoopInContinuing_UnreachableInNestedBody_WithResults) {
+    auto* func = b.Function("foo", ty.i32());
+    b.Append(func->Block(), [&] {
+        auto* outer_result = b.InstructionResult(ty.i32());
+        auto* outer_loop = b.Loop();
+        outer_loop->SetResults(Vector{outer_result});
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* inner_result = b.InstructionResult(ty.i32());
+                auto* inner_loop = b.Loop();
+                inner_loop->SetResults(Vector{inner_result});
+                b.Append(inner_loop->Body(), [&] {
+                    auto* ifelse = b.If(true);
+                    b.Append(ifelse->True(), [&] {  //
+                        b.ExitLoop(inner_loop, 1_i);
+                    });
+                    b.Append(ifelse->False(), [&] {  //
+                        b.ExitLoop(inner_loop, 2_i);
+                    });
+                    b.Unreachable();
+
+                    b.Append(inner_loop->Continuing(), [&] {  //
+                        b.BreakIf(inner_loop, true, Empty, 3_i);
+                    });
+                });
+                b.BreakIf(outer_loop, true, Empty, inner_result);
+            });
+        });
+        b.Return(func, outer_result);
+    });
+
+    ASSERT_TRUE(Generate()) << Error() << output_;
+    EXPECT_INST(R"(
+          %4 = OpLabel
+               OpBranch %7
+          %7 = OpLabel
+               OpLoopMerge %8 %6 None
+               OpBranch %5
+          %5 = OpLabel
+               OpBranch %6
+          %6 = OpLabel
+               OpBranch %11
+         %11 = OpLabel
+               OpLoopMerge %12 %10 None
+               OpBranch %9
+          %9 = OpLabel
+               OpSelectionMerge %13 None
+               OpBranchConditional %true %14 %15
+         %14 = OpLabel
+               OpBranch %12
+         %15 = OpLabel
+               OpBranch %12
+         %13 = OpLabel
+               OpBranch %12
+         %10 = OpLabel
+               OpBranchConditional %true %12 %11
+         %12 = OpLabel
+         %18 = OpPhi %int %int_3 %10 %20 %13 %int_1 %14 %int_2 %15
+               OpBranchConditional %true %8 %7
+          %8 = OpLabel
+         %23 = OpPhi %int %18 %12
+               OpReturnValue %23
+               OpFunctionEnd
+
+               ; Function unused_entry_point
+%unused_entry_point = OpFunction %void None %26
+         %27 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)");
+}
+
 TEST_F(SpirvWriterTest, Loop_Phi_SingleValue) {
     auto* func = b.Function("foo", ty.void_());
 
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index e403d0f..647ee0a 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -896,6 +896,7 @@
     /// Emit all instructions of @p block.
     /// @param block the block's instructions to emit
     void EmitBlockInstructions(core::ir::Block* block) {
+        TINT_ASSERT(!block->IsEmpty());
         for (auto* inst : *block) {
             Switch(
                 inst,                                                                 //
@@ -928,11 +929,6 @@
                 }
             }
         }
-
-        if (block->IsEmpty()) {
-            // If the last emitted instruction is not a branch, then this should be unreachable.
-            current_function_.push_inst(spv::Op::OpUnreachable, {});
-        }
     }
 
     /// Emit a terminator instruction.
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.bazel b/src/tint/lang/spirv/writer/raise/BUILD.bazel
index 970bbab..3895fd0 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/raise/BUILD.bazel
@@ -45,6 +45,7 @@
     "merge_return.cc",
     "pass_matrix_by_pointer.cc",
     "raise.cc",
+    "remove_unreachable_in_loop_continuing.cc",
     "shader_io.cc",
     "var_for_dynamic_index.cc",
   ],
@@ -55,6 +56,7 @@
     "merge_return.h",
     "pass_matrix_by_pointer.h",
     "raise.h",
+    "remove_unreachable_in_loop_continuing.h",
     "shader_io.h",
     "var_for_dynamic_index.h",
   ],
@@ -107,6 +109,7 @@
     "handle_matrix_arithmetic_test.cc",
     "merge_return_test.cc",
     "pass_matrix_by_pointer_test.cc",
+    "remove_unreachable_in_loop_continuing_test.cc",
     "shader_io_test.cc",
     "var_for_dynamic_index_test.cc",
   ],
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.cmake b/src/tint/lang/spirv/writer/raise/BUILD.cmake
index 33ff0e9..71c2ef4 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/raise/BUILD.cmake
@@ -53,6 +53,8 @@
   lang/spirv/writer/raise/pass_matrix_by_pointer.h
   lang/spirv/writer/raise/raise.cc
   lang/spirv/writer/raise/raise.h
+  lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.cc
+  lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h
   lang/spirv/writer/raise/shader_io.cc
   lang/spirv/writer/raise/shader_io.h
   lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -112,6 +114,7 @@
   lang/spirv/writer/raise/handle_matrix_arithmetic_test.cc
   lang/spirv/writer/raise/merge_return_test.cc
   lang/spirv/writer/raise/pass_matrix_by_pointer_test.cc
+  lang/spirv/writer/raise/remove_unreachable_in_loop_continuing_test.cc
   lang/spirv/writer/raise/shader_io_test.cc
   lang/spirv/writer/raise/var_for_dynamic_index_test.cc
 )
diff --git a/src/tint/lang/spirv/writer/raise/BUILD.gn b/src/tint/lang/spirv/writer/raise/BUILD.gn
index a6a8303..2927a46 100644
--- a/src/tint/lang/spirv/writer/raise/BUILD.gn
+++ b/src/tint/lang/spirv/writer/raise/BUILD.gn
@@ -56,6 +56,8 @@
       "pass_matrix_by_pointer.h",
       "raise.cc",
       "raise.h",
+      "remove_unreachable_in_loop_continuing.cc",
+      "remove_unreachable_in_loop_continuing.h",
       "shader_io.cc",
       "shader_io.h",
       "var_for_dynamic_index.cc",
@@ -107,6 +109,7 @@
         "handle_matrix_arithmetic_test.cc",
         "merge_return_test.cc",
         "pass_matrix_by_pointer_test.cc",
+        "remove_unreachable_in_loop_continuing_test.cc",
         "shader_io_test.cc",
         "var_for_dynamic_index_test.cc",
       ]
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index 38b8e63..6314869 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -51,6 +51,7 @@
 #include "src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h"
 #include "src/tint/lang/spirv/writer/raise/merge_return.h"
 #include "src/tint/lang/spirv/writer/raise/pass_matrix_by_pointer.h"
+#include "src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h"
 #include "src/tint/lang/spirv/writer/raise/shader_io.h"
 #include "src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h"
 
@@ -142,6 +143,7 @@
     RUN_TRANSFORM(raise::ExpandImplicitSplats, module);
     RUN_TRANSFORM(raise::HandleMatrixArithmetic, module);
     RUN_TRANSFORM(raise::MergeReturn, module);
+    RUN_TRANSFORM(raise::RemoveUnreachableInLoopContinuing, module);
     RUN_TRANSFORM(raise::ShaderIO, module,
                   raise::ShaderIOConfig{options.clamp_frag_depth, options.emit_vertex_point_size,
                                         !options.use_storage_input_output_16});
diff --git a/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.cc b/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.cc
new file mode 100644
index 0000000..4f1fcdb
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.cc
@@ -0,0 +1,107 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+
+using namespace tint::core::number_suffixes;  // NOLINT
+using namespace tint::core::fluent_types;     // NOLINT
+
+namespace tint::spirv::writer::raise {
+
+namespace {
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    core::ir::Module& ir;
+
+    /// The IR builder.
+    core::ir::Builder b{ir};
+
+    /// Process the module.
+    void Process() {
+        // Find all unreachable instructions.
+        for (auto* inst : ir.Instructions()) {
+            if (auto* unreachable = inst->As<core::ir::Unreachable>()) {
+                Process(unreachable);
+            }
+        }
+    }
+
+    /// Check and replace an unreachable instruction if necessary.
+    /// @param unreachable the instruction to check and maybe replace
+    void Process(core::ir::Unreachable* unreachable) {
+        // Walk up the control stack to see if we are inside a loop continuing block.
+        auto* block = unreachable->Block();
+        while (block->Parent()) {
+            auto* control = block->Parent();
+            if (auto* loop = control->As<core::ir::Loop>()) {
+                if (loop->Continuing() == block) {
+                    Replace(unreachable);
+                    return;
+                }
+            }
+            block = control->Block();
+        }
+    }
+
+    /// Replace an unreachable instruction.
+    /// @param unreachable the instruction to replace
+    void Replace(core::ir::Unreachable* unreachable) {
+        auto* control = unreachable->Block()->Parent();
+
+        // Fill the exit argument with `undef`.
+        Vector<core::ir::Value*, 4> exit_args;
+        exit_args.Resize(control->Results().Length());
+
+        // Replace the `unreachable` with an instruction that exits from the control construct.
+        auto* exit = b.Exit(unreachable->Block()->Parent(), std::move(exit_args));
+        unreachable->ReplaceWith(exit);
+        unreachable->Destroy();
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> RemoveUnreachableInLoopContinuing(core::ir::Module& ir) {
+    auto result = ValidateAndDumpIfNeeded(ir, "RemoveUnreachableInLoopContinuing transform");
+    if (result != Success) {
+        return result;
+    }
+
+    State{ir}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::spirv::writer::raise
diff --git a/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h b/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h
new file mode 100644
index 0000000..17602fc
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h
@@ -0,0 +1,48 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_WRITER_RAISE_REMOVE_UNREACHABLE_IN_LOOP_CONTINUING_H_
+#define SRC_TINT_LANG_SPIRV_WRITER_RAISE_REMOVE_UNREACHABLE_IN_LOOP_CONTINUING_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::spirv::writer::raise {
+
+/// RemoveUnreachableInLoopContinuing is a transform that replaces unreachable statements that are
+/// nested inside a loop continuing block, as SPIR-V's structured control flow rules prohibit this.
+/// @param module the module to transform
+/// @returns success or failure
+Result<SuccessType> RemoveUnreachableInLoopContinuing(core::ir::Module& module);
+
+}  // namespace tint::spirv::writer::raise
+
+#endif  // SRC_TINT_LANG_SPIRV_WRITER_RAISE_REMOVE_UNREACHABLE_IN_LOOP_CONTINUING_H_
diff --git a/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing_test.cc b/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing_test.cc
new file mode 100644
index 0000000..196a744
--- /dev/null
+++ b/src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing_test.cc
@@ -0,0 +1,624 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/writer/raise/remove_unreachable_in_loop_continuing.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+namespace tint::spirv::writer::raise {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+using SpirvWriter_RemoveUnreachableInLoopContinuingTest = core::ir::transform::TransformTest;
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest, NoModify_TopLevel) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* ifelse = b.If(true);
+        b.Append(ifelse->True(), [&] {  //
+            b.Return(func);
+        });
+        b.Append(ifelse->False(), [&] {  //
+            b.Return(func);
+        });
+        b.Unreachable();
+    });
+
+    auto* src = R"(
+%foo = func():void {
+  $B1: {
+    if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        ret
+      }
+      $B3: {  # false
+        ret
+      }
+    }
+    unreachable
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest, NoModify_InLoopBody) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* outer_loop = b.Loop();
+        b.Append(outer_loop->Body(), [&] {
+            auto* inner_loop = b.Loop();
+            b.Append(inner_loop->Body(), [&] {
+                auto* ifelse = b.If(true);
+                b.Append(ifelse->True(), [&] {  //
+                    b.ExitLoop(inner_loop);
+                });
+                b.Append(ifelse->False(), [&] {  //
+                    b.ExitLoop(inner_loop);
+                });
+                b.Unreachable();
+
+                b.Append(inner_loop->Continuing(), [&] {  //
+                    b.BreakIf(inner_loop, true);
+                });
+            });
+            b.ExitLoop(outer_loop);
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2] {  # loop_1
+      $B2: {  # body
+        loop [b: $B3, c: $B4] {  # loop_2
+          $B3: {  # body
+            if true [t: $B5, f: $B6] {  # if_1
+              $B5: {  # true
+                exit_loop  # loop_2
+              }
+              $B6: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            unreachable
+          }
+          $B4: {  # continuing
+            break_if true  # -> [t: exit_loop loop_2, f: $B3]
+          }
+        }
+        exit_loop  # loop_1
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest, InContinuing_NestedInIfBlock) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* outer_loop = b.Loop();
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* ifelse = b.If(true);
+                b.Append(ifelse->True(), [&] {  //
+                    b.ExitIf(ifelse);
+                });
+                b.Append(ifelse->False(), [&] {  //
+                    b.Unreachable();
+                });
+                b.BreakIf(outer_loop, true);
+            });
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        if true [t: $B4, f: $B5] {  # if_1
+          $B4: {  # true
+            exit_if  # if_1
+          }
+          $B5: {  # false
+            unreachable
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        if true [t: $B4, f: $B5] {  # if_1
+          $B4: {  # true
+            exit_if  # if_1
+          }
+          $B5: {  # false
+            exit_if  # if_1
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest, InContinuing_NestedInSwitchCase) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* outer_loop = b.Loop();
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* swtch = b.Switch(1_i);
+                auto* case_1 = b.Case(swtch, {b.Constant(1_i)});
+                auto* def = b.DefaultCase(swtch);
+                b.Append(case_1, [&] {  //
+                    b.ExitSwitch(swtch);
+                });
+                b.Append(def, [&] {  //
+                    b.Unreachable();
+                });
+                b.BreakIf(outer_loop, true);
+            });
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        switch 1i [c: (1i, $B4), c: (default, $B5)] {  # switch_1
+          $B4: {  # case
+            exit_switch  # switch_1
+          }
+          $B5: {  # case
+            unreachable
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        switch 1i [c: (1i, $B4), c: (default, $B5)] {  # switch_1
+          $B4: {  # case
+            exit_switch  # switch_1
+          }
+          $B5: {  # case
+            exit_switch  # switch_1
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest, InContinuing_NestedInLoopBody) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* outer_loop = b.Loop();
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* inner_loop = b.Loop();
+                b.Append(inner_loop->Body(), [&] {
+                    auto* ifelse = b.If(true);
+                    b.Append(ifelse->True(), [&] {  //
+                        b.ExitLoop(inner_loop);
+                    });
+                    b.Append(ifelse->False(), [&] {  //
+                        b.ExitLoop(inner_loop);
+                    });
+                    b.Unreachable();
+
+                    b.Append(inner_loop->Continuing(), [&] {  //
+                        b.BreakIf(inner_loop, true);
+                    });
+                });
+                b.BreakIf(outer_loop, true);
+            });
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        loop [b: $B4, c: $B5] {  # loop_2
+          $B4: {  # body
+            if true [t: $B6, f: $B7] {  # if_1
+              $B6: {  # true
+                exit_loop  # loop_2
+              }
+              $B7: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            unreachable
+          }
+          $B5: {  # continuing
+            break_if true  # -> [t: exit_loop loop_2, f: $B4]
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        loop [b: $B4, c: $B5] {  # loop_2
+          $B4: {  # body
+            if true [t: $B6, f: $B7] {  # if_1
+              $B6: {  # true
+                exit_loop  # loop_2
+              }
+              $B7: {  # false
+                exit_loop  # loop_2
+              }
+            }
+            exit_loop  # loop_2
+          }
+          $B5: {  # continuing
+            break_if true  # -> [t: exit_loop loop_2, f: $B4]
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest,
+       InContinuing_NestedInLoopBody_WithResults) {
+    auto* func = b.Function("foo", ty.i32());
+    b.Append(func->Block(), [&] {
+        auto* outer_result = b.InstructionResult(ty.i32());
+        auto* outer_loop = b.Loop();
+        outer_loop->SetResults(Vector{outer_result});
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* inner_result = b.InstructionResult(ty.i32());
+                auto* inner_loop = b.Loop();
+                inner_loop->SetResults(Vector{inner_result});
+                b.Append(inner_loop->Body(), [&] {
+                    auto* ifelse = b.If(true);
+                    b.Append(ifelse->True(), [&] {  //
+                        b.ExitLoop(inner_loop, 1_i);
+                    });
+                    b.Append(ifelse->False(), [&] {  //
+                        b.ExitLoop(inner_loop, 2_i);
+                    });
+                    b.Unreachable();
+
+                    b.Append(inner_loop->Continuing(), [&] {  //
+                        b.BreakIf(inner_loop, true, Empty, 3_i);
+                    });
+                });
+                b.BreakIf(outer_loop, true, Empty, inner_result);
+            });
+        });
+        b.Return(func, outer_result);
+    });
+
+    auto* src = R"(
+%foo = func():i32 {
+  $B1: {
+    %2:i32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        %3:i32 = loop [b: $B4, c: $B5] {  # loop_2
+          $B4: {  # body
+            if true [t: $B6, f: $B7] {  # if_1
+              $B6: {  # true
+                exit_loop 1i  # loop_2
+              }
+              $B7: {  # false
+                exit_loop 2i  # loop_2
+              }
+            }
+            unreachable
+          }
+          $B5: {  # continuing
+            break_if true exit_loop: [ 3i ]  # -> [t: exit_loop loop_2, f: $B4]
+          }
+        }
+        break_if true exit_loop: [ %3 ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret %2
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():i32 {
+  $B1: {
+    %2:i32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        %3:i32 = loop [b: $B4, c: $B5] {  # loop_2
+          $B4: {  # body
+            if true [t: $B6, f: $B7] {  # if_1
+              $B6: {  # true
+                exit_loop 1i  # loop_2
+              }
+              $B7: {  # false
+                exit_loop 2i  # loop_2
+              }
+            }
+            exit_loop undef  # loop_2
+          }
+          $B5: {  # continuing
+            break_if true exit_loop: [ 3i ]  # -> [t: exit_loop loop_2, f: $B4]
+          }
+        }
+        break_if true exit_loop: [ %3 ]  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret %2
+  }
+}
+)";
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvWriter_RemoveUnreachableInLoopContinuingTest, InContinuing_DeeplyNestedInLoopBody) {
+    auto* func = b.Function("foo", ty.void_());
+    b.Append(func->Block(), [&] {
+        auto* outer_loop = b.Loop();
+        b.Append(outer_loop->Body(), [&] {
+            b.Continue(outer_loop);
+
+            b.Append(outer_loop->Continuing(), [&] {
+                auto* outer_if = b.If(true);
+                b.Append(outer_if->True(), [&] {
+                    auto* inner_loop = b.Loop();
+                    b.Append(inner_loop->Body(), [&] {
+                        auto* inner_if = b.If(true);
+                        b.Append(inner_if->True(), [&] {
+                            auto* ifelse = b.If(true);
+                            b.Append(ifelse->True(), [&] {  //
+                                b.ExitLoop(inner_loop);
+                            });
+                            b.Append(ifelse->False(), [&] {  //
+                                b.ExitLoop(inner_loop);
+                            });
+                            b.Unreachable();
+                        });
+                        b.Unreachable();
+
+                        b.Append(inner_loop->Continuing(), [&] {  //
+                            b.BreakIf(inner_loop, true);
+                        });
+                    });
+                    b.ExitIf(outer_if);
+                });
+                b.BreakIf(outer_loop, true);
+            });
+        });
+        b.Return(func);
+    });
+
+    auto* src = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        if true [t: $B4] {  # if_1
+          $B4: {  # true
+            loop [b: $B5, c: $B6] {  # loop_2
+              $B5: {  # body
+                if true [t: $B7] {  # if_2
+                  $B7: {  # true
+                    if true [t: $B8, f: $B9] {  # if_3
+                      $B8: {  # true
+                        exit_loop  # loop_2
+                      }
+                      $B9: {  # false
+                        exit_loop  # loop_2
+                      }
+                    }
+                    unreachable
+                  }
+                }
+                unreachable
+              }
+              $B6: {  # continuing
+                break_if true  # -> [t: exit_loop loop_2, f: $B5]
+              }
+            }
+            exit_if  # if_1
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%foo = func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        if true [t: $B4] {  # if_1
+          $B4: {  # true
+            loop [b: $B5, c: $B6] {  # loop_2
+              $B5: {  # body
+                if true [t: $B7] {  # if_2
+                  $B7: {  # true
+                    if true [t: $B8, f: $B9] {  # if_3
+                      $B8: {  # true
+                        exit_loop  # loop_2
+                      }
+                      $B9: {  # false
+                        exit_loop  # loop_2
+                      }
+                    }
+                    exit_if  # if_2
+                  }
+                }
+                exit_loop  # loop_2
+              }
+              $B6: {  # continuing
+                break_if true  # -> [t: exit_loop loop_2, f: $B5]
+              }
+            }
+            exit_if  # if_1
+          }
+        }
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)";
+
+    Run(RemoveUnreachableInLoopContinuing);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace
+}  // namespace tint::spirv::writer::raise
diff --git a/test/tint/bug/tint/354627692.wgsl b/test/tint/bug/tint/354627692.wgsl
new file mode 100644
index 0000000..b3f18aa
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl
@@ -0,0 +1,22 @@
+@group(0) @binding(0)
+var<storage, read_write> buffer : i32;
+
+@compute @workgroup_size(1)
+fn main() {
+  var i : i32 = buffer;
+  loop {
+    continuing {
+      loop {
+        if (i > 5) {
+          i = i * 2;
+          break;
+        } else {
+          i = i * 2;
+          break;
+        }
+      }
+      break if i > 10;
+    }
+  }
+  buffer = i;
+}
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.dxc.hlsl b/test/tint/bug/tint/354627692.wgsl.expected.dxc.hlsl
new file mode 100644
index 0000000..aacc43f
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.dxc.hlsl
@@ -0,0 +1,22 @@
+RWByteAddressBuffer buffer : register(u0);
+
+[numthreads(1, 1, 1)]
+void main() {
+  int i = asint(buffer.Load(0u));
+  while (true) {
+    {
+      while (true) {
+        if ((i > 5)) {
+          i = (i * 2);
+          break;
+        } else {
+          i = (i * 2);
+          break;
+        }
+      }
+      if ((i > 10)) { break; }
+    }
+  }
+  buffer.Store(0u, asuint(i));
+  return;
+}
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.fxc.hlsl b/test/tint/bug/tint/354627692.wgsl.expected.fxc.hlsl
new file mode 100644
index 0000000..aacc43f
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.fxc.hlsl
@@ -0,0 +1,22 @@
+RWByteAddressBuffer buffer : register(u0);
+
+[numthreads(1, 1, 1)]
+void main() {
+  int i = asint(buffer.Load(0u));
+  while (true) {
+    {
+      while (true) {
+        if ((i > 5)) {
+          i = (i * 2);
+          break;
+        } else {
+          i = (i * 2);
+          break;
+        }
+      }
+      if ((i > 10)) { break; }
+    }
+  }
+  buffer.Store(0u, asuint(i));
+  return;
+}
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.glsl b/test/tint/bug/tint/354627692.wgsl.expected.glsl
new file mode 100644
index 0000000..b2c6d01
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.glsl
@@ -0,0 +1,30 @@
+#version 310 es
+
+layout(binding = 0, std430) buffer tint_symbol_block_ssbo {
+  int inner;
+} tint_symbol;
+
+void tint_symbol_1() {
+  int i = tint_symbol.inner;
+  while (true) {
+    {
+      while (true) {
+        if ((i > 5)) {
+          i = (i * 2);
+          break;
+        } else {
+          i = (i * 2);
+          break;
+        }
+      }
+      if ((i > 10)) { break; }
+    }
+  }
+  tint_symbol.inner = i;
+}
+
+layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
+void main() {
+  tint_symbol_1();
+  return;
+}
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.ir.dxc.hlsl b/test/tint/bug/tint/354627692.wgsl.expected.ir.dxc.hlsl
new file mode 100644
index 0000000..7b45bb1
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.ir.dxc.hlsl
@@ -0,0 +1,28 @@
+
+RWByteAddressBuffer buffer : register(u0);
+[numthreads(1, 1, 1)]
+void main() {
+  int i = asint(buffer.Load(0u));
+  {
+    while(true) {
+      {
+        {
+          while(true) {
+            if ((i > 5)) {
+              i = (i * 2);
+              break;
+            } else {
+              i = (i * 2);
+              break;
+            }
+            /* unreachable */
+          }
+        }
+        if ((i > 10)) { break; }
+      }
+      continue;
+    }
+  }
+  buffer.Store(0u, asuint(i));
+}
+
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.ir.fxc.hlsl b/test/tint/bug/tint/354627692.wgsl.expected.ir.fxc.hlsl
new file mode 100644
index 0000000..7b45bb1
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.ir.fxc.hlsl
@@ -0,0 +1,28 @@
+
+RWByteAddressBuffer buffer : register(u0);
+[numthreads(1, 1, 1)]
+void main() {
+  int i = asint(buffer.Load(0u));
+  {
+    while(true) {
+      {
+        {
+          while(true) {
+            if ((i > 5)) {
+              i = (i * 2);
+              break;
+            } else {
+              i = (i * 2);
+              break;
+            }
+            /* unreachable */
+          }
+        }
+        if ((i > 10)) { break; }
+      }
+      continue;
+    }
+  }
+  buffer.Store(0u, asuint(i));
+}
+
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.ir.msl b/test/tint/bug/tint/354627692.wgsl.expected.ir.msl
new file mode 100644
index 0000000..806cf8e
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.ir.msl
@@ -0,0 +1,32 @@
+#include <metal_stdlib>
+using namespace metal;
+
+struct tint_module_vars_struct {
+  device int* tint_symbol;
+};
+
+kernel void tint_symbol_1(device int* tint_symbol [[buffer(0)]]) {
+  tint_module_vars_struct const tint_module_vars = tint_module_vars_struct{.tint_symbol=tint_symbol};
+  int i = (*tint_module_vars.tint_symbol);
+  {
+    while(true) {
+      {
+        {
+          while(true) {
+            if ((i > 5)) {
+              i = (i * 2);
+              break;
+            } else {
+              i = (i * 2);
+              break;
+            }
+            /* unreachable */
+          }
+        }
+        if ((i > 10)) { break; }
+      }
+      continue;
+    }
+  }
+  (*tint_module_vars.tint_symbol) = i;
+}
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.ir.spvasm b/test/tint/bug/tint/354627692.wgsl.expected.ir.spvasm
new file mode 100644
index 0000000..47b9c26
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.ir.spvasm
@@ -0,0 +1,77 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 1
+; Bound: 41
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpMemberName %tint_symbol_1 0 "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %main "main"
+               OpName %i "i"
+               OpMemberDecorate %tint_symbol_1 0 Offset 0
+               OpDecorate %tint_symbol_1 Block
+               OpDecorate %1 DescriptorSet 0
+               OpDecorate %1 Binding 0
+        %int = OpTypeInt 32 1
+%tint_symbol_1 = OpTypeStruct %int
+%_ptr_StorageBuffer_tint_symbol_1 = OpTypePointer StorageBuffer %tint_symbol_1
+          %1 = OpVariable %_ptr_StorageBuffer_tint_symbol_1 StorageBuffer
+       %void = OpTypeVoid
+          %7 = OpTypeFunction %void
+%_ptr_StorageBuffer_int = OpTypePointer StorageBuffer %int
+       %uint = OpTypeInt 32 0
+     %uint_0 = OpConstant %uint 0
+%_ptr_Function_int = OpTypePointer Function %int
+      %int_5 = OpConstant %int 5
+       %bool = OpTypeBool
+      %int_2 = OpConstant %int 2
+     %int_10 = OpConstant %int 10
+       %main = OpFunction %void None %7
+          %8 = OpLabel
+          %i = OpVariable %_ptr_Function_int Function
+          %9 = OpAccessChain %_ptr_StorageBuffer_int %1 %uint_0
+         %13 = OpLoad %int %9
+               OpStore %i %13
+               OpBranch %18
+         %18 = OpLabel
+               OpLoopMerge %19 %17 None
+               OpBranch %16
+         %16 = OpLabel
+               OpBranch %17
+         %17 = OpLabel
+               OpBranch %22
+         %22 = OpLabel
+               OpLoopMerge %23 %21 None
+               OpBranch %20
+         %20 = OpLabel
+         %24 = OpLoad %int %i
+         %25 = OpSGreaterThan %bool %24 %int_5
+               OpSelectionMerge %28 None
+               OpBranchConditional %25 %29 %30
+         %29 = OpLabel
+         %31 = OpLoad %int %i
+         %32 = OpIMul %int %31 %int_2
+               OpStore %i %32
+               OpBranch %23
+         %30 = OpLabel
+         %34 = OpLoad %int %i
+         %35 = OpIMul %int %34 %int_2
+               OpStore %i %35
+               OpBranch %23
+         %28 = OpLabel
+               OpBranch %23
+         %21 = OpLabel
+               OpBranch %22
+         %23 = OpLabel
+         %36 = OpLoad %int %i
+         %37 = OpSGreaterThan %bool %36 %int_10
+               OpBranchConditional %37 %19 %18
+         %19 = OpLabel
+         %39 = OpLoad %int %i
+         %40 = OpAccessChain %_ptr_StorageBuffer_int %1 %uint_0
+               OpStore %40 %39
+               OpReturn
+               OpFunctionEnd
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.msl b/test/tint/bug/tint/354627692.wgsl.expected.msl
new file mode 100644
index 0000000..977f57a
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.msl
@@ -0,0 +1,28 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+#define TINT_ISOLATE_UB(VOLATILE_NAME) \
+  volatile bool VOLATILE_NAME = true; \
+  if (VOLATILE_NAME)
+
+kernel void tint_symbol_1(device int* tint_symbol_2 [[buffer(0)]]) {
+  int i = *(tint_symbol_2);
+  TINT_ISOLATE_UB(tint_volatile_true) while(true) {
+    {
+      TINT_ISOLATE_UB(tint_volatile_true_1) while(true) {
+        if ((i > 5)) {
+          i = as_type<int>((as_type<uint>(i) * as_type<uint>(2)));
+          break;
+        } else {
+          i = as_type<int>((as_type<uint>(i) * as_type<uint>(2)));
+          break;
+        }
+      }
+      if ((i > 10)) { break; }
+    }
+  }
+  *(tint_symbol_2) = i;
+  return;
+}
+
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.spvasm b/test/tint/bug/tint/354627692.wgsl.expected.spvasm
new file mode 100644
index 0000000..2fee22e
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.spvasm
@@ -0,0 +1,79 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 42
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %buffer_block "buffer_block"
+               OpMemberName %buffer_block 0 "inner"
+               OpName %buffer "buffer"
+               OpName %main "main"
+               OpName %i "i"
+               OpDecorate %buffer_block Block
+               OpMemberDecorate %buffer_block 0 Offset 0
+               OpDecorate %buffer DescriptorSet 0
+               OpDecorate %buffer Binding 0
+        %int = OpTypeInt 32 1
+%buffer_block = OpTypeStruct %int
+%_ptr_StorageBuffer_buffer_block = OpTypePointer StorageBuffer %buffer_block
+     %buffer = OpVariable %_ptr_StorageBuffer_buffer_block StorageBuffer
+       %void = OpTypeVoid
+          %5 = OpTypeFunction %void
+       %uint = OpTypeInt 32 0
+     %uint_0 = OpConstant %uint 0
+%_ptr_StorageBuffer_int = OpTypePointer StorageBuffer %int
+%_ptr_Function_int = OpTypePointer Function %int
+         %16 = OpConstantNull %int
+      %int_5 = OpConstant %int 5
+       %bool = OpTypeBool
+      %int_2 = OpConstant %int 2
+     %int_10 = OpConstant %int 10
+       %main = OpFunction %void None %5
+          %8 = OpLabel
+          %i = OpVariable %_ptr_Function_int Function %16
+         %12 = OpAccessChain %_ptr_StorageBuffer_int %buffer %uint_0
+         %13 = OpLoad %int %12
+               OpStore %i %13
+               OpBranch %17
+         %17 = OpLabel
+               OpLoopMerge %18 %19 None
+               OpBranch %20
+         %20 = OpLabel
+               OpBranch %19
+         %19 = OpLabel
+               OpBranch %21
+         %21 = OpLabel
+               OpLoopMerge %22 %23 None
+               OpBranch %24
+         %24 = OpLabel
+         %25 = OpLoad %int %i
+         %27 = OpSGreaterThan %bool %25 %int_5
+               OpSelectionMerge %29 None
+               OpBranchConditional %27 %30 %31
+         %30 = OpLabel
+         %32 = OpLoad %int %i
+         %34 = OpIMul %int %32 %int_2
+               OpStore %i %34
+               OpBranch %22
+         %31 = OpLabel
+         %35 = OpLoad %int %i
+         %36 = OpIMul %int %35 %int_2
+               OpStore %i %36
+               OpBranch %22
+         %29 = OpLabel
+               OpBranch %23
+         %23 = OpLabel
+               OpBranch %21
+         %22 = OpLabel
+         %37 = OpLoad %int %i
+         %39 = OpSGreaterThan %bool %37 %int_10
+               OpBranchConditional %39 %18 %17
+         %18 = OpLabel
+         %40 = OpAccessChain %_ptr_StorageBuffer_int %buffer %uint_0
+         %41 = OpLoad %int %i
+               OpStore %40 %41
+               OpReturn
+               OpFunctionEnd
diff --git a/test/tint/bug/tint/354627692.wgsl.expected.wgsl b/test/tint/bug/tint/354627692.wgsl.expected.wgsl
new file mode 100644
index 0000000..1543167
--- /dev/null
+++ b/test/tint/bug/tint/354627692.wgsl.expected.wgsl
@@ -0,0 +1,22 @@
+@group(0) @binding(0) var<storage, read_write> buffer : i32;
+
+@compute @workgroup_size(1)
+fn main() {
+  var i : i32 = buffer;
+  loop {
+
+    continuing {
+      loop {
+        if ((i > 5)) {
+          i = (i * 2);
+          break;
+        } else {
+          i = (i * 2);
+          break;
+        }
+      }
+      break if (i > 10);
+    }
+  }
+  buffer = i;
+}