[ir] Validate terminator operands

If they are not nullptr, we need to make sure they are alive and in
scope.

Fixed: 420684375
Change-Id: Ie1216a1f216d385c71e844600a7555d6d04208c0
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/243994
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Auto-Submit: James Price <jrprice@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index b3ed46a..3e9cea6 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -1698,12 +1698,13 @@
 bool Validator::CheckOperand(const Instruction* inst, size_t idx) {
     auto* operand = inst->Operand(idx);
 
-    // var instructions are allowed to have a nullptr operands
-    if (inst->Is<Var>() && operand == nullptr) {
-        return true;
-    }
-
     if (DAWN_UNLIKELY(operand == nullptr)) {
+        // var instructions are allowed to have a nullptr initializers.
+        // terminator instructions use nullptr operands to signal 'undef'.
+        if (inst->IsAnyOf<Terminator, Var>()) {
+            return true;
+        }
+
         AddError(inst, idx) << "operand is undefined";
         return false;
     }
@@ -3462,8 +3463,10 @@
         return;
     }
 
-    // Note, transforms create `undef` terminator arguments (this is done in MergeReturn and
-    // DemoteToHelper) so we can't add validation.
+    // Operands must be alive and in scope if they are not nullptr.
+    if (!CheckOperands(b)) {
+        return;
+    }
 
     tint::Switch(
         b,                                                           //
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 08bc348..20ce4db 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -1208,6 +1208,34 @@
 )")) << res.Failure();
 }
 
+TEST_F(IR_ValidatorTest, Scoping_UseAfterNestedDecl_InTerminator) {
+    auto* f = b.Function("my_func", ty.void_());
+    b.Append(f->Block(), [&] {
+        auto* outer = b.If(true);
+        outer->AddResult(b.InstructionResult<u32>());
+
+        b.Append(outer->True(), [&] {
+            Let* decl = nullptr;
+            auto* inner = b.If(true);
+            b.Append(inner->True(), [&] {
+                decl = b.Let("decl", 42_u);
+                b.ExitIf(inner);
+            });
+            b.ExitIf(outer, decl);
+        });
+
+        b.Return(f);
+    });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_THAT(res.Failure().reason,
+                testing::HasSubstr(R"(:11:17 error: exit_if: %decl is not in scope
+        exit_if %decl  # if_1
+                ^^^^^
+)")) << res.Failure();
+}
+
 TEST_F(IR_ValidatorTest, OverrideWithoutCapability) {
     b.Append(mod.root_block, [&] { b.Override("a", 1_u); });