[ir][validation] Walk through if/switch/loop in the validator

Add support for walking through the blocks of if/switch and loop now
that the structure has settled. Fixup a few tests along the way that are
broken due to failed validation.

This updates the disassembler to assume that an if always has a true
block and a loop always has a body. This is then validated by the generic block code that a terminator is required in a block.

Bug: tint:1952
Change-Id: I8e18e8b314b1b113602fa279143b96d870706690
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/139505
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 38e9be4..5bb2201 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -538,18 +538,11 @@
     out_ << "if ";
     EmitOperand(if_, if_->Condition(), If::kConditionOperandOffset);
 
-    bool has_true = !if_->True()->IsEmpty();
     bool has_false = !if_->False()->IsEmpty();
 
-    out_ << " [";
-    if (has_true) {
-        out_ << "t: %b" << IdOf(if_->True());
-    }
+    out_ << " [t: %b" << IdOf(if_->True());
     if (has_false) {
-        if (has_true) {
-            out_ << ", ";
-        }
-        out_ << "f: %b" << IdOf(if_->False());
+        out_ << ", f: %b" << IdOf(if_->False());
     }
     out_ << "]";
     sm.Store(if_);
@@ -557,10 +550,12 @@
     out_ << " {  # " << NameOf(if_);
     EmitLine();
 
-    if (has_true) {
+    // True block is assumed to have instructions
+    {
         ScopedIndent si(indent_size_);
         EmitBlock(if_->True(), "true");
     }
+
     if (has_false) {
         ScopedIndent si(indent_size_);
         EmitBlock(if_->False(), "false");
@@ -584,9 +579,7 @@
     if (!l->Initializer()->IsEmpty()) {
         parts.Push("i: %b" + std::to_string(IdOf(l->Initializer())));
     }
-    if (!l->Body()->IsEmpty()) {
-        parts.Push("b: %b" + std::to_string(IdOf(l->Body())));
-    }
+    parts.Push("b: %b" + std::to_string(IdOf(l->Body())));
 
     if (!l->Continuing()->IsEmpty()) {
         parts.Push("c: %b" + std::to_string(IdOf(l->Continuing())));
@@ -603,7 +596,8 @@
         EmitBlock(l->Initializer(), "initializer");
     }
 
-    if (!l->Body()->IsEmpty()) {
+    // Loop is assumed to always have a body
+    {
         ScopedIndent si(indent_size_);
         EmitBlock(l->Body(), "body");
     }
diff --git a/src/tint/ir/transform/merge_return_test.cc b/src/tint/ir/transform/merge_return_test.cc
index 0991bb5..2e59642 100644
--- a/src/tint/ir/transform/merge_return_test.cc
+++ b/src/tint/ir/transform/merge_return_test.cc
@@ -100,7 +100,8 @@
         auto* swtch = b.Switch(in);
         b.With(b.Case(swtch, {Switch::CaseSelector{}}), [&] { b.ExitSwitch(swtch); });
 
-        b.Loop();
+        auto* l = b.Loop();
+        b.With(l->Body(), [&] { b.ExitLoop(l); });
 
         auto* ifelse = b.If(cond);
         ifelse->SetResults(b.InstructionResult(ty.i32()));
@@ -118,14 +119,17 @@
         exit_switch  # switch_1
       }
     }
-    loop [] {  # loop_1
+    loop [b: %b3] {  # loop_1
+      %b3 = block {  # body
+        exit_loop  # loop_1
+      }
     }
-    %3:i32 = if %4 [t: %b3, f: %b4] {  # if_1
-      %b3 = block {  # true
+    %3:i32 = if %4 [t: %b4, f: %b5] {  # if_1
+      %b4 = block {  # true
         %5:i32 = add %2, 1i
         exit_if %5  # if_1
       }
-      %b4 = block {  # false
+      %b5 = block {  # false
         %6:i32 = add %2, 2i
         exit_if %6  # if_1
       }
@@ -1506,7 +1510,8 @@
     b.With(func->Block(), [&] {
         auto* sw = b.Switch(cond);
         b.With(b.Case(sw, {Switch::CaseSelector{b.Constant(1_i)}}), [&] {
-            auto* ifelse = b.If(cond);
+            auto* ifcond = b.Equal(ty.bool_(), cond, 1_i);
+            auto* ifelse = b.If(ifcond);
             b.With(ifelse->True(), [&] { b.Return(func, 42_i); });
             b.With(ifelse->False(), [&] { b.ExitIf(ifelse); });
 
@@ -1528,7 +1533,8 @@
   %b2 = block {
     switch %3 [c: (1i, %b3), c: (default, %b4)] {  # switch_1
       %b3 = block {  # case
-        if %3 [t: %b5, f: %b6] {  # if_1
+        %4:bool = eq %3, 1i
+        if %4 [t: %b5, f: %b6] {  # if_1
           %b5 = block {  # true
             ret 42i
           }
@@ -1560,7 +1566,8 @@
     %continue_execution:ptr<function, bool, read_write> = var, true
     switch %3 [c: (1i, %b3), c: (default, %b4)] {  # switch_1
       %b3 = block {  # case
-        if %3 [t: %b5, f: %b6] {  # if_1
+        %6:bool = eq %3, 1i
+        if %6 [t: %b5, f: %b6] {  # if_1
           %b5 = block {  # true
             store %continue_execution, false
             store %return_value, 42i
@@ -1570,8 +1577,8 @@
             exit_if  # if_1
           }
         }
-        %6:bool = load %continue_execution
-        if %6 [t: %b7] {  # if_2
+        %7:bool = load %continue_execution
+        if %7 [t: %b7] {  # if_2
           %b7 = block {  # true
             store %1, 2i
             exit_switch  # switch_1
@@ -1583,15 +1590,15 @@
         exit_switch  # switch_1
       }
     }
-    %7:bool = load %continue_execution
-    if %7 [t: %b8] {  # if_3
+    %8:bool = load %continue_execution
+    if %8 [t: %b8] {  # if_3
       %b8 = block {  # true
         store %return_value, 0i
         exit_if  # if_3
       }
     }
-    %8:i32 = load %return_value
-    ret %8
+    %9:i32 = load %return_value
+    ret %9
   }
 }
 )";
diff --git a/src/tint/ir/validate.cc b/src/tint/ir/validate.cc
index a743786..e43a208 100644
--- a/src/tint/ir/validate.cc
+++ b/src/tint/ir/validate.cc
@@ -35,6 +35,7 @@
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/load.h"
 #include "src/tint/ir/loop.h"
+#include "src/tint/ir/multi_in_block.h"
 #include "src/tint/ir/next_iteration.h"
 #include "src/tint/ir/return.h"
 #include "src/tint/ir/store.h"
@@ -266,9 +267,9 @@
             [&](Call* c) { CheckCall(c); },              //
             [&](If* if_) { CheckIf(if_); },              //
             [&](Load*) {},                               //
-            [&](Loop*) {},                               //
+            [&](Loop* l) { CheckLoop(l); },              //
             [&](Store*) {},                              //
-            [&](Switch*) {},                             //
+            [&](Switch* s) { CheckSwitch(s); },          //
             [&](Swizzle*) {},                            //
             [&](Terminator* b) { CheckTerminator(b); },  //
             [&](Unary* u) { CheckUnary(u); },            //
@@ -405,6 +406,28 @@
         if (if_->Condition() && !if_->Condition()->Type()->Is<type::Bool>()) {
             AddError(if_, If::kConditionOperandOffset, "if: condition must be a `bool` type");
         }
+
+        CheckBlock(if_->True());
+        if (!if_->False()->IsEmpty()) {
+            CheckBlock(if_->False());
+        }
+    }
+
+    void CheckLoop(Loop* l) {
+        if (!l->Initializer()->IsEmpty()) {
+            CheckBlock(l->Initializer());
+        }
+        CheckBlock(l->Body());
+
+        if (!l->Continuing()->IsEmpty()) {
+            CheckBlock(l->Continuing());
+        }
+    }
+
+    void CheckSwitch(Switch* s) {
+        for (auto& cse : s->Cases()) {
+            CheckBlock(cse.block);
+        }
     }
 
     void CheckVar(Var* var) {
diff --git a/src/tint/ir/validate_test.cc b/src/tint/ir/validate_test.cc
index 723c3fc..db8079f 100644
--- a/src/tint/ir/validate_test.cc
+++ b/src/tint/ir/validate_test.cc
@@ -515,6 +515,50 @@
 )");
 }
 
+TEST_F(IR_ValidateTest, If_EmptyFalse) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(true);
+    if_->True()->Append(b.Return(f));
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    EXPECT_TRUE(res) << res.Failure().str();
+}
+
+TEST_F(IR_ValidateTest, If_EmptyTrue) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* if_ = b.If(true);
+    if_->False()->Append(b.Return(f));
+
+    f->Block()->Append(if_);
+    f->Block()->Append(b.Return(f));
+
+    auto res = ir::Validate(mod);
+    ASSERT_FALSE(res);
+    EXPECT_EQ(res.Failure().str(), R"(:4:7 error: block: does not end in a terminator instruction
+      %b2 = block {  # true
+      ^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    if true [t: %b2, f: %b3] {  # if_1
+      %b2 = block {  # true
+      }
+      %b3 = block {  # false
+        ret
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidateTest, If_ConditionIsBool) {
     auto* f = b.Function("my_func", ty.void_());
 
@@ -589,6 +633,46 @@
 )");
 }
 
+TEST_F(IR_ValidateTest, Loop_OnlyBody) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto* l = b.Loop();
+    l->Body()->Append(b.ExitLoop(l));
+
+    auto sb = b.With(f->Block());
+    sb.Append(l);
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    EXPECT_TRUE(res) << res.Failure().str();
+}
+
+TEST_F(IR_ValidateTest, Loop_EmptyBody) {
+    auto* f = b.Function("my_func", ty.void_());
+
+    auto sb = b.With(f->Block());
+    sb.Append(b.Loop());
+    sb.Return(f);
+
+    auto res = ir::Validate(mod);
+    ASSERT_FALSE(res);
+    EXPECT_EQ(res.Failure().str(), R"(:4:7 error: block: does not end in a terminator instruction
+      %b2 = block {  # body
+      ^^^^^^^^^^^
+
+note: # Disassembly
+%my_func = func():void -> %b1 {
+  %b1 = block {
+    loop [b: %b2] {  # loop_1
+      %b2 = block {  # body
+      }
+    }
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidateTest, Var_RootBlock_NullResult) {
     auto* v = mod.instructions.Create<ir::Var>(nullptr);
     b.RootBlock()->Append(v);