Import Tint changes from Dawn

Changes:
  - 5e8ee8581d802b08c189858eadbad6de8a3090fc [ir] Handle some unexpected ast nodes. by dan sinclair <dsinclair@chromium.org>
  - 97b98619e818b2cd8896ca4b2b15e3cb902bdcd4 [ir] Convert tests to use disassembler. by dan sinclair <dsinclair@chromium.org>
  - e3992f240821992499bb5ce7ffe9e5fdcee7392f [ir] Cleanup disassembler output by dan sinclair <dsinclair@chromium.org>
  - a7644371384f47db8b2486087260523cec3a4c82 [ir] Allow branching arguments. by dan sinclair <dsinclair@chromium.org>
  - 1d04cf841c5d628fe0fc19e5626fbf95b519bf55 Enable the 1D -> 2D texture transform in GLSL writer. by Stephen White <senorblanco@chromium.org>
  - fb8a6dbb5d15d208d03f25af94cf41892c27ed22 Implement a Texture1D -> Texture2D transform. by Stephen White <senorblanco@chromium.org>
  - 4a62d91a9e45ef7ca376c9ac9c9eb67a28ef55b2 [ir] Remove program in ir tests. by dan sinclair <dsinclair@chromium.org>
  - 8bcb4e9e77db4f69a2b55926e58f64c39415f3d0 [ir] Remove ast pointers. by dan sinclair <dsinclair@chromium.org>
  - 0258f276b50d708bcc4bf3ce7550252b9c6bb1aa Convert CaseSelector to IR. by dan sinclair <dsinclair@chromium.org>
  - 19bc66a3754ab5b3e2b6dd0627edad0d14601329 [ir] Cleanup some comments and un-needed code. by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 5e8ee8581d802b08c189858eadbad6de8a3090fc
Change-Id: I2c0de371e95d1d19f660c13d8fe2c4d0fd8bda8e
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/116372
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index f57a2d7..f3fd25b 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -535,6 +535,8 @@
     "transform/std140.h",
     "transform/substitute_override.cc",
     "transform/substitute_override.h",
+    "transform/texture_1d_to_2d.cc",
+    "transform/texture_1d_to_2d.h",
     "transform/transform.cc",
     "transform/transform.h",
     "transform/truncate_interstage_variables.cc",
@@ -1328,6 +1330,7 @@
       "transform/std140_test.cc",
       "transform/substitute_override_test.cc",
       "transform/test_helper.h",
+      "transform/texture_1d_to_2d_test.cc",
       "transform/transform_test.cc",
       "transform/truncate_interstage_variables_test.cc",
       "transform/unshadow_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index f35aa57..2f7bda7 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -441,6 +441,8 @@
   transform/std140.h
   transform/substitute_override.cc
   transform/substitute_override.h
+  transform/texture_1d_to_2d.cc
+  transform/texture_1d_to_2d.h
   transform/transform.cc
   transform/transform.h
   transform/truncate_interstage_variables.cc
@@ -1261,6 +1263,7 @@
       transform/std140_test.cc
       transform/substitute_override_test.cc
       transform/test_helper.h
+      transform/texture_1d_to_2d_test.cc
       transform/truncate_interstage_variables_test.cc
       transform/unshadow_test.cc
       transform/var_for_dynamic_index_test.cc
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index f234879..68341ad 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -48,7 +48,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 & 2");
 }
 
@@ -75,7 +75,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 | 2");
 }
 
@@ -102,7 +102,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 ^ 2");
 }
 
@@ -129,7 +129,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 && 2");
 }
 
@@ -156,7 +156,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 || 2");
 }
 
@@ -183,7 +183,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 == 2");
 }
 
@@ -210,7 +210,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 != 2");
 }
 
@@ -237,7 +237,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 < 2");
 }
 
@@ -264,7 +264,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 > 2");
 }
 
@@ -291,7 +291,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 <= 2");
 }
 
@@ -318,7 +318,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 >= 2");
 }
 
@@ -345,7 +345,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 << 2");
 }
 
@@ -372,7 +372,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 >> 2");
 }
 
@@ -399,7 +399,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 + 2");
 }
 
@@ -426,7 +426,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 - 2");
 }
 
@@ -453,7 +453,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 * 2");
 }
 
@@ -480,7 +480,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 / 2");
 }
 
@@ -507,7 +507,7 @@
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 % 2");
 }
 
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index 91c5c4b..a6924df 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -41,7 +41,7 @@
     EXPECT_EQ(4_i, val->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     std::stringstream str;
-    instr->ToString(str, program->Symbols());
+    instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = bitcast(4)");
 }
 
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index ac9028b..4258167 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_IR_BLOCK_H_
 #define SRC_TINT_IR_BLOCK_H_
 
+#include "src/tint/ir/branch.h"
 #include "src/tint/ir/flow_node.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/utils/vector.h"
@@ -30,8 +31,12 @@
     Block();
     ~Block() override;
 
+    /// @returns true if this is a dead block. This can happen in the case like a loop merge block
+    /// which is never reached.
+    bool IsDead() const { return branch.target == nullptr; }
+
     /// The node this block branches too.
-    const FlowNode* branch_target = nullptr;
+    Branch branch = {};
 
     /// The instructions in the block
     utils::Vector<const Instruction*, 16> instructions;
diff --git a/src/tint/ir/branch.h b/src/tint/ir/branch.h
new file mode 100644
index 0000000..4fc8d7c
--- /dev/null
+++ b/src/tint/ir/branch.h
@@ -0,0 +1,36 @@
+// Copyright 2023 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_TINT_IR_BRANCH_H_
+#define SRC_TINT_IR_BRANCH_H_
+
+#include "src/tint/ir/flow_node.h"
+#include "src/tint/ir/value.h"
+
+namespace tint::ir {
+
+/// A information on a branch to another block
+struct Branch {
+    /// The block being branched too.
+    FlowNode* target = nullptr;
+
+    /// The arguments provided for that branch. These arguments could be the
+    /// return value in the case of a branch to the terminator, or they could
+    /// be the basic block arguments passed into the block.
+    utils::Vector<Value*, 2> args;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_BRANCH_H_
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index e65c2fc..1452dc4 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -17,11 +17,10 @@
 #include <utility>
 
 #include "src/tint/ir/builder_impl.h"
-#include "src/tint/program.h"
 
 namespace tint::ir {
 
-Builder::Builder(const Program* prog) : ir(prog) {}
+Builder::Builder() {}
 
 Builder::Builder(Module&& mod) : ir(std::move(mod)) {}
 
@@ -35,8 +34,8 @@
     return ir.flow_nodes.Create<Terminator>();
 }
 
-Function* Builder::CreateFunction(const ast::Function* ast_func) {
-    auto* ir_func = ir.flow_nodes.Create<Function>(ast_func);
+Function* Builder::CreateFunction() {
+    auto* ir_func = ir.flow_nodes.Create<Function>();
     ir_func->start_target = CreateBlock();
     ir_func->end_target = CreateTerminator();
 
@@ -46,50 +45,51 @@
     return ir_func;
 }
 
-If* Builder::CreateIf(const ast::Statement* stmt) {
-    auto* ir_if = ir.flow_nodes.Create<If>(stmt);
-    ir_if->true_target = CreateBlock();
-    ir_if->false_target = CreateBlock();
-    ir_if->merge_target = CreateBlock();
+If* Builder::CreateIf() {
+    auto* ir_if = ir.flow_nodes.Create<If>();
+    ir_if->true_.target = CreateBlock();
+    ir_if->false_.target = CreateBlock();
+    ir_if->merge.target = CreateBlock();
 
     // An if always branches to both the true and false block.
-    ir_if->true_target->inbound_branches.Push(ir_if);
-    ir_if->false_target->inbound_branches.Push(ir_if);
+    ir_if->true_.target->inbound_branches.Push(ir_if);
+    ir_if->false_.target->inbound_branches.Push(ir_if);
 
     return ir_if;
 }
 
-Loop* Builder::CreateLoop(const ast::Statement* stmt) {
-    auto* ir_loop = ir.flow_nodes.Create<Loop>(stmt);
-    ir_loop->start_target = CreateBlock();
-    ir_loop->continuing_target = CreateBlock();
-    ir_loop->merge_target = CreateBlock();
+Loop* Builder::CreateLoop() {
+    auto* ir_loop = ir.flow_nodes.Create<Loop>();
+    ir_loop->start.target = CreateBlock();
+    ir_loop->continuing.target = CreateBlock();
+    ir_loop->merge.target = CreateBlock();
 
     // A loop always branches to the start block.
-    ir_loop->start_target->inbound_branches.Push(ir_loop);
+    ir_loop->start.target->inbound_branches.Push(ir_loop);
 
     return ir_loop;
 }
 
-Switch* Builder::CreateSwitch(const ast::SwitchStatement* stmt) {
-    auto* ir_switch = ir.flow_nodes.Create<Switch>(stmt);
-    ir_switch->merge_target = CreateBlock();
+Switch* Builder::CreateSwitch() {
+    auto* ir_switch = ir.flow_nodes.Create<Switch>();
+    ir_switch->merge.target = CreateBlock();
     return ir_switch;
 }
 
-Block* Builder::CreateCase(Switch* s, utils::VectorRef<const ast::CaseSelector*> selectors) {
-    s->cases.Push(Switch::Case{selectors, CreateBlock()});
+Block* Builder::CreateCase(Switch* s, utils::VectorRef<Switch::CaseSelector> selectors) {
+    s->cases.Push(Switch::Case{selectors, {CreateBlock(), utils::Empty}});
 
-    Block* b = s->cases.Back().start_target;
+    Block* b = s->cases.Back().start.target->As<Block>();
     // Switch branches into the case block
     b->inbound_branches.Push(s);
     return b;
 }
 
-void Builder::Branch(Block* from, FlowNode* to) {
+void Builder::Branch(Block* from, FlowNode* to, utils::VectorRef<Value*> args) {
     TINT_ASSERT(IR, from);
     TINT_ASSERT(IR, to);
-    from->branch_target = to;
+    from->branch.target = to;
+    from->branch.args = args;
     to->inbound_branches.Push(from);
 }
 
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index ad9e2e1..147e4c4 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -35,22 +35,13 @@
 #include "src/tint/type/i32.h"
 #include "src/tint/type/u32.h"
 
-// Forward Declarations
-namespace tint {
-class Program;
-}  // namespace tint
-namespace tint::ast {
-class CaseSelector;
-}  // namespace tint::ast
-
 namespace tint::ir {
 
-/// Builds an ir::Module from a given Program
+/// Builds an ir::Module
 class Builder {
   public:
     /// Constructor
-    /// @param prog the program this ir is associated with
-    explicit Builder(const Program* prog);
+    Builder();
     /// Constructor
     /// @param mod the ir::Module to wrap with this builder
     explicit Builder(Module&& mod);
@@ -63,36 +54,33 @@
     /// @returns a new terminator flow node
     Terminator* CreateTerminator();
 
-    /// Creates a function flow node for the given ast::Function
-    /// @param func the ast::Function
+    /// Creates a function flow node
     /// @returns the flow node
-    Function* CreateFunction(const ast::Function* func);
+    Function* CreateFunction();
 
-    /// Creates an if flow node for the given ast::IfStatement or ast::BreakIfStatement
-    /// @param stmt the ast::IfStatement or ast::BreakIfStatement
+    /// Creates an if flow node
     /// @returns the flow node
-    If* CreateIf(const ast::Statement* stmt);
+    If* CreateIf();
 
-    /// Creates a loop flow node for the given ast loop, while or for statement
-    /// @param stmt the ast loop, while or for statement
+    /// Creates a loop flow node
     /// @returns the flow node
-    Loop* CreateLoop(const ast::Statement* stmt);
+    Loop* CreateLoop();
 
-    /// Creates a switch flow node for the given ast::SwitchStatement
-    /// @param stmt the ast::SwitchStatment
+    /// Creates a switch flow node
     /// @returns the flow node
-    Switch* CreateSwitch(const ast::SwitchStatement* stmt);
+    Switch* CreateSwitch();
 
     /// Creates a case flow node for the given case branch.
     /// @param s the switch to create the case into
     /// @param selectors the case selectors for the case statement
     /// @returns the start block for the case flow node
-    Block* CreateCase(Switch* s, utils::VectorRef<const ast::CaseSelector*> selectors);
+    Block* CreateCase(Switch* s, utils::VectorRef<Switch::CaseSelector> selectors);
 
     /// Branches the given block to the given flow node.
     /// @param from the block to branch from
     /// @param to the node to branch too
-    void Branch(Block* from, FlowNode* to);
+    /// @param args arguments to the branch
+    void Branch(Block* from, FlowNode* to, utils::VectorRef<Value*> args);
 
     /// Creates a constant::Value
     /// @param args the arguments
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 7c4c63f1..0bcaf24 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -25,13 +25,18 @@
 #include "src/tint/ast/float_literal_expression.h"
 #include "src/tint/ast/for_loop_statement.h"
 #include "src/tint/ast/function.h"
+#include "src/tint/ast/id_attribute.h"
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/ast/literal_expression.h"
 #include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/override.h"
 #include "src/tint/ast/return_statement.h"
 #include "src/tint/ast/statement.h"
 #include "src/tint/ast/static_assert.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/struct_member_align_attribute.h"
+#include "src/tint/ast/struct_member_size_attribute.h"
 #include "src/tint/ast/switch_statement.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/while_statement.h"
@@ -44,6 +49,8 @@
 #include "src/tint/program.h"
 #include "src/tint/sem/expression.h"
 #include "src/tint/sem/module.h"
+#include "src/tint/sem/switch_statement.h"
+#include "src/tint/type/void.h"
 
 namespace tint::ir {
 namespace {
@@ -63,7 +70,7 @@
 };
 
 bool IsBranched(const Block* b) {
-    return b->branch_target != nullptr;
+    return b->branch.target != nullptr;
 }
 
 bool IsConnected(const FlowNode* b) {
@@ -84,18 +91,18 @@
 }  // namespace
 
 BuilderImpl::BuilderImpl(const Program* program)
-    : builder(program),
+    : program_(program),
       clone_ctx_{
           type::CloneContext{{&program->Symbols()}, {&builder.ir.symbols, &builder.ir.types}},
           {&builder.ir.constants}} {}
 
 BuilderImpl::~BuilderImpl() = default;
 
-void BuilderImpl::BranchTo(FlowNode* node) {
+void BuilderImpl::BranchTo(FlowNode* node, utils::VectorRef<Value*> args) {
     TINT_ASSERT(IR, current_flow_block);
     TINT_ASSERT(IR, !IsBranched(current_flow_block));
 
-    builder.Branch(current_flow_block, node);
+    builder.Branch(current_flow_block, node, args);
     current_flow_block = nullptr;
 }
 
@@ -121,20 +128,33 @@
     return nullptr;
 }
 
+Symbol BuilderImpl::CloneSymbol(Symbol sym) const {
+    return clone_ctx_.type_ctx.dst.st->Register(clone_ctx_.type_ctx.src.st->NameFor(sym));
+}
+
 ResultType BuilderImpl::Build() {
-    auto* sem = builder.ir.program->Sem().Module();
+    auto* sem = program_->Sem().Module();
 
     for (auto* decl : sem->DependencyOrderedDeclarations()) {
         bool ok = tint::Switch(
             decl,  //
-            // [&](const ast::Struct* str) { },
+            [&](const ast::Struct*) {
+                // Will be encoded into the `type::Struct` when used. We will then hoist all
+                // used structs up to module scope when converting IR.
+                return true;
+            },
             [&](const ast::Alias*) {
                 // Folded away and doesn't appear in the IR.
                 return true;
             },
-            // [&](const ast::Variable* var) { },
+            // [&](const ast::Variable* var) {
+            // TODO(dsinclair): Implement
+            // },
             [&](const ast::Function* func) { return EmitFunction(func); },
-            // [&](const ast::Enable*) { },
+            // [&](const ast::Enable*) {
+            // TODO(dsinclair): Implement? I think these need to be passed along so further stages
+            // know what is enabled.
+            // },
             [&](const ast::StaticAssert*) {
                 // Evaluated by the resolver, drop from the IR.
                 return true;
@@ -154,10 +174,11 @@
 }
 
 bool BuilderImpl::EmitFunction(const ast::Function* ast_func) {
-    // The flow stack should have been emptied when the previous function finshed building.
+    // The flow stack should have been emptied when the previous function finished building.
     TINT_ASSERT(IR, flow_stack.IsEmpty());
 
-    auto* ir_func = builder.CreateFunction(ast_func);
+    auto* ir_func = builder.CreateFunction();
+    ir_func->name = CloneSymbol(ast_func->symbol);
     current_function_ = ir_func;
     builder.ir.functions.Push(ir_func);
 
@@ -175,6 +196,10 @@
             return false;
         }
 
+        // TODO(dsinclair): Store return type and attributes
+        // TODO(dsinclair): Store parameters
+        // TODO(dsinclair): Store attributes
+
         // If the branch target has already been set then a `return` was called. Only set in the
         // case where `return` wasn't called.
         BranchToIfNeeded(current_function_->end_target);
@@ -205,14 +230,22 @@
 bool BuilderImpl::EmitStatement(const ast::Statement* stmt) {
     return tint::Switch(
         stmt,
-        // [&](const ast::AssignmentStatement* a) { },
+        // [&](const ast::AssignmentStatement* a) {
+        // TODO(dsinclair): Implement
+        // },
         [&](const ast::BlockStatement* b) { return EmitBlock(b); },
         [&](const ast::BreakStatement* b) { return EmitBreak(b); },
         [&](const ast::BreakIfStatement* b) { return EmitBreakIf(b); },
-        // [&](const ast::CallStatement* c) { },
-        // [&](const ast::CompoundAssignmentStatement* c) { },
+        // [&](const ast::CallStatement* c) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::CompoundAssignmentStatement* c) {
+        // TODO(dsinclair): Implement
+        // },
         [&](const ast::ContinueStatement* c) { return EmitContinue(c); },
-        // [&](const ast::DiscardStatement* d) { },
+        // [&](const ast::DiscardStatement* d) {
+        // TODO(dsinclair): Implement
+        // },
         [&](const ast::IfStatement* i) { return EmitIf(i); },
         [&](const ast::LoopStatement* l) { return EmitLoop(l); },
         [&](const ast::ForLoopStatement* l) { return EmitForLoop(l); },
@@ -227,6 +260,8 @@
             diagnostics_.add_warning(
                 tint::diag::System::IR,
                 "unknown statement type: " + std::string(stmt->TypeInfo().name), stmt->source);
+            // TODO(dsinclair): This should return `false`, switch back when all
+            // the cases are handled.
             return true;
         });
 }
@@ -239,9 +274,9 @@
 }
 
 bool BuilderImpl::EmitIf(const ast::IfStatement* stmt) {
-    auto* if_node = builder.CreateIf(stmt);
+    auto* if_node = builder.CreateIf();
 
-    // Emit the if condition into the end of the preceeding block
+    // Emit the if condition into the end of the preceding block
     auto reg = EmitExpression(stmt->condition);
     if (!reg) {
         return false;
@@ -255,34 +290,34 @@
     {
         FlowStackScope scope(this, if_node);
 
-        current_flow_block = if_node->true_target;
+        current_flow_block = if_node->true_.target->As<Block>();
         if (!EmitStatement(stmt->body)) {
             return false;
         }
         // If the true branch did not execute control flow, then go to the merge target
-        BranchToIfNeeded(if_node->merge_target);
+        BranchToIfNeeded(if_node->merge.target);
 
-        current_flow_block = if_node->false_target;
+        current_flow_block = if_node->false_.target->As<Block>();
         if (stmt->else_statement && !EmitStatement(stmt->else_statement)) {
             return false;
         }
         // If the false branch did not execute control flow, then go to the merge target
-        BranchToIfNeeded(if_node->merge_target);
+        BranchToIfNeeded(if_node->merge.target);
     }
     current_flow_block = nullptr;
 
     // If both branches went somewhere, then they both returned, continued or broke. So,
     // there is no need for the if merge-block and there is nothing to branch to the merge
     // block anyway.
-    if (IsConnected(if_node->merge_target)) {
-        current_flow_block = if_node->merge_target;
+    if (IsConnected(if_node->merge.target)) {
+        current_flow_block = if_node->merge.target->As<Block>();
     }
 
     return true;
 }
 
 bool BuilderImpl::EmitLoop(const ast::LoopStatement* stmt) {
-    auto* loop_node = builder.CreateLoop(stmt);
+    auto* loop_node = builder.CreateLoop();
 
     BranchTo(loop_node);
 
@@ -291,15 +326,15 @@
     {
         FlowStackScope scope(this, loop_node);
 
-        current_flow_block = loop_node->start_target;
+        current_flow_block = loop_node->start.target->As<Block>();
         if (!EmitStatement(stmt->body)) {
             return false;
         }
 
         // The current block didn't `break`, `return` or `continue`, go to the continuing block.
-        BranchToIfNeeded(loop_node->continuing_target);
+        BranchToIfNeeded(loop_node->continuing.target);
 
-        current_flow_block = loop_node->continuing_target;
+        current_flow_block = loop_node->continuing.target->As<Block>();
         if (stmt->continuing) {
             if (!EmitStatement(stmt->continuing)) {
                 return false;
@@ -307,22 +342,24 @@
         }
 
         // Branch back to the start node if the continue target didn't branch out already
-        BranchToIfNeeded(loop_node->start_target);
+        BranchToIfNeeded(loop_node->start.target);
     }
 
     // The loop merge can get disconnected if the loop returns directly, or the continuing target
     // branches, eventually, to the merge, but nothing branched to the continuing target.
-    current_flow_block = loop_node->merge_target;
-    if (!IsConnected(loop_node->merge_target)) {
+    current_flow_block = loop_node->merge.target->As<Block>();
+    if (!IsConnected(loop_node->merge.target)) {
         current_flow_block = nullptr;
     }
     return true;
 }
 
 bool BuilderImpl::EmitWhile(const ast::WhileStatement* stmt) {
-    auto* loop_node = builder.CreateLoop(stmt);
+    auto* loop_node = builder.CreateLoop();
     // Continue is always empty, just go back to the start
-    builder.Branch(loop_node->continuing_target, loop_node->start_target);
+    TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
+    builder.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
+                   utils::Empty);
 
     BranchTo(loop_node);
 
@@ -331,7 +368,7 @@
     {
         FlowStackScope scope(this, loop_node);
 
-        current_flow_block = loop_node->start_target;
+        current_flow_block = loop_node->start.target->As<Block>();
 
         // Emit the while condition into the start target of the loop
         auto reg = EmitExpression(stmt->condition);
@@ -339,30 +376,35 @@
             return false;
         }
 
-        // Create an if (cond) {} else {break;} control flow
-        auto* if_node = builder.CreateIf(nullptr);
-        builder.Branch(if_node->true_target, if_node->merge_target);
-        builder.Branch(if_node->false_target, loop_node->merge_target);
+        // Create an `if (cond) {} else {break;}` control flow
+        auto* if_node = builder.CreateIf();
+        TINT_ASSERT(IR, if_node->true_.target->Is<Block>());
+        builder.Branch(if_node->true_.target->As<Block>(), if_node->merge.target, utils::Empty);
+
+        TINT_ASSERT(IR, if_node->false_.target->Is<Block>());
+        builder.Branch(if_node->false_.target->As<Block>(), loop_node->merge.target, utils::Empty);
         if_node->condition = reg.Get();
 
         BranchTo(if_node);
 
-        current_flow_block = if_node->merge_target;
+        current_flow_block = if_node->merge.target->As<Block>();
         if (!EmitStatement(stmt->body)) {
             return false;
         }
 
-        BranchToIfNeeded(loop_node->continuing_target);
+        BranchToIfNeeded(loop_node->continuing.target);
     }
     // The while loop always has a path to the merge target as the break statement comes before
     // anything inside the loop.
-    current_flow_block = loop_node->merge_target;
+    current_flow_block = loop_node->merge.target->As<Block>();
     return true;
 }
 
 bool BuilderImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
-    auto* loop_node = builder.CreateLoop(stmt);
-    builder.Branch(loop_node->continuing_target, loop_node->start_target);
+    auto* loop_node = builder.CreateLoop();
+    TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
+    builder.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
+                   utils::Empty);
 
     if (stmt->initializer) {
         // Emit the for initializer before branching to the loop
@@ -378,7 +420,7 @@
     {
         FlowStackScope scope(this, loop_node);
 
-        current_flow_block = loop_node->start_target;
+        current_flow_block = loop_node->start.target->As<Block>();
 
         if (stmt->condition) {
             // Emit the condition into the target target of the loop
@@ -387,24 +429,28 @@
                 return false;
             }
 
-            // Create an if (cond) {} else {break;} control flow
-            auto* if_node = builder.CreateIf(nullptr);
-            builder.Branch(if_node->true_target, if_node->merge_target);
-            builder.Branch(if_node->false_target, loop_node->merge_target);
+            // Create an `if (cond) {} else {break;}` control flow
+            auto* if_node = builder.CreateIf();
+            TINT_ASSERT(IR, if_node->true_.target->Is<Block>());
+            builder.Branch(if_node->true_.target->As<Block>(), if_node->merge.target, utils::Empty);
+
+            TINT_ASSERT(IR, if_node->false_.target->Is<Block>());
+            builder.Branch(if_node->false_.target->As<Block>(), loop_node->merge.target,
+                           utils::Empty);
             if_node->condition = reg.Get();
 
             BranchTo(if_node);
-            current_flow_block = if_node->merge_target;
+            current_flow_block = if_node->merge.target->As<Block>();
         }
 
         if (!EmitStatement(stmt->body)) {
             return false;
         }
 
-        BranchToIfNeeded(loop_node->continuing_target);
+        BranchToIfNeeded(loop_node->continuing.target);
 
         if (stmt->continuing) {
-            current_flow_block = loop_node->continuing_target;
+            current_flow_block = loop_node->continuing.target->As<Block>();
             if (!EmitStatement(stmt->continuing)) {
                 return false;
             }
@@ -412,14 +458,14 @@
     }
     // The while loop always has a path to the merge target as the break statement comes before
     // anything inside the loop.
-    current_flow_block = loop_node->merge_target;
+    current_flow_block = loop_node->merge.target->As<Block>();
     return true;
 }
 
 bool BuilderImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
-    auto* switch_node = builder.CreateSwitch(stmt);
+    auto* switch_node = builder.CreateSwitch();
 
-    // Emit the condition into the preceeding block
+    // Emit the condition into the preceding block
     auto reg = EmitExpression(stmt->condition);
     if (!reg) {
         return false;
@@ -433,27 +479,44 @@
     {
         FlowStackScope scope(this, switch_node);
 
-        for (const auto* c : stmt->body) {
-            current_flow_block = builder.CreateCase(switch_node, c->selectors);
-            if (!EmitStatement(c->body)) {
+        const auto* sem = program_->Sem().Get(stmt);
+        for (const auto* c : sem->Cases()) {
+            utils::Vector<Switch::CaseSelector, 4> selectors;
+            for (const auto* selector : c->Selectors()) {
+                if (selector->IsDefault()) {
+                    selectors.Push({nullptr});
+                } else {
+                    selectors.Push({builder.Constant(selector->Value()->Clone(clone_ctx_))});
+                }
+            }
+
+            current_flow_block = builder.CreateCase(switch_node, selectors);
+            if (!EmitStatement(c->Body()->Declaration())) {
                 return false;
             }
-            BranchToIfNeeded(switch_node->merge_target);
+            BranchToIfNeeded(switch_node->merge.target);
         }
     }
     current_flow_block = nullptr;
 
-    if (IsConnected(switch_node->merge_target)) {
-        current_flow_block = switch_node->merge_target;
+    if (IsConnected(switch_node->merge.target)) {
+        current_flow_block = switch_node->merge.target->As<Block>();
     }
 
     return true;
 }
 
-bool BuilderImpl::EmitReturn(const ast::ReturnStatement*) {
-    // TODO(dsinclair): Emit the return value ....
+bool BuilderImpl::EmitReturn(const ast::ReturnStatement* stmt) {
+    utils::Vector<Value*, 1> ret_value;
+    if (stmt->value) {
+        auto ret = EmitExpression(stmt->value);
+        if (!ret) {
+            return false;
+        }
+        ret_value.Push(ret.Get());
+    }
 
-    BranchTo(current_function_->end_target);
+    BranchTo(current_function_->end_target, std::move(ret_value));
     return true;
 }
 
@@ -462,9 +525,9 @@
     TINT_ASSERT(IR, current_control);
 
     if (auto* c = current_control->As<Loop>()) {
-        BranchTo(c->merge_target);
+        BranchTo(c->merge.target);
     } else if (auto* s = current_control->As<Switch>()) {
-        BranchTo(s->merge_target);
+        BranchTo(s->merge.target);
     } else {
         TINT_UNREACHABLE(IR, diagnostics_);
         return false;
@@ -478,7 +541,7 @@
     TINT_ASSERT(IR, current_control);
 
     if (auto* c = current_control->As<Loop>()) {
-        BranchTo(c->continuing_target);
+        BranchTo(c->continuing.target);
     } else {
         TINT_UNREACHABLE(IR, diagnostics_);
     }
@@ -487,9 +550,9 @@
 }
 
 bool BuilderImpl::EmitBreakIf(const ast::BreakIfStatement* stmt) {
-    auto* if_node = builder.CreateIf(stmt);
+    auto* if_node = builder.CreateIf();
 
-    // Emit the break-if condition into the end of the preceeding block
+    // Emit the break-if condition into the end of the preceding block
     auto reg = EmitExpression(stmt->condition);
     if (!reg) {
         return false;
@@ -506,18 +569,17 @@
 
     auto* loop = current_control->As<Loop>();
 
-    current_flow_block = if_node->true_target;
-    BranchTo(loop->merge_target);
+    current_flow_block = if_node->true_.target->As<Block>();
+    BranchTo(loop->merge.target);
 
-    current_flow_block = if_node->false_target;
-    BranchTo(if_node->merge_target);
+    current_flow_block = if_node->false_.target->As<Block>();
+    BranchTo(if_node->merge.target);
 
-    current_flow_block = if_node->merge_target;
+    current_flow_block = if_node->merge.target->As<Block>();
 
     // The `break-if` has to be the last item in the continuing block. The false branch of the
     // `break-if` will always take us back to the start of the loop.
-    // break then we go back to the start of the loop.
-    BranchTo(loop->start_target);
+    BranchTo(loop->start.target);
 
     return true;
 }
@@ -525,35 +587,65 @@
 utils::Result<Value*> BuilderImpl::EmitExpression(const ast::Expression* expr) {
     return tint::Switch(
         expr,
-        // [&](const ast::IndexAccessorExpression* a) { return EmitIndexAccessor(a); },
+        // [&](const ast::IndexAccessorExpression* a) {
+        // TODO(dsinclair): Implement
+        // },
         [&](const ast::BinaryExpression* b) { return EmitBinary(b); },
         [&](const ast::BitcastExpression* b) { return EmitBitcast(b); },
-        // [&](const ast::CallExpression* c) { return EmitCall(c); },
-        // [&](const ast::IdentifierExpression* i) { return EmitIdentifier(i); },
+        // [&](const ast::CallExpression* c) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::IdentifierExpression* i) {
+        // TODO(dsinclair): Implement
+        // },
         [&](const ast::LiteralExpression* l) { return EmitLiteral(l); },
-        // [&](const ast::MemberAccessorExpression* m) { return EmitMemberAccessor(m); },
-        // [&](const ast::PhonyExpression*) { return true; },
-        // [&](const ast::UnaryOpExpression* u) { return EmitUnaryOp(u); },
+        // [&](const ast::MemberAccessorExpression* m) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::PhonyExpression*) {
+        // TODO(dsinclair): Implement. The call may have side effects so has to be made.
+        // },
+        // [&](const ast::UnaryOpExpression* u) {
+        // TODO(dsinclair): Implement
+        // },
         [&](Default) {
             diagnostics_.add_warning(
                 tint::diag::System::IR,
                 "unknown expression type: " + std::string(expr->TypeInfo().name), expr->source);
-            return utils::Failure;
+            // TODO(dsinclair): This should return utils::Failure; Switch back
+            // once all the above cases are handled.
+            auto* v = builder.ir.types.Get<type::Void>();
+            return builder.Temp(v);
         });
 }
 
 bool BuilderImpl::EmitVariable(const ast::Variable* var) {
     return tint::Switch(  //
         var,
-        // [&](const ast::Var* var) {},
-        // [&](const ast::Let*) {},
-        // [&](const ast::Override*) { },
-        // [&](const ast::Const* c) { },
+        // [&](const ast::Var* var) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::Let*) {
+        // TODO(dsinclair): Implement
+        // },
+        [&](const ast::Override*) {
+            diagnostics_.add_warning(tint::diag::System::IR,
+                                     "found an `Override` variable. The SubstituteOverrides "
+                                     "transform must be run before converting to IR",
+                                     var->source);
+            return false;
+        },
+        // [&](const ast::Const* c) {
+        // TODO(dsinclair): Implement
+        // },
         [&](Default) {
             diagnostics_.add_warning(tint::diag::System::IR,
                                      "unknown variable: " + std::string(var->TypeInfo().name),
                                      var->source);
-            return false;
+
+            // TODO(dsinclair): This should return `false`, switch back when all
+            // the cases are handled.
+            return true;
         });
 }
 
@@ -568,7 +660,7 @@
         return utils::Failure;
     }
 
-    auto* sem = builder.ir.program->Sem().Get(expr);
+    auto* sem = program_->Sem().Get(expr);
     auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
 
     Binary* instr = nullptr;
@@ -642,7 +734,7 @@
         return utils::Failure;
     }
 
-    auto* sem = builder.ir.program->Sem().Get(expr);
+    auto* sem = program_->Sem().Get(expr);
     auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
     auto* instr = builder.Bitcast(ty, val.Get());
 
@@ -651,7 +743,7 @@
 }
 
 utils::Result<Value*> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
-    auto* sem = builder.ir.program->Sem().Get(lit);
+    auto* sem = program_->Sem().Get(lit);
     if (!sem) {
         diagnostics_.add_error(
             tint::diag::System::IR,
@@ -671,45 +763,6 @@
     return builder.Constant(cv);
 }
 
-bool BuilderImpl::EmitType(const ast::Type* ty) {
-    return tint::Switch(
-        ty,
-        // [&](const ast::Array* ary) { },
-        // [&](const ast::Bool* b) { },
-        // [&](const ast::F32* f) { },
-        // [&](const ast::F16* f) { },
-        // [&](const ast::I32* i) { },
-        // [&](const ast::U32* u) { },
-        // [&](const ast::Vector* v) { },
-        // [&](const ast::Matrix* mat) { },
-        // [&](const ast::Pointer* ptr) { },'
-        // [&](const ast::Atomic* a) { },
-        // [&](const ast::Sampler* s) { },
-        // [&](const ast::ExternalTexture* t) { },
-        // [&](const ast::Texture* t) {
-        //      return tint::Switch(
-        //          t,
-        //          [&](const ast::DepthTexture*) { },
-        //          [&](const ast::DepthMultisampledTexture*) { },
-        //          [&](const ast::SampledTexture*) { },
-        //          [&](const ast::MultisampledTexture*) { },
-        //          [&](const ast::StorageTexture*) {  },
-        //          [&](Default) {
-        //              diagnostics_.add_warning(tint::diag::System::IR,
-        //                  "unknown texture: " + std::string(t->TypeInfo().name), t->source);
-        //              return false;
-        //          });
-        // },
-        // [&](const ast::Void* v) { },
-        // [&](const ast::TypeName* tn) { },
-        [&](Default) {
-            diagnostics_.add_warning(tint::diag::System::IR,
-                                     "unknown type: " + std::string(ty->TypeInfo().name),
-                                     ty->source);
-            return false;
-        });
-}
-
 bool BuilderImpl::EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs) {
     for (auto* attr : attrs) {
         if (!EmitAttribute(attr)) {
@@ -722,19 +775,53 @@
 bool BuilderImpl::EmitAttribute(const ast::Attribute* attr) {
     return tint::Switch(  //
         attr,
-        // [&](const ast::WorkgroupAttribute* wg) {},
-        // [&](const ast::StageAttribute* s) {},
-        // [&](const ast::BindingAttribute* b) {},
-        // [&](const ast::GroupAttribute* g) {},
-        // [&](const ast::LocationAttribute* l) {},
-        // [&](const ast::BuiltinAttribute* b) {},
-        // [&](const ast::InterpolateAttribute* i) {},
-        // [&](const ast::InvariantAttribute* i) {},
-        // [&](const ast::IdAttribute* i) {},
-        // [&](const ast::StructMemberSizeAttribute* s) {},
-        // [&](const ast::StructMemberAlignAttribute* a) {},
-        // [&](const ast::StrideAttribute* s) {}
-        // [&](const ast::InternalAttribute *i) {},
+        // [&](const ast::WorkgroupAttribute* wg) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::StageAttribute* s) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::BindingAttribute* b) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::GroupAttribute* g) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::LocationAttribute* l) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::BuiltinAttribute* b) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::InterpolateAttribute* i) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::InvariantAttribute* i) {
+        // TODO(dsinclair): Implement
+        // },
+        [&](const ast::IdAttribute*) {
+            diagnostics_.add_warning(tint::diag::System::IR,
+                                     "found an `Id` attribute. The SubstituteOverrides transform "
+                                     "must be run before converting to IR",
+                                     attr->source);
+            return false;
+        },
+        [&](const ast::StructMemberSizeAttribute*) {
+            TINT_ICE(IR, diagnostics_)
+                << "StructMemberSizeAttribute encountered during IR conversion";
+            return false;
+        },
+        [&](const ast::StructMemberAlignAttribute*) {
+            TINT_ICE(IR, diagnostics_)
+                << "StructMemberAlignAttribute encountered during IR conversion";
+            return false;
+        },
+        // [&](const ast::StrideAttribute* s) {
+        // TODO(dsinclair): Implement
+        // },
+        // [&](const ast::InternalAttribute *i) {
+        // TODO(dsinclair): Implement
+        // },
         [&](Default) {
             diagnostics_.add_warning(tint::diag::System::IR,
                                      "unknown attribute: " + std::string(attr->TypeInfo().name),
diff --git a/src/tint/ir/builder_impl.h b/src/tint/ir/builder_impl.h
index 6c858ac..c086677 100644
--- a/src/tint/ir/builder_impl.h
+++ b/src/tint/ir/builder_impl.h
@@ -32,20 +32,26 @@
 class Program;
 }  // namespace tint
 namespace tint::ast {
+class Attribute;
 class BinaryExpression;
 class BitcastExpression;
 class BlockStatement;
 class BreakIfStatement;
 class BreakStatement;
 class ContinueStatement;
+class Expression;
 class ForLoopStatement;
 class Function;
 class IfStatement;
 class LoopStatement;
 class LiteralExpression;
+class Node;
 class ReturnStatement;
 class Statement;
+class SwitchStatement;
+class Type;
 class WhileStatement;
+class Variable;
 }  // namespace tint::ast
 namespace tint::ir {
 class Block;
@@ -164,11 +170,6 @@
     /// @returns true if successful, false otherwise
     utils::Result<Value*> EmitLiteral(const ast::LiteralExpression* lit);
 
-    /// Emits a type
-    /// @param ty the type to emit
-    /// @returns true if successful, false otherwise
-    bool EmitType(const ast::Type* ty);
-
     /// Emits a set of attributes
     /// @param attrs the attributes to emit
     /// @returns true if successful, false otherwise
@@ -201,11 +202,15 @@
   private:
     enum class ControlFlags { kNone, kExcludeSwitch };
 
-    void BranchTo(ir::FlowNode* node);
+    void BranchTo(ir::FlowNode* node, utils::VectorRef<Value*> args = {});
     void BranchToIfNeeded(ir::FlowNode* node);
 
     FlowNode* FindEnclosingControl(ControlFlags flags);
 
+    const Program* program_ = nullptr;
+
+    Symbol CloneSymbol(Symbol sym) const;
+
     diag::List diagnostics_;
 
     Function* current_function_ = nullptr;
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index ced23f6..9dc908d 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -16,6 +16,7 @@
 
 #include "src/tint/ast/case_selector.h"
 #include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/constant/scalar.h"
 
 namespace tint::ir {
 namespace {
@@ -25,35 +26,34 @@
 using IR_BuilderImplTest = TestHelper;
 
 TEST_F(IR_BuilderImplTest, Func) {
-    // func -> start -> end
-
     Func("f", utils::Empty, ty.void_(), utils::Empty);
-    auto& b = CreateBuilder();
-
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
     ASSERT_EQ(0u, m.entry_points.Length());
     ASSERT_EQ(1u, m.functions.Length());
 
     auto* f = m.functions[0];
-    EXPECT_NE(f->start_target, nullptr);
-    EXPECT_NE(f->end_target, nullptr);
+    ASSERT_NE(f->start_target, nullptr);
+    ASSERT_NE(f->end_target, nullptr);
 
     EXPECT_EQ(1u, f->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, f->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(f->start_target->branch_target, f->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function f
+  %bb1 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, EntryPoint) {
     Func("f", utils::Empty, ty.void_(), utils::Empty,
          utils::Vector{Stage(ast::PipelineStage::kFragment)});
-    auto& b = CreateBuilder();
-
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
     ASSERT_EQ(1u, m.entry_points.Length());
@@ -61,687 +61,718 @@
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement) {
-    // func -> start -> if -> true block
-    //                     -> false block
-    //
-    //   [true block]  -> if merge
-    //   [false block] -> if merge
-    //   [if merge]    -> func end
-    //
     auto* ast_if = If(true, Block(), Else(Block()));
     WrapInFunction(ast_if);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     EXPECT_TRUE(ir_if->Is<ir::If>());
 
     auto* flow = ir_if->As<ir::If>();
-    ASSERT_NE(flow->true_target, nullptr);
-    ASSERT_NE(flow->false_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->true_.target, nullptr);
+    ASSERT_NE(flow->false_.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->true_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->false_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    // Check condition
-    ASSERT_TRUE(flow->condition->Is<Constant>());
-    auto* instr = flow->condition->As<Constant>()->value;
-    ASSERT_TRUE(instr->Is<constant::Scalar<bool>>());
-    EXPECT_TRUE(instr->As<constant::Scalar<bool>>()->ValueAs<bool>());
+  %bb2 = if (true)
+    # true branch
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # false branch
+    %bb5 = Block
+    BranchTo %bb4 ()
+
+  # if merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement_TrueReturns) {
-    // func -> start -> if -> true block
-    //                     -> false block
-    //
-    //   [true block]  -> func end
-    //   [false block] -> if merge
-    //   [if merge]    -> func end
-    //
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(ast_if);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     EXPECT_TRUE(ir_if->Is<ir::If>());
 
     auto* flow = ir_if->As<ir::If>();
-    ASSERT_NE(flow->true_target, nullptr);
-    ASSERT_NE(flow->false_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->true_.target, nullptr);
+    ASSERT_NE(flow->false_.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->true_target->branch_target, func->end_target);
-    EXPECT_EQ(flow->false_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = if (true)
+    # true branch
+    %bb3 = Block
+    Return ()
+    # false branch
+    %bb4 = Block
+    BranchTo %bb5 ()
+
+  # if merge
+  %bb5 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement_FalseReturns) {
-    // func -> start -> if -> true block
-    //                     -> false block
-    //
-    //   [true block]  -> if merge
-    //   [false block] -> func end
-    //   [if merge]    -> func end
-    //
     auto* ast_if = If(true, Block(), Else(Block(Return())));
     WrapInFunction(ast_if);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     EXPECT_TRUE(ir_if->Is<ir::If>());
 
     auto* flow = ir_if->As<ir::If>();
-    ASSERT_NE(flow->true_target, nullptr);
-    ASSERT_NE(flow->false_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->true_.target, nullptr);
+    ASSERT_NE(flow->false_.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->true_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->false_target->branch_target, func->end_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = if (true)
+    # true branch
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # false branch
+    %bb5 = Block
+    Return ()
+  # if merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement_BothReturn) {
-    // func -> start -> if -> true block
-    //                     -> false block
-    //
-    //   [true block]  -> func end
-    //   [false block] -> func end
-    //   [if merge]    -> nullptr
-    //
     auto* ast_if = If(true, Block(Return()), Else(Block(Return())));
     WrapInFunction(ast_if);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     EXPECT_TRUE(ir_if->Is<ir::If>());
 
     auto* flow = ir_if->As<ir::If>();
-    ASSERT_NE(flow->true_target, nullptr);
-    ASSERT_NE(flow->false_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->true_.target, nullptr);
+    ASSERT_NE(flow->false_.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(0u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(0u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->true_target->branch_target, func->end_target);
-    EXPECT_EQ(flow->false_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = if (true)
+    # true branch
+    %bb3 = Block
+    Return ()
+    # false branch
+    %bb4 = Block
+    Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement_JumpChainToMerge) {
-    // if (true) {
-    //   loop {
-    //     break;
-    //   }
-    // }
-    //
-    // func -> start -> if true
-    //               -> if false
-    //
-    //   [if true] -> loop
-    //   [if false] -> if merge
-    //   [if merge] -> func end
-    //   [loop] ->  loop start
-    //   [loop start] -> loop merge
-    //   [loop continuing] -> loop start
-    //   [loop merge] -> if merge
-    //
     auto* ast_loop = Loop(Block(Break()));
     auto* ast_if = If(true, Block(ast_loop));
     WrapInFunction(ast_if);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     EXPECT_TRUE(ir_if->Is<ir::If>());
 
     auto* if_flow = ir_if->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    EXPECT_EQ(func->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, loop_flow);
-    EXPECT_EQ(loop_flow->start_target->branch_target, loop_flow->merge_target);
-    EXPECT_EQ(loop_flow->merge_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, loop_flow->start_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(if_flow->merge_target->branch_target, func->end_target);
+  %bb2 = if (true)
+    # true branch
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    %bb4 = loop
+      # loop start
+      %bb5 = Block
+      BranchTo %bb6 ()
+
+      # loop continuing
+      %bb7 = Block
+      BranchTo %bb5 ()
+
+    # loop merge
+    %bb6 = Block
+    BranchTo %bb8 ()
+
+    # false branch
+    %bb9 = Block
+    BranchTo %bb8 ()
+
+  # if merge
+  %bb8 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithBreak) {
-    // func -> start -> loop -> loop start -> loop merge -> func end
-    //
-    //   [continuing] -> loop start
-    //
     auto* ast_loop = Loop(Block(Break()));
     WrapInFunction(ast_loop);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(flow->start_target, nullptr);
-    ASSERT_NE(flow->continuing_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target, nullptr);
+    ASSERT_NE(flow->continuing.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->continuing_target->branch_target, flow->start_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # loop continuing
+    %bb5 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithContinue) {
-    // func -> start -> loop -> loop start -> if -> true block
-    //                                           -> false block
-    //
-    //   [if true]  -> loop merge
-    //   [if false] -> if merge
-    //   [if merge] -> loop continuing
-    //   [loop continuing] -> loop start
-    //   [loop merge] -> func end
-    //
     auto* ast_if = If(true, Block(Break()));
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     ASSERT_TRUE(ir_if->Is<ir::If>());
 
     auto* if_flow = ir_if->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, loop_flow);
-    EXPECT_EQ(loop_flow->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, loop_flow->merge_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, loop_flow->start_target);
-    EXPECT_EQ(loop_flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    %bb4 = if (true)
+      # true branch
+      %bb5 = Block
+      BranchTo %bb6 ()
+
+      # false branch
+      %bb7 = Block
+      BranchTo %bb8 ()
+
+    # if merge
+    %bb8 = Block
+    BranchTo %bb9 ()
+
+    # loop continuing
+    %bb9 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb6 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithContinuing_BreakIf) {
-    // func -> start -> loop -> loop start -> continuing
-    //
-    //   [loop continuing] -> if -> true branch
-    //                           -> false branch
-    //   [if true] -> loop merge
-    //   [if false] -> if merge
-    //   [if merge] -> loop start
-    //   [loop merge] -> func end
-    //
     auto* ast_break_if = BreakIf(true);
     auto* ast_loop = Loop(Block(), Block(ast_break_if));
     WrapInFunction(ast_loop);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_break_if = b.FlowNodeForAstNode(ast_break_if);
+    auto* ir_break_if = FlowNodeForAstNode(ast_break_if);
     ASSERT_NE(ir_break_if, nullptr);
     ASSERT_TRUE(ir_break_if->Is<ir::If>());
 
     auto* break_if_flow = ir_break_if->As<ir::If>();
-    ASSERT_NE(break_if_flow->true_target, nullptr);
-    ASSERT_NE(break_if_flow->false_target, nullptr);
-    ASSERT_NE(break_if_flow->merge_target, nullptr);
+    ASSERT_NE(break_if_flow->true_.target, nullptr);
+    ASSERT_NE(break_if_flow->false_.target, nullptr);
+    ASSERT_NE(break_if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, break_if_flow->inbound_branches.Length());
-    EXPECT_EQ(1u, break_if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, break_if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, break_if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, break_if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, break_if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, break_if_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, loop_flow);
-    EXPECT_EQ(loop_flow->start_target->branch_target, loop_flow->continuing_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, break_if_flow);
-    EXPECT_EQ(break_if_flow->true_target->branch_target, loop_flow->merge_target);
-    EXPECT_EQ(break_if_flow->false_target->branch_target, break_if_flow->merge_target);
-    EXPECT_EQ(break_if_flow->merge_target->branch_target, loop_flow->start_target);
-    EXPECT_EQ(loop_flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # loop continuing
+    %bb4 = Block
+    BranchTo %bb5 ()
+
+    %bb5 = if (true)
+      # true branch
+      %bb6 = Block
+      BranchTo %bb7 ()
+
+      # false branch
+      %bb8 = Block
+      BranchTo %bb9 ()
+
+    # if merge
+    %bb9 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb7 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithReturn) {
-    // func -> start -> loop -> loop start -> if -> true block
-    //                                           -> false block
-    //
-    //   [if true]  -> func end
-    //   [if false] -> if merge
-    //   [if merge] -> loop continuing
-    //   [loop continuing] -> loop start
-    //   [loop merge] -> nullptr
-    //
     auto* ast_if = If(true, Block(Return()));
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     ASSERT_TRUE(ir_if->Is<ir::If>());
 
     auto* if_flow = ir_if->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(0u, loop_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(0u, loop_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(loop_flow->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, func->end_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, loop_flow->start_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    EXPECT_EQ(func->start_target->branch_target, ir_loop);
-    EXPECT_EQ(loop_flow->merge_target->branch_target, nullptr);
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
 
-    // Check condition
-    ASSERT_TRUE(if_flow->condition->Is<Constant>());
-    auto* instr = if_flow->condition->As<Constant>()->value;
-    ASSERT_TRUE(instr->Is<constant::Scalar<bool>>());
-    EXPECT_TRUE(instr->As<constant::Scalar<bool>>()->ValueAs<bool>());
+    %bb4 = if (true)
+      # true branch
+      %bb5 = Block
+      Return ()
+      # false branch
+      %bb6 = Block
+      BranchTo %bb7 ()
+
+    # if merge
+    %bb7 = Block
+    BranchTo %bb8 ()
+
+    # loop continuing
+    %bb8 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  # Dead
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithOnlyReturn) {
-    // {
-    //   loop {
-    //     return;
-    //     continue;
-    //   }
-    //   if true { return; }
-    // }
-    //
-    // func -> start -> loop -> loop start -> return -> func end
-    //
-    //   [loop continuing] -> loop start
-    //   [loop merge] -> nullptr
-    //
-    // Note, the continue; is here is a dead call, so we won't emit a branch to the continuing block
-    // so the inbound_branches will be zero for continuing.
-    //
-    // The `if` after the `loop` is also eliminated as there is no control-flow path reaching the
-    // block.
     auto* ast_loop = Loop(Block(Return(), Continue()));
     WrapInFunction(ast_loop, If(true, Block(Return())));
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, loop_flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(0u, loop_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, loop_flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(0u, loop_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(loop_flow->start_target->branch_target, func->end_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, loop_flow->start_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    EXPECT_EQ(func->start_target->branch_target, ir_loop);
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    Return ()
+    # loop continuing
+    %bb4 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  # Dead
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithOnlyReturn_ContinuingBreakIf) {
-    // {
-    //   loop {
-    //     return;
-    //     continuing {
-    //       break if true;
-    //     }
-    //   }
-    //   if (true) { return; }
-    // }
-    //
-    // func -> start -> loop -> loop start -> return -> func end
-    //
-    //   [loop continuing] -> break if true
-    //                     -> break if false
-    //   [break if true] -> loop merge
-    //   [break if false] -> if merge
-    //   [break if merge] -> loop start
-    //   [loop merge] -> nullptr
-    //
-    // In this case, the continuing block is dead code, but we don't really know that when parsing
-    // so we end up with a branch into the loop merge target. The loop merge can tell it's dead code
-    // so we can drop the if ater the loop.
+    // Note, even though there is code in the loop merge (specifically, the
+    // `ast_if` below), it doesn't get emitted as there is no way to reach the
+    // loop merge due to the loop itself doing a `return`. This is why the
+    // loop merge gets marked as Dead and the `ast_if` doesn't appear.
     auto* ast_break_if = BreakIf(true);
     auto* ast_loop = Loop(Block(Return()), Block(ast_break_if));
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(Block(ast_loop, ast_if));
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     EXPECT_EQ(ir_if, nullptr);
 
-    auto* ir_break_if = b.FlowNodeForAstNode(ast_break_if);
+    auto* ir_break_if = FlowNodeForAstNode(ast_break_if);
     ASSERT_NE(ir_break_if, nullptr);
     EXPECT_TRUE(ir_break_if->Is<ir::If>());
 
     auto* break_if_flow = ir_break_if->As<ir::If>();
-    ASSERT_NE(break_if_flow->true_target, nullptr);
-    ASSERT_NE(break_if_flow->false_target, nullptr);
-    ASSERT_NE(break_if_flow->merge_target, nullptr);
+    ASSERT_NE(break_if_flow->true_.target, nullptr);
+    ASSERT_NE(break_if_flow->false_.target, nullptr);
+    ASSERT_NE(break_if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, loop_flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, loop_flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     // This is 1 because only the loop branch happens. The subsequent if return is dead code.
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(loop_flow->start_target->branch_target, func->end_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, break_if_flow);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    EXPECT_EQ(func->start_target->branch_target, ir_loop);
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    Return ()
+    # loop continuing
+    %bb4 = Block
+    BranchTo %bb5 ()
+
+    %bb5 = if (true)
+      # true branch
+      %bb6 = Block
+      BranchTo %bb7 ()
+
+      # false branch
+      %bb8 = Block
+      BranchTo %bb9 ()
+
+    # if merge
+    %bb9 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  # Dead
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_WithIf_BothBranchesBreak) {
-    // func -> start -> loop -> loop start -> if -> true branch
-    //                                           -> false branch
-    //
-    //   [if true] -> loop merge
-    //   [if false] -> loop merge
-    //   [if merge] -> nullptr
-    //   [loop continuing] -> loop start
-    //   [loop merge] -> func end
-    //
     auto* ast_if = If(true, Block(Break()), Else(Block(Break())));
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop = b.FlowNodeForAstNode(ast_loop);
+    auto* ir_loop = FlowNodeForAstNode(ast_loop);
     ASSERT_NE(ir_loop, nullptr);
     EXPECT_TRUE(ir_loop->Is<ir::Loop>());
 
     auto* loop_flow = ir_loop->As<ir::Loop>();
-    ASSERT_NE(loop_flow->start_target, nullptr);
-    ASSERT_NE(loop_flow->continuing_target, nullptr);
-    ASSERT_NE(loop_flow->merge_target, nullptr);
+    ASSERT_NE(loop_flow->start.target, nullptr);
+    ASSERT_NE(loop_flow->continuing.target, nullptr);
+    ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = b.FlowNodeForAstNode(ast_if);
+    auto* ir_if = FlowNodeForAstNode(ast_if);
     ASSERT_NE(ir_if, nullptr);
     ASSERT_TRUE(ir_if->Is<ir::If>());
 
     auto* if_flow = ir_if->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, loop_flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, loop_flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(0u, if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(0u, if_flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    // Note, the `continue` is dead code because both if branches go out of loop, so it just gets
-    // dropped.
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    EXPECT_EQ(func->start_target->branch_target, loop_flow);
-    EXPECT_EQ(loop_flow->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, loop_flow->merge_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, loop_flow->merge_target);
-    EXPECT_EQ(loop_flow->continuing_target->branch_target, loop_flow->start_target);
-    EXPECT_EQ(loop_flow->merge_target->branch_target, func->end_target);
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    %bb4 = if (true)
+      # true branch
+      %bb5 = Block
+      BranchTo %bb6 ()
+
+      # false branch
+      %bb7 = Block
+      BranchTo %bb6 ()
+
+    # loop continuing
+    %bb8 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb6 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Loop_Nested) {
-    // loop {   // loop_a
-    //   loop {  // loop_b
-    //      if (true) { break; }  // if_a
-    //      if (true) { continue; }  // if_b
-    //      continuing {
-    //        loop {  // loop_c
-    //          break;
-    //        }
-    //
-    //        loop {  // loop_d
-    //          continuing {
-    //            break if (true);  // if_c
-    //          }
-    //        }
-    //      }
-    //    }
-    //    if (true) { break; }  // if_d
-    //  }
-    //
-    // func -> start -> loop_a -> loop_a start
-    //
-    //   [loop_a start] -> loop_b
-    //   [loop_b start] -> if_a
-    //   [if_a true]  -> loop_b merge
-    //   [if_a false] -> if_a merge
-    //   [if_a merge] -> if_b
-    //   [if_b true] -> loop_b continuing
-    //   [if_b false] -> if_b merge
-    //   [if_b merge] -> loop_b continug
-    //   [loop_b continuing] -> loop_c
-    //   [loop_c start] -> loop_c merge
-    //   [loop_c continuing] -> loop_c start
-    //   [loop_c merge] -> loop_d
-    //   [loop_d start] -> loop_d continuing
-    //   [loop_d continuing] -> if_c
-    //   [if_c true]  -> loop_d merge
-    //   [if_c false] -> if_c merge
-    //   [if c merge] -> loop_d start
-    //   [loop_d merge] -> loop_b start
-    //   [loop_b merge] -> if_d
-    //   [if_d true]  -> loop_a merge
-    //   [if_d false] -> if_d merge
-    //   [if_d merge] -> loop_a continuing
-    //   [loop_a continuing] -> loop_a start
-    //   [loop_a merge] -> func end
-    //
-
     auto* ast_if_a = If(true, Block(Break()));
     auto* ast_if_b = If(true, Block(Continue()));
     auto* ast_if_c = BreakIf(true);
@@ -754,265 +785,362 @@
     auto* ast_loop_a = Loop(Block(ast_loop_b, ast_if_d));
 
     WrapInFunction(ast_loop_a);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_loop_a = b.FlowNodeForAstNode(ast_loop_a);
+    auto* ir_loop_a = FlowNodeForAstNode(ast_loop_a);
     ASSERT_NE(ir_loop_a, nullptr);
     EXPECT_TRUE(ir_loop_a->Is<ir::Loop>());
     auto* loop_flow_a = ir_loop_a->As<ir::Loop>();
-    ASSERT_NE(loop_flow_a->start_target, nullptr);
-    ASSERT_NE(loop_flow_a->continuing_target, nullptr);
-    ASSERT_NE(loop_flow_a->merge_target, nullptr);
+    ASSERT_NE(loop_flow_a->start.target, nullptr);
+    ASSERT_NE(loop_flow_a->continuing.target, nullptr);
+    ASSERT_NE(loop_flow_a->merge.target, nullptr);
 
-    auto* ir_loop_b = b.FlowNodeForAstNode(ast_loop_b);
+    auto* ir_loop_b = FlowNodeForAstNode(ast_loop_b);
     ASSERT_NE(ir_loop_b, nullptr);
     EXPECT_TRUE(ir_loop_b->Is<ir::Loop>());
     auto* loop_flow_b = ir_loop_b->As<ir::Loop>();
-    ASSERT_NE(loop_flow_b->start_target, nullptr);
-    ASSERT_NE(loop_flow_b->continuing_target, nullptr);
-    ASSERT_NE(loop_flow_b->merge_target, nullptr);
+    ASSERT_NE(loop_flow_b->start.target, nullptr);
+    ASSERT_NE(loop_flow_b->continuing.target, nullptr);
+    ASSERT_NE(loop_flow_b->merge.target, nullptr);
 
-    auto* ir_loop_c = b.FlowNodeForAstNode(ast_loop_c);
+    auto* ir_loop_c = FlowNodeForAstNode(ast_loop_c);
     ASSERT_NE(ir_loop_c, nullptr);
     EXPECT_TRUE(ir_loop_c->Is<ir::Loop>());
     auto* loop_flow_c = ir_loop_c->As<ir::Loop>();
-    ASSERT_NE(loop_flow_c->start_target, nullptr);
-    ASSERT_NE(loop_flow_c->continuing_target, nullptr);
-    ASSERT_NE(loop_flow_c->merge_target, nullptr);
+    ASSERT_NE(loop_flow_c->start.target, nullptr);
+    ASSERT_NE(loop_flow_c->continuing.target, nullptr);
+    ASSERT_NE(loop_flow_c->merge.target, nullptr);
 
-    auto* ir_loop_d = b.FlowNodeForAstNode(ast_loop_d);
+    auto* ir_loop_d = FlowNodeForAstNode(ast_loop_d);
     ASSERT_NE(ir_loop_d, nullptr);
     EXPECT_TRUE(ir_loop_d->Is<ir::Loop>());
     auto* loop_flow_d = ir_loop_d->As<ir::Loop>();
-    ASSERT_NE(loop_flow_d->start_target, nullptr);
-    ASSERT_NE(loop_flow_d->continuing_target, nullptr);
-    ASSERT_NE(loop_flow_d->merge_target, nullptr);
+    ASSERT_NE(loop_flow_d->start.target, nullptr);
+    ASSERT_NE(loop_flow_d->continuing.target, nullptr);
+    ASSERT_NE(loop_flow_d->merge.target, nullptr);
 
-    auto* ir_if_a = b.FlowNodeForAstNode(ast_if_a);
+    auto* ir_if_a = FlowNodeForAstNode(ast_if_a);
     ASSERT_NE(ir_if_a, nullptr);
     EXPECT_TRUE(ir_if_a->Is<ir::If>());
     auto* if_flow_a = ir_if_a->As<ir::If>();
-    ASSERT_NE(if_flow_a->true_target, nullptr);
-    ASSERT_NE(if_flow_a->false_target, nullptr);
-    ASSERT_NE(if_flow_a->merge_target, nullptr);
+    ASSERT_NE(if_flow_a->true_.target, nullptr);
+    ASSERT_NE(if_flow_a->false_.target, nullptr);
+    ASSERT_NE(if_flow_a->merge.target, nullptr);
 
-    auto* ir_if_b = b.FlowNodeForAstNode(ast_if_b);
+    auto* ir_if_b = FlowNodeForAstNode(ast_if_b);
     ASSERT_NE(ir_if_b, nullptr);
     EXPECT_TRUE(ir_if_b->Is<ir::If>());
     auto* if_flow_b = ir_if_b->As<ir::If>();
-    ASSERT_NE(if_flow_b->true_target, nullptr);
-    ASSERT_NE(if_flow_b->false_target, nullptr);
-    ASSERT_NE(if_flow_b->merge_target, nullptr);
+    ASSERT_NE(if_flow_b->true_.target, nullptr);
+    ASSERT_NE(if_flow_b->false_.target, nullptr);
+    ASSERT_NE(if_flow_b->merge.target, nullptr);
 
-    auto* ir_if_c = b.FlowNodeForAstNode(ast_if_c);
+    auto* ir_if_c = FlowNodeForAstNode(ast_if_c);
     ASSERT_NE(ir_if_c, nullptr);
     EXPECT_TRUE(ir_if_c->Is<ir::If>());
     auto* if_flow_c = ir_if_c->As<ir::If>();
-    ASSERT_NE(if_flow_c->true_target, nullptr);
-    ASSERT_NE(if_flow_c->false_target, nullptr);
-    ASSERT_NE(if_flow_c->merge_target, nullptr);
+    ASSERT_NE(if_flow_c->true_.target, nullptr);
+    ASSERT_NE(if_flow_c->false_.target, nullptr);
+    ASSERT_NE(if_flow_c->merge.target, nullptr);
 
-    auto* ir_if_d = b.FlowNodeForAstNode(ast_if_d);
+    auto* ir_if_d = FlowNodeForAstNode(ast_if_d);
     ASSERT_NE(ir_if_d, nullptr);
     EXPECT_TRUE(ir_if_d->Is<ir::If>());
     auto* if_flow_d = ir_if_d->As<ir::If>();
-    ASSERT_NE(if_flow_d->true_target, nullptr);
-    ASSERT_NE(if_flow_d->false_target, nullptr);
-    ASSERT_NE(if_flow_d->merge_target, nullptr);
+    ASSERT_NE(if_flow_d->true_.target, nullptr);
+    ASSERT_NE(if_flow_d->false_.target, nullptr);
+    ASSERT_NE(if_flow_d->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, loop_flow_a->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow_a->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow_a->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow_a->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow_a->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow_a->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow_a->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, loop_flow_b->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow_b->start_target->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow_b->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow_b->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow_b->start.target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow_b->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow_b->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, loop_flow_c->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow_c->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, loop_flow_c->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow_c->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow_c->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, loop_flow_c->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow_c->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, loop_flow_d->inbound_branches.Length());
-    EXPECT_EQ(2u, loop_flow_d->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow_d->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, loop_flow_d->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, loop_flow_d->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow_d->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, loop_flow_d->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow_a->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_a->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_a->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_a->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_a->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_a->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_a->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow_b->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_b->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_b->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_b->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_b->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_b->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_b->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow_c->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_c->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_c->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_c->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_c->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_c->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_c->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow_d->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_d->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_d->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow_d->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_d->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_d->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow_d->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, loop_flow_a);
-    EXPECT_EQ(loop_flow_a->start_target->branch_target, loop_flow_b);
-    EXPECT_EQ(loop_flow_b->start_target->branch_target, if_flow_a);
-    EXPECT_EQ(if_flow_a->true_target->branch_target, loop_flow_b->merge_target);
-    EXPECT_EQ(if_flow_a->false_target->branch_target, if_flow_a->merge_target);
-    EXPECT_EQ(if_flow_a->merge_target->branch_target, if_flow_b);
-    EXPECT_EQ(if_flow_b->true_target->branch_target, loop_flow_b->continuing_target);
-    EXPECT_EQ(if_flow_b->false_target->branch_target, if_flow_b->merge_target);
-    EXPECT_EQ(if_flow_b->merge_target->branch_target, loop_flow_b->continuing_target);
-    EXPECT_EQ(loop_flow_b->continuing_target->branch_target, loop_flow_c);
-    EXPECT_EQ(loop_flow_c->start_target->branch_target, loop_flow_c->merge_target);
-    EXPECT_EQ(loop_flow_c->continuing_target->branch_target, loop_flow_c->start_target);
-    EXPECT_EQ(loop_flow_c->merge_target->branch_target, loop_flow_d);
-    EXPECT_EQ(loop_flow_d->start_target->branch_target, loop_flow_d->continuing_target);
-    EXPECT_EQ(loop_flow_d->continuing_target->branch_target, if_flow_c);
-    EXPECT_EQ(if_flow_c->true_target->branch_target, loop_flow_d->merge_target);
-    EXPECT_EQ(if_flow_c->false_target->branch_target, if_flow_c->merge_target);
-    EXPECT_EQ(if_flow_c->merge_target->branch_target, loop_flow_d->start_target);
-    EXPECT_EQ(loop_flow_d->merge_target->branch_target, loop_flow_b->start_target);
-    EXPECT_EQ(loop_flow_b->merge_target->branch_target, if_flow_d);
-    EXPECT_EQ(if_flow_d->true_target->branch_target, loop_flow_a->merge_target);
-    EXPECT_EQ(if_flow_d->false_target->branch_target, if_flow_d->merge_target);
-    EXPECT_EQ(if_flow_d->merge_target->branch_target, loop_flow_a->continuing_target);
-    EXPECT_EQ(loop_flow_a->continuing_target->branch_target, loop_flow_a->start_target);
-    EXPECT_EQ(loop_flow_a->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    %bb4 = loop
+      # loop start
+      %bb5 = Block
+      BranchTo %bb6 ()
+
+      %bb6 = if (true)
+        # true branch
+        %bb7 = Block
+        BranchTo %bb8 ()
+
+        # false branch
+        %bb9 = Block
+        BranchTo %bb10 ()
+
+      # if merge
+      %bb10 = Block
+      BranchTo %bb11 ()
+
+      %bb11 = if (true)
+        # true branch
+        %bb12 = Block
+        BranchTo %bb13 ()
+
+        # false branch
+        %bb14 = Block
+        BranchTo %bb15 ()
+
+      # if merge
+      %bb15 = Block
+      BranchTo %bb13 ()
+
+      # loop continuing
+      %bb13 = Block
+      BranchTo %bb16 ()
+
+      %bb16 = loop
+        # loop start
+        %bb17 = Block
+        BranchTo %bb18 ()
+
+        # loop continuing
+        %bb19 = Block
+        BranchTo %bb17 ()
+
+      # loop merge
+      %bb18 = Block
+      BranchTo %bb20 ()
+
+      %bb20 = loop
+        # loop start
+        %bb21 = Block
+        BranchTo %bb22 ()
+
+        # loop continuing
+        %bb22 = Block
+        BranchTo %bb23 ()
+
+        %bb23 = if (true)
+          # true branch
+          %bb24 = Block
+          BranchTo %bb25 ()
+
+          # false branch
+          %bb26 = Block
+          BranchTo %bb27 ()
+
+        # if merge
+        %bb27 = Block
+        BranchTo %bb21 ()
+
+      # loop merge
+      %bb25 = Block
+      BranchTo %bb5 ()
+
+    # loop merge
+    %bb8 = Block
+    BranchTo %bb28 ()
+
+    %bb28 = if (true)
+      # true branch
+      %bb29 = Block
+      BranchTo %bb30 ()
+
+      # false branch
+      %bb31 = Block
+      BranchTo %bb32 ()
+
+    # if merge
+    %bb32 = Block
+    BranchTo %bb33 ()
+
+    # loop continuing
+    %bb33 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb30 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, While) {
-    // {
-    //   while false {
-    //   }
-    // }
-    //
-    // func -> while -> loop_start -> if true
-    //                             -> if false
-    //
-    //   [if true] -> if merge
-    //   [if false] -> while merge
-    //   [if merge] -> loop continuing
-    //   [loop continuing] -> loop start
-    //   [while merge] -> func end
-    //
     auto* ast_while = While(false, Block());
     WrapInFunction(ast_while);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_while = b.FlowNodeForAstNode(ast_while);
+    auto* ir_while = FlowNodeForAstNode(ast_while);
     ASSERT_NE(ir_while, nullptr);
     ASSERT_TRUE(ir_while->Is<ir::Loop>());
 
     auto* flow = ir_while->As<ir::Loop>();
-    ASSERT_NE(flow->start_target, nullptr);
-    ASSERT_NE(flow->continuing_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target, nullptr);
+    ASSERT_NE(flow->continuing.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_NE(flow->start_target->branch_target, nullptr);
-    ASSERT_TRUE(flow->start_target->branch_target->Is<ir::If>());
-    auto* if_flow = flow->start_target->branch_target->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target->As<ir::Block>()->branch.target, nullptr);
+    ASSERT_TRUE(flow->start.target->As<ir::Block>()->branch.target->Is<ir::If>());
+    auto* if_flow = flow->start.target->As<ir::Block>()->branch.target->As<ir::If>();
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, flow->merge_target);
-    EXPECT_EQ(if_flow->merge_target->branch_target, flow->continuing_target);
-    EXPECT_EQ(flow->continuing_target->branch_target, flow->start_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    // Check condition
-    ASSERT_TRUE(if_flow->condition->Is<Constant>());
-    auto* instr = if_flow->condition->As<Constant>()->value;
-    ASSERT_TRUE(instr->Is<constant::Scalar<bool>>());
-    EXPECT_FALSE(instr->As<constant::Scalar<bool>>()->ValueAs<bool>());
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    %bb4 = if (false)
+      # true branch
+      %bb5 = Block
+      BranchTo %bb6 ()
+
+      # false branch
+      %bb7 = Block
+      BranchTo %bb8 ()
+
+    # if merge
+    %bb6 = Block
+    BranchTo %bb9 ()
+
+    # loop continuing
+    %bb9 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb8 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, While_Return) {
-    // {
-    //   while true {
-    //     return;
-    //   }
-    // }
-    //
-    // func -> while -> if true
-    //                  if false
-    //
-    //   [if true] -> if merge
-    //   [if false] -> while merge
-    //   [if merge] -> func end
-    //   [while merge] -> func end
-    //
     auto* ast_while = While(true, Block(Return()));
     WrapInFunction(ast_while);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_while = b.FlowNodeForAstNode(ast_while);
+    auto* ir_while = FlowNodeForAstNode(ast_while);
     ASSERT_NE(ir_while, nullptr);
     ASSERT_TRUE(ir_while->Is<ir::Loop>());
 
     auto* flow = ir_while->As<ir::Loop>();
-    ASSERT_NE(flow->start_target, nullptr);
-    ASSERT_NE(flow->continuing_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target, nullptr);
+    ASSERT_NE(flow->continuing.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_NE(flow->start_target->branch_target, nullptr);
-    ASSERT_TRUE(flow->start_target->branch_target->Is<ir::If>());
-    auto* if_flow = flow->start_target->branch_target->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target->As<ir::Block>()->branch.target, nullptr);
+    ASSERT_TRUE(flow->start.target->As<ir::Block>()->branch.target->Is<ir::If>());
+    auto* if_flow = flow->start.target->As<ir::Block>()->branch.target->As<ir::If>();
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, flow->merge_target);
-    EXPECT_EQ(if_flow->merge_target->branch_target, func->end_target);
-    EXPECT_EQ(flow->continuing_target->branch_target, flow->start_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    %bb4 = if (true)
+      # true branch
+      %bb5 = Block
+      BranchTo %bb6 ()
+
+      # false branch
+      %bb7 = Block
+      BranchTo %bb8 ()
+
+    # if merge
+    %bb6 = Block
+    Return ()
+    # loop continuing
+    %bb9 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb8 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 // TODO(dsinclair): Enable when variable declarations and increment are supported
@@ -1031,314 +1159,311 @@
     //
     auto* ast_for = For(Decl(Var("i", ty.i32())), LessThan("i", 10_a), Increment("i"), Block());
     WrapInFunction(ast_for);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_for = b.FlowNodeForAstNode(ast_for);
+    auto* ir_for = FlowNodeForAstNode(ast_for);
     ASSERT_NE(ir_for, nullptr);
     ASSERT_TRUE(ir_for->Is<ir::Loop>());
 
     auto* flow = ir_for->As<ir::Loop>();
-    ASSERT_NE(flow->start_target, nullptr);
-    ASSERT_NE(flow->continuing_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target, nullptr);
+    ASSERT_NE(flow->continuing.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_NE(flow->start_target->branch_target, nullptr);
-    ASSERT_TRUE(flow->start_target->branch_target->Is<ir::If>());
-    auto* if_flow = flow->start_target->branch_target->As<ir::If>();
-    ASSERT_NE(if_flow->true_target, nullptr);
-    ASSERT_NE(if_flow->false_target, nullptr);
-    ASSERT_NE(if_flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target->As<ir::Block>()->branch.target, nullptr);
+    ASSERT_TRUE(flow->start.target->As<ir::Block>()->branch.target->Is<ir::If>());
+    auto* if_flow = flow->start.target->As<ir::Block>()->branch.target->As<ir::If>();
+    ASSERT_NE(if_flow->true_.target, nullptr);
+    ASSERT_NE(if_flow->false_.target, nullptr);
+    ASSERT_NE(if_flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->true_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->false_target->inbound_branches.Length());
-    EXPECT_EQ(1u, if_flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->true_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
+    EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, flow);
-    EXPECT_EQ(flow->start_target->branch_target, if_flow);
-    EXPECT_EQ(if_flow->true_target->branch_target, if_flow->merge_target);
-    EXPECT_EQ(if_flow->false_target->branch_target, flow->merge_target);
-    EXPECT_EQ(if_flow->merge_target->branch_target, flow->continuing_target);
-    EXPECT_EQ(flow->continuing_target->branch_target, flow->start_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
-
-    // Check condition
-    ASSERT_TRUE(if_flow->condition->Is<Constant>());
-    auto* instr = if_flow->condition->As<Constant>()->value;
-    ASSERT_TRUE(instr->Is<constant::Scalar<bool>>());
-    EXPECT_FALSE(instr->As<constant::Scalar<bool>>()->ValueAs<bool>());
+    EXPECT_EQ(Disassemble(m), R"()");
 }
 
 TEST_F(IR_BuilderImplTest, For_NoInitCondOrContinuing) {
-    // for (;;) {
-    //   break;
-    // }
-    //
-    // func -> loop -> loop start -> loop merge -> func end
-    //
     auto* ast_for = For(nullptr, nullptr, nullptr, Block(Break()));
     WrapInFunction(ast_for);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_for = b.FlowNodeForAstNode(ast_for);
+    auto* ir_for = FlowNodeForAstNode(ast_for);
     ASSERT_NE(ir_for, nullptr);
     ASSERT_TRUE(ir_for->Is<ir::Loop>());
 
     auto* flow = ir_for->As<ir::Loop>();
-    ASSERT_NE(flow->start_target, nullptr);
-    ASSERT_NE(flow->continuing_target, nullptr);
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->start.target, nullptr);
+    ASSERT_NE(flow->continuing.target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, flow->continuing_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, flow->continuing.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(flow->start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->continuing_target->branch_target, flow->start_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = loop
+    # loop start
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # loop continuing
+    %bb5 = Block
+    BranchTo %bb3 ()
+
+  # loop merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Switch) {
-    // func -> switch -> case 1
-    //                -> case 2
-    //                -> default
-    //
-    //   [case 1] -> switch merge
-    //   [case 2] -> switch merge
-    //   [default] -> switch merge
-    //   [switch merge] -> func end
-    //
     auto* ast_switch = Switch(
         1_i, utils::Vector{Case(utils::Vector{CaseSelector(0_i)}, Block()),
                            Case(utils::Vector{CaseSelector(1_i)}, Block()), DefaultCase(Block())});
 
     WrapInFunction(ast_switch);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_switch = b.FlowNodeForAstNode(ast_switch);
+    auto* ir_switch = FlowNodeForAstNode(ast_switch);
     ASSERT_NE(ir_switch, nullptr);
     ASSERT_TRUE(ir_switch->Is<ir::Switch>());
 
     auto* flow = ir_switch->As<ir::Switch>();
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(3u, flow->cases.Length());
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
-    ASSERT_TRUE(flow->cases[0].selectors[0]->expr->Is<ast::IntLiteralExpression>());
-    EXPECT_EQ(0_i, flow->cases[0].selectors[0]->expr->As<ast::IntLiteralExpression>()->value);
+    ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
+    EXPECT_EQ(0_i,
+              flow->cases[0].selectors[0].val->value->As<constant::Scalar<tint::i32>>()->ValueOf());
 
     ASSERT_EQ(1u, flow->cases[1].selectors.Length());
-    ASSERT_TRUE(flow->cases[1].selectors[0]->expr->Is<ast::IntLiteralExpression>());
-    EXPECT_EQ(1_i, flow->cases[1].selectors[0]->expr->As<ast::IntLiteralExpression>()->value);
+    ASSERT_TRUE(flow->cases[1].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
+    EXPECT_EQ(1_i,
+              flow->cases[1].selectors[0].val->value->As<constant::Scalar<tint::i32>>()->ValueOf());
 
     ASSERT_EQ(1u, flow->cases[2].selectors.Length());
-    EXPECT_TRUE(flow->cases[2].selectors[0]->IsDefault());
+    EXPECT_TRUE(flow->cases[2].selectors[0].IsDefault());
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[0].start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[1].start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[2].start_target->inbound_branches.Length());
-    EXPECT_EQ(3u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[0].start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[1].start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[2].start.target->inbound_branches.Length());
+    EXPECT_EQ(3u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, ir_switch);
-    EXPECT_EQ(flow->cases[0].start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->cases[1].start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->cases[2].start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
 
-    // Check condition
-    ASSERT_TRUE(flow->condition->Is<Constant>());
-    auto* instr = flow->condition->As<Constant>()->value;
-    ASSERT_TRUE(instr->Is<constant::Scalar<i32>>());
-    EXPECT_EQ(1_i, instr->As<constant::Scalar<i32>>()->ValueAs<i32>());
+  %bb2 = Switch (1)
+    # Case 0
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # Case 1
+    %bb5 = Block
+    BranchTo %bb4 ()
+
+    # Case default
+    %bb6 = Block
+    BranchTo %bb4 ()
+
+  # Switch Merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Switch_OnlyDefault) {
-    // func -> switch -> default -> switch merge -> func end
-    //
     auto* ast_switch = Switch(1_i, utils::Vector{DefaultCase(Block())});
-
     WrapInFunction(ast_switch);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_switch = b.FlowNodeForAstNode(ast_switch);
+    auto* ir_switch = FlowNodeForAstNode(ast_switch);
     ASSERT_NE(ir_switch, nullptr);
     ASSERT_TRUE(ir_switch->Is<ir::Switch>());
 
     auto* flow = ir_switch->As<ir::Switch>();
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(1u, flow->cases.Length());
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
-    EXPECT_TRUE(flow->cases[0].selectors[0]->IsDefault());
+    EXPECT_TRUE(flow->cases[0].selectors[0].IsDefault());
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[0].start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[0].start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, ir_switch);
-    EXPECT_EQ(flow->cases[0].start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = Switch (1)
+    # Case default
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+  # Switch Merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Switch_WithBreak) {
-    // {
-    //   switch(1) {
-    //     case 0: {
-    //       break;
-    //       if true { return;}   // Dead code
-    //     }
-    //     default: {}
-    //   }
-    // }
-    //
-    // func -> switch -> case 1
-    //                -> default
-    //
-    //   [case 1] -> switch merge
-    //   [default] -> switch merge
-    //   [switch merge] -> func end
     auto* ast_switch = Switch(1_i, utils::Vector{Case(utils::Vector{CaseSelector(0_i)},
                                                       Block(Break(), If(true, Block(Return())))),
                                                  DefaultCase(Block())});
-
     WrapInFunction(ast_switch);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    auto* ir_switch = b.FlowNodeForAstNode(ast_switch);
+    auto* ir_switch = FlowNodeForAstNode(ast_switch);
     ASSERT_NE(ir_switch, nullptr);
     ASSERT_TRUE(ir_switch->Is<ir::Switch>());
 
     auto* flow = ir_switch->As<ir::Switch>();
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(2u, flow->cases.Length());
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
-    ASSERT_TRUE(flow->cases[0].selectors[0]->expr->Is<ast::IntLiteralExpression>());
-    EXPECT_EQ(0_i, flow->cases[0].selectors[0]->expr->As<ast::IntLiteralExpression>()->value);
+    ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
+    EXPECT_EQ(0_i,
+              flow->cases[0].selectors[0].val->value->As<constant::Scalar<tint::i32>>()->ValueOf());
 
     ASSERT_EQ(1u, flow->cases[1].selectors.Length());
-    EXPECT_TRUE(flow->cases[1].selectors[0]->IsDefault());
+    EXPECT_TRUE(flow->cases[1].selectors[0].IsDefault());
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[0].start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[1].start_target->inbound_branches.Length());
-    EXPECT_EQ(2u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[0].start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[1].start.target->inbound_branches.Length());
+    EXPECT_EQ(2u, flow->merge.target->inbound_branches.Length());
     // This is 1 because the if is dead-code eliminated and the return doesn't happen.
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, ir_switch);
-    EXPECT_EQ(flow->cases[0].start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->cases[1].start_target->branch_target, flow->merge_target);
-    EXPECT_EQ(flow->merge_target->branch_target, func->end_target);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = Switch (1)
+    # Case 0
+    %bb3 = Block
+    BranchTo %bb4 ()
+
+    # Case default
+    %bb5 = Block
+    BranchTo %bb4 ()
+
+  # Switch Merge
+  %bb4 = Block
+  Return ()
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, Switch_AllReturn) {
-    // {
-    //   switch(1) {
-    //     case 0: {
-    //       return;
-    //     }
-    //     default: {
-    //       return;
-    //     }
-    //   }
-    //   if true { return; }  // Dead code
-    // }
-    //
-    // func -> switch -> case 1
-    //                -> default
-    //
-    //   [case 1] -> func end
-    //   [default] -> func end
-    //   [switch merge] -> nullptr
-    //
     auto* ast_switch =
         Switch(1_i, utils::Vector{Case(utils::Vector{CaseSelector(0_i)}, Block(Return())),
                                   DefaultCase(Block(Return()))});
-
     auto* ast_if = If(true, Block(Return()));
-
     WrapInFunction(ast_switch, ast_if);
-    auto& b = CreateBuilder();
 
-    auto r = b.Build();
-    ASSERT_TRUE(r) << b.error();
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
     auto m = r.Move();
 
-    ASSERT_EQ(b.FlowNodeForAstNode(ast_if), nullptr);
+    ASSERT_EQ(FlowNodeForAstNode(ast_if), nullptr);
 
-    auto* ir_switch = b.FlowNodeForAstNode(ast_switch);
+    auto* ir_switch = FlowNodeForAstNode(ast_switch);
     ASSERT_NE(ir_switch, nullptr);
     ASSERT_TRUE(ir_switch->Is<ir::Switch>());
 
     auto* flow = ir_switch->As<ir::Switch>();
-    ASSERT_NE(flow->merge_target, nullptr);
+    ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(2u, flow->cases.Length());
 
     ASSERT_EQ(1u, m.functions.Length());
     auto* func = m.functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
-    ASSERT_TRUE(flow->cases[0].selectors[0]->expr->Is<ast::IntLiteralExpression>());
-    EXPECT_EQ(0_i, flow->cases[0].selectors[0]->expr->As<ast::IntLiteralExpression>()->value);
+    ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
+    EXPECT_EQ(0_i,
+              flow->cases[0].selectors[0].val->value->As<constant::Scalar<tint::i32>>()->ValueOf());
 
     ASSERT_EQ(1u, flow->cases[1].selectors.Length());
-    EXPECT_TRUE(flow->cases[1].selectors[0]->IsDefault());
+    EXPECT_TRUE(flow->cases[1].selectors[0].IsDefault());
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[0].start_target->inbound_branches.Length());
-    EXPECT_EQ(1u, flow->cases[1].start_target->inbound_branches.Length());
-    EXPECT_EQ(0u, flow->merge_target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[0].start.target->inbound_branches.Length());
+    EXPECT_EQ(1u, flow->cases[1].start.target->inbound_branches.Length());
+    EXPECT_EQ(0u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(func->start_target->branch_target, ir_switch);
-    EXPECT_EQ(flow->cases[0].start_target->branch_target, func->end_target);
-    EXPECT_EQ(flow->cases[1].start_target->branch_target, func->end_target);
-    EXPECT_EQ(flow->merge_target->branch_target, nullptr);
+    EXPECT_EQ(Disassemble(m), R"(%bb0 = Function test_function
+  %bb1 = Block
+  BranchTo %bb2 ()
+
+  %bb2 = Switch (1)
+    # Case 0
+    %bb3 = Block
+    Return ()
+    # Case default
+    %bb4 = Block
+    Return ()
+  # Switch Merge
+  # Dead
+FunctionEnd
+
+)");
 }
 
 TEST_F(IR_BuilderImplTest, EmitLiteral_Bool_True) {
@@ -1436,7 +1561,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 + 4
 )");
 }
@@ -1451,7 +1576,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 - 4
 )");
 }
@@ -1466,7 +1591,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 * 4
 )");
 }
@@ -1481,7 +1606,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 / 4
 )");
 }
@@ -1496,7 +1621,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 % 4
 )");
 }
@@ -1511,7 +1636,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 & 4
 )");
 }
@@ -1526,7 +1651,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 | 4
 )");
 }
@@ -1541,7 +1666,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 ^ 4
 )");
 }
@@ -1556,7 +1681,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = true && false
 )");
 }
@@ -1571,7 +1696,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = false || true
 )");
 }
@@ -1586,7 +1711,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 == 4
 )");
 }
@@ -1601,7 +1726,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 != 4
 )");
 }
@@ -1616,7 +1741,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 < 4
 )");
 }
@@ -1631,7 +1756,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 > 4
 )");
 }
@@ -1646,7 +1771,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 <= 4
 )");
 }
@@ -1661,7 +1786,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (bool) = 3 >= 4
 )");
 }
@@ -1676,7 +1801,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 << 4
 )");
 }
@@ -1691,7 +1816,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 >> 4
 )");
 }
@@ -1707,7 +1832,7 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 >> 4
 %2 (u32) = %1 (u32) + 9
 %3 (bool) = 1 < %2 (u32)
@@ -1728,9 +1853,10 @@
     ASSERT_TRUE(r) << b.error();
 
     Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
     EXPECT_EQ(d.AsString(), R"(%1 (f32) = bitcast(3)
 )");
 }
+
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/constant_test.cc b/src/tint/ir/constant_test.cc
index 87eec65..c6fe2e4 100644
--- a/src/tint/ir/constant_test.cc
+++ b/src/tint/ir/constant_test.cc
@@ -32,7 +32,7 @@
     auto* c = b.builder.Constant(1.2_f);
     EXPECT_EQ(1.2_f, c->value->As<constant::Scalar<f32>>()->ValueAs<f32>());
 
-    c->ToString(str, program->Symbols());
+    c->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("1.2", str.str());
 
     EXPECT_TRUE(c->value->Is<constant::Scalar<f32>>());
@@ -50,7 +50,7 @@
     auto* c = b.builder.Constant(1.1_h);
     EXPECT_EQ(1.1_h, c->value->As<constant::Scalar<f16>>()->ValueAs<f16>());
 
-    c->ToString(str, program->Symbols());
+    c->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("1.09961", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
@@ -68,7 +68,7 @@
     auto* c = b.builder.Constant(1_i);
     EXPECT_EQ(1_i, c->value->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    c->ToString(str, program->Symbols());
+    c->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("1", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
@@ -86,7 +86,7 @@
     auto* c = b.builder.Constant(2_u);
     EXPECT_EQ(2_u, c->value->As<constant::Scalar<u32>>()->ValueAs<u32>());
 
-    c->ToString(str, program->Symbols());
+    c->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("2", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
@@ -104,14 +104,14 @@
     auto* c = b.builder.Constant(false);
     EXPECT_FALSE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
-    c->ToString(str, program->Symbols());
+    c->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("false", str.str());
 
     str.str("");
     c = b.builder.Constant(true);
     EXPECT_TRUE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
-    c->ToString(str, program->Symbols());
+    c->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("true", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
diff --git a/src/tint/ir/debug.cc b/src/tint/ir/debug.cc
index be83111..617b207 100644
--- a/src/tint/ir/debug.cc
+++ b/src/tint/ir/debug.cc
@@ -23,7 +23,6 @@
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
-#include "src/tint/program.h"
 
 namespace tint::ir {
 
@@ -60,24 +59,24 @@
                 if (node_to_name.count(b) == 0) {
                     out << name_for(b) << R"( [label="block"])" << std::endl;
                 }
-                out << name_for(b) << " -> " << name_for(b->branch_target);
+                out << name_for(b) << " -> " << name_for(b->branch.target);
 
                 // Dashed lines to merge blocks
-                if (merge_nodes.count(b->branch_target) != 0) {
+                if (merge_nodes.count(b->branch.target) != 0) {
                     out << " [style=dashed]";
                 }
 
                 out << std::endl;
-                Graph(b->branch_target);
+                Graph(b->branch.target);
             },
             [&](const ir::Switch* s) {
                 out << name_for(s) << R"( [label="switch"])" << std::endl;
-                out << name_for(s->merge_target) << R"( [label="switch merge"])" << std::endl;
-                merge_nodes.insert(s->merge_target);
+                out << name_for(s->merge.target) << R"( [label="switch merge"])" << std::endl;
+                merge_nodes.insert(s->merge.target);
 
                 size_t i = 0;
                 for (const auto& c : s->cases) {
-                    out << name_for(c.start_target)
+                    out << name_for(c.start.target)
                         << R"( [label="case )" + std::to_string(i++) + R"("])" << std::endl;
                 }
                 out << name_for(s) << " -> {";
@@ -85,56 +84,56 @@
                     if (&c != &(s->cases[0])) {
                         out << ", ";
                     }
-                    out << name_for(c.start_target);
+                    out << name_for(c.start.target);
                 }
                 out << "}" << std::endl;
 
                 for (const auto& c : s->cases) {
-                    Graph(c.start_target);
+                    Graph(c.start.target);
                 }
-                Graph(s->merge_target);
+                Graph(s->merge.target);
             },
             [&](const ir::If* i) {
                 out << name_for(i) << R"( [label="if"])" << std::endl;
-                out << name_for(i->true_target) << R"( [label="true"])" << std::endl;
-                out << name_for(i->false_target) << R"( [label="false"])" << std::endl;
-                out << name_for(i->merge_target) << R"( [label="if merge"])" << std::endl;
-                merge_nodes.insert(i->merge_target);
+                out << name_for(i->true_.target) << R"( [label="true"])" << std::endl;
+                out << name_for(i->false_.target) << R"( [label="false"])" << std::endl;
+                out << name_for(i->merge.target) << R"( [label="if merge"])" << std::endl;
+                merge_nodes.insert(i->merge.target);
 
                 out << name_for(i) << " -> {";
-                out << name_for(i->true_target) << ", " << name_for(i->false_target);
+                out << name_for(i->true_.target) << ", " << name_for(i->false_.target);
                 out << "}" << std::endl;
 
                 // Subgraph if true/false branches so they draw on the same line
                 out << "subgraph sub_" << name_for(i) << " {" << std::endl;
                 out << R"(rank="same")" << std::endl;
-                out << name_for(i->true_target) << std::endl;
-                out << name_for(i->false_target) << std::endl;
+                out << name_for(i->true_.target) << std::endl;
+                out << name_for(i->false_.target) << std::endl;
                 out << "}" << std::endl;
 
-                Graph(i->true_target);
-                Graph(i->false_target);
-                Graph(i->merge_target);
+                Graph(i->true_.target);
+                Graph(i->false_.target);
+                Graph(i->merge.target);
             },
             [&](const ir::Loop* l) {
                 out << name_for(l) << R"( [label="loop"])" << std::endl;
-                out << name_for(l->start_target) << R"( [label="start"])" << std::endl;
-                out << name_for(l->continuing_target) << R"( [label="continuing"])" << std::endl;
-                out << name_for(l->merge_target) << R"( [label="loop merge"])" << std::endl;
-                merge_nodes.insert(l->merge_target);
+                out << name_for(l->start.target) << R"( [label="start"])" << std::endl;
+                out << name_for(l->continuing.target) << R"( [label="continuing"])" << std::endl;
+                out << name_for(l->merge.target) << R"( [label="loop merge"])" << std::endl;
+                merge_nodes.insert(l->merge.target);
 
                 // Subgraph the continuing and merge so they get drawn on the same line
                 out << "subgraph sub_" << name_for(l) << " {" << std::endl;
                 out << R"(rank="same")" << std::endl;
-                out << name_for(l->continuing_target) << std::endl;
-                out << name_for(l->merge_target) << std::endl;
+                out << name_for(l->continuing.target) << std::endl;
+                out << name_for(l->merge.target) << std::endl;
                 out << "}" << std::endl;
 
-                out << name_for(l) << " -> " << name_for(l->start_target) << std::endl;
+                out << name_for(l) << " -> " << name_for(l->start.target) << std::endl;
 
-                Graph(l->start_target);
-                Graph(l->continuing_target);
-                Graph(l->merge_target);
+                Graph(l->start.target);
+                Graph(l->continuing.target);
+                Graph(l->merge.target);
             },
             [&](const ir::Terminator*) {
                 // Already done
@@ -145,8 +144,7 @@
     for (const auto* func : mod->functions) {
         // Cluster each function to label and draw a box around it.
         out << "subgraph cluster_" << name_for(func) << " {" << std::endl;
-        out << R"(label=")" << mod->program->Symbols().NameFor(func->source->symbol) << R"(")"
-            << std::endl;
+        out << R"(label=")" << mod->symbols.NameFor(func->name) << R"(")" << std::endl;
         out << name_for(func->start_target) << R"( [label="start"])" << std::endl;
         out << name_for(func->end_target) << R"( [label="end"])" << std::endl;
         Graph(func->start_target);
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 37f294c..4665c80 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -19,7 +19,6 @@
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
-#include "src/tint/program.h"
 
 namespace tint::ir {
 namespace {
@@ -63,10 +62,23 @@
 
 void Disassembler::EmitBlockInstructions(const Block* b) {
     for (const auto* instr : b->instructions) {
-        instr->ToString(out_, mod_.program->Symbols()) << std::endl;
+        Indent();
+        instr->ToString(out_, mod_.symbols) << std::endl;
     }
 }
 
+size_t Disassembler::GetIdForNode(const FlowNode* node) {
+    TINT_ASSERT(IR, node);
+
+    auto it = flow_node_to_id_.find(node);
+    if (it != flow_node_to_id_.end()) {
+        return it->second;
+    }
+    size_t id = next_node_id_++;
+    flow_node_to_id_[node] = id;
+    return id;
+}
+
 void Disassembler::Walk(const FlowNode* node) {
     if ((visited_.count(node) > 0) || (stop_nodes_.count(node) > 0)) {
         return;
@@ -76,7 +88,8 @@
     tint::Switch(
         node,
         [&](const ir::Function* f) {
-            Indent() << "Function" << std::endl;
+            Indent() << "%bb" << GetIdForNode(f) << " = Function " << mod_.symbols.NameFor(f->name)
+                     << std::endl;
 
             {
                 ScopedIndent func_indent(&indent_size_);
@@ -86,62 +99,106 @@
             Walk(f->end_target);
         },
         [&](const ir::Block* b) {
-            Indent() << "Block" << std::endl;
+            // If this block is dead, nothing to do
+            if (b->IsDead()) {
+                Indent() << "# Dead" << std::endl;
+                return;
+            }
+
+            Indent() << "%bb" << GetIdForNode(b) << " = Block" << std::endl;
             EmitBlockInstructions(b);
-            Walk(b->branch_target);
+
+            if (b->branch.target->Is<Terminator>()) {
+                Indent() << "Return";
+            } else {
+                Indent() << "BranchTo "
+                         << "%bb" << GetIdForNode(b->branch.target);
+            }
+            out_ << " (";
+            for (const auto* v : b->branch.args) {
+                if (v != b->branch.args.Front()) {
+                    out_ << ", ";
+                }
+                v->ToString(out_, mod_.symbols);
+            }
+            out_ << ")" << std::endl;
+
+            if (!b->branch.target->Is<Terminator>()) {
+                out_ << std::endl;
+            }
+
+            Walk(b->branch.target);
         },
         [&](const ir::Switch* s) {
-            Indent() << "Switch (" << s->condition << ")" << std::endl;
+            Indent() << "%bb" << GetIdForNode(s) << " = Switch (";
+            s->condition->ToString(out_, mod_.symbols);
+            out_ << ")" << std::endl;
 
             {
                 ScopedIndent switch_indent(&indent_size_);
-                ScopedStopNode scope(&stop_nodes_, s->merge_target);
+                ScopedStopNode scope(&stop_nodes_, s->merge.target);
                 for (const auto& c : s->cases) {
-                    Indent() << "Case" << std::endl;
-                    ScopedIndent case_indent(&indent_size_);
-                    Walk(c.start_target);
+                    Indent() << "# Case ";
+                    for (const auto& selector : c.selectors) {
+                        if (&selector != &c.selectors.Front()) {
+                            out_ << " ";
+                        }
+
+                        if (selector.IsDefault()) {
+                            out_ << "default";
+                        } else {
+                            selector.val->ToString(out_, mod_.symbols);
+                        }
+                    }
+                    out_ << std::endl;
+                    Walk(c.start.target);
                 }
             }
 
-            Indent() << "Switch Merge" << std::endl;
-            Walk(s->merge_target);
+            Indent() << "# Switch Merge" << std::endl;
+            Walk(s->merge.target);
         },
         [&](const ir::If* i) {
-            Indent() << "if (" << i->condition << ")" << std::endl;
+            Indent() << "%bb" << GetIdForNode(i) << " = if (";
+            i->condition->ToString(out_, mod_.symbols);
+            out_ << ")" << std::endl;
+
             {
                 ScopedIndent if_indent(&indent_size_);
-                ScopedStopNode scope(&stop_nodes_, i->merge_target);
+                ScopedStopNode scope(&stop_nodes_, i->merge.target);
 
-                Indent() << "true branch" << std::endl;
-                Walk(i->true_target);
+                Indent() << "# true branch" << std::endl;
+                Walk(i->true_.target);
 
-                Indent() << "false branch" << std::endl;
-                Walk(i->false_target);
+                Indent() << "# false branch" << std::endl;
+                Walk(i->false_.target);
             }
 
-            Indent() << "if merge" << std::endl;
-            Walk(i->merge_target);
+            if (!i->merge.target->IsDisconnected()) {
+                Indent() << "# if merge" << std::endl;
+                Walk(i->merge.target);
+            }
         },
         [&](const ir::Loop* l) {
-            Indent() << "loop" << std::endl;
+            Indent() << "%bb" << GetIdForNode(l) << " = loop" << std::endl;
             {
-                ScopedStopNode loop_scope(&stop_nodes_, l->merge_target);
+                ScopedStopNode loop_scope(&stop_nodes_, l->merge.target);
                 ScopedIndent loop_indent(&indent_size_);
                 {
-                    ScopedStopNode inner_scope(&stop_nodes_, l->continuing_target);
-                    Indent() << "loop start" << std::endl;
-                    Walk(l->start_target);
+                    ScopedStopNode inner_scope(&stop_nodes_, l->continuing.target);
+                    Indent() << "# loop start" << std::endl;
+                    Walk(l->start.target);
                 }
 
-                Indent() << "loop continuing" << std::endl;
-                ScopedIndent continuing_indent(&indent_size_);
-                Walk(l->continuing_target);
+                Indent() << "# loop continuing" << std::endl;
+                Walk(l->continuing.target);
             }
 
-            Indent() << "loop merge" << std::endl;
-            Walk(l->merge_target);
+            Indent() << "# loop merge" << std::endl;
+            Walk(l->merge.target);
         },
-        [&](const ir::Terminator*) { Indent() << "Function end" << std::endl; });
+        [&](const ir::Terminator*) { Indent() << "FunctionEnd" << std::endl
+                                              << std::endl; });
 }
 
 std::string Disassembler::Disassemble() {
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index 46b4ffd..03cbea1 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -17,6 +17,7 @@
 
 #include <sstream>
 #include <string>
+#include <unordered_map>
 #include <unordered_set>
 
 #include "src/tint/ir/flow_node.h"
@@ -46,11 +47,14 @@
   private:
     std::ostream& Indent();
     void Walk(const FlowNode* node);
+    size_t GetIdForNode(const FlowNode* node);
 
     const Module& mod_;
     std::stringstream out_;
     std::unordered_set<const FlowNode*> visited_;
     std::unordered_set<const FlowNode*> stop_nodes_;
+    std::unordered_map<const FlowNode*, size_t> flow_node_to_id_;
+    size_t next_node_id_ = 0;
     uint32_t indent_size_ = 0;
 };
 
diff --git a/src/tint/ir/flow_node.h b/src/tint/ir/flow_node.h
index 2b5ec39..019a8a0 100644
--- a/src/tint/ir/flow_node.h
+++ b/src/tint/ir/flow_node.h
@@ -32,6 +32,9 @@
     ///   - Node is a continue target outside control flow (loop that returns)
     utils::Vector<FlowNode*, 2> inbound_branches;
 
+    /// @returns true if this node has no inbound branches
+    bool IsDisconnected() const { return inbound_branches.IsEmpty(); }
+
   protected:
     /// Constructor
     FlowNode();
diff --git a/src/tint/ir/function.cc b/src/tint/ir/function.cc
index f74550e..167664a 100644
--- a/src/tint/ir/function.cc
+++ b/src/tint/ir/function.cc
@@ -18,7 +18,7 @@
 
 namespace tint::ir {
 
-Function::Function(const ast::Function* f) : Base(), source(f) {}
+Function::Function() : Base() {}
 
 Function::~Function() = default;
 
diff --git a/src/tint/ir/function.h b/src/tint/ir/function.h
index c63aec0..c6032ba 100644
--- a/src/tint/ir/function.h
+++ b/src/tint/ir/function.h
@@ -15,8 +15,8 @@
 #ifndef SRC_TINT_IR_FUNCTION_H_
 #define SRC_TINT_IR_FUNCTION_H_
 
-#include "src/tint/ast/function.h"
 #include "src/tint/ir/flow_node.h"
+#include "src/tint/symbol.h"
 
 // Forward declarations
 namespace tint::ir {
@@ -30,12 +30,11 @@
 class Function : public Castable<Function, FlowNode> {
   public:
     /// Constructor
-    /// @param func the ast::Function to create from
-    explicit Function(const ast::Function* func);
+    Function();
     ~Function() override;
 
-    /// The ast function this ir function is created from.
-    const ast::Function* source;
+    /// The function name
+    Symbol name;
 
     /// The start target is the first block in a function.
     Block* start_target = nullptr;
diff --git a/src/tint/ir/if.cc b/src/tint/ir/if.cc
index df017d2..22a9078 100644
--- a/src/tint/ir/if.cc
+++ b/src/tint/ir/if.cc
@@ -18,7 +18,7 @@
 
 namespace tint::ir {
 
-If::If(const ast::Statement* stmt) : Base(), source(stmt) {}
+If::If() : Base() {}
 
 If::~If() = default;
 
diff --git a/src/tint/ir/if.h b/src/tint/ir/if.h
index 905f311..823700e 100644
--- a/src/tint/ir/if.h
+++ b/src/tint/ir/if.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_IR_IF_H_
 #define SRC_TINT_IR_IF_H_
 
-#include "src/tint/ast/if_statement.h"
+#include "src/tint/ir/branch.h"
 #include "src/tint/ir/flow_node.h"
 #include "src/tint/ir/value.h"
 
@@ -30,20 +30,16 @@
 class If : public Castable<If, FlowNode> {
   public:
     /// Constructor
-    /// @param stmt the ast::IfStatement or ast::BreakIfStatement
-    explicit If(const ast::Statement* stmt);
+    If();
     ~If() override;
 
-    /// The ast::IfStatement or ast::BreakIfStatement source for this flow node.
-    const ast::Statement* source;
-
     /// The true branch block
-    Block* true_target = nullptr;
+    Branch true_ = {};
     /// The false branch block
-    Block* false_target = nullptr;
-    /// An block to reconvert the true/false barnches. The block always exists, but there maybe no
+    Branch false_ = {};
+    /// An block to converge the true/false branches. The block always exists, but there maybe no
     /// branches into it. (e.g. if both branches `return`)
-    Block* merge_target = nullptr;
+    Branch merge = {};
     /// Value holding the condition result
     const Value* condition = nullptr;
 };
diff --git a/src/tint/ir/loop.cc b/src/tint/ir/loop.cc
index 46ac5a1..9a1af45 100644
--- a/src/tint/ir/loop.cc
+++ b/src/tint/ir/loop.cc
@@ -14,17 +14,11 @@
 
 #include "src/tint/ir/loop.h"
 
-#include "src/tint/ast/for_loop_statement.h"
-#include "src/tint/ast/loop_statement.h"
-#include "src/tint/ast/while_statement.h"
-
 TINT_INSTANTIATE_TYPEINFO(tint::ir::Loop);
 
 namespace tint::ir {
 
-Loop::Loop(const ast::Statement* s) : Base(), source(s) {
-    TINT_ASSERT(IR, (s->IsAnyOf<ast::LoopStatement, ast::WhileStatement, ast::ForLoopStatement>()));
-}
+Loop::Loop() : Base() {}
 
 Loop::~Loop() = default;
 
diff --git a/src/tint/ir/loop.h b/src/tint/ir/loop.h
index 52d24fb..2b026e6 100644
--- a/src/tint/ir/loop.h
+++ b/src/tint/ir/loop.h
@@ -15,8 +15,8 @@
 #ifndef SRC_TINT_IR_LOOP_H_
 #define SRC_TINT_IR_LOOP_H_
 
-#include "src/tint/ast/statement.h"
 #include "src/tint/ir/block.h"
+#include "src/tint/ir/branch.h"
 #include "src/tint/ir/flow_node.h"
 
 namespace tint::ir {
@@ -25,21 +25,17 @@
 class Loop : public Castable<Loop, FlowNode> {
   public:
     /// Constructor
-    /// @param stmt the loop, while or for statement.
-    explicit Loop(const ast::Statement* stmt);
+    Loop();
     ~Loop() override;
 
-    /// The ast loop, while or for statement this ir loop is created from.
-    const ast::Statement* source;
-
     /// The start block is the first block in a loop.
-    Block* start_target = nullptr;
+    Branch start = {};
     /// The continue target of the block.
-    Block* continuing_target = nullptr;
+    Branch continuing = {};
     /// The loop merge target. If the `loop` does a `return` then this block may not actually
     /// end up in the control flow. We need it if the loop does a `break` we know where to break
     /// too.
-    Block* merge_target = nullptr;
+    Branch merge = {};
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/module.cc b/src/tint/ir/module.cc
index 0f9be09..bf05e08 100644
--- a/src/tint/ir/module.cc
+++ b/src/tint/ir/module.cc
@@ -34,7 +34,7 @@
     return Result{r.Move()};
 }
 
-Module::Module(const Program* prog) : program(prog) {}
+Module::Module() = default;
 
 Module::Module(Module&&) = default;
 
diff --git a/src/tint/ir/module.h b/src/tint/ir/module.h
index 5e7a9dd..5cfc883 100644
--- a/src/tint/ir/module.h
+++ b/src/tint/ir/module.h
@@ -52,8 +52,7 @@
     static Result FromProgram(const Program* program);
 
     /// Constructor
-    /// @param program the program this module was constructed from
-    explicit Module(const Program* program);
+    Module();
     /// Move constructor
     /// @param o the module to move from
     Module(Module&& o);
@@ -89,9 +88,6 @@
     /// List of indexes into the functions list for the entry points
     utils::Vector<Function*, 8> entry_points;
 
-    /// The source ast::Program this module was constucted from
-    const Program* program;
-
     /// The type manager for the module
     type::Manager types;
 
diff --git a/src/tint/ir/switch.cc b/src/tint/ir/switch.cc
index 23b7fbb..9ad6d30 100644
--- a/src/tint/ir/switch.cc
+++ b/src/tint/ir/switch.cc
@@ -18,7 +18,7 @@
 
 namespace tint::ir {
 
-Switch::Switch(const ast::SwitchStatement* stmt) : Base(), source(stmt) {}
+Switch::Switch() : Base() {}
 
 Switch::~Switch() = default;
 
diff --git a/src/tint/ir/switch.h b/src/tint/ir/switch.h
index 7fff0a0..6ba1d4c 100644
--- a/src/tint/ir/switch.h
+++ b/src/tint/ir/switch.h
@@ -16,38 +16,39 @@
 #define SRC_TINT_IR_SWITCH_H_
 
 #include "src/tint/ir/block.h"
+#include "src/tint/ir/branch.h"
+#include "src/tint/ir/constant.h"
 #include "src/tint/ir/flow_node.h"
 #include "src/tint/ir/value.h"
 
-// Forward declarations
-namespace tint::ast {
-class CaseSelector;
-class SwitchStatement;
-}  // namespace tint::ast
-
 namespace tint::ir {
 
 /// Flow node representing a switch statement
 class Switch : public Castable<Switch, FlowNode> {
   public:
+    /// A case selector
+    struct CaseSelector {
+        /// @returns true if this is a default selector
+        bool IsDefault() const { return val == nullptr; }
+
+        /// The selector value, or nullptr if this is the default selector
+        Constant* val = nullptr;
+    };
+
     /// A case label in the struct
     struct Case {
         /// The case selector for this node
-        utils::Vector<const ast::CaseSelector*, 4> selectors;
+        utils::Vector<CaseSelector, 4> selectors;
         /// The start block for the case block.
-        Block* start_target;
+        Branch start = {};
     };
 
     /// Constructor
-    /// @param stmt the originating ast switch statement
-    explicit Switch(const ast::SwitchStatement* stmt);
+    Switch();
     ~Switch() override;
 
-    /// The originating switch statment in the AST
-    const ast::SwitchStatement* source;
-
     /// The switch merge target
-    Block* merge_target;
+    Branch merge = {};
 
     /// The switch case statements
     utils::Vector<Case, 4> cases;
diff --git a/src/tint/ir/temp_test.cc b/src/tint/ir/temp_test.cc
index 81a4992..ddb0124 100644
--- a/src/tint/ir/temp_test.cc
+++ b/src/tint/ir/temp_test.cc
@@ -33,7 +33,7 @@
     auto* val = b.builder.Temp(b.builder.ir.types.Get<type::I32>());
     EXPECT_EQ(4u, val->AsId());
 
-    val->ToString(str, program->Symbols());
+    val->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ("%4 (i32)", str.str());
 }
 
diff --git a/src/tint/ir/test_helper.h b/src/tint/ir/test_helper.h
index e3d66cf..9fb54cc 100644
--- a/src/tint/ir/test_helper.h
+++ b/src/tint/ir/test_helper.h
@@ -16,6 +16,7 @@
 #define SRC_TINT_IR_TEST_HELPER_H_
 
 #include <memory>
+#include <string>
 #include <utility>
 
 #include "gtest/gtest.h"
@@ -44,9 +45,9 @@
         }
         diag::Formatter formatter;
 
-        program = std::make_unique<Program>(std::move(*this));
-        [&]() { ASSERT_TRUE(program->IsValid()) << formatter.format(program->Diagnostics()); }();
-        gen_ = std::make_unique<BuilderImpl>(program.get());
+        program_ = std::make_unique<Program>(std::move(*this));
+        [&]() { ASSERT_TRUE(program_->IsValid()) << formatter.format(program_->Diagnostics()); }();
+        gen_ = std::make_unique<BuilderImpl>(program_.get());
         return *gen_;
     }
 
@@ -63,17 +64,52 @@
     /// is initialized with an empty block.
     /// @returns the BuilderImpl for testing.
     BuilderImpl& CreateEmptyBuilder() {
-        program = std::make_unique<Program>();
-        gen_ = std::make_unique<BuilderImpl>(program.get());
+        program_ = std::make_unique<Program>();
+        gen_ = std::make_unique<BuilderImpl>(program_.get());
         gen_->current_flow_block = gen_->builder.CreateBlock();
         return *gen_;
     }
 
-    /// The program built with a call to Build()
-    std::unique_ptr<Program> program;
+    /// Build the module, cleaning up the program before returning.
+    /// @returns the generated module
+    utils::Result<Module> Build() {
+        auto& b = CreateBuilder();
+        auto m = b.Build();
+
+        // Store the error away in case we need it
+        error_ = b.error();
+
+        // Explicitly remove program to guard against pointers back to ast. Note, this does mean the
+        // BuilderImpl is pointing to an invalid program. We keep the BuilderImpl around because we
+        // need to be able to map from ast pointers to flow nodes in tests.
+        program_ = nullptr;
+        return m;
+    }
+
+    /// @param node the ast node to lookup
+    /// @returns the IR flow node for the given ast node.
+    const ir::FlowNode* FlowNodeForAstNode(const ast::Node* node) const {
+        return gen_->FlowNodeForAstNode(node);
+    }
+
+    /// @param mod the module
+    /// @returns the disassembly string of the module
+    std::string Disassemble(Module& mod) const {
+        Disassembler d(mod);
+        return d.Disassemble();
+    }
+
+    /// @returns the error generated during build, if any
+    std::string Error() const { return error_; }
 
   private:
     std::unique_ptr<BuilderImpl> gen_;
+
+    /// The program built with a call to Build()
+    std::unique_ptr<Program> program_;
+
+    /// Error generated when calling `Build`
+    std::string error_;
 };
 using TestHelper = TestHelperBase<testing::Test>;
 
diff --git a/src/tint/transform/texture_1d_to_2d.cc b/src/tint/transform/texture_1d_to_2d.cc
new file mode 100644
index 0000000..0e19d02
--- /dev/null
+++ b/src/tint/transform/texture_1d_to_2d.cc
@@ -0,0 +1,186 @@
+// Copyright 2022 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/tint/transform/texture_1d_to_2d.h"
+
+#include <utility>
+
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/function.h"
+#include "src/tint/sem/statement.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::transform::Texture1DTo2D);
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::transform {
+
+namespace {
+
+bool ShouldRun(const Program* program) {
+    for (auto* fn : program->AST().Functions()) {
+        if (auto* sem_fn = program->Sem().Get(fn)) {
+            for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
+                const auto& signature = builtin->Signature();
+                auto texture = signature.Parameter(sem::ParameterUsage::kTexture);
+                if (texture) {
+                    auto* tex = texture->Type()->As<type::Texture>();
+                    if (tex->dim() == ast::TextureDimension::k1d) {
+                        return true;
+                    }
+                }
+            }
+        }
+    }
+    for (auto* var : program->AST().GlobalVariables()) {
+        if (Switch(
+                program->Sem().Get(var->type),
+                [&](const type::SampledTexture* tex) {
+                    return tex->dim() == ast::TextureDimension::k1d;
+                },
+                [&](const type::StorageTexture* storage_tex) {
+                    return storage_tex->dim() == ast::TextureDimension::k1d;
+                })) {
+            return true;
+        }
+    }
+    return false;
+}
+
+}  // namespace
+
+/// PIMPL state for the transform
+struct Texture1DTo2D::State {
+    /// The source program
+    const Program* const src;
+    /// The target program builder
+    ProgramBuilder b;
+    /// The clone context
+    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+
+    /// Constructor
+    /// @param program the source program
+    explicit State(const Program* program) : src(program) {}
+
+    /// Runs the transform
+    /// @returns the new program or SkipTransform if the transform is not required
+    ApplyResult Run() {
+        auto& sem = src->Sem();
+
+        if (!ShouldRun(ctx.src)) {
+            return SkipTransform;
+        }
+
+        auto create_var = [&](const ast::Variable* v, ast::Type* type) -> const ast::Variable* {
+            if (v->As<ast::Parameter>()) {
+                return ctx.dst->Param(ctx.Clone(v->symbol), type, ctx.Clone(v->attributes));
+            } else {
+                return ctx.dst->Var(ctx.Clone(v->symbol), type, ctx.Clone(v->attributes));
+            }
+        };
+
+        ctx.ReplaceAll([&](const ast::Variable* v) -> const ast::Variable* {
+            const ast::Variable* r = Switch(
+                sem.Get(v->type),
+                [&](const type::SampledTexture* tex) -> const ast::Variable* {
+                    if (tex->dim() == ast::TextureDimension::k1d) {
+                        auto* type = ctx.dst->create<ast::SampledTexture>(
+                            ast::TextureDimension::k2d, CreateASTTypeFor(ctx, tex->type()));
+                        return create_var(v, type);
+                    } else {
+                        return nullptr;
+                    }
+                },
+                [&](const type::StorageTexture* storage_tex) -> const ast::Variable* {
+                    if (storage_tex->dim() == ast::TextureDimension::k1d) {
+                        auto* type = ctx.dst->create<ast::StorageTexture>(
+                            ast::TextureDimension::k2d, storage_tex->texel_format(),
+                            CreateASTTypeFor(ctx, storage_tex->type()), storage_tex->access());
+                        return create_var(v, type);
+                    } else {
+                        return nullptr;
+                    }
+                },
+                [](Default) { return nullptr; });
+            return r;
+        });
+
+        ctx.ReplaceAll([&](const ast::CallExpression* c) -> const ast::Expression* {
+            auto* call = sem.Get(c)->UnwrapMaterialize()->As<sem::Call>();
+            if (!call) {
+                return nullptr;
+            }
+            auto* builtin = call->Target()->As<sem::Builtin>();
+            if (!builtin) {
+                return nullptr;
+            }
+            const auto& signature = builtin->Signature();
+            auto texture = signature.Parameter(sem::ParameterUsage::kTexture);
+            auto* tex = texture->Type()->As<type::Texture>();
+            if (tex->dim() != ast::TextureDimension::k1d) {
+                return nullptr;
+            }
+
+            if (builtin->Type() == sem::BuiltinType::kTextureDimensions) {
+                // If this textureDimensions() call is in a CallStatement, we can leave it
+                // unmodified since the return value will be dropped on the floor anyway.
+                if (call->Stmt()->Declaration()->Is<ast::CallStatement>()) {
+                    return nullptr;
+                }
+                auto* new_call = ctx.CloneWithoutTransform(c);
+                return ctx.dst->MemberAccessor(new_call, "x");
+            }
+
+            auto coords_index = signature.IndexOf(sem::ParameterUsage::kCoords);
+            if (coords_index == -1) {
+                return nullptr;
+            }
+
+            utils::Vector<const ast::Expression*, 8> args;
+            int index = 0;
+            for (auto* arg : c->args) {
+                if (index == coords_index) {
+                    auto* ctype = call->Arguments()[static_cast<size_t>(coords_index)]->Type();
+                    auto* coords = c->args[static_cast<size_t>(coords_index)];
+
+                    const ast::LiteralExpression* half = nullptr;
+                    if (ctype->is_integer_scalar()) {
+                        half = ctx.dst->Expr(0_a);
+                    } else {
+                        half = ctx.dst->Expr(0.5_a);
+                    }
+                    args.Push(
+                        ctx.dst->vec(CreateASTTypeFor(ctx, ctype), 2u, ctx.Clone(coords), half));
+                } else {
+                    args.Push(ctx.Clone(arg));
+                }
+                index++;
+            }
+            return ctx.dst->Call(ctx.Clone(c->target.name), args);
+        });
+
+        ctx.Clone();
+        return Program(std::move(b));
+    }
+};
+
+Texture1DTo2D::Texture1DTo2D() = default;
+
+Texture1DTo2D::~Texture1DTo2D() = default;
+
+Transform::ApplyResult Texture1DTo2D::Apply(const Program* src, const DataMap&, DataMap&) const {
+    return State(src).Run();
+}
+
+}  // namespace tint::transform
diff --git a/src/tint/transform/texture_1d_to_2d.h b/src/tint/transform/texture_1d_to_2d.h
new file mode 100644
index 0000000..9999821
--- /dev/null
+++ b/src/tint/transform/texture_1d_to_2d.h
@@ -0,0 +1,43 @@
+// Copyright 2022 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_TINT_TRANSFORM_TEXTURE_1D_TO_2D_H_
+#define SRC_TINT_TRANSFORM_TEXTURE_1D_TO_2D_H_
+
+#include "src/tint/transform/transform.h"
+
+namespace tint::transform {
+
+/// This transform converts all 1D texture types and accesses to 2D.
+/// This is required for GLSL ES, which does not support 1D textures.
+class Texture1DTo2D final : public Castable<Texture1DTo2D, Transform> {
+  public:
+    /// Constructor
+    Texture1DTo2D();
+
+    /// Destructor
+    ~Texture1DTo2D() override;
+
+    /// @copydoc Transform::Apply
+    ApplyResult Apply(const Program* program,
+                      const DataMap& inputs,
+                      DataMap& outputs) const override;
+
+  private:
+    struct State;
+};
+
+}  // namespace tint::transform
+
+#endif  // SRC_TINT_TRANSFORM_TEXTURE_1D_TO_2D_H_
diff --git a/src/tint/transform/texture_1d_to_2d_test.cc b/src/tint/transform/texture_1d_to_2d_test.cc
new file mode 100644
index 0000000..83a12bd
--- /dev/null
+++ b/src/tint/transform/texture_1d_to_2d_test.cc
@@ -0,0 +1,279 @@
+// Copyright 2022 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/tint/transform/texture_1d_to_2d.h"
+
+// #include <memory>
+// #include <utility>
+
+#include "src/tint/transform/test_helper.h"
+
+namespace tint::transform {
+namespace {
+
+using Texture1DTo2DTest = TransformTest;
+
+TEST_F(Texture1DTo2DTest, EmptyModule) {
+    auto* src = "";
+
+    DataMap data;
+    EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
+}
+
+TEST_F(Texture1DTo2DTest, Global1DDecl) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_1d<f32>;
+
+@group(0) @binding(1) var s : sampler;
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@group(0) @binding(1) var s : sampler;
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, Global1DDeclAndSample) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_1d<f32>;
+
+@group(0) @binding(1) var s : sampler;
+
+fn main() -> vec4<f32> {
+  return textureSample(t, s, 0.5);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@group(0) @binding(1) var s : sampler;
+
+fn main() -> vec4<f32> {
+  return textureSample(t, s, vec2<f32>(0.5, 0.5));
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, Global1DDeclAndLoad) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_1d<f32>;
+
+fn main() -> vec4<f32> {
+  return textureLoad(t, 1, 0);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+fn main() -> vec4<f32> {
+  return textureLoad(t, vec2<i32>(1, 0), 0);
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, Global1DDeclAndTextureDimensions) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_1d<f32>;
+
+fn main() -> u32 {
+  return textureDimensions(t);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+fn main() -> u32 {
+  return textureDimensions(t).x;
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, Global1DDeclAndTextureNumLevels) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_1d<f32>;
+
+fn main() -> u32 {
+  return textureNumLevels(t);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+fn main() -> u32 {
+  return textureNumLevels(t);
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, Global1DDeclAndTextureDimensionsInCallStmt) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_1d<f32>;
+
+fn main() {
+  textureDimensions(t);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+fn main() {
+  textureDimensions(t);
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, GlobalStorage1DDecl) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_storage_1d<r32float, write>;
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var t : texture_storage_2d<r32float, write>;
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, Global2DDeclAndSample) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@group(0) @binding(1) var s : sampler;
+
+fn main() -> vec4<f32> {
+  return textureSample(t, s, vec2<f32>(0.5, 1.5));
+}
+)";
+
+    DataMap data;
+    EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
+}
+
+TEST_F(Texture1DTo2DTest, PrivateIntNoop) {
+    auto* src = R"(
+var<private> i : i32;
+)";
+
+    DataMap data;
+    EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
+}
+
+TEST_F(Texture1DTo2DTest, GlobalMatrixNoop) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> m : mat2x2<f32>;
+)";
+
+    DataMap data;
+    EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
+}
+
+TEST_F(Texture1DTo2DTest, Texture1DFuncParam) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_1d<f32>;
+
+@group(0) @binding(1) var samp : sampler;
+
+fn f(t : texture_1d<f32>, s : sampler) -> vec4<f32> {
+  return textureSample(t, s, 0.7);
+}
+
+fn main() -> vec4<f32> {
+  return f(tex, samp);
+}
+)";
+    auto* expect = R"(
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+@group(0) @binding(1) var samp : sampler;
+
+fn f(t : texture_2d<f32>, s : sampler) -> vec4<f32> {
+  return textureSample(t, s, vec2<f32>(0.7, 0.5));
+}
+
+fn main() -> vec4<f32> {
+  return f(tex, samp);
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Texture1DTo2DTest, TextureStorage1DFuncParam) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_storage_1d<rgba8unorm, write>;
+
+fn f(t : texture_storage_1d<rgba8unorm, write>) {
+  textureStore(t, 3, vec4<f32>(42.0, 21.0, 84.0, 10.5));
+}
+
+fn main() {
+  f(tex);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var tex : texture_storage_2d<rgba8unorm, write>;
+
+fn f(t : texture_storage_2d<rgba8unorm, write>) {
+  textureStore(t, vec2<i32>(3, 0), vec4<f32>(42.0, 21.0, 84.0, 10.5));
+}
+
+fn main() {
+  f(tex);
+}
+)";
+
+    DataMap data;
+    auto got = Run<Texture1DTo2D>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::transform
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 8450be3..bc52f15 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -60,6 +60,7 @@
 #include "src/tint/transform/simplify_pointers.h"
 #include "src/tint/transform/single_entry_point.h"
 #include "src/tint/transform/std140.h"
+#include "src/tint/transform/texture_1d_to_2d.h"
 #include "src/tint/transform/unshadow.h"
 #include "src/tint/transform/zero_init_workgroup_memory.h"
 #include "src/tint/type/array.h"
@@ -253,6 +254,8 @@
     // Std140 must come after PromoteSideEffectsToDecl and before SimplifyPointers.
     manager.Add<transform::Std140>();
 
+    manager.Add<transform::Texture1DTo2D>();
+
     manager.Add<transform::SimplifyPointers>();
 
     data.Add<transform::CanonicalizeEntryPointIO::Config>(
diff --git a/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc b/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc
index 8fc9844..cb5a4c8 100644
--- a/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc
+++ b/src/tint/writer/glsl/generator_impl_builtin_texture_test.cc
@@ -117,7 +117,7 @@
         case ValidTextureOverload::kNumSamplesMultisampled2d:
             return {"textureSamples"};
         case ValidTextureOverload::kSample1dF32:
-            return R"(texture(tint_symbol_sampler, 1.0f);)";
+            return R"(texture(tint_symbol_sampler, vec2(1.0f, 0.5f));)";
         case ValidTextureOverload::kSample2dF32:
             return R"(texture(tint_symbol_sampler, vec2(1.0f, 2.0f));)";
         case ValidTextureOverload::kSample2dOffsetF32:
@@ -231,10 +231,10 @@
         case ValidTextureOverload::kSampleCompareLevelDepthCubeArrayF32:
             return R"(texture(tint_symbol_sampler, vec4(1.0f, 2.0f, 3.0f, float(4)), 5.0f);)";
         case ValidTextureOverload::kLoad1dLevelF32:
-            return R"(texelFetch(tint_symbol_2, int(1u), int(3u));)";
+            return R"(texelFetch(tint_symbol_2, ivec2(uvec2(1u, 0u)), int(3u));)";
         case ValidTextureOverload::kLoad1dLevelU32:
         case ValidTextureOverload::kLoad1dLevelI32:
-            return R"(texelFetch(tint_symbol_2, 1, 3);)";
+            return R"(texelFetch(tint_symbol_2, ivec2(1, 0), 3);)";
         case ValidTextureOverload::kLoad2dLevelU32:
             return R"(texelFetch(tint_symbol_2, ivec2(1, 2), 3);)";
         case ValidTextureOverload::kLoad2dLevelF32:
@@ -260,7 +260,7 @@
         case ValidTextureOverload::kLoadDepthMultisampled2dF32:
             return R"(texelFetch(tint_symbol_2, ivec2(uvec2(1u, 2u)), int(3u)).x;)";
         case ValidTextureOverload::kStoreWO1dRgba32float:
-            return R"(imageStore(tint_symbol, 1, vec4(2.0f, 3.0f, 4.0f, 5.0f));)";
+            return R"(imageStore(tint_symbol, ivec2(1, 0), vec4(2.0f, 3.0f, 4.0f, 5.0f));)";
         case ValidTextureOverload::kStoreWO2dRgba32float:
             return R"(imageStore(tint_symbol, ivec2(1, 2), vec4(3.0f, 4.0f, 5.0f, 6.0f));)";
         case ValidTextureOverload::kStoreWO2dArrayRgba32float: