sem: Add CompoundStatement

This change introduces sem::CompoundStatement, a new base class for
statements that can hold other statements.

sem::BlockStatements now derives from sem::CompoundStatement, and
this change introduces the following new CompoundStatements:
* `sem::IfStatement`
* `sem::ElseStatement`
* `sem::ForLoopStatement`
* `sem::LoopStatement`
* `sem::SwitchStatement`.
These new CompoundStatements are now inserted into the semantic
tree as now documented in `docs/compound_statements.md`.

The `sem::BlockStatement::FindFirstParent()` methods have been
moved down to `sem::Statement`.

The `Resolver::BlockScope()` method has been replaced with
`Resolver::Scope()` which now maintains the `current_statement_`,
`current_compound_statement_ ` and `current_block_`. This
simplifies statement nesting.

The most significant change in behavior is that statements now
always have a parent, so calling Block() on the initializer or
continuing of a for-loop statement will now return the
BlockStatement that holds the for-loop. Before this would
return nullptr.

Fixed: tint:979
Change-Id: I90e38fd719da2a281ed9210e975ab96171cb6842
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/57707
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/docs/compound_statements.md b/docs/compound_statements.md
new file mode 100644
index 0000000..7d28b1d
--- /dev/null
+++ b/docs/compound_statements.md
@@ -0,0 +1,115 @@
+# Compound Statements
+
+Compound statements are statements that can hold other statements.
+
+This document maps the WGSL compound statements to their semantic tree representations.
+
+## if statement
+
+WGSL:
+```
+if (condition_a) {
+    statement_a;
+} else if (condition_b) {
+    statement_b;
+} else {
+    statement_c;
+}
+```
+
+Semantic tree:
+```
+sem::IfStatement {
+    condition_a
+    sem::BlockStatement {
+        statement_a
+    }
+    sem::ElseStatement {
+        condition_b
+        sem::BlockStatement {
+            statement_b
+        }
+    }
+    sem::ElseStatement {
+        sem::BlockStatement {
+            statement_c
+        }
+    }
+}
+```
+
+## for loop
+
+WGSL:
+```
+for (initializer; condition; continuing) {
+    statement;
+}
+```
+
+Semantic tree:
+```
+sem::ForLoopStatement {
+    sem::Statement  initializer
+    sem::Expression condition
+    sem::Statement  continuing
+
+    sem::LoopBlockStatement {
+        sem::Statement statement
+    }
+}
+```
+
+## loop
+
+WGSL:
+```
+loop (condition) {
+    statement_a;
+    continuing {
+        statement_b;
+    }
+}
+```
+
+Semantic tree:
+```
+sem::LoopStatement {
+    sem::Expression condition
+
+    sem::LoopBlockStatement {
+        sem::Statement statement_a
+        sem::LoopContinuingBlockStatement {
+            sem::Statement statement_b
+        }
+    }
+}
+```
+
+
+## switch statement
+
+WGSL:
+```
+switch (condition) {
+    case literal_a, literal_b: {
+        statement_a;
+    }
+    default {
+        statement_b;
+    }
+}
+```
+
+Semantic tree:
+```
+sem::SwitchStatement {
+    sem::Expression condition
+    sem::SwitchCaseBlockStatement {
+        sem::Statement statement_a
+    }
+    sem::SwitchCaseBlockStatement {
+        sem::Statement statement_b
+    }
+}
+```
diff --git a/src/BUILD.gn b/src/BUILD.gn
index a7c9334..83c42bf 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -497,58 +497,40 @@
     "resolver/resolver.h",
     "scope_stack.h",
     "sem/array.h",
-    "sem/atomic_type.cc",
     "sem/atomic_type.h",
     "sem/binding_point.h",
-    "sem/bool_type.cc",
     "sem/bool_type.h",
     "sem/call.h",
     "sem/call_target.h",
-    "sem/constant.cc",
     "sem/constant.h",
-    "sem/depth_texture_type.cc",
     "sem/depth_texture_type.h",
     "sem/expression.h",
-    "sem/external_texture_type.cc",
     "sem/external_texture_type.h",
-    "sem/f32_type.cc",
     "sem/f32_type.h",
-    "sem/i32_type.cc",
+    "sem/for_loop_statement.h",
     "sem/i32_type.h",
+    "sem/if_statement.h",
     "sem/info.h",
     "sem/intrinsic.h",
-    "sem/intrinsic_type.cc",
     "sem/intrinsic_type.h",
-    "sem/matrix_type.cc",
+    "sem/loop_statement.h",
     "sem/matrix_type.h",
-    "sem/multisampled_texture_type.cc",
     "sem/multisampled_texture_type.h",
     "sem/node.h",
-    "sem/parameter_usage.cc",
     "sem/parameter_usage.h",
     "sem/pipeline_stage_set.h",
-    "sem/pointer_type.cc",
     "sem/pointer_type.h",
-    "sem/reference_type.cc",
     "sem/reference_type.h",
-    "sem/sampled_texture_type.cc",
     "sem/sampled_texture_type.h",
-    "sem/sampler_type.cc",
     "sem/sampler_type.h",
-    "sem/storage_texture_type.cc",
     "sem/storage_texture_type.h",
-    "sem/texture_type.cc",
+    "sem/switch_statement.h",
     "sem/texture_type.h",
-    "sem/type.cc",
     "sem/type.h",
-    "sem/type_manager.cc",
     "sem/type_manager.h",
     "sem/type_mappings.h",
-    "sem/u32_type.cc",
     "sem/u32_type.h",
-    "sem/vector_type.cc",
     "sem/vector_type.h",
-    "sem/void_type.cc",
     "sem/void_type.h",
     "source.cc",
     "source.h",
@@ -633,18 +615,80 @@
 libtint_source_set("libtint_sem_src") {
   sources = [
     "sem/array.cc",
+    "sem/array.h",
+    "sem/atomic_type.cc",
+    "sem/atomic_type.h",
+    "sem/binding_point.h",
     "sem/block_statement.cc",
+    "sem/bool_type.cc",
+    "sem/bool_type.h",
     "sem/call.cc",
+    "sem/call.h",
     "sem/call_target.cc",
+    "sem/call_target.h",
+    "sem/constant.cc",
+    "sem/constant.h",
+    "sem/depth_texture_type.cc",
+    "sem/depth_texture_type.h",
     "sem/expression.cc",
+    "sem/expression.h",
+    "sem/external_texture_type.cc",
+    "sem/external_texture_type.h",
+    "sem/f32_type.cc",
+    "sem/f32_type.h",
+    "sem/for_loop_statement.cc",
+    "sem/for_loop_statement.h",
     "sem/function.cc",
+    "sem/i32_type.cc",
+    "sem/i32_type.h",
+    "sem/if_statement.cc",
+    "sem/if_statement.h",
     "sem/info.cc",
+    "sem/info.h",
     "sem/intrinsic.cc",
+    "sem/intrinsic.h",
+    "sem/intrinsic_type.cc",
+    "sem/intrinsic_type.h",
+    "sem/loop_statement.cc",
+    "sem/loop_statement.h",
+    "sem/matrix_type.cc",
+    "sem/matrix_type.h",
     "sem/member_accessor_expression.cc",
+    "sem/multisampled_texture_type.cc",
+    "sem/multisampled_texture_type.h",
     "sem/node.cc",
+    "sem/node.h",
+    "sem/parameter_usage.cc",
+    "sem/parameter_usage.h",
+    "sem/pipeline_stage_set.h",
+    "sem/pointer_type.cc",
+    "sem/pointer_type.h",
+    "sem/reference_type.cc",
+    "sem/reference_type.h",
+    "sem/sampled_texture_type.cc",
+    "sem/sampled_texture_type.h",
+    "sem/sampler_type.cc",
+    "sem/sampler_type.h",
     "sem/statement.cc",
+    "sem/storage_texture_type.cc",
+    "sem/storage_texture_type.h",
     "sem/struct.cc",
+    "sem/switch_statement.cc",
+    "sem/switch_statement.h",
+    "sem/texture_type.cc",
+    "sem/texture_type.h",
+    "sem/type.cc",
+    "sem/type.h",
+    "sem/type_manager.cc",
+    "sem/type_manager.h",
+    "sem/type_mappings.h",
+    "sem/u32_type.cc",
+    "sem/u32_type.h",
     "sem/variable.cc",
+    "sem/vector_type.cc",
+    "sem/vector_type.h",
+    "sem/void_type.cc",
+    "sem/void_type.h",
   ]
 
   public_deps = [ ":libtint_core_all_src" ]
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 9a98007..f5685bd 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -336,8 +336,14 @@
   sem/external_texture_type.h
   sem/f32_type.cc
   sem/f32_type.h
+  sem/for_loop_statement.cc
+  sem/for_loop_statement.h
   sem/i32_type.cc
   sem/i32_type.h
+  sem/if_statement.cc
+  sem/if_statement.h
+  sem/loop_statement.cc
+  sem/loop_statement.h
   sem/matrix_type.cc
   sem/matrix_type.h
   sem/multisampled_texture_type.cc
@@ -352,6 +358,8 @@
   sem/sampler_type.h
   sem/storage_texture_type.cc
   sem/storage_texture_type.h
+  sem/switch_statement.cc
+  sem/switch_statement.h
   sem/texture_type.cc
   sem/texture_type.h
   sem/type.cc
@@ -622,10 +630,10 @@
     resolver/assignment_validation_test.cc
     resolver/atomics_test.cc
     resolver/atomics_validation_test.cc
-    resolver/block_test.cc
     resolver/builtins_validation_test.cc
     resolver/call_test.cc
     resolver/call_validation_test.cc
+    resolver/compound_statement_test.cc
     resolver/control_block_validation_test.cc
     resolver/decoration_validation_test.cc
     resolver/entry_point_validation_test.cc
diff --git a/src/resolver/block_test.cc b/src/resolver/block_test.cc
deleted file mode 100644
index ab213f6..0000000
--- a/src/resolver/block_test.cc
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright 2021 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/resolver/resolver.h"
-
-#include "gmock/gmock.h"
-#include "src/resolver/resolver_test_helper.h"
-#include "src/sem/block_statement.h"
-
-namespace tint {
-namespace resolver {
-namespace {
-
-using ResolverBlockTest = ResolverTest;
-
-TEST_F(ResolverBlockTest, FunctionBlock) {
-  // fn F() {
-  //   var x : 32;
-  // }
-  auto* stmt = Decl(Var("x", ty.i32()));
-  auto* f = Func("F", {}, ty.void_(), {stmt});
-
-  ASSERT_TRUE(r()->Resolve()) << r()->error();
-
-  auto* s = Sem().Get(stmt);
-  ASSERT_NE(s, nullptr);
-  ASSERT_NE(s->Block(), nullptr);
-  ASSERT_TRUE(s->Block()->Is<sem::FunctionBlockStatement>());
-  EXPECT_EQ(s->Block(), s->Block()->FindFirstParent<sem::BlockStatement>());
-  EXPECT_EQ(s->Block(),
-            s->Block()->FindFirstParent<sem::FunctionBlockStatement>());
-  EXPECT_EQ(s->Block()->As<sem::FunctionBlockStatement>()->Function(), f);
-  EXPECT_EQ(s->Block()->Parent(), nullptr);
-}
-
-TEST_F(ResolverBlockTest, Block) {
-  // fn F() {
-  //   {
-  //     var x : 32;
-  //   }
-  // }
-  auto* stmt = Decl(Var("x", ty.i32()));
-  auto* block = Block(stmt);
-  auto* f = Func("F", {}, ty.void_(), {block});
-
-  ASSERT_TRUE(r()->Resolve()) << r()->error();
-
-  auto* s = Sem().Get(stmt);
-  ASSERT_NE(s, nullptr);
-  ASSERT_NE(s->Block(), nullptr);
-  EXPECT_EQ(s->Block(), s->Block()->FindFirstParent<sem::BlockStatement>());
-  EXPECT_EQ(s->Block()->Parent(),
-            s->Block()->FindFirstParent<sem::FunctionBlockStatement>());
-  ASSERT_TRUE(s->Block()->Parent()->Is<sem::FunctionBlockStatement>());
-  EXPECT_EQ(s->Block()->Parent()->As<sem::FunctionBlockStatement>()->Function(),
-            f);
-  EXPECT_EQ(s->Block()->Parent()->Parent(), nullptr);
-}
-
-TEST_F(ResolverBlockTest, LoopBlock) {
-  // fn F() {
-  //   loop {
-  //     var x : 32;
-  //   }
-  // }
-  auto* stmt = Decl(Var("x", ty.i32()));
-  auto* loop = Loop(Block(stmt));
-  auto* f = Func("F", {}, ty.void_(), {loop});
-
-  ASSERT_TRUE(r()->Resolve()) << r()->error();
-
-  auto* s = Sem().Get(stmt);
-  ASSERT_NE(s, nullptr);
-  ASSERT_NE(s->Block(), nullptr);
-  EXPECT_EQ(s->Block(), s->Block()->FindFirstParent<sem::LoopBlockStatement>());
-  ASSERT_TRUE(Is<sem::FunctionBlockStatement>(s->Block()->Parent()->Parent()));
-  EXPECT_EQ(s->Block()->Parent()->Parent(),
-            s->Block()->FindFirstParent<sem::FunctionBlockStatement>());
-  EXPECT_EQ(s->Block()
-                ->Parent()
-                ->Parent()
-                ->As<sem::FunctionBlockStatement>()
-                ->Function(),
-            f);
-  EXPECT_EQ(s->Block()->Parent()->Parent()->Parent(), nullptr);
-}
-
-TEST_F(ResolverBlockTest, ForLoopBlock) {
-  // fn F() {
-  //   for (var i : u32; true; i = i + 1u) {
-  //     return;
-  //   }
-  // }
-  auto* init = Decl(Var("i", ty.u32()));
-  auto* cond = Expr(true);
-  auto* cont = Assign("i", Add("i", 1u));
-  auto* stmt = Return();
-  auto* body = Block(stmt);
-  auto* for_ = For(init, cond, cont, body);
-  auto* f = Func("F", {}, ty.void_(), {for_});
-
-  ASSERT_TRUE(r()->Resolve()) << r()->error();
-
-  {
-    auto* s = Sem().Get(init);
-    ASSERT_NE(s, nullptr);
-    ASSERT_NE(s->Block(), nullptr);
-    EXPECT_EQ(s->Block(),
-              s->Block()->FindFirstParent<sem::LoopBlockStatement>());
-    ASSERT_TRUE(
-        Is<sem::FunctionBlockStatement>(s->Block()->Parent()->Parent()));
-  }
-  {  // Condition expression's statement is the for-loop itself
-    auto* s = Sem().Get(cond);
-    ASSERT_NE(s, nullptr);
-    ASSERT_NE(s->Stmt()->Block(), nullptr);
-    EXPECT_EQ(
-        s->Stmt()->Block(),
-        s->Stmt()->Block()->FindFirstParent<sem::FunctionBlockStatement>());
-    ASSERT_TRUE(Is<sem::FunctionBlockStatement>(s->Stmt()->Block()));
-  }
-  {
-    auto* s = Sem().Get(cont);
-    ASSERT_NE(s, nullptr);
-    ASSERT_NE(s->Block(), nullptr);
-    EXPECT_EQ(s->Block(),
-              s->Block()->FindFirstParent<sem::LoopBlockStatement>());
-    ASSERT_TRUE(
-        Is<sem::FunctionBlockStatement>(s->Block()->Parent()->Parent()));
-  }
-  {
-    auto* s = Sem().Get(stmt);
-    ASSERT_NE(s, nullptr);
-    ASSERT_NE(s->Block(), nullptr);
-    EXPECT_EQ(s->Block(),
-              s->Block()->FindFirstParent<sem::LoopBlockStatement>());
-    ASSERT_TRUE(
-        Is<sem::FunctionBlockStatement>(s->Block()->Parent()->Parent()));
-    EXPECT_EQ(s->Block()->Parent()->Parent(),
-              s->Block()->FindFirstParent<sem::FunctionBlockStatement>());
-    EXPECT_EQ(s->Block()
-                  ->Parent()
-                  ->Parent()
-                  ->As<sem::FunctionBlockStatement>()
-                  ->Function(),
-              f);
-    EXPECT_EQ(s->Block()->Parent()->Parent()->Parent(), nullptr);
-  }
-}
-// TODO(bclayton): Add tests for other block types
-//                 (LoopContinuingBlockStatement, SwitchCaseBlockStatement)
-
-}  // namespace
-}  // namespace resolver
-}  // namespace tint
diff --git a/src/resolver/compound_statement_test.cc b/src/resolver/compound_statement_test.cc
new file mode 100644
index 0000000..a13cac6
--- /dev/null
+++ b/src/resolver/compound_statement_test.cc
@@ -0,0 +1,384 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/resolver/resolver.h"
+
+#include "gmock/gmock.h"
+#include "src/resolver/resolver_test_helper.h"
+#include "src/sem/block_statement.h"
+#include "src/sem/for_loop_statement.h"
+#include "src/sem/if_statement.h"
+#include "src/sem/loop_statement.h"
+#include "src/sem/switch_statement.h"
+
+namespace tint {
+namespace resolver {
+namespace {
+
+using ResolverCompoundStatementTest = ResolverTest;
+
+TEST_F(ResolverCompoundStatementTest, FunctionBlock) {
+  // fn F() {
+  //   var x : 32;
+  // }
+  auto* stmt = Decl(Var("x", ty.i32()));
+  auto* f = Func("F", {}, ty.void_(), {stmt});
+
+  ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+  auto* s = Sem().Get(stmt);
+  ASSERT_NE(s, nullptr);
+  ASSERT_NE(s->Block(), nullptr);
+  ASSERT_TRUE(s->Block()->Is<sem::FunctionBlockStatement>());
+  EXPECT_EQ(s->Block(), s->FindFirstParent<sem::BlockStatement>());
+  EXPECT_EQ(s->Block(), s->FindFirstParent<sem::FunctionBlockStatement>());
+  EXPECT_EQ(s->Block()->As<sem::FunctionBlockStatement>()->Function(), f);
+  EXPECT_EQ(s->Block()->Parent(), nullptr);
+}
+
+TEST_F(ResolverCompoundStatementTest, Block) {
+  // fn F() {
+  //   {
+  //     var x : 32;
+  //   }
+  // }
+  auto* stmt = Decl(Var("x", ty.i32()));
+  auto* block = Block(stmt);
+  auto* f = Func("F", {}, ty.void_(), {block});
+
+  ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+  {
+    auto* s = Sem().Get(block);
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::BlockStatement>());
+    EXPECT_EQ(s, s->Block());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+  {
+    auto* s = Sem().Get(stmt);
+    ASSERT_NE(s, nullptr);
+    ASSERT_NE(s->Block(), nullptr);
+    EXPECT_EQ(s->Block(), s->FindFirstParent<sem::BlockStatement>());
+    EXPECT_EQ(s->Block()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+    ASSERT_TRUE(s->Block()->Parent()->Is<sem::FunctionBlockStatement>());
+    EXPECT_EQ(
+        s->Block()->Parent()->As<sem::FunctionBlockStatement>()->Function(), f);
+    EXPECT_EQ(s->Block()->Parent()->Parent(), nullptr);
+  }
+}
+
+TEST_F(ResolverCompoundStatementTest, Loop) {
+  // fn F() {
+  //   loop {
+  //     stmt_a;
+  //     continuing {
+  //       stmt_b;
+  //     }
+  //   }
+  // }
+  auto* stmt_a = Ignore(1);
+  auto* stmt_b = Ignore(1);
+  auto* loop = Loop(Block(stmt_a), Block(stmt_b));
+  auto* f = Func("F", {}, ty.void_(), {loop});
+
+  ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+  {
+    auto* s = Sem().Get(loop);
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::LoopStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+  }
+  {
+    auto* s = Sem().Get(stmt_a);
+    ASSERT_NE(s, nullptr);
+    ASSERT_NE(s->Block(), nullptr);
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::LoopBlockStatement>());
+
+    EXPECT_EQ(s->Parent()->Parent(), s->FindFirstParent<sem::LoopStatement>());
+    EXPECT_TRUE(Is<sem::LoopStatement>(s->Parent()->Parent()));
+
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_TRUE(
+        Is<sem::FunctionBlockStatement>(s->Parent()->Parent()->Parent()));
+
+    EXPECT_EQ(s->FindFirstParent<sem::FunctionBlockStatement>()->Function(), f);
+
+    EXPECT_EQ(s->Parent()->Parent()->Parent()->Parent(), nullptr);
+  }
+  {
+    auto* s = Sem().Get(stmt_b);
+    ASSERT_NE(s, nullptr);
+    ASSERT_NE(s->Block(), nullptr);
+    EXPECT_EQ(s->Parent(), s->Block());
+
+    EXPECT_EQ(s->Parent(),
+              s->FindFirstParent<sem::LoopContinuingBlockStatement>());
+    EXPECT_TRUE(Is<sem::LoopContinuingBlockStatement>(s->Parent()));
+
+    EXPECT_EQ(s->Parent()->Parent(),
+              s->FindFirstParent<sem::LoopBlockStatement>());
+    EXPECT_TRUE(Is<sem::LoopBlockStatement>(s->Parent()->Parent()));
+
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::LoopStatement>());
+    EXPECT_TRUE(Is<sem::LoopStatement>(s->Parent()->Parent()->Parent()));
+
+    EXPECT_EQ(s->Parent()->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_TRUE(Is<sem::FunctionBlockStatement>(
+        s->Parent()->Parent()->Parent()->Parent()));
+    EXPECT_EQ(s->FindFirstParent<sem::FunctionBlockStatement>()->Function(), f);
+
+    EXPECT_EQ(s->Parent()->Parent()->Parent()->Parent()->Parent(), nullptr);
+  }
+}
+
+TEST_F(ResolverCompoundStatementTest, ForLoop) {
+  // fn F() {
+  //   for (var i : u32; true; i = i + 1u) {
+  //     return;
+  //   }
+  // }
+  auto* init = Decl(Var("i", ty.u32()));
+  auto* cond = Expr(true);
+  auto* cont = Assign("i", Add("i", 1u));
+  auto* stmt = Return();
+  auto* body = Block(stmt);
+  auto* for_ = For(init, cond, cont, body);
+  auto* f = Func("F", {}, ty.void_(), {for_});
+
+  ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+  {
+    auto* s = Sem().Get(for_);
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::ForLoopStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+  }
+  {
+    auto* s = Sem().Get(init);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::ForLoopStatement>());
+    EXPECT_TRUE(Is<sem::ForLoopStatement>(s->Parent()));
+    EXPECT_EQ(s->Block(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_TRUE(Is<sem::FunctionBlockStatement>(s->Parent()->Parent()));
+  }
+  {  // Condition expression's statement is the for-loop itself
+    auto* e = Sem().Get(cond);
+    ASSERT_NE(e, nullptr);
+    auto* s = e->Stmt();
+    ASSERT_NE(s, nullptr);
+    ASSERT_TRUE(Is<sem::ForLoopStatement>(s));
+    ASSERT_NE(s->Parent(), nullptr);
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_TRUE(Is<sem::FunctionBlockStatement>(s->Block()));
+  }
+  {
+    auto* s = Sem().Get(cont);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::ForLoopStatement>());
+    EXPECT_TRUE(Is<sem::ForLoopStatement>(s->Parent()));
+    EXPECT_EQ(s->Block(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_TRUE(Is<sem::FunctionBlockStatement>(s->Parent()->Parent()));
+  }
+  {
+    auto* s = Sem().Get(stmt);
+    ASSERT_NE(s, nullptr);
+    ASSERT_NE(s->Block(), nullptr);
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Block(), s->FindFirstParent<sem::LoopBlockStatement>());
+    EXPECT_TRUE(Is<sem::ForLoopStatement>(s->Parent()->Parent()));
+    EXPECT_EQ(s->Block()->Parent(),
+              s->FindFirstParent<sem::ForLoopStatement>());
+    ASSERT_TRUE(
+        Is<sem::FunctionBlockStatement>(s->Block()->Parent()->Parent()));
+    EXPECT_EQ(s->Block()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Block()
+                  ->Parent()
+                  ->Parent()
+                  ->As<sem::FunctionBlockStatement>()
+                  ->Function(),
+              f);
+    EXPECT_EQ(s->Block()->Parent()->Parent()->Parent(), nullptr);
+  }
+}
+
+TEST_F(ResolverCompoundStatementTest, If) {
+  // fn F() {
+  //   if (cond_a) {
+  //     stat_a;
+  //   } elseif (cond_b) {
+  //     stat_b;
+  //   } else {
+  //     stat_c;
+  //   }
+  // }
+
+  auto* cond_a = Expr(true);
+  auto* stmt_a = Ignore(1);
+  auto* cond_b = Expr(true);
+  auto* stmt_b = Ignore(1);
+  auto* stmt_c = Ignore(1);
+  auto* if_stmt = If(cond_a, Block(stmt_a), Else(cond_b, Block(stmt_b)),
+                     Else(nullptr, Block(stmt_c)));
+  WrapInFunction(if_stmt);
+
+  ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+  {
+    auto* s = Sem().Get(if_stmt);
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::IfStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+  }
+  {
+    auto* e = Sem().Get(cond_a);
+    ASSERT_NE(e, nullptr);
+    auto* s = e->Stmt();
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::IfStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+  }
+  {
+    auto* s = Sem().Get(stmt_a);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::BlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent()->Parent(), s->FindFirstParent<sem::IfStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+  {
+    auto* e = Sem().Get(cond_b);
+    ASSERT_NE(e, nullptr);
+    auto* s = e->Stmt();
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::ElseStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::IfStatement>());
+    EXPECT_EQ(s->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent()->Parent(), s->Block());
+  }
+  {
+    auto* s = Sem().Get(stmt_b);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::BlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent()->Parent(), s->FindFirstParent<sem::ElseStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::IfStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+  {
+    auto* s = Sem().Get(stmt_c);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::BlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent()->Parent(), s->FindFirstParent<sem::ElseStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::IfStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+}
+
+TEST_F(ResolverCompoundStatementTest, Switch) {
+  // fn F() {
+  //   switch (expr) {
+  //     case 1: {
+  //        stmt_a;
+  //     }
+  //     case 2: {
+  //        stmt_b;
+  //     }
+  //     default: {
+  //        stmt_c;
+  //     }
+  //   }
+  // }
+
+  auto* expr = Expr(5);
+  auto* stmt_a = Ignore(1);
+  auto* stmt_b = Ignore(1);
+  auto* stmt_c = Ignore(1);
+  auto* swi =
+      Switch(expr, Case(Literal(1), Block(stmt_a)),
+             Case(Literal(2), Block(stmt_b)), DefaultCase(Block(stmt_c)));
+  WrapInFunction(swi);
+
+  ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+  {
+    auto* s = Sem().Get(swi);
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::SwitchStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+  }
+  {
+    auto* e = Sem().Get(expr);
+    ASSERT_NE(e, nullptr);
+    auto* s = e->Stmt();
+    ASSERT_NE(s, nullptr);
+    EXPECT_TRUE(s->Is<sem::SwitchStatement>());
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::FunctionBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+  }
+  {
+    auto* s = Sem().Get(stmt_a);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::SwitchCaseBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent()->Parent(),
+              s->FindFirstParent<sem::SwitchStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+  {
+    auto* s = Sem().Get(stmt_b);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::SwitchCaseBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent()->Parent(),
+              s->FindFirstParent<sem::SwitchStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+  {
+    auto* s = Sem().Get(stmt_c);
+    ASSERT_NE(s, nullptr);
+    EXPECT_EQ(s->Parent(), s->FindFirstParent<sem::SwitchCaseBlockStatement>());
+    EXPECT_EQ(s->Parent(), s->Block());
+    EXPECT_EQ(s->Parent()->Parent(),
+              s->FindFirstParent<sem::SwitchStatement>());
+    EXPECT_EQ(s->Parent()->Parent()->Parent(),
+              s->FindFirstParent<sem::FunctionBlockStatement>());
+  }
+}
+
+}  // namespace
+}  // namespace resolver
+}  // namespace tint
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index 8390108..eac0b63 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -51,7 +51,10 @@
 #include "src/sem/atomic_type.h"
 #include "src/sem/call.h"
 #include "src/sem/depth_texture_type.h"
+#include "src/sem/for_loop_statement.h"
 #include "src/sem/function.h"
+#include "src/sem/if_statement.h"
+#include "src/sem/loop_statement.h"
 #include "src/sem/member_accessor_expression.h"
 #include "src/sem/multisampled_texture_type.h"
 #include "src/sem/pointer_type.h"
@@ -61,6 +64,7 @@
 #include "src/sem/statement.h"
 #include "src/sem/storage_texture_type.h"
 #include "src/sem/struct.h"
+#include "src/sem/switch_statement.h"
 #include "src/sem/variable.h"
 #include "src/utils/defer.h"
 #include "src/utils/get_or_create.h"
@@ -1568,16 +1572,14 @@
 
   if (func->body()) {
     Mark(func->body());
-    if (current_statement_) {
+    if (current_compound_statement_) {
       TINT_ICE(Resolver, diagnostics_)
-          << "Resolver::Function() called with a current statement";
+          << "Resolver::Function() called with a current compound statement";
       return false;
     }
     auto* sem_block = builder_->create<sem::FunctionBlockStatement>(func);
     builder_->Sem().Add(func->body(), sem_block);
-    TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block);
-    if (!BlockScope(func->body(),
-                    [&] { return Statements(func->body()->list()); })) {
+    if (!Scope(sem_block, [&] { return Statements(func->body()->list()); })) {
       return false;
     }
   }
@@ -1725,17 +1727,11 @@
 }
 
 bool Resolver::Statement(ast::Statement* stmt) {
-  sem::Statement* sem_statement;
-  if (stmt->As<ast::BlockStatement>()) {
-    sem_statement = builder_->create<sem::BlockStatement>(
-        stmt->As<ast::BlockStatement>(), current_statement_);
-  } else {
-    sem_statement = builder_->create<sem::Statement>(stmt, current_statement_);
+  if (stmt->Is<ast::CaseStatement>()) {
+    AddError("case statement can only be used inside a switch statement",
+             stmt->source());
+    return false;
   }
-  builder_->Sem().Add(stmt, sem_statement);
-
-  TINT_SCOPED_ASSIGNMENT(current_statement_, sem_statement);
-
   if (stmt->Is<ast::ElseStatement>()) {
     TINT_ICE(Resolver, diagnostics_)
         << "Resolver::Statement() encountered an Else statement. Else "
@@ -1744,15 +1740,35 @@
     return false;
   }
 
+  // Compound statements. These create their own sem::CompoundStatement
+  // bindings.
+  if (auto* b = stmt->As<ast::BlockStatement>()) {
+    return BlockStatement(b);
+  }
+  if (auto* l = stmt->As<ast::ForLoopStatement>()) {
+    return ForLoopStatement(l);
+  }
+  if (auto* l = stmt->As<ast::LoopStatement>()) {
+    return LoopStatement(l);
+  }
+  if (auto* i = stmt->As<ast::IfStatement>()) {
+    return IfStatement(i);
+  }
+  if (auto* s = stmt->As<ast::SwitchStatement>()) {
+    return SwitchStatement(s);
+  }
+
+  // Non-Compound statements
+  sem::Statement* sem_statement =
+      builder_->create<sem::Statement>(stmt, current_compound_statement_);
+  builder_->Sem().Add(stmt, sem_statement);
+  TINT_SCOPED_ASSIGNMENT(current_statement_, sem_statement);
   if (auto* a = stmt->As<ast::AssignmentStatement>()) {
     return Assignment(a);
   }
-  if (auto* b = stmt->As<ast::BlockStatement>()) {
-    return BlockScope(b, [&] { return Statements(b->list()); });
-  }
   if (stmt->Is<ast::BreakStatement>()) {
-    if (!current_block_->FindFirstParent<sem::LoopBlockStatement>() &&
-        !current_block_->FindFirstParent<sem::SwitchCaseBlockStatement>()) {
+    if (!sem_statement->FindFirstParent<sem::LoopBlockStatement>() &&
+        !sem_statement->FindFirstParent<sem::SwitchCaseBlockStatement>()) {
       AddError("break statement must be in a loop or switch case",
                stmt->source());
       return false;
@@ -1769,9 +1785,6 @@
     }
     return true;
   }
-  if (auto* c = stmt->As<ast::CaseStatement>()) {
-    return CaseStatement(c);
-  }
   if (stmt->Is<ast::ContinueStatement>()) {
     // Set if we've hit the first continue statement in our parent loop
     if (auto* loop_block =
@@ -1793,21 +1806,9 @@
   if (stmt->Is<ast::FallthroughStatement>()) {
     return true;
   }
-  if (auto* i = stmt->As<ast::IfStatement>()) {
-    return IfStatement(i);
-  }
-  if (auto* l = stmt->As<ast::LoopStatement>()) {
-    return LoopStatement(l);
-  }
-  if (auto* l = stmt->As<ast::ForLoopStatement>()) {
-    return ForLoopStatement(l);
-  }
   if (auto* r = stmt->As<ast::ReturnStatement>()) {
     return Return(r);
   }
-  if (auto* s = stmt->As<ast::SwitchStatement>()) {
-    return Switch(s);
-  }
   if (auto* v = stmt->As<ast::VariableDeclStatement>()) {
     return VariableDeclStatement(v);
   }
@@ -1819,51 +1820,59 @@
 }
 
 bool Resolver::CaseStatement(ast::CaseStatement* stmt) {
+  auto* sem = builder_->create<sem::SwitchCaseBlockStatement>(
+      stmt->body(), current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  builder_->Sem().Add(stmt->body(), sem);
   Mark(stmt->body());
   for (auto* sel : stmt->selectors()) {
     Mark(sel);
   }
-  auto* sem_block = builder_->create<sem::SwitchCaseBlockStatement>(
-      stmt->body(), current_statement_);
-  builder_->Sem().Add(stmt->body(), sem_block);
-  TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block);
-  return BlockScope(stmt->body(),
-                    [&] { return Statements(stmt->body()->list()); });
+  return Scope(sem, [&] { return Statements(stmt->body()->list()); });
 }
 
 bool Resolver::IfStatement(ast::IfStatement* stmt) {
-  Mark(stmt->condition());
-  if (!Expression(stmt->condition())) {
-    return false;
-  }
-
-  auto* cond_type = TypeOf(stmt->condition())->UnwrapRef();
-  if (!cond_type->Is<sem::Bool>()) {
-    AddError("if statement condition must be bool, got " +
-                 cond_type->FriendlyName(builder_->Symbols()),
-             stmt->condition()->source());
-    return false;
-  }
-
-  Mark(stmt->body());
-  {
-    auto* sem_block =
-        builder_->create<sem::BlockStatement>(stmt->body(), current_statement_);
-    builder_->Sem().Add(stmt->body(), sem_block);
-    TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block);
-    if (!BlockScope(stmt->body(),
-                    [&] { return Statements(stmt->body()->list()); })) {
+  auto* sem =
+      builder_->create<sem::IfStatement>(stmt, current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  return Scope(sem, [&] {
+    Mark(stmt->condition());
+    if (!Expression(stmt->condition())) {
       return false;
     }
-  }
 
-  for (auto* else_stmt : stmt->else_statements()) {
-    Mark(else_stmt);
-    auto* sem_else_stmt =
-        builder_->create<sem::Statement>(else_stmt, current_statement_);
-    builder_->Sem().Add(else_stmt, sem_else_stmt);
-    TINT_SCOPED_ASSIGNMENT(current_statement_, sem_else_stmt);
-    if (auto* cond = else_stmt->condition()) {
+    auto* cond_type = TypeOf(stmt->condition())->UnwrapRef();
+    if (!cond_type->Is<sem::Bool>()) {
+      AddError("if statement condition must be bool, got " +
+                   cond_type->FriendlyName(builder_->Symbols()),
+               stmt->condition()->source());
+      return false;
+    }
+
+    Mark(stmt->body());
+    auto* body = builder_->create<sem::BlockStatement>(
+        stmt->body(), current_compound_statement_);
+    builder_->Sem().Add(stmt->body(), body);
+    if (!Scope(body, [&] { return Statements(stmt->body()->list()); })) {
+      return false;
+    }
+
+    for (auto* else_stmt : stmt->else_statements()) {
+      Mark(else_stmt);
+      if (!ElseStatement(else_stmt)) {
+        return false;
+      }
+    }
+    return true;
+  });
+}
+
+bool Resolver::ElseStatement(ast::ElseStatement* stmt) {
+  auto* sem =
+      builder_->create<sem::ElseStatement>(stmt, current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  return Scope(sem, [&] {
+    if (auto* cond = stmt->condition()) {
       Mark(cond);
       if (!Expression(cond)) {
         return false;
@@ -1877,95 +1886,93 @@
         return false;
       }
     }
-    Mark(else_stmt->body());
-    {
-      auto* sem_block = builder_->create<sem::BlockStatement>(
-          else_stmt->body(), current_statement_);
-      builder_->Sem().Add(else_stmt->body(), sem_block);
-      TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block);
-      if (!BlockScope(else_stmt->body(),
-                      [&] { return Statements(else_stmt->body()->list()); })) {
-        return false;
-      }
-    }
-  }
-  return true;
+
+    Mark(stmt->body());
+    auto* body = builder_->create<sem::BlockStatement>(
+        stmt->body(), current_compound_statement_);
+    builder_->Sem().Add(stmt->body(), body);
+    return Scope(body, [&] { return Statements(stmt->body()->list()); });
+  });
+}
+
+bool Resolver::BlockStatement(ast::BlockStatement* stmt) {
+  auto* sem = builder_->create<sem::BlockStatement>(
+      stmt->As<ast::BlockStatement>(), current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  return Scope(sem, [&] { return Statements(stmt->list()); });
 }
 
 bool Resolver::LoopStatement(ast::LoopStatement* stmt) {
-  // We don't call DetermineBlockStatement on the body and continuing block as
-  // these would make their BlockInfo siblings as in the AST, but we want the
-  // body BlockInfo to parent the continuing BlockInfo for semantics and
-  // validation. Also, we need to set their types differently.
-  Mark(stmt->body());
+  auto* sem =
+      builder_->create<sem::LoopStatement>(stmt, current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  return Scope(sem, [&] {
+    Mark(stmt->body());
 
-  auto* sem_block_body = builder_->create<sem::LoopBlockStatement>(
-      stmt->body(), current_statement_);
-  builder_->Sem().Add(stmt->body(), sem_block_body);
-  TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block_body);
-  return BlockScope(stmt->body(), [&] {
-    if (!Statements(stmt->body()->list())) {
-      return false;
-    }
-    if (stmt->continuing()) {  // has_continuing() also checks for empty()
-      Mark(stmt->continuing());
-    }
-    if (stmt->has_continuing()) {
-      auto* sem_block_continuing =
-          builder_->create<sem::LoopContinuingBlockStatement>(
-              stmt->continuing(), current_statement_);
-      builder_->Sem().Add(stmt->continuing(), sem_block_continuing);
-      TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block_continuing);
-      if (!BlockScope(stmt->continuing(),
-                      [&] { return Statements(stmt->continuing()->list()); })) {
+    auto* body = builder_->create<sem::LoopBlockStatement>(
+        stmt->body(), current_compound_statement_);
+    builder_->Sem().Add(stmt->body(), body);
+    return Scope(body, [&] {
+      if (!Statements(stmt->body()->list())) {
         return false;
       }
-    }
-    return true;
+      if (stmt->continuing()) {  // has_continuing() also checks for empty()
+        Mark(stmt->continuing());
+      }
+      if (stmt->has_continuing()) {
+        auto* continuing = builder_->create<sem::LoopContinuingBlockStatement>(
+            stmt->continuing(), current_compound_statement_);
+        builder_->Sem().Add(stmt->continuing(), continuing);
+        if (!Scope(continuing,
+                   [&] { return Statements(stmt->continuing()->list()); })) {
+          return false;
+        }
+      }
+      return true;
+    });
   });
 }
 
 bool Resolver::ForLoopStatement(ast::ForLoopStatement* stmt) {
-  Mark(stmt->body());
-
-  auto* sem_block_body = builder_->create<sem::LoopBlockStatement>(
-      stmt->body(), current_statement_);
-  builder_->Sem().Add(stmt->body(), sem_block_body);
-  TINT_SCOPED_ASSIGNMENT(current_statement_, sem_block_body);
-  TINT_SCOPED_ASSIGNMENT(current_block_, sem_block_body);
-
-  variable_stack_.push_scope();
-  TINT_DEFER(variable_stack_.pop_scope());
-
-  if (auto* initializer = stmt->initializer()) {
-    Mark(initializer);
-    if (!Statement(initializer)) {
-      return false;
-    }
-  }
-
-  if (auto* condition = stmt->condition()) {
-    Mark(condition);
-    if (!Expression(condition)) {
-      return false;
+  auto* sem = builder_->create<sem::ForLoopStatement>(
+      stmt, current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  return Scope(sem, [&] {
+    if (auto* initializer = stmt->initializer()) {
+      Mark(initializer);
+      if (!Statement(initializer)) {
+        return false;
+      }
     }
 
-    if (!TypeOf(condition)->Is<sem::Bool>()) {
-      AddError("for-loop condition must be bool, got " + TypeNameOf(condition),
-               condition->source());
-      return false;
-    }
-  }
+    if (auto* condition = stmt->condition()) {
+      Mark(condition);
+      if (!Expression(condition)) {
+        return false;
+      }
 
-  if (auto* continuing = stmt->continuing()) {
-    Mark(continuing);
-    if (!Statement(continuing)) {
-      return false;
+      if (!TypeOf(condition)->Is<sem::Bool>()) {
+        AddError(
+            "for-loop condition must be bool, got " + TypeNameOf(condition),
+            condition->source());
+        return false;
+      }
     }
-  }
 
-  return BlockScope(stmt->body(),
-                    [&] { return Statements(stmt->body()->list()); });
+    if (auto* continuing = stmt->continuing()) {
+      Mark(continuing);
+      if (!Statement(continuing)) {
+        return false;
+      }
+    }
+
+    Mark(stmt->body());
+
+    auto* body = builder_->create<sem::LoopBlockStatement>(
+        stmt->body(), current_compound_statement_);
+    builder_->Sem().Add(stmt->body(), body);
+    return Scope(body, [&] { return Statements(stmt->body()->statements()); });
+  });
 }
 
 bool Resolver::Expressions(const ast::ExpressionList& list) {
@@ -3036,7 +3043,9 @@
   }
 
   variable_stack_.set(var->symbol(), info);
-  current_block_->AddDecl(var);
+  if (current_block_) {  // Not all statements are inside a block
+    current_block_->AddDecl(var);
+  }
 
   if (!ValidateVariable(info)) {
     return false;
@@ -3858,26 +3867,26 @@
   return true;
 }
 
-bool Resolver::Switch(ast::SwitchStatement* s) {
-  Mark(s->condition());
-  if (!Expression(s->condition())) {
-    return false;
-  }
-  for (auto* case_stmt : s->body()) {
-    Mark(case_stmt);
-
-    sem::Statement* sem_statement =
-        builder_->create<sem::Statement>(case_stmt, current_statement_);
-    builder_->Sem().Add(case_stmt, sem_statement);
-    TINT_SCOPED_ASSIGNMENT(current_statement_, sem_statement);
-    if (!CaseStatement(case_stmt)) {
+bool Resolver::SwitchStatement(ast::SwitchStatement* stmt) {
+  auto* sem =
+      builder_->create<sem::SwitchStatement>(stmt, current_compound_statement_);
+  builder_->Sem().Add(stmt, sem);
+  return Scope(sem, [&] {
+    Mark(stmt->condition());
+    if (!Expression(stmt->condition())) {
       return false;
     }
-  }
-  if (!ValidateSwitch(s)) {
-    return false;
-  }
-  return true;
+    for (auto* case_stmt : stmt->body()) {
+      Mark(case_stmt);
+      if (!CaseStatement(case_stmt)) {
+        return false;
+      }
+    }
+    if (!ValidateSwitch(stmt)) {
+      return false;
+    }
+    return true;
+  });
 }
 
 bool Resolver::Assignment(ast::AssignmentStatement* a) {
@@ -4032,18 +4041,22 @@
 }
 
 template <typename F>
-bool Resolver::BlockScope(const ast::BlockStatement* block, F&& callback) {
-  auto* sem_block = builder_->Sem().Get<sem::BlockStatement>(block);
-  if (!sem_block) {
-    TINT_ICE(Resolver, diagnostics_)
-        << "Resolver::BlockScope() called on a block for "
-           "which semantic information is not available";
-    return false;
-  }
-  TINT_SCOPED_ASSIGNMENT(current_block_,
-                         const_cast<sem::BlockStatement*>(sem_block));
+bool Resolver::Scope(sem::CompoundStatement* stmt, F&& callback) {
+  auto* prev_current_statement = current_statement_;
+  auto* prev_current_compound_statement = current_compound_statement_;
+  auto* prev_current_block = current_block_;
+  current_statement_ = stmt;
+  current_compound_statement_ = stmt;
+  current_block_ = stmt->As<sem::BlockStatement>();
   variable_stack_.push_scope();
-  TINT_DEFER(variable_stack_.pop_scope());
+
+  TINT_DEFER({
+    TINT_DEFER(variable_stack_.pop_scope());
+    current_block_ = prev_current_block;
+    current_compound_statement_ = prev_current_compound_statement;
+    current_statement_ = prev_current_statement;
+  });
+
   return callback();
 }
 
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index bd2bf62..680efe5 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -242,9 +242,11 @@
   bool Assignment(ast::AssignmentStatement* a);
   bool Binary(ast::BinaryExpression*);
   bool Bitcast(ast::BitcastExpression*);
+  bool BlockStatement(ast::BlockStatement*);
   bool Call(ast::CallExpression*);
   bool CaseStatement(ast::CaseStatement*);
   bool Constructor(ast::ConstructorExpression*);
+  bool ElseStatement(ast::ElseStatement*);
   bool Expression(ast::Expression*);
   bool Expressions(const ast::ExpressionList&);
   bool ForLoopStatement(ast::ForLoopStatement*);
@@ -260,7 +262,7 @@
   bool Return(ast::ReturnStatement* ret);
   bool Statement(ast::Statement*);
   bool Statements(const ast::StatementList&);
-  bool Switch(ast::SwitchStatement* s);
+  bool SwitchStatement(ast::SwitchStatement* s);
   bool UnaryOp(ast::UnaryOpExpression*);
   bool VariableDeclStatement(const ast::VariableDeclStatement*);
 
@@ -394,11 +396,14 @@
                    const sem::Type* type,
                    std::string type_name = "");
 
-  /// Constructs a new semantic BlockStatement with the given type and with
-  /// #current_block_ as its parent, assigns this to #current_block_, and then
-  /// calls `callback`. The original #current_block_ is restored on exit.
+  /// Assigns `stmt` to #current_statement_, #current_compound_statement_, and
+  /// possibly #current_block_, pushes the variable scope, then calls
+  /// `callback`. Before returning #current_statement_,
+  /// #current_compound_statement_, and #current_block_ are restored to their
+  /// original values, and the variable scope is popped.
+  /// @returns the value returned by callback
   template <typename F>
-  bool BlockScope(const ast::BlockStatement* block, F&& callback);
+  bool Scope(sem::CompoundStatement* stmt, F&& callback);
 
   /// Returns a human-readable string representation of the vector type name
   /// with the given parameters.
@@ -449,7 +454,6 @@
   ProgramBuilder* const builder_;
   diag::List& diagnostics_;
   std::unique_ptr<IntrinsicTable> const intrinsic_table_;
-  sem::BlockStatement* current_block_ = nullptr;
   ScopeStack<VariableInfo*> variable_stack_;
   std::unordered_map<Symbol, FunctionInfo*> symbol_to_function_;
   std::vector<FunctionInfo*> entry_points_;
@@ -466,6 +470,8 @@
 
   FunctionInfo* current_function_ = nullptr;
   sem::Statement* current_statement_ = nullptr;
+  sem::CompoundStatement* current_compound_statement_ = nullptr;
+  sem::BlockStatement* current_block_ = nullptr;
   BlockAllocator<VariableInfo> variable_infos_;
   BlockAllocator<FunctionInfo> function_infos_;
 };
diff --git a/src/sem/block_statement.cc b/src/sem/block_statement.cc
index 9cdb421..614fd06 100644
--- a/src/sem/block_statement.cc
+++ b/src/sem/block_statement.cc
@@ -21,14 +21,12 @@
 TINT_INSTANTIATE_TYPEINFO(tint::sem::BlockStatement);
 TINT_INSTANTIATE_TYPEINFO(tint::sem::FunctionBlockStatement);
 TINT_INSTANTIATE_TYPEINFO(tint::sem::LoopBlockStatement);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::LoopContinuingBlockStatement);
-TINT_INSTANTIATE_TYPEINFO(tint::sem::SwitchCaseBlockStatement);
 
 namespace tint {
 namespace sem {
 
 BlockStatement::BlockStatement(const ast::BlockStatement* declaration,
-                               const Statement* parent)
+                               const CompoundStatement* parent)
     : Base(declaration, parent) {}
 
 BlockStatement::~BlockStatement() = default;
@@ -47,25 +45,15 @@
 FunctionBlockStatement::~FunctionBlockStatement() = default;
 
 LoopBlockStatement::LoopBlockStatement(const ast::BlockStatement* declaration,
-                                       const Statement* parent)
-    : Base(declaration, parent) {}
+                                       const CompoundStatement* parent)
+    : Base(declaration, parent) {
+  TINT_ASSERT(Semantic, parent);
+}
 LoopBlockStatement::~LoopBlockStatement() = default;
 
 void LoopBlockStatement::SetFirstContinue(size_t first_continue) {
   first_continue_ = first_continue;
 }
 
-LoopContinuingBlockStatement::LoopContinuingBlockStatement(
-    const ast::BlockStatement* declaration,
-    const Statement* parent)
-    : Base(declaration, parent) {}
-LoopContinuingBlockStatement::~LoopContinuingBlockStatement() = default;
-
-SwitchCaseBlockStatement::SwitchCaseBlockStatement(
-    const ast::BlockStatement* declaration,
-    const Statement* parent)
-    : Base(declaration, parent) {}
-SwitchCaseBlockStatement::~SwitchCaseBlockStatement() = default;
-
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/block_statement.h b/src/sem/block_statement.h
index c61c3f8..cc76148 100644
--- a/src/sem/block_statement.h
+++ b/src/sem/block_statement.h
@@ -34,13 +34,13 @@
 
 /// Holds semantic information about a block, such as parent block and variables
 /// declared in the block.
-class BlockStatement : public Castable<BlockStatement, Statement> {
+class BlockStatement : public Castable<BlockStatement, CompoundStatement> {
  public:
   /// Constructor
   /// @param declaration the AST node for this block statement
   /// @param parent the owning statement
   BlockStatement(const ast::BlockStatement* declaration,
-                 const Statement* parent);
+                 const CompoundStatement* parent);
 
   /// Destructor
   ~BlockStatement() override;
@@ -49,33 +49,6 @@
   /// statement
   const ast::BlockStatement* Declaration() const;
 
-  /// @returns the closest enclosing block that satisfies the given predicate,
-  /// which may be the block itself, or nullptr if no match is found
-  /// @param pred a predicate that the resulting block must satisfy
-  template <typename Pred>
-  const BlockStatement* FindFirstParent(Pred&& pred) const {
-    const BlockStatement* curr = this;
-    while (curr && !pred(curr)) {
-      curr = curr->Block();
-    }
-    return curr;
-  }
-
-  /// @returns the statement itself if it matches the template type `T`,
-  /// otherwise the nearest enclosing block that matches `T`, or nullptr if
-  /// there is none.
-  template <typename T>
-  const T* FindFirstParent() const {
-    const BlockStatement* curr = this;
-    while (curr) {
-      if (auto* block = curr->As<T>()) {
-        return block;
-      }
-      curr = curr->Block();
-    }
-    return nullptr;
-  }
-
   /// @returns the declarations associated with this block
   const std::vector<const ast::Variable*>& Decls() const { return decls_; }
 
@@ -105,14 +78,14 @@
   ast::Function const* const function_;
 };
 
-/// Holds semantic information about a loop block or a for-loop block
+/// Holds semantic information about a loop body block or for-loop body block
 class LoopBlockStatement : public Castable<LoopBlockStatement, BlockStatement> {
  public:
   /// Constructor
   /// @param declaration the AST node for this block statement
   /// @param parent the owning statement
   LoopBlockStatement(const ast::BlockStatement* declaration,
-                     const Statement* parent);
+                     const CompoundStatement* parent);
 
   /// Destructor
   ~LoopBlockStatement() override;
@@ -134,34 +107,6 @@
   size_t first_continue_ = kNoContinue;
 };
 
-/// Holds semantic information about a loop continuing block
-class LoopContinuingBlockStatement
-    : public Castable<LoopContinuingBlockStatement, BlockStatement> {
- public:
-  /// Constructor
-  /// @param declaration the AST node for this block statement
-  /// @param parent the owning statement
-  LoopContinuingBlockStatement(const ast::BlockStatement* declaration,
-                               const Statement* parent);
-
-  /// Destructor
-  ~LoopContinuingBlockStatement() override;
-};
-
-/// Holds semantic information about a switch case block
-class SwitchCaseBlockStatement
-    : public Castable<SwitchCaseBlockStatement, BlockStatement> {
- public:
-  /// Constructor
-  /// @param declaration the AST node for this block statement
-  /// @param parent the owning statement
-  SwitchCaseBlockStatement(const ast::BlockStatement* declaration,
-                           const Statement* parent);
-
-  /// Destructor
-  ~SwitchCaseBlockStatement() override;
-};
-
 }  // namespace sem
 }  // namespace tint
 
diff --git a/src/sem/for_loop_statement.cc b/src/sem/for_loop_statement.cc
new file mode 100644
index 0000000..59b6738
--- /dev/null
+++ b/src/sem/for_loop_statement.cc
@@ -0,0 +1,31 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/sem/for_loop_statement.h"
+
+#include "src/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::ForLoopStatement);
+
+namespace tint {
+namespace sem {
+
+ForLoopStatement::ForLoopStatement(const ast::ForLoopStatement* declaration,
+                                   CompoundStatement* parent)
+    : Base(declaration, parent) {}
+
+ForLoopStatement::~ForLoopStatement() = default;
+
+}  // namespace sem
+}  // namespace tint
diff --git a/src/sem/for_loop_statement.h b/src/sem/for_loop_statement.h
new file mode 100644
index 0000000..2ee92e4
--- /dev/null
+++ b/src/sem/for_loop_statement.h
@@ -0,0 +1,45 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0(the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SEM_FOR_LOOP_STATEMENT_H_
+#define SRC_SEM_FOR_LOOP_STATEMENT_H_
+
+#include "src/sem/statement.h"
+
+namespace tint {
+namespace ast {
+class ForLoopStatement;
+}  // namespace ast
+}  // namespace tint
+
+namespace tint {
+namespace sem {
+
+/// Holds semantic information about a for-loop statement
+class ForLoopStatement : public Castable<ForLoopStatement, CompoundStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this for-loop statement
+  /// @param parent the owning statement
+  ForLoopStatement(const ast::ForLoopStatement* declaration,
+                   CompoundStatement* parent);
+
+  /// Destructor
+  ~ForLoopStatement() override;
+};
+
+}  // namespace sem
+}  // namespace tint
+
+#endif  // SRC_SEM_FOR_LOOP_STATEMENT_H_
diff --git a/src/sem/if_statement.cc b/src/sem/if_statement.cc
new file mode 100644
index 0000000..4b5d843
--- /dev/null
+++ b/src/sem/if_statement.cc
@@ -0,0 +1,38 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/sem/if_statement.h"
+
+#include "src/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::IfStatement);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::ElseStatement);
+
+namespace tint {
+namespace sem {
+
+IfStatement::IfStatement(const ast::IfStatement* declaration,
+                         CompoundStatement* parent)
+    : Base(declaration, parent) {}
+
+IfStatement::~IfStatement() = default;
+
+ElseStatement::ElseStatement(const ast::ElseStatement* declaration,
+                             CompoundStatement* parent)
+    : Base(declaration, parent) {}
+
+ElseStatement::~ElseStatement() = default;
+
+}  // namespace sem
+}  // namespace tint
diff --git a/src/sem/if_statement.h b/src/sem/if_statement.h
new file mode 100644
index 0000000..d615a44
--- /dev/null
+++ b/src/sem/if_statement.h
@@ -0,0 +1,59 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0(the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SEM_IF_STATEMENT_H_
+#define SRC_SEM_IF_STATEMENT_H_
+
+#include "src/sem/statement.h"
+
+// Forward declarations
+namespace tint {
+namespace ast {
+class IfStatement;
+class ElseStatement;
+}  // namespace ast
+}  // namespace tint
+
+namespace tint {
+namespace sem {
+
+/// Holds semantic information about an if statement
+class IfStatement : public Castable<IfStatement, CompoundStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this if statement
+  /// @param parent the owning statement
+  IfStatement(const ast::IfStatement* declaration, CompoundStatement* parent);
+
+  /// Destructor
+  ~IfStatement() override;
+};
+
+/// Holds semantic information about an else statement
+class ElseStatement : public Castable<ElseStatement, CompoundStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this else statement
+  /// @param parent the owning statement
+  ElseStatement(const ast::ElseStatement* declaration,
+                CompoundStatement* parent);
+
+  /// Destructor
+  ~ElseStatement() override;
+};
+
+}  // namespace sem
+}  // namespace tint
+
+#endif  // SRC_SEM_IF_STATEMENT_H_
diff --git a/src/sem/loop_statement.cc b/src/sem/loop_statement.cc
new file mode 100644
index 0000000..62ea00d
--- /dev/null
+++ b/src/sem/loop_statement.cc
@@ -0,0 +1,40 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/sem/loop_statement.h"
+
+#include "src/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::LoopStatement);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::LoopContinuingBlockStatement);
+
+namespace tint {
+namespace sem {
+
+LoopStatement::LoopStatement(const ast::LoopStatement* declaration,
+                             CompoundStatement* parent)
+    : Base(declaration, parent) {}
+
+LoopStatement::~LoopStatement() = default;
+
+LoopContinuingBlockStatement::LoopContinuingBlockStatement(
+    const ast::BlockStatement* declaration,
+    const CompoundStatement* parent)
+    : Base(declaration, parent) {
+  TINT_ASSERT(Semantic, parent);
+}
+LoopContinuingBlockStatement::~LoopContinuingBlockStatement() = default;
+
+}  // namespace sem
+}  // namespace tint
diff --git a/src/sem/loop_statement.h b/src/sem/loop_statement.h
new file mode 100644
index 0000000..c80bc8c
--- /dev/null
+++ b/src/sem/loop_statement.h
@@ -0,0 +1,60 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0(the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SEM_LOOP_STATEMENT_H_
+#define SRC_SEM_LOOP_STATEMENT_H_
+
+#include "src/sem/block_statement.h"
+
+// Forward declarations
+namespace tint {
+namespace ast {
+class LoopStatement;
+}  // namespace ast
+}  // namespace tint
+
+namespace tint {
+namespace sem {
+
+/// Holds semantic information about a loop statement
+class LoopStatement : public Castable<LoopStatement, CompoundStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this loop statement
+  /// @param parent the owning statement
+  LoopStatement(const ast::LoopStatement* declaration,
+                CompoundStatement* parent);
+
+  /// Destructor
+  ~LoopStatement() override;
+};
+
+/// Holds semantic information about a loop continuing block
+class LoopContinuingBlockStatement
+    : public Castable<LoopContinuingBlockStatement, BlockStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this block statement
+  /// @param parent the owning statement
+  LoopContinuingBlockStatement(const ast::BlockStatement* declaration,
+                               const CompoundStatement* parent);
+
+  /// Destructor
+  ~LoopContinuingBlockStatement() override;
+};
+
+}  // namespace sem
+}  // namespace tint
+
+#endif  // SRC_SEM_LOOP_STATEMENT_H_
diff --git a/src/sem/statement.cc b/src/sem/statement.cc
index 85ece66..0accb48 100644
--- a/src/sem/statement.cc
+++ b/src/sem/statement.cc
@@ -21,32 +21,31 @@
 #include "src/sem/statement.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::sem::Statement);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::CompoundStatement);
 
 namespace tint {
 namespace sem {
 
-Statement::Statement(const ast::Statement* declaration, const Statement* parent)
+Statement::Statement(const ast::Statement* declaration,
+                     const CompoundStatement* parent)
     : declaration_(declaration), parent_(parent) {}
 
 const BlockStatement* Statement::Block() const {
-  auto* stmt = parent_;
-  while (stmt != nullptr) {
-    if (auto* block_stmt = stmt->As<BlockStatement>()) {
-      return block_stmt;
-    }
-    stmt = stmt->parent_;
+  return FindFirstParent<BlockStatement>();
+}
+
+const ast::Function* Statement::Function() const {
+  if (auto* fbs = FindFirstParent<FunctionBlockStatement>()) {
+    return fbs->Function();
   }
   return nullptr;
 }
 
-const ast::Function* Statement::Function() const {
-  if (auto* block = Block()) {
-    if (auto* fbs = block->FindFirstParent<FunctionBlockStatement>()) {
-      return fbs->Function();
-    }
-  }
-  return nullptr;
-}
+CompoundStatement::CompoundStatement(const ast::Statement* declaration,
+                                     const CompoundStatement* parent)
+    : Base(declaration, parent) {}
+
+CompoundStatement::~CompoundStatement() = default;
 
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/statement.h b/src/sem/statement.h
index f58e7a7..821a8a5 100644
--- a/src/sem/statement.h
+++ b/src/sem/statement.h
@@ -31,19 +31,34 @@
 namespace tint {
 namespace sem {
 
+/// Forward declaration
+class CompoundStatement;
+
 /// Statement holds the semantic information for a statement.
 class Statement : public Castable<Statement, Node> {
  public:
   /// Constructor
   /// @param declaration the AST node for this statement
   /// @param parent the owning statement
-  Statement(const ast::Statement* declaration, const Statement* parent);
+  Statement(const ast::Statement* declaration, const CompoundStatement* parent);
 
   /// @return the AST node for this statement
   const ast::Statement* Declaration() const { return declaration_; }
 
   /// @return the statement that encloses this statement
-  const Statement* Parent() const { return parent_; }
+  const CompoundStatement* Parent() const { return parent_; }
+
+  /// @returns the closest enclosing parent that satisfies the given predicate,
+  /// which may be the statement itself, or nullptr if no match is found
+  /// @param pred a predicate that the resulting block must satisfy
+  template <typename Pred>
+  const CompoundStatement* FindFirstParent(Pred&& pred) const;
+
+  /// @returns the statement itself if it matches the template type `T`,
+  /// otherwise the nearest enclosing statement that matches `T`, or nullptr if
+  /// there is none.
+  template <typename T>
+  const T* FindFirstParent() const;
 
   /// @return the closest enclosing block for this statement
   const BlockStatement* Block() const;
@@ -53,9 +68,52 @@
 
  private:
   ast::Statement const* const declaration_;
-  Statement const* const parent_;
+  CompoundStatement const* const parent_;
 };
 
+/// CompoundStatement is the base class of statements that can hold other
+/// statements.
+class CompoundStatement : public Castable<Statement, Statement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this statement
+  /// @param parent the owning statement
+  CompoundStatement(const ast::Statement* declaration,
+                    const CompoundStatement* parent);
+
+  /// Destructor
+  ~CompoundStatement() override;
+};
+
+template <typename Pred>
+const CompoundStatement* Statement::FindFirstParent(Pred&& pred) const {
+  if (auto* self = As<CompoundStatement>()) {
+    if (pred(self)) {
+      return self;
+    }
+  }
+  const auto* curr = parent_;
+  while (curr && !pred(curr)) {
+    curr = curr->Parent();
+  }
+  return curr;
+}
+
+template <typename T>
+const T* Statement::FindFirstParent() const {
+  if (auto* p = As<T>()) {
+    return p;
+  }
+  const auto* curr = parent_;
+  while (curr) {
+    if (auto* p = curr->As<T>()) {
+      return p;
+    }
+    curr = curr->Parent();
+  }
+  return nullptr;
+}
+
 }  // namespace sem
 }  // namespace tint
 
diff --git a/src/sem/switch_statement.cc b/src/sem/switch_statement.cc
new file mode 100644
index 0000000..c99ff71
--- /dev/null
+++ b/src/sem/switch_statement.cc
@@ -0,0 +1,40 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/sem/switch_statement.h"
+
+#include "src/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::SwitchStatement);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::SwitchCaseBlockStatement);
+
+namespace tint {
+namespace sem {
+
+SwitchStatement::SwitchStatement(const ast::SwitchStatement* declaration,
+                                 CompoundStatement* parent)
+    : Base(declaration, parent) {}
+
+SwitchStatement::~SwitchStatement() = default;
+
+SwitchCaseBlockStatement::SwitchCaseBlockStatement(
+    const ast::BlockStatement* declaration,
+    const CompoundStatement* parent)
+    : Base(declaration, parent) {
+  TINT_ASSERT(Semantic, parent);
+}
+SwitchCaseBlockStatement::~SwitchCaseBlockStatement() = default;
+
+}  // namespace sem
+}  // namespace tint
diff --git a/src/sem/switch_statement.h b/src/sem/switch_statement.h
new file mode 100644
index 0000000..f7bb735
--- /dev/null
+++ b/src/sem/switch_statement.h
@@ -0,0 +1,60 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0(the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SEM_SWITCH_STATEMENT_H_
+#define SRC_SEM_SWITCH_STATEMENT_H_
+
+#include "src/sem/block_statement.h"
+
+// Forward declarations
+namespace tint {
+namespace ast {
+class SwitchStatement;
+}  // namespace ast
+}  // namespace tint
+
+namespace tint {
+namespace sem {
+
+/// Holds semantic information about an switch statement
+class SwitchStatement : public Castable<SwitchStatement, CompoundStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this switch statement
+  /// @param parent the owning statement
+  SwitchStatement(const ast::SwitchStatement* declaration,
+                  CompoundStatement* parent);
+
+  /// Destructor
+  ~SwitchStatement() override;
+};
+
+/// Holds semantic information about a switch case block
+class SwitchCaseBlockStatement
+    : public Castable<SwitchCaseBlockStatement, BlockStatement> {
+ public:
+  /// Constructor
+  /// @param declaration the AST node for this block statement
+  /// @param parent the owning statement
+  SwitchCaseBlockStatement(const ast::BlockStatement* declaration,
+                           const CompoundStatement* parent);
+
+  /// Destructor
+  ~SwitchCaseBlockStatement() override;
+};
+
+}  // namespace sem
+}  // namespace tint
+
+#endif  // SRC_SEM_SWITCH_STATEMENT_H_
diff --git a/test/BUILD.gn b/test/BUILD.gn
index f80e072..5192ad9 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -229,10 +229,10 @@
     "../src/resolver/assignment_validation_test.cc",
     "../src/resolver/atomics_test.cc",
     "../src/resolver/atomics_validation_test.cc",
-    "../src/resolver/block_test.cc",
     "../src/resolver/builtins_validation_test.cc",
     "../src/resolver/call_test.cc",
     "../src/resolver/call_validation_test.cc",
+    "../src/resolver/compound_statement_test.cc",
     "../src/resolver/control_block_validation_test.cc",
     "../src/resolver/decoration_validation_test.cc",
     "../src/resolver/entry_point_validation_test.cc",