[spirv-reader][ir] Add OpPhi support.

Add support for `OpPhi`.

Bug: 398889961
Change-Id: I749266373eefc82784cb7bad9aff1874ca956322
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/243894
Auto-Submit: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/core/ir/operand_instruction.h b/src/tint/lang/core/ir/operand_instruction.h
index 1daf3f4..301df24 100644
--- a/src/tint/lang/core/ir/operand_instruction.h
+++ b/src/tint/lang/core/ir/operand_instruction.h
@@ -83,10 +83,14 @@
 
     /// Appends a new operand
     /// @param operand the new operand
-    void PushOperand(ir::Value* operand) {
-        auto idx = operands_.Length();
+    /// @returns the operand index
+    uint32_t PushOperand(ir::Value* operand) {
+        uint32_t idx = static_cast<uint32_t>(operands_.Length());
         operands_.Push(operand);
-        operand->AddUsage({this, static_cast<uint32_t>(idx)});
+        if (operand) {
+            operand->AddUsage({this, idx});
+        }
+        return idx;
     }
 
     /// Removes all operands from the instruction
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.bazel b/src/tint/lang/spirv/reader/parser/BUILD.bazel
index 1f7b7c4..48335aa 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/parser/BUILD.bazel
@@ -105,6 +105,7 @@
     "memory_test.cc",
     "misc_test.cc",
     "name_test.cc",
+    "phi_test.cc",
     "struct_test.cc",
     "unary_test.cc",
     "var_test.cc",
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.cmake b/src/tint/lang/spirv/reader/parser/BUILD.cmake
index 66dc68d..c832152 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/parser/BUILD.cmake
@@ -115,6 +115,7 @@
   lang/spirv/reader/parser/memory_test.cc
   lang/spirv/reader/parser/misc_test.cc
   lang/spirv/reader/parser/name_test.cc
+  lang/spirv/reader/parser/phi_test.cc
   lang/spirv/reader/parser/struct_test.cc
   lang/spirv/reader/parser/unary_test.cc
   lang/spirv/reader/parser/var_test.cc
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.gn b/src/tint/lang/spirv/reader/parser/BUILD.gn
index a5e2017..949d77a 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/parser/BUILD.gn
@@ -112,6 +112,7 @@
         "memory_test.cc",
         "misc_test.cc",
         "name_test.cc",
+        "phi_test.cc",
         "struct_test.cc",
         "unary_test.cc",
         "var_test.cc",
diff --git a/src/tint/lang/spirv/reader/parser/helper_test.h b/src/tint/lang/spirv/reader/parser/helper_test.h
index 5a0959d..297ef85 100644
--- a/src/tint/lang/spirv/reader/parser/helper_test.h
+++ b/src/tint/lang/spirv/reader/parser/helper_test.h
@@ -73,11 +73,12 @@
 
         // Validate the IR module against the capabilities supported by the SPIR-V dialect.
         auto validated =
-            core::ir::Validate(parsed.Get(), core::ir::Capabilities{
-                                                 core::ir::Capability::kAllowMultipleEntryPoints,
-                                                 core::ir::Capability::kAllowOverrides,
-                                                 core::ir::Capability::kAllowVectorElementPointer,
-                                             });
+            ValidateAndDumpIfNeeded(parsed.Get(), "spirv.test",
+                                    core::ir::Capabilities{
+                                        core::ir::Capability::kAllowMultipleEntryPoints,
+                                        core::ir::Capability::kAllowOverrides,
+                                        core::ir::Capability::kAllowVectorElementPointer,
+                                    });
         if (validated != Success) {
             return validated.Failure();
         }
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index d718701..84a3a22 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -67,6 +67,18 @@
 
 namespace {
 
+// Stores information for operands which need to be calculated after a block is complete. Because
+// a phi can store values which come after it, we can't calculate the value when the `OpPhi` is
+// seen.
+struct ReplacementValue {
+    // The terminator instruction the operand belongs to
+    core::ir::Terminator* terminator;
+    // The operand index in `terminator` to replace
+    uint32_t idx;
+    // The SPIR-V value id to create the value from
+    uint32_t value_id;
+};
+
 /// The SPIR-V environment that we validate against.
 constexpr auto kTargetEnv = SPV_ENV_VULKAN_1_1;
 
@@ -716,53 +728,127 @@
         return functions_.GetOrAdd(id, [&] { return b_.Function(ty_.void_()); });
     }
 
+    // Passes a value up through control flow to make it visible in an outer scope. Because SPIR-V
+    // allows an id to be referenced as long as it's dominated, you can access a variable which is
+    // defined inside an if branch for example. In order for that to be accessed in IR, we have to
+    // propagate the value as a return of the control instruction (like the if).
+    //
+    // e.g. if the IR is similar to the following:
+    // ```
+    // if (b) {
+    //    %a:i32 = let 4;
+    //    exit_if
+    // }
+    // %c:i32 = %a + %a;
+    // ```
+    //
+    // We propagate to something like:
+    // ```
+    // %d:i32 = if (b) {
+    //   %a:i32 = let 4;  // The spir-v ID refers to %a at this point
+    //   exit_if %a
+    // }
+    // %c:i32 = %d + %d  // The spir-v ID will now refer to %d instead of %a
+    // ```
+    //
+    // We can end up propagating up through multiple levels, so we can end up with something like:
+    // ```
+    // %k:i32 = if (true) {
+    //   %l:i32 = if (false) {
+    //     %m:i32 = if (true) {
+    //       %n:i32 = switch 4 {
+    //         default: {
+    //           %o:i32 = loop {
+    //             %a:i32 = let 4;
+    //             exit_loop %a
+    //           }
+    //           exit_switch %o
+    //         }
+    //       }
+    //       exit_if %n
+    //     }
+    //     exit_if %m
+    //   }
+    //   exit_if %l
+    // }
+    // %b:i32 = %k + %k
+    // ```
+    //
+    // @param id the spir-v ID to propagate up
+    // @param src the source value being propagated
     core::ir::Value* Propagate(uint32_t id, core::ir::Value* src) {
-        auto* src_res = src->As<core::ir::InstructionResult>();
-        TINT_ASSERT(src_res);
+        // Function params are always in scope so we should never need to propagate.
+        if (src->Is<core::ir::FunctionParam>()) {
+            return src;
+        }
 
-        auto* blk = src_res->Instruction()->Block();
+        auto* blk = tint::Switch(
+            src,  //
+            [&](core::ir::BlockParam* bp) { return bp->Block(); },
+            [&](core::ir::InstructionResult* res) { return res->Instruction()->Block(); },
+            TINT_ICE_ON_NO_MATCH);
+
+        // Walk up the set of control instructions from the current `blk`. We'll update the `src`
+        // instruction as the new result which is to be used for the given SPIR-V `id`. At each
+        // control instruction we'll add the current `src` as a result of each exit from the control
+        // instruction, making a new result which is available in the parent scope.
         while (blk) {
             if (InBlock(blk)) {
                 break;
             }
 
-            TINT_ASSERT(blk->Terminator());
+            core::ir::ControlInstruction* ctrl = nullptr;
+            if (auto* mb = blk->As<core::ir::MultiInBlock>()) {
+                ctrl = mb->Parent()->As<core::ir::ControlInstruction>();
+                TINT_ASSERT(ctrl);
 
-            core::ir::ControlInstruction* ctrl = tint::Switch(
-                blk->Terminator(),  //
-                [&](core::ir::ExitIf* ei) { return ei->If(); },
-                [&](core::ir::ExitSwitch* es) { return es->Switch(); },
-                [&](core::ir::ExitLoop* el) { return el->Loop(); },
-                [&](core::ir::Continue* cont) {
-                    // The propagation is going through a `continue`. This means
-                    // this is the only path to the continuing block, but it also
-                    // means we're current in the continuing block. We can't do
-                    // normal propagation here, we have to pass a block param
-                    // instead.
+                for (auto exit : ctrl->Exits()) {
+                    tint::Switch(
+                        exit.Value(),  //
+                        [&](core::ir::ExitLoop* el) { el->PushOperand(src); },
+                        [&](core::ir::BreakIf* bi) { bi->PushOperand(src); },  //
+                        TINT_ICE_ON_NO_MATCH);
+                }
+            } else {
+                TINT_ASSERT(blk->Terminator());
 
-                    auto* param = b_.BlockParam(src->Type());
+                ctrl = tint::Switch(
+                    blk->Terminator(),  //
+                    [&](core::ir::ExitIf* ei) { return ei->If(); },
+                    [&](core::ir::ExitSwitch* es) { return es->Switch(); },
+                    [&](core::ir::ExitLoop* el) { return el->Loop(); },
+                    [&](core::ir::NextIteration* ni) { return ni->Loop(); },
+                    [&](core::ir::Continue* cont) {
+                        // The propagation is going through a `continue`. This means
+                        // this is the only path to the continuing block, but it also
+                        // means we're current in the continuing block. We can't do
+                        // normal propagation here, we have to pass a block param
+                        // instead.
 
-                    // We're in the continuing block, so make the block param available in the
-                    // scope.
-                    id_stack_.back().insert(id);
+                        auto* param = b_.BlockParam(src->Type());
 
-                    auto* loop = cont->Loop();
-                    loop->Continuing()->AddParam(param);
+                        // We're in the continuing block, so make the block param available in the
+                        // scope.
+                        id_stack_.back().insert(id);
 
-                    cont->PushOperand(src);
+                        auto* loop = cont->Loop();
+                        loop->Continuing()->AddParam(param);
 
-                    // Set `src` as the `param` so it's returned as the new value
-                    src = param;
-                    return nullptr;
-                },                                                      //
-                [&](core::ir::Unreachable*) { return blk->Parent(); },  //
-                TINT_ICE_ON_NO_MATCH);
-            if (!ctrl) {
-                break;
-            }
+                        cont->PushOperand(src);
 
-            for (auto& exit : ctrl->Exits()) {
-                exit->PushOperand(src);
+                        // Set `src` as the `param` so it's returned as the new value
+                        src = param;
+                        return nullptr;
+                    },  //
+                    TINT_ICE_ON_NO_MATCH);
+
+                if (!ctrl) {
+                    break;
+                }
+
+                for (auto& exit : ctrl->Exits()) {
+                    exit->PushOperand(src);
+                }
             }
 
             // Add a new result to the control instruction
@@ -786,26 +872,16 @@
         return false;
     }
 
+    /// Attempts to retrieve the current Tint IR value for `id`. This ignores scoping for the
+    /// variable, if it exists it's returned (or if it's constant it's created). The value will not
+    /// propagate up through control instructions.
+    ///
     /// @param id a SPIR-V result ID
     /// @returns a Tint value object
-    core::ir::Value* Value(uint32_t id) {
+    core::ir::Value* ValueNoPropagate(uint32_t id) {
         auto v = values_.Get(id);
         if (v) {
-            if (!(*v)->Is<core::ir::InstructionResult>()) {
-                return *v;
-            }
-            if (IdIsInScope(id)) {
-                return *v;
-            }
-
-            // The Value is not in scope, so we need to find the originating Value, and then
-            // propagate it up through the control instructions. That will then change the
-            // `Value` which is returned so, set it into the values map as the new "Value" and
-            // return it.
-
-            auto* new_v = Propagate(id, *v);
-            values_.Replace(id, new_v);
-            return new_v;
+            return *v;
         }
 
         if (auto* c = spirv_context_->get_constant_mgr()->FindDeclaredConstant(id)) {
@@ -813,9 +889,30 @@
             values_.Add(id, val);
             return val;
         }
+
         TINT_UNREACHABLE() << "missing value for result ID " << id;
     }
 
+    /// Attempts to retrieve the current Tint IR value for `id`. If the value exists and is not in
+    /// scope it will propagate the value up through the control instructions.
+    ///
+    /// @param id a SPIR-V result ID
+    /// @returns a Tint value object
+    core::ir::Value* Value(uint32_t id) {
+        auto v = ValueNoPropagate(id);
+        TINT_ASSERT(v);
+
+        if (v->Is<core::ir::Constant>() || IdIsInScope(id)) {
+            return v;
+        }
+
+        auto* new_v = Propagate(id, v);
+        values_.Replace(id, new_v);
+        return new_v;
+    }
+
+    /// Creates the Tint IR constant for the SPIR-V `constant` value.
+    ///
     /// @param constant a SPIR-V constant object
     /// @returns a Tint constant value
     const core::constant::Value* Constant(const spvtools::opt::analysis::Constant* constant) {
@@ -1057,7 +1154,7 @@
 
     // A block parent is a container for a scope, like a `{}`d section in code. It controls the
     // block addition to the current blocks and the ID stack entry for the block.
-    void EmitBlockParent(core::ir::Block* dst, const spvtools::opt::BasicBlock& src) {
+    void EmitBlockParent(core::ir::Block* dst, spvtools::opt::BasicBlock& src) {
         TINT_ASSERT(!InBlock(dst));
 
         id_stack_.emplace_back();
@@ -1072,9 +1169,11 @@
     /// Emit the contents of SPIR-V block @p src into Tint IR block @p dst.
     /// @param dst the Tint IR block to append to
     /// @param src the SPIR-V block to emit
-    void EmitBlock(core::ir::Block* dst, const spvtools::opt::BasicBlock& src) {
+    void EmitBlock(core::ir::Block* dst, spvtools::opt::BasicBlock& src) {
         TINT_SCOPED_ASSIGNMENT(current_block_, dst);
 
+        values_to_replace_.push_back({});
+
         auto* loop_merge_inst = src.GetLoopMergeInst();
         // This is a loop merge block, so we need to treat it as a Loop.
         if (loop_merge_inst) {
@@ -1094,6 +1193,8 @@
             current_block_ = loop->Body();
         }
 
+        spirv_id_to_block_.insert({src.id(), current_block_});
+
         for (auto& inst : src) {
             switch (inst.opcode()) {
                 case spv::Op::OpNop:
@@ -1498,6 +1599,9 @@
                 case spv::Op::OpImageWrite:
                     EmitImageWrite(inst);
                     break;
+                case spv::Op::OpPhi:
+                    EmitPhi(inst);
+                    break;
                 default:
                     TINT_UNIMPLEMENTED()
                         << "unhandled SPIR-V instruction: " << static_cast<uint32_t>(inst.opcode());
@@ -1510,31 +1614,312 @@
             auto* loop = StopWalkingAt(src.id())->As<core::ir::Loop>();
             TINT_ASSERT(loop);
 
-            auto continue_id = loop_merge_inst->GetSingleWordInOperand(1);
-            if (continue_id != src.id()) {
-                const auto& bb_continue = current_spirv_function_->FindBlock(continue_id);
-
-                // Emit the continuing block.
-                EmitBlockParent(loop->Continuing(), *bb_continue);
-            }
-
             // Add the body terminator if necessary
             if (!loop->Body()->Terminator()) {
                 loop->Body()->Append(b_.Unreachable());
             }
+
+            // Push id stack entry for the continuing block. We don't use EmitBlockParent to do this
+            // because we need the scope to exist until after we process any `continue_blk_phis_`.
+            id_stack_.emplace_back();
+
+            auto continue_id = loop_merge_inst->GetSingleWordInOperand(1);
+            if (continue_id != src.id()) {
+                const auto& bb_continue = current_spirv_function_->FindBlock(continue_id);
+
+                current_blocks_.insert(loop->Continuing());
+                // Emit the continuing block.
+                EmitBlock(loop->Continuing(), *bb_continue);
+
+                current_blocks_.erase(loop->Continuing());
+            }
+
             if (!loop->Continuing()->Terminator()) {
                 loop->Continuing()->Append(b_.NextIteration(loop));
             }
 
+            // If this continue block needs to pass any `phi` instructions back to
+            // the main loop body.
+            //
+            // We have to do this here because we need to have emitted the loop
+            // body before we can get the values used in the continue block.
+            auto phis = continue_blk_phis_.find(continue_id);
+            if (phis != continue_blk_phis_.end()) {
+                for (auto value_id : phis->second) {
+                    auto* value = Value(value_id);
+
+                    tint::Switch(
+                        loop->Continuing()->Terminator(),  //
+                        [&](core::ir::NextIteration* ni) { ni->PushOperand(value); },
+                        [&](core::ir::BreakIf* bi) {
+                            // TODO(dsinclair): Need to change the break-if insertion of there
+                            // happens to be exit values, but those are rare, so leave this for when
+                            // we have test case.
+                            TINT_ASSERT(bi->ExitValues().IsEmpty());
+
+                            auto len = bi->NextIterValues().Length();
+                            bi->PushOperand(value);
+                            bi->SetNumNextIterValues(len + 1);
+                        },
+                        TINT_ICE_ON_NO_MATCH);
+                }
+            }
+
+            id_stack_.pop_back();
+        }
+
+        // For any `OpPhi` values we saw, insert their `Value` now. We do this
+        // at the end of the loop because a phi can refer to instructions
+        // defined after it in the block.
+        auto replace = values_to_replace_.back();
+        for (auto& val : replace) {
+            auto* value = ValueNoPropagate(val.value_id);
+            val.terminator->SetOperand(val.idx, value);
+        }
+        values_to_replace_.pop_back();
+
+        if (loop_merge_inst) {
+            auto* loop = StopWalkingAt(src.id())->As<core::ir::Loop>();
+            TINT_ASSERT(loop);
+
             current_blocks_.erase(loop->Body());
             id_stack_.pop_back();
 
+            // If we added phi's to the continuing block, we may have exits from the body which
+            // aren't valid.
+            auto continuing_param_count = loop->Continuing()->Params().Length();
+            if (continuing_param_count > 0) {
+                for (auto incoming : loop->Continuing()->InboundSiblingBranches()) {
+                    TINT_ASSERT(incoming->Is<core::ir::Continue>());
+
+                    // Check if the block this instruction exists in has default phi result that we
+                    // can append.
+                    auto inst_to_blk_iter = inst_to_spirv_block_.find(incoming);
+                    if (inst_to_blk_iter != inst_to_spirv_block_.end()) {
+                        uint32_t spirv_blk = inst_to_blk_iter->second;
+                        auto phi_values_from_loop_header = block_phi_values_[spirv_blk];
+                        // If there were phi values, push them to this instruction
+                        for (auto value_id : phi_values_from_loop_header) {
+                            auto* value = Value(value_id);
+                            incoming->PushOperand(value);
+                        }
+                    }
+                }
+            }
+
+            // Emit the merge block
             auto merge_id = loop_merge_inst->GetSingleWordInOperand(0);
             const auto& merge_bb = current_spirv_function_->FindBlock(merge_id);
             EmitBlock(dst, *merge_bb);
         }
     }
 
+    struct IfBranchValue {
+        core::ir::Value* value;
+        core::ir::If* if_;
+    };
+
+    void EmitPhi(spvtools::opt::Instruction& inst) {
+        auto num_ops = inst.NumInOperands();
+
+        // If there are only 2 arguments, that means we came directly from a block, so just emit the
+        // value directly.
+        if (num_ops == 2) {
+            AddValue(inst.result_id(), Value(inst.GetSingleWordInOperand(0)));
+            return;
+        }
+
+        std::unordered_map<core::ir::ControlInstruction*, const core::type::Type*>
+            ctrl_inst_result_types;
+        std::unordered_map<core::ir::MultiInBlock*, const core::type::Type*> blk_to_param_types;
+
+        std::optional<IfBranchValue> if_to_update_branch;
+
+        auto add_ctrl_inst = [&](core::ir::ControlInstruction* ctrl, const core::type::Type* type) {
+            auto iter = ctrl_inst_result_types.find(ctrl);
+            if (iter != ctrl_inst_result_types.end()) {
+                TINT_ASSERT(iter->second == type);
+                return;
+            }
+            ctrl_inst_result_types.insert({ctrl, type});
+        };
+
+        auto* type = Type(inst.type_id());
+        auto add_blk_inst = [&](core::ir::MultiInBlock* blk) {
+            auto iter = blk_to_param_types.find(blk);
+            if (iter != blk_to_param_types.end()) {
+                TINT_ASSERT(iter->second == type);
+                return;
+            }
+            blk_to_param_types.insert({blk, type});
+        };
+
+        auto* phi_spirv_block = spirv_context_->get_instr_block(&inst);
+        auto* phi_loop_merge_inst = phi_spirv_block->GetLoopMergeInst();
+
+        for (uint32_t i = 0; i < num_ops; i += 2) {
+            auto value_id = inst.GetSingleWordInOperand(i);
+            auto blk_id = inst.GetSingleWordInOperand(i + 1);
+
+            // Store this value away as a default phi value for this loop header.
+            block_phi_values_[blk_id].push_back(value_id);
+
+            auto value_blk_iter = spirv_id_to_block_.find(blk_id);
+
+            // The referenced block hasn't been emitted yet (continue blocks have this
+            // behaviour). So, store the fact that it needs to return a given value away for
+            // when we do emit the block.
+            if (value_blk_iter == spirv_id_to_block_.end()) {
+                auto continue_id = phi_loop_merge_inst->GetSingleWordInOperand(1);
+
+                // Note, we push it to the `continue_id` as the block and not
+                // `blk_id` so that we can emit them into the continuing block as
+                // a group.
+                continue_blk_phis_[continue_id].push_back(value_id);
+
+                // Add the phi to the current block set of input parameters
+                auto* mb = current_block_->As<core::ir::MultiInBlock>();
+                TINT_ASSERT(mb);
+                add_blk_inst(mb);
+                continue;
+            }
+
+            core::ir::Terminator* term = nullptr;
+
+            // The `OpPhi` is part of a loop header block, treat it special as we need to insert
+            // things into the phi's loop initializer/body/continuing block as needed.
+            if (phi_loop_merge_inst) {
+                auto* loop = StopWalkingAt(phi_spirv_block->id())->As<core::ir::Loop>();
+                TINT_ASSERT(loop);
+
+                // A phi from an explicit continue block is handled above as we haven't emitted the
+                // continue block so we wouldn't find it in the `spirv_id_to_block` list.
+
+                // If this loop header is also the continue block
+                if (blk_id == phi_spirv_block->id()) {
+                    if (loop->Continuing()->IsEmpty()) {
+                        b_.Append(loop->Continuing(), [&] { term = b_.NextIteration(loop); });
+                        add_blk_inst(loop->Body());
+                    } else {
+                        // With multiple phis we my have already created the continuing
+                        // block, so just get the terminator.
+                        term = loop->Continuing()->Terminator();
+                        TINT_ASSERT(term->Is<core::ir::NextIteration>());
+                    }
+                } else {
+                    // We know this isn't the continue as it hasn't emitted yet, so this has to be
+                    // coming from the calling block. So, we need to add this item into the
+                    // initializer `NextIteration` and as a parameter to the body.
+                    if (loop->Initializer()->IsEmpty()) {
+                        b_.Append(loop->Initializer(), [&] { term = b_.NextIteration(loop); });
+                        add_blk_inst(loop->Body());
+                    } else {
+                        term = loop->Initializer()->Terminator();
+                        TINT_ASSERT(term->Is<core::ir::NextIteration>());
+                    }
+                }
+
+            } else {
+                auto* value_ir_blk = value_blk_iter->second;
+
+                // We know the phi isn't part of a loop. That means, all of the blocks making up the
+                // phi are known. The one trick is that an `if` may only have a single block (the
+                // true or false). In that case, we have to push the value into the other block
+                // as it has to return something.
+                //
+                // For a `Switch` we will have all the cases already so we can just get the
+                // terminator for the block.
+
+                if (!value_ir_blk->Terminator()) {
+                    auto* if_ = value_ir_blk->Back()->As<core::ir::If>();
+                    TINT_ASSERT(if_);
+                    // No block terminator means the block that the `phi` is referencing
+                    // isn't finished (in IR-land). This can only happen with an `if`
+                    // instruction where you've branched directly to the `phi` as either the
+                    // `then` or `else` clause.
+
+                    TINT_ASSERT(!if_to_update_branch.has_value());
+
+                    auto* value = ValueNoPropagate(value_id);
+                    if_to_update_branch = IfBranchValue{
+                        .value = value,
+                        .if_ = if_,
+                    };
+
+                    continue;
+                }
+                term = value_ir_blk->Terminator();
+            }
+
+            // If we can't get to this part of the control flow, ignore the phi
+            if (term->Is<core::ir::Unreachable>()) {
+                continue;
+            }
+
+            // Push a placeholder for the operand value at this point. We'll
+            // store away the terminator/index pair along with the required
+            // value and then fill it in at the end of the block emission.
+            auto operand_idx = term->PushOperand(nullptr);
+            values_to_replace_.back().push_back(ReplacementValue{
+                .terminator = term,
+                .idx = operand_idx,
+                .value_id = value_id,
+            });
+
+            // For each incoming block to the phi, store either the control
+            // instruction to be updated, or the block to be updated and the
+            // type of result to return.
+            tint::Switch(
+                term,  //
+                [&](core::ir::Exit* exit) { add_ctrl_inst(exit->ControlInstruction(), type); },
+                [&](core::ir::BreakIf* bi) { add_ctrl_inst(bi->Loop(), type); },
+                [&](core::ir::Continue* cont) { add_blk_inst(cont->Loop()->Continuing()); },
+                [&](core::ir::NextIteration* ni) { add_blk_inst(ni->Loop()->Body()); },
+                TINT_ICE_ON_NO_MATCH);
+        }
+
+        // We need to update one of the two `if` branches with the return value.
+        // Find the one where the terminator has less operands and update that
+        // one.
+        if (if_to_update_branch.has_value()) {
+            auto* value = if_to_update_branch->value;
+            auto* if_ = if_to_update_branch->if_;
+
+            core::ir::Terminator* term = nullptr;
+
+            if (if_->True()->Terminator()->Operands().Length() <
+                if_->False()->Terminator()->Operands().Length()) {
+                term = if_->True()->Terminator();
+            } else {
+                term = if_->False()->Terminator();
+            }
+
+            term->PushOperand(value);
+            add_ctrl_inst(if_, value->Type());
+        }
+
+        // Update control instruction results to contain the new type.
+        for (auto info : ctrl_inst_result_types) {
+            auto* ctrl = info.first;
+            auto* res_type = info.second;
+            auto* res = b_.InstructionResult(res_type);
+            ctrl->AddResult(res);
+            values_.Replace(inst.result_id(), res);
+        }
+
+        // Update block params to contain the new type.
+        for (auto info : blk_to_param_types) {
+            auto* blk = info.first;
+            auto* param_type = info.second;
+
+            auto* p = b_.BlockParam(param_type);
+            blk->AddParam(p);
+
+            TINT_ASSERT(blk == current_block_);
+            AddValue(inst.result_id(), p);
+        }
+    }
+
     void EmitSampledImage(const spvtools::opt::Instruction& inst) {
         auto* tex = Value(inst.GetSingleWordInOperand(0));
         Emit(b_.CallExplicit<spirv::ir::BuiltinCall>(Type(inst.type_id()),
@@ -1810,6 +2195,7 @@
         if (auto* ctrl_inst = StopWalkingAt(dest_id)) {
             if (auto* loop = ctrl_inst->As<core::ir::Loop>()) {
                 // Going to the merge in a loop body has to be a break regardless of nesting level.
+
                 if (InBlock(loop->Body()) && !InBlock(loop->Continuing())) {
                     EmitWithoutResult(b_.Exit(ctrl_inst));
                 }
@@ -1907,27 +2293,30 @@
         return ctrl;
     }
 
-    void EmitBranchStopBlock(core::ir::ControlInstruction* ctrl,
-                             core::ir::If* if_,
-                             core::ir::Block* blk,
-                             uint32_t target) {
+    core::ir::Instruction* EmitBranchStopBlock(core::ir::ControlInstruction* ctrl,
+                                               core::ir::If* if_,
+                                               core::ir::Block* blk,
+                                               uint32_t target) {
         if (auto* loop = ContinueTarget(target)) {
-            blk->Append(b_.Continue(loop));
-        } else {
-            auto iter = merge_to_premerge_.find(target);
-            if (iter != merge_to_premerge_.end()) {
-                // Branch to a merge block, but skipping over an expected premerge block
-                // so we need a guard.
-                if (!iter->second.condition) {
-                    b_.InsertBefore(iter->second.parent, [&] {
-                        iter->second.condition = b_.Var("execute_premerge", true);
-                    });
-                }
-                b_.Append(blk, [&] { b_.Store(iter->second.condition, false); });
-            }
-
-            blk->Append(b_.Exit(ExitFor(ctrl, if_)));
+            auto* cont = b_.Continue(loop);
+            blk->Append(cont);
+            return cont;
         }
+
+        auto iter = merge_to_premerge_.find(target);
+        if (iter != merge_to_premerge_.end()) {
+            // Branch to a merge block, but skipping over an expected premerge block
+            // so we need a guard.
+            if (!iter->second.condition) {
+                b_.InsertBefore(iter->second.parent,
+                                [&] { iter->second.condition = b_.Var("execute_premerge", true); });
+            }
+            b_.Append(blk, [&] { b_.Store(iter->second.condition, false); });
+        }
+
+        auto* exit = b_.Exit(ExitFor(ctrl, if_));
+        blk->Append(exit);
+        return exit;
     }
 
     bool ProcessBranchAsLoopHeader(core::ir::Value* cond, uint32_t true_id, uint32_t false_id) {
@@ -2062,7 +2451,8 @@
         }
 
         if (auto* ctrl = StopWalkingAt(true_id)) {
-            EmitBranchStopBlock(ctrl, if_, if_->True(), true_id);
+            auto* new_inst = EmitBranchStopBlock(ctrl, if_, if_->True(), true_id);
+            inst_to_spirv_block_[new_inst] = bb.id();
         } else {
             EmitIfBranch(true_id, if_, if_->True());
         }
@@ -2072,7 +2462,8 @@
         if (false_id == true_id) {
             if_->False()->Append(b_.Unreachable());
         } else if (auto* ctrl = StopWalkingAt(false_id)) {
-            EmitBranchStopBlock(ctrl, if_, if_->False(), false_id);
+            auto* new_inst = EmitBranchStopBlock(ctrl, if_, if_->False(), false_id);
+            inst_to_spirv_block_[new_inst] = bb.id();
         } else {
             EmitIfBranch(false_id, if_, if_->False());
         }
@@ -2891,7 +3282,7 @@
     std::unordered_map<uint32_t, core::ir::ControlInstruction*> walk_stop_blocks_;
     // Map of continue target ID to the controlling IR loop.
     std::unordered_map<uint32_t, core::ir::Loop*> continue_targets_;
-    // Map of continue target ID to the controlling IR loop.
+    // Map of header target ID to the controlling IR loop.
     std::unordered_map<uint32_t, core::ir::Loop*> loop_headers_;
 
     struct PremergeInfo {
@@ -2902,11 +3293,31 @@
     std::unordered_map<uint32_t, PremergeInfo> merge_to_premerge_;
 
     std::unordered_set<core::ir::Block*> current_blocks_;
+
+    // For each block, we keep a set of SPIR-V `id`s which are known in that scope.
     std::vector<std::unordered_set<uint32_t>> id_stack_;
 
     // If we're in a switch, is populated with the IDs of the blocks for each of the switch
     // selectors. This lets us watch for fallthrough when emitting branch instructions.
     std::vector<std::unordered_set<uint32_t>> current_switch_blocks_;
+
+    /// Maps from a spirv-v block id to the corresponding block in the IR
+    std::unordered_map<uint32_t, core::ir::Block*> spirv_id_to_block_;
+
+    // Map of continue block id to the phi types which need to be returned by
+    // the continue target
+    std::unordered_map<uint32_t, std::vector<uint32_t>> continue_blk_phis_;
+
+    // A stack of values which need to be replaced as we finish processing a
+    // block. Used to store `phi` information so we can retrieve values which
+    // are defined after the `OpPhi` instruction.
+    std::vector<std::vector<ReplacementValue>> values_to_replace_;
+
+    // A map of loop header to phi values returned by that loop header
+    std::unordered_map<uint32_t, std::vector<uint32_t>> block_phi_values_;
+
+    // Map of certain instructions back to their originating spirv block
+    std::unordered_map<core::ir::Instruction*, uint32_t> inst_to_spirv_block_;
 };
 
 }  // namespace
diff --git a/src/tint/lang/spirv/reader/parser/phi_test.cc b/src/tint/lang/spirv/reader/parser/phi_test.cc
new file mode 100644
index 0000000..22a6610
--- /dev/null
+++ b/src/tint/lang/spirv/reader/parser/phi_test.cc
@@ -0,0 +1,1538 @@
+// Copyright 2025 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/reader/parser/helper_test.h"
+
+namespace tint::spirv::reader {
+namespace {
+
+TEST_F(SpirvParserTest, Phi_FromBlock) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+       %true = OpConstantTrue %bool
+      %int_2 = OpConstant %int 2
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %1
+          %1 = OpLabel
+          %2 = OpPhi %int %int_2 %main_start
+          %3 = OpIAdd %int %2 %2
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = spirv.add<i32> 2i, 2i
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_If_Undef) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+       %true = OpConstantTrue %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %1
+          %1 = OpLabel
+               OpSelectionMerge %5 None
+               OpBranchConditional %true %3 %4
+          %3 = OpLabel
+          %6 = OpUndef %int
+               OpBranch %5
+          %4 = OpLabel
+          %7 = OpUndef %int
+               OpBranch %5
+          %5 = OpLabel
+          %8 = OpPhi %int %6 %3 %7 %4
+          %9 = OpIAdd %int %8 %8
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        exit_if 0i  # if_1
+      }
+      $B3: {  # false
+        exit_if 0i  # if_1
+      }
+    }
+    %3:i32 = spirv.add<i32> %2, %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_If_ThenAndElse) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+       %true = OpConstantTrue %bool
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+      %int_3 = OpConstant %int 3
+      %int_4 = OpConstant %int 4
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %1
+          %1 = OpLabel
+               OpSelectionMerge %5 None
+               OpBranchConditional %true %3 %4
+          %3 = OpLabel
+          %6 = OpIAdd %int %int_1 %int_2
+               OpBranch %5
+          %4 = OpLabel
+          %7 = OpIAdd %int %int_3 %int_4
+               OpBranch %5
+          %5 = OpLabel
+          %8 = OpPhi %int %6 %3 %7 %4
+          %9 = OpIAdd %int %8 %8
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        %3:i32 = spirv.add<i32> 1i, 2i
+        exit_if %3  # if_1
+      }
+      $B3: {  # false
+        %4:i32 = spirv.add<i32> 3i, 4i
+        exit_if %4  # if_1
+      }
+    }
+    %5:i32 = spirv.add<i32> %2, %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_If_ThenNoElse) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+       %true = OpConstantTrue %bool
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+      %int_3 = OpConstant %int 3
+      %int_4 = OpConstant %int 4
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %1
+          %1 = OpLabel
+               OpSelectionMerge %5 None
+               OpBranchConditional %true %3 %5
+          %3 = OpLabel
+          %6 = OpIAdd %int %int_1 %int_2
+               OpBranch %5
+          %5 = OpLabel
+          %8 = OpPhi %int %int_2 %1 %6 %3
+          %9 = OpIAdd %int %8 %8
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        %3:i32 = spirv.add<i32> 1i, 2i
+        exit_if %3  # if_1
+      }
+      $B3: {  # false
+        exit_if 2i  # if_1
+      }
+    }
+    %4:i32 = spirv.add<i32> %2, %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_If_NoThenElse) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+       %true = OpConstantTrue %bool
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+      %int_3 = OpConstant %int 3
+      %int_4 = OpConstant %int 4
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %1
+          %1 = OpLabel
+               OpSelectionMerge %5 None
+               OpBranchConditional %true %5 %3
+          %3 = OpLabel
+          %6 = OpIAdd %int %int_1 %int_2
+               OpBranch %5
+          %5 = OpLabel
+          %8 = OpPhi %int %6 %3 %int_2 %1
+          %9 = OpIAdd %int %8 %8
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        exit_if 2i  # if_1
+      }
+      $B3: {  # false
+        %3:i32 = spirv.add<i32> 1i, 2i
+        exit_if %3  # if_1
+      }
+    }
+    %4:i32 = spirv.add<i32> %2, %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_Switch) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+          %3 = OpTypeFunction %void
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+       %main = OpFunction %void None %3
+          %4 = OpLabel
+               OpSelectionMerge %13 None
+               OpSwitch %int_0 %10 0 %11 1 %12
+         %10 = OpLabel
+               OpBranch %13
+         %11 = OpLabel
+               OpBranch %13
+         %12 = OpLabel
+               OpBranch %13
+         %13 = OpLabel
+         %14 = OpPhi %int %int_0 %10 %int_1 %11 %int_2 %12
+         %15 = OpIAdd %int %14 %14
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = switch 0i [c: (default, $B2), c: (0i, $B3), c: (1i, $B4)] {  # switch_1
+      $B2: {  # case
+        exit_switch 0i  # switch_1
+      }
+      $B3: {  # case
+        exit_switch 1i  # switch_1
+      }
+      $B4: {  # case
+        exit_switch 2i  # switch_1
+      }
+    }
+    %3:i32 = spirv.add<i32> %2, %2
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_Loop_ContinueIsHeader) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %false %20
+               OpLoopMerge %99 %20 None
+               OpBranchConditional %101 %99 %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%2:bool): {  # body
+        if %2 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            continue  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4: {  # continuing
+        next_iteration false  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_Loop_WithContinue) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %false %30
+               OpLoopMerge %99 %30 None
+               OpBranchConditional %101 %99 %25
+         %25 = OpLabel
+               OpBranch %30
+         %30 = OpLabel
+        %102 = OpCopyObject %bool %101
+               OpBranch %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%2:bool): {  # body
+        if %2 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            continue  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4: {  # continuing
+        %3:bool = let %2
+        next_iteration false  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_Loop_WithContinue_PhiInContinue) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+               OpLoopMerge %99 %30 None
+               OpBranchConditional %true %24 %30
+         %24 = OpLabel
+               OpBranch %30
+         %30 = OpLabel
+        %101 = OpPhi %bool %true %24 %false %20
+        %102 = OpCopyObject %bool %101
+               OpBranchConditional %true %99 %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        if true [t: $B4, f: $B5] {  # if_1
+          $B4: {  # true
+            continue true  # -> $B3
+          }
+          $B5: {  # false
+            continue false  # -> $B3
+          }
+        }
+        unreachable
+      }
+      $B3 (%2:bool): {  # continuing
+        %3:bool = let %2
+        break_if true  # -> [t: exit_loop loop_1, f: $B2]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_Loop_WithMultiblockContinue) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %false %31
+               OpLoopMerge %99 %30 None
+               OpBranchConditional %101 %99 %25
+         %25 = OpLabel
+               OpBranch %30
+         %30 = OpLabel
+        %102 = OpCopyObject %bool %101
+               OpBranch %31
+         %31 = OpLabel
+               OpBranch %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%2:bool): {  # body
+        if %2 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            continue  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4: {  # continuing
+        %3:bool = let %2
+        next_iteration false  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_Loop_BranchConditionalBreak) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+       %true = OpConstantTrue %bool
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+      %int_3 = OpConstant %int 3
+      %int_4 = OpConstant %int 4
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %1
+          %1 = OpLabel
+               OpLoopMerge %99 %20 None
+               OpBranchConditional %true %50 %20
+         %50 = OpLabel
+          %6 = OpIAdd %int %int_1 %int_2
+               OpBranch %99
+         %20 = OpLabel
+               OpBranch %1
+         %99 = OpLabel
+          %8 = OpPhi %int %6 %50
+          %9 = OpIAdd %int %8 %8
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:i32 = loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        if true [t: $B4, f: $B5] {  # if_1
+          $B4: {  # true
+            %3:i32 = spirv.add<i32> 1i, 2i
+            exit_loop %3  # loop_1
+          }
+          $B5: {  # false
+            continue  # -> $B3
+          }
+        }
+        unreachable
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    %4:i32 = spirv.add<i32> %2, %2
+    ret
+  }
+}
+)");
+}
+
+// Phis must act as if they are simultaneously assigned. %101 and %102 should exchange values on
+// each iteration, and never have the same value.
+TEST_F(SpirvParserTest, Phi_SimultaneousAssignment) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %101 "default_true"
+               OpName %102 "default_false"
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %102 %20
+        %102 = OpPhi %bool %false %10 %101 %20
+               OpLoopMerge %99 %20 None
+               OpBranchConditional %true %99 %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true, false  # -> $B3
+      }
+      $B3 (%2:bool, %3:bool): {  # body
+        if true [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            continue  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4: {  # continuing
+        next_iteration %3, %2  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_SingleBlockLoopIndex) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %2 "computed"
+               OpName %3 "copied"
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %uint = OpTypeInt 32 0
+        %pty = OpTypePointer Private %uint
+          %1 = OpVariable %pty Private
+    %boolpty = OpTypePointer Private %bool
+          %7 = OpVariable %boolpty Private
+          %8 = OpVariable %boolpty Private
+     %uint_0 = OpConstant %uint 0
+     %uint_1 = OpConstant %uint 1
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpBranch %10
+; Use an outer loop to show we put the new variable in the
+; smallest enclosing scope.
+         %10 = OpLabel
+        %101 = OpLoad %bool %7
+        %102 = OpLoad %bool %8
+        %103 = OpIAdd %uint %uint_0 %uint_0
+               OpLoopMerge %99 %89 None
+               OpBranchConditional %101 %99 %20
+         %20 = OpLabel
+          %2 = OpPhi %uint %103 %10 %4 %20  ; gets computed value
+          %3 = OpPhi %uint %uint_1 %10 %3 %20  ; gets itself
+          %4 = OpIAdd %uint %2 %uint_1
+               OpLoopMerge %79 %20 None
+               OpBranchConditional %102 %79 %20
+         %79 = OpLabel
+               OpBranch %89
+         %89 = OpLabel
+               OpBranch %10
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+$B1: {  # root
+  %1:ptr<private, u32, read_write> = var undef
+  %2:ptr<private, bool, read_write> = var undef
+  %3:ptr<private, bool, read_write> = var undef
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    loop [b: $B3, c: $B4] {  # loop_1
+      $B3: {  # body
+        %5:bool = load %2
+        %6:bool = load %3
+        %7:u32 = spirv.add<u32> 0u, 0u
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            loop [i: $B7, b: $B8, c: $B9] {  # loop_2
+              $B7: {  # initializer
+                next_iteration %7, 1u  # -> $B8
+              }
+              $B8 (%8:u32, %9:u32): {  # body
+                %10:u32 = spirv.add<u32> %8, 1u
+                if %6 [t: $B10, f: $B11] {  # if_2
+                  $B10: {  # true
+                    exit_loop  # loop_2
+                  }
+                  $B11: {  # false
+                    continue  # -> $B9
+                  }
+                }
+                unreachable
+              }
+              $B9: {  # continuing
+                next_iteration %10, %9  # -> $B8
+              }
+            }
+            continue  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4: {  # continuing
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_MultiBlockLoopIndex) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %uint = OpTypeInt 32 0
+        %pty = OpTypePointer Private %uint
+          %1 = OpVariable %pty Private
+    %boolpty = OpTypePointer Private %bool
+          %7 = OpVariable %boolpty Private
+          %8 = OpVariable %boolpty Private
+     %uint_0 = OpConstant %uint 0
+     %uint_1 = OpConstant %uint 1
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+          %5 = OpLabel
+               OpBranch %10
+; Use an outer loop to show we put the new variable in the
+; smallest enclosing scope.
+         %10 = OpLabel
+        %101 = OpLoad %bool %7
+        %102 = OpLoad %bool %8
+               OpLoopMerge %99 %89 None
+               OpBranchConditional %101 %99 %20
+         %20 = OpLabel
+          %2 = OpPhi %uint %uint_0 %10 %4 %30  ; gets computed value
+          %3 = OpPhi %uint %uint_1 %10 %3 %30  ; gets itself
+               OpLoopMerge %79 %30 None
+               OpBranchConditional %102 %79 %30
+         %30 = OpLabel  ; continue target for inner loop
+          %4 = OpIAdd %uint %2 %uint_1
+               OpBranch %20
+         %79 = OpLabel  ; merge for inner loop
+               OpBranch %89
+         %89 = OpLabel  ; continue target for outer loop
+               OpBranch %10
+         %99 = OpLabel  ; merge for outer loop
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+$B1: {  # root
+  %1:ptr<private, u32, read_write> = var undef
+  %2:ptr<private, bool, read_write> = var undef
+  %3:ptr<private, bool, read_write> = var undef
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    loop [b: $B3, c: $B4] {  # loop_1
+      $B3: {  # body
+        %5:bool = load %2
+        %6:bool = load %3
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            loop [i: $B7, b: $B8, c: $B9] {  # loop_2
+              $B7: {  # initializer
+                next_iteration 0u, 1u  # -> $B8
+              }
+              $B8 (%7:u32, %8:u32): {  # body
+                if %6 [t: $B10, f: $B11] {  # if_2
+                  $B10: {  # true
+                    exit_loop  # loop_2
+                  }
+                  $B11: {  # false
+                    continue  # -> $B9
+                  }
+                }
+                unreachable
+              }
+              $B9: {  # continuing
+                %9:u32 = spirv.add<u32> %7, 1u
+                next_iteration %9, %8  # -> $B8
+              }
+            }
+            continue  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4: {  # continuing
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_ValueFromLoopBodyAndContinuing) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %uint = OpTypeInt 32 0
+        %pty = OpTypePointer Private %uint
+          %1 = OpVariable %pty Private
+    %boolpty = OpTypePointer Private %bool
+         %17 = OpVariable %boolpty Private
+     %uint_0 = OpConstant %uint 0
+     %uint_1 = OpConstant %uint 1
+       %true = OpConstantTrue %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+          %9 = OpLabel
+        %101 = OpLoad %bool %17
+               OpBranch %10
+; Use an outer loop to show we put the new variable in the
+; smallest enclosing scope.
+         %10 = OpLabel
+               OpLoopMerge %99 %89 None
+               OpBranch %20
+         %20 = OpLabel
+          %2 = OpPhi %uint %uint_0 %10 %4 %30  ; gets computed value
+          %5 = OpPhi %uint %uint_1 %10 %7 %30
+          %4 = OpIAdd %uint %2 %uint_1 ; define %4
+          %6 = OpIAdd %uint %4 %uint_1 ; use %4
+               OpLoopMerge %79 %30 None
+               OpBranchConditional %101 %79 %30
+         %30 = OpLabel
+          %7 = OpIAdd %uint %4 %6 ; use %4 again
+          %8 = OpCopyObject %uint %5 ; use %5
+               OpBranchConditional %true %20 %79
+         %79 = OpLabel
+               OpBranch %89
+         %89 = OpLabel
+               OpBranchConditional %true %10 %99
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+$B1: {  # root
+  %1:ptr<private, u32, read_write> = var undef
+  %2:ptr<private, bool, read_write> = var undef
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    %4:bool = load %2
+    loop [b: $B3, c: $B4] {  # loop_1
+      $B3: {  # body
+        loop [i: $B5, b: $B6, c: $B7] {  # loop_2
+          $B5: {  # initializer
+            next_iteration 0u, 1u  # -> $B6
+          }
+          $B6 (%5:u32, %6:u32): {  # body
+            %7:u32 = spirv.add<u32> %5, 1u
+            %8:u32 = spirv.add<u32> %7, 1u
+            if %4 [t: $B8, f: $B9] {  # if_1
+              $B8: {  # true
+                exit_loop  # loop_2
+              }
+              $B9: {  # false
+                continue  # -> $B7
+              }
+            }
+            unreachable
+          }
+          $B7: {  # continuing
+            %9:u32 = spirv.add<u32> %7, %8
+            %10:u32 = let %6
+            %11:bool = not true
+            break_if %11 next_iteration: [ %7, %9 ]  # -> [t: exit_loop loop_2, f: $B6]
+          }
+        }
+        continue  # -> $B4
+      }
+      $B4: {  # continuing
+        %12:bool = not true
+        break_if %12  # -> [t: exit_loop loop_1, f: $B3]
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_FromElseAndThen) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %uint = OpTypeInt 32 0
+        %pty = OpTypePointer Private %uint
+          %1 = OpVariable %pty Private
+    %boolpty = OpTypePointer Private %bool
+          %7 = OpVariable %boolpty Private
+          %8 = OpVariable %boolpty Private
+     %uint_0 = OpConstant %uint 0
+     %uint_1 = OpConstant %uint 1
+     %uint_3 = OpConstant %uint 3
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+          %5 = OpLabel
+        %101 = OpLoad %bool %7
+        %102 = OpLoad %bool %8
+               OpBranch %10
+; Use an outer loop to show we put the new variable in the
+; smallest enclosing scope.
+         %10 = OpLabel
+               OpLoopMerge %99 %89 None
+               OpBranchConditional %101 %99 %20
+         %20 = OpLabel ; if seleciton
+               OpSelectionMerge %79 None
+               OpBranchConditional %102 %30 %40
+         %30 = OpLabel
+               OpBranch %89
+         %40 = OpLabel
+               OpBranch %89
+         %79 = OpLabel ; disconnected selection merge node
+               OpBranch %89
+         %89 = OpLabel
+          %2 = OpPhi %uint %uint_0 %30 %uint_1 %40 %uint_3 %79
+               OpStore %1 %2
+               OpBranch %10
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+$B1: {  # root
+  %1:ptr<private, u32, read_write> = var undef
+  %2:ptr<private, bool, read_write> = var undef
+  %3:ptr<private, bool, read_write> = var undef
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    %5:bool = load %2
+    %6:bool = load %3
+    loop [b: $B3, c: $B4] {  # loop_1
+      $B3: {  # body
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            if %6 [t: $B7, f: $B8] {  # if_2
+              $B7: {  # true
+                continue 0u  # -> $B4
+              }
+              $B8: {  # false
+                continue 1u  # -> $B4
+              }
+            }
+            continue 3u  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4 (%7:u32): {  # continuing
+        store %1, %7
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, Phi_FromHeaderAndThen) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %uint = OpTypeInt 32 0
+        %pty = OpTypePointer Private %uint
+          %1 = OpVariable %pty Private
+    %boolpty = OpTypePointer Private %bool
+          %7 = OpVariable %boolpty Private
+          %8 = OpVariable %boolpty Private
+     %uint_0 = OpConstant %uint 0
+     %uint_1 = OpConstant %uint 1
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+          %5 = OpLabel
+        %101 = OpLoad %bool %7
+        %102 = OpLoad %bool %8
+               OpBranch %10
+; Use an outer loop to show we put the new variable in the
+; smallest enclosing scope.
+         %10 = OpLabel
+               OpLoopMerge %99 %89 None
+               OpBranchConditional %101 %99 %20
+         %20 = OpLabel ; if seleciton
+               OpSelectionMerge %79 None
+               OpBranchConditional %102 %30 %89
+         %30 = OpLabel
+               OpBranch %89
+         %79 = OpLabel ; disconnected selection merge node
+               OpUnreachable
+         %89 = OpLabel
+          %2 = OpPhi %uint %uint_0 %20 %uint_1 %30
+          %3 = OpPhi %uint %uint_1 %20 %uint_0 %30
+          %4 = OpIAdd %uint %2 %3
+               OpStore %1 %4
+               OpBranch %10
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+$B1: {  # root
+  %1:ptr<private, u32, read_write> = var undef
+  %2:ptr<private, bool, read_write> = var undef
+  %3:ptr<private, bool, read_write> = var undef
+}
+
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B2: {
+    %5:bool = load %2
+    %6:bool = load %3
+    loop [b: $B3, c: $B4] {  # loop_1
+      $B3: {  # body
+        if %5 [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            if %6 [t: $B7, f: $B8] {  # if_2
+              $B7: {  # true
+                continue 1u, 0u  # -> $B4
+              }
+              $B8: {  # false
+                continue 0u, 1u  # -> $B4
+              }
+            }
+            unreachable
+          }
+        }
+        unreachable
+      }
+      $B4 (%7:u32, %8:u32): {  # continuing
+        %9:u32 = spirv.add<u32> %7, %8
+        store %1, %9
+        next_iteration  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+// If the only use of a combinatorially computed ID is as the value in an OpPhi, then we still have
+// to emit it.  The algorithm fix is to always count uses in Phis. This is the reduced case from the
+// bug report.
+//
+// * The only use of %12 is in the phi.
+// * The only use of %11 is in %12.
+// * Both definitions need to be emitted to the output.
+//
+// https://crbug.com/215
+TEST_F(SpirvParserTest, Phi_UseInPhiCountsAsUse) {
+    EXPECT_IR(
+        R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+         %11 = OpLogicalAnd %bool %true %true
+         %12 = OpLogicalNot %bool %11  ;
+               OpSelectionMerge %99 None
+               OpBranchConditional %true %20 %99
+         %20 = OpLabel
+               OpBranch %99
+         %99 = OpLabel
+        %101 = OpPhi %bool %11 %10 %12 %20
+        %102 = OpCopyObject %bool %101  ;; ensure a use of %101
+               OpReturn
+               OpFunctionEnd
+)",
+        R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:bool = and true, true
+    %3:bool = not %2
+    %4:bool = if true [t: $B2, f: $B3] {  # if_1
+      $B2: {  # true
+        exit_if %3  # if_1
+      }
+      $B3: {  # false
+        exit_if %2  # if_1
+      }
+    }
+    %5:bool = let %4
+    ret
+  }
+}
+)");
+}
+
+// Value %999 is defined deep in control flow, then we arrange for it to dominate the backedge of
+// the outer loop. The %999 value is then fed back into the phi in the loop header.  So %999 needs
+// to be hoisted out of the loop.  The phi assignment needs to use the hoisted variable. The hoisted
+// variable needs to be placed such that its scope encloses that phi in the header of the outer
+// loop. The compiler needs to "see" that there is an implicit use of %999 in the backedge block of
+// that outer loop.
+//
+// https://crbug.com/1649
+TEST_F(SpirvParserTest, Phi_PhiInLoopHeader_FedByHoistedVar_PhiUnused) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %999 %80
+               OpLoopMerge %99 %80 None
+               OpBranchConditional %true %30 %99
+         %30 = OpLabel
+               OpSelectionMerge %50 None
+               OpBranchConditional %true %40 %50
+         %40 = OpLabel
+        %999 = OpCopyObject %bool %true
+               OpBranch %60
+         %50 = OpLabel
+               OpReturn
+         %60 = OpLabel ; if merge
+               OpBranch %80
+         %80 = OpLabel ; continue target
+               OpBranch %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%2:bool): {  # body
+        if true [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            if true [t: $B7, f: $B8] {  # if_2
+              $B7: {  # true
+                %3:bool = let true
+                continue %3  # -> $B4
+              }
+              $B8: {  # false
+                exit_if  # if_2
+              }
+            }
+            ret
+          }
+          $B6: {  # false
+            exit_loop  # loop_1
+          }
+        }
+        unreachable
+      }
+      $B4 (%4:bool): {  # continuing
+        next_iteration %4  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+// Value %999 is defined deep in control flow, then we arrange for it to dominate the backedge of
+// the outer loop. The %999 value is then fed back into the phi in the loop header.  So %999 needs
+// to be hoisted out of the loop.  The phi assignment needs to use the hoisted variable. The hoisted
+// variable needs to be placed such that its scope encloses that phi in the header of the outer
+// loop. The compiler needs to "see" that there is an implicit use of %999 in the backedge block of
+// that outer loop.
+//
+// https://crbug.com/1649
+TEST_F(SpirvParserTest, Phi_PhiInLoopHeader_FedByHoistedVar_PhiUsed) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %999 %80
+               OpLoopMerge %99 %80 None
+               OpBranchConditional %true %30 %99
+         %30 = OpLabel
+               OpSelectionMerge %50 None
+               OpBranchConditional %true %40 %50
+         %40 = OpLabel
+        %999 = OpCopyObject %bool %true
+               OpBranch %60
+         %50 = OpLabel
+               OpReturn
+         %60 = OpLabel ; if merge
+               OpBranch %80
+         %80 = OpLabel ; continue target
+               OpBranch %20
+         %99 = OpLabel
+       %1000 = OpCopyObject %bool %101
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:bool = loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%3:bool): {  # body
+        if true [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            if true [t: $B7, f: $B8] {  # if_2
+              $B7: {  # true
+                %4:bool = let true
+                continue %4  # -> $B4
+              }
+              $B8: {  # false
+                exit_if  # if_2
+              }
+            }
+            ret
+          }
+          $B6: {  # false
+            exit_loop %3  # loop_1
+          }
+        }
+        unreachable
+      }
+      $B4 (%5:bool): {  # continuing
+        next_iteration %5  # -> $B3
+      }
+    }
+    %6:bool = let %2
+    ret
+  }
+}
+)");
+}
+
+// This is a reduction of one of the hard parts of test case
+// vk-gl-cts/graphicsfuzz/stable-binarysearch-tree-false-if-discard-loop/1.spvasm
+// In particular, see the data flow around %114 in that case.
+//
+// Here value %999 is is a *phi* defined deep in control flow, then we arrange for it to dominate
+// the backedge of the outer loop. The %999 value is then fed back into the phi in the loop header.
+// The variable generated to hold the %999 value needs to be placed such that its scope encloses
+// that phi in the header of the outer loop. The compiler needs to "see" that there is an implicit
+// use of %999 in the backedge block of that outer loop.
+//
+// https://crbug.com/1649
+TEST_F(SpirvParserTest, Phi_PhiInLoopHeader_FedByPhi_PhiUnused) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %999 %80
+               OpLoopMerge %99 %80 None
+               OpBranchConditional %true %99 %30
+         %30 = OpLabel
+               OpLoopMerge %70 %60 None
+               OpBranch %40
+         %40 = OpLabel
+               OpBranchConditional %true %60 %50
+         %50 = OpLabel
+               OpBranch %60
+         %60 = OpLabel ; inner continue
+        %999 = OpPhi %bool %true %40 %false %50
+               OpBranchConditional %true %70 %30
+         %70 = OpLabel  ; inner merge
+               OpBranch %80
+         %80 = OpLabel ; outer continue target
+               OpBranch %20
+         %99 = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%2:bool): {  # body
+        if true [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop  # loop_1
+          }
+          $B6: {  # false
+            %3:bool = loop [b: $B7, c: $B8] {  # loop_2
+              $B7: {  # body
+                if true [t: $B9, f: $B10] {  # if_2
+                  $B9: {  # true
+                    continue true  # -> $B8
+                  }
+                  $B10: {  # false
+                    continue false  # -> $B8
+                  }
+                }
+                unreachable
+              }
+              $B8 (%4:bool): {  # continuing
+                break_if true exit_loop: [ %4 ]  # -> [t: exit_loop loop_2, f: $B7]
+              }
+            }
+            continue %3  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4 (%5:bool): {  # continuing
+        next_iteration %5  # -> $B3
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
+// This is a reduction of one of the hard parts of test case
+// vk-gl-cts/graphicsfuzz/stable-binarysearch-tree-false-if-discard-loop/1.spvasm
+// In particular, see the data flow around %114 in that case.
+//
+// Here value %999 is is a *phi* defined deep in control flow, then we arrange for it to dominate
+// the backedge of the outer loop. The %999 value is then fed back into the phi in the loop header.
+// The variable generated to hold the %999 value needs to be placed such that its scope encloses
+// that phi in the header of the outer loop. The compiler needs to "see" that there is an implicit
+// use of %999 in the backedge block of that outer loop.
+//
+// https://crbug.com/1649
+TEST_F(SpirvParserTest, Phi_PhiInLoopHeader_FedByPhi_PhiUsed) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+       %true = OpConstantTrue %bool
+      %false = OpConstantFalse %bool
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+        %101 = OpPhi %bool %true %10 %999 %80
+               OpLoopMerge %99 %80 None
+               OpBranchConditional %true %99 %30
+         %30 = OpLabel
+               OpLoopMerge %70 %60 None
+               OpBranch %40
+         %40 = OpLabel
+               OpBranchConditional %true %60 %50
+         %50 = OpLabel
+               OpBranch %60
+         %60 = OpLabel ; inner continue
+        %999 = OpPhi %bool %true %40 %false %50
+               OpBranchConditional %true %70 %30
+         %70 = OpLabel  ; inner merge
+               OpBranch %80
+         %80 = OpLabel ; outer continue target
+               OpBranch %20
+         %99 = OpLabel
+       %1000 = OpCopyObject %bool %101
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    %2:bool = loop [i: $B2, b: $B3, c: $B4] {  # loop_1
+      $B2: {  # initializer
+        next_iteration true  # -> $B3
+      }
+      $B3 (%3:bool): {  # body
+        if true [t: $B5, f: $B6] {  # if_1
+          $B5: {  # true
+            exit_loop %3  # loop_1
+          }
+          $B6: {  # false
+            %4:bool = loop [b: $B7, c: $B8] {  # loop_2
+              $B7: {  # body
+                if true [t: $B9, f: $B10] {  # if_2
+                  $B9: {  # true
+                    continue true  # -> $B8
+                  }
+                  $B10: {  # false
+                    continue false  # -> $B8
+                  }
+                }
+                unreachable
+              }
+              $B8 (%5:bool): {  # continuing
+                break_if true exit_loop: [ %5 ]  # -> [t: exit_loop loop_2, f: $B7]
+              }
+            }
+            continue %4  # -> $B4
+          }
+        }
+        unreachable
+      }
+      $B4 (%6:bool): {  # continuing
+        next_iteration %6  # -> $B3
+      }
+    }
+    %7:bool = let %2
+    ret
+  }
+}
+)");
+}
+
+// A phi in an unreachable block may have no operands.
+TEST_F(SpirvParserTest, Phi_UnreachableLoopMerge) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+       %uint = OpTypeInt 32 0
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %99
+         %99 = OpLabel
+               OpLoopMerge %101 %99 None
+               OpBranch %99
+        %101 = OpLabel
+        %102 = OpPhi %uint
+               OpUnreachable
+               OpFunctionEnd
+)",
+              R"(
+%main = @compute @workgroup_size(1u, 1u, 1u) func():void {
+  $B1: {
+    loop [b: $B2, c: $B3] {  # loop_1
+      $B2: {  # body
+        continue  # -> $B3
+      }
+      $B3: {  # continuing
+        next_iteration  # -> $B2
+      }
+    }
+    unreachable
+  }
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader