Import Tint changes from Dawn

Changes:
  - 07a1d65fbc0a4c76a099d9b9c54d55dbdc3ec769 [ir] Cleanup composite creation in tests by dan sinclair <dsinclair@chromium.org>
  - c4722c2f0beca9f1afae47d7dcaf389867373338 [ir] Test cleanup by dan sinclair <dsinclair@chromium.org>
  - 68b4e6460ffba5e8f19d4886b31293838f4b75f8 [ir] Dissolve the flow graph by dan sinclair <dsinclair@chromium.org>
  - 506b4f05d006fc39eb5f348f93e20f8b46b515e2 [spirv-reader] Use type inference for var and let by James Price <jrprice@google.com>
  - 0f203b12826a158c50960e25d24466982d90d80f [ir] Cleanup deleted methods by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 07a1d65fbc0a4c76a099d9b9c54d55dbdc3ec769
Change-Id: Icb8f4f2b300debe8e7146926586c096037ea4941
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/133965
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index a704487..b14851f 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -1210,6 +1210,8 @@
       "ir/block.h",
       "ir/block_param.cc",
       "ir/block_param.h",
+      "ir/branch.cc",
+      "ir/branch.h",
       "ir/builder.cc",
       "ir/builder.h",
       "ir/builtin.cc",
@@ -1240,6 +1242,8 @@
       "ir/if.h",
       "ir/instruction.cc",
       "ir/instruction.h",
+      "ir/jump.cc",
+      "ir/jump.h",
       "ir/load.cc",
       "ir/load.h",
       "ir/loop.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 8ec573a..979c434 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -718,6 +718,8 @@
     ir/block.h
     ir/block_param.cc
     ir/block_param.h
+    ir/branch.cc
+    ir/branch.h
     ir/builder.cc
     ir/builder.h
     ir/builtin.cc
@@ -750,6 +752,8 @@
     ir/if.h
     ir/instruction.cc
     ir/instruction.h
+    ir/jump.cc
+    ir/jump.h
     ir/load.cc
     ir/load.h
     ir/loop.cc
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index 7ea6c08..3de078a 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -52,13 +52,8 @@
     /// @param lhs the lhs of the instruction
     /// @param rhs the rhs of the instruction
     Binary(enum Kind kind, const type::Type* type, Value* lhs, Value* rhs);
-    Binary(const Binary& inst) = delete;
-    Binary(Binary&& inst) = delete;
     ~Binary() override;
 
-    Binary& operator=(const Binary& inst) = delete;
-    Binary& operator=(Binary&& inst) = delete;
-
     /// @returns the kind of the binary instruction
     enum Kind Kind() const { return kind_; }
 
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index 0ad1acb..ddf74b9 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -27,12 +27,7 @@
     /// @param type the result type
     /// @param val the value being bitcast
     Bitcast(const type::Type* type, Value* val);
-    Bitcast(const Bitcast& inst) = delete;
-    Bitcast(Bitcast&& inst) = delete;
     ~Bitcast() override;
-
-    Bitcast& operator=(const Bitcast& inst) = delete;
-    Bitcast& operator=(Bitcast&& inst) = delete;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/block.cc b/src/tint/ir/block.cc
index f5b5bfb..2030c19 100644
--- a/src/tint/ir/block.cc
+++ b/src/tint/ir/block.cc
@@ -22,13 +22,4 @@
 
 Block::~Block() = default;
 
-void Block::BranchTo(FlowNode* to, utils::VectorRef<Value*> args) {
-    TINT_ASSERT(IR, to);
-    branch_.target = to;
-    branch_.args = args;
-    if (to) {
-        to->AddInboundBranch(this);
-    }
-}
-
 }  // namespace tint::ir
diff --git a/src/tint/ir/block.h b/src/tint/ir/block.h
index 2b01fd0..abdf2a3 100644
--- a/src/tint/ir/block.h
+++ b/src/tint/ir/block.h
@@ -32,23 +32,32 @@
   public:
     /// Constructor
     Block();
-    Block(const Block&) = delete;
-    Block(Block&&) = delete;
     ~Block() override;
 
-    Block& operator=(const Block&) = delete;
-    Block& operator=(Block&&) = delete;
-
-    /// Sets the blocks branch target to the given node.
-    /// @param to the node to branch too
-    /// @param args the branch arguments
-    void BranchTo(FlowNode* to, utils::VectorRef<Value*> args = {});
-
     /// @returns true if this is block has a branch target set
-    bool HasBranchTarget() const override { return branch_.target != nullptr; }
+    bool HasBranchTarget() const override {
+        return !instructions_.IsEmpty() && instructions_.Back()->Is<ir::Branch>();
+    }
 
-    /// @return the node this block branches too.
-    const ir::Branch& Branch() const { return branch_; }
+    /// @return the node this block branches to or nullptr if the block doesn't branch
+    const ir::Branch* Branch() const {
+        if (!HasBranchTarget()) {
+            return nullptr;
+        }
+        return instructions_.Back()->As<ir::Branch>();
+    }
+
+    /// @param target the block to see if we trampoline too
+    /// @returns if this block just branches to the provided target.
+    bool IsTrampoline(const FlowNode* target) const {
+        if (instructions_.Length() != 1) {
+            return false;
+        }
+        if (auto* inst = instructions_.Front()->As<ir::Branch>()) {
+            return inst->To() == target;
+        }
+        return false;
+    }
 
     /// Sets the instructions in the block
     /// @param instructions the instructions to set
@@ -64,14 +73,12 @@
     /// Sets the params to the block
     /// @param params the params for the block
     void SetParams(utils::VectorRef<const BlockParam*> params) { params_ = std::move(params); }
+    /// @return the parameters passed into the block
+    utils::VectorRef<const BlockParam*> Params() const { return params_; }
     /// @returns the params to the block
     utils::Vector<const BlockParam*, 0>& Params() { return params_; }
 
-    /// @return the parameters passed into the block
-    utils::VectorRef<const BlockParam*> Params() const { return params_; }
-
   private:
-    ir::Branch branch_ = {};
     utils::Vector<const Instruction*, 16> instructions_;
     utils::Vector<const BlockParam*, 0> params_;
 };
diff --git a/src/tint/ir/block_param.h b/src/tint/ir/block_param.h
index 036ddbf..386ea4d 100644
--- a/src/tint/ir/block_param.h
+++ b/src/tint/ir/block_param.h
@@ -26,13 +26,8 @@
     /// Constructor
     /// @param type the type of the var
     explicit BlockParam(const type::Type* type);
-    BlockParam(const BlockParam& inst) = delete;
-    BlockParam(BlockParam&& inst) = delete;
     ~BlockParam() override;
 
-    BlockParam& operator=(const BlockParam& inst) = delete;
-    BlockParam& operator=(BlockParam&& inst) = delete;
-
     /// @returns the type of the var
     const type::Type* Type() const override { return type_; }
 
diff --git a/src/tint/ir/branch.cc b/src/tint/ir/branch.cc
new file mode 100644
index 0000000..a16b7ae
--- /dev/null
+++ b/src/tint/ir/branch.cc
@@ -0,0 +1,35 @@
+// 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.
+
+#include "src/tint/ir/branch.h"
+
+#include <utility>
+
+#include "src/tint/ir/flow_node.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Branch);
+
+namespace tint::ir {
+
+Branch::Branch(FlowNode* to, utils::VectorRef<Value*> args) : to_(to), args_(std::move(args)) {
+    TINT_ASSERT(IR, to_);
+    to_->AddInboundBranch(this);
+    for (auto* arg : args) {
+        arg->AddUsage(this);
+    }
+}
+
+Branch::~Branch() = default;
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/branch.h b/src/tint/ir/branch.h
index 667f131..fcb1256 100644
--- a/src/tint/ir/branch.h
+++ b/src/tint/ir/branch.h
@@ -15,20 +15,35 @@
 #ifndef SRC_TINT_IR_BRANCH_H_
 #define SRC_TINT_IR_BRANCH_H_
 
-#include "src/tint/ir/flow_node.h"
+#include "src/tint/ir/instruction.h"
 #include "src/tint/ir/value.h"
+#include "src/tint/utils/castable.h"
+
+// Forward declarations
+namespace tint::ir {
+class FlowNode;
+}  // namespace tint::ir
 
 namespace tint::ir {
 
-/// A information on a branch to another block
-struct Branch {
-    /// The block being branched too.
-    FlowNode* target = nullptr;
+/// A branch instruction. A branch is a walk terminating jump.
+class Branch : public utils::Castable<Branch, Instruction> {
+  public:
+    /// Constructor
+    /// @param to the block to branch too
+    /// @param args the branch arguments
+    explicit Branch(FlowNode* to, utils::VectorRef<Value*> args = {});
+    ~Branch() override;
 
-    /// The arguments provided for that branch. These arguments could be the
-    /// return value in the case of a branch to the function terminator, or they could
-    /// be the basic block arguments passed into the block.
-    utils::Vector<Value*, 2> args;
+    /// @returns the block being branched too.
+    const FlowNode* To() const { return to_; }
+
+    /// @returns the branch arguments
+    utils::VectorRef<Value*> Args() const { return args_; }
+
+  private:
+    FlowNode* to_;
+    utils::Vector<Value*, 2> args_;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index b0b04d6..205754b 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -29,10 +29,6 @@
 ir::Block* Builder::CreateRootBlockIfNeeded() {
     if (!ir.root_block) {
         ir.root_block = CreateBlock();
-
-        // Everything in the module scope must have been const-eval's, so everything will go into a
-        // single block. So, we can create the root terminator for the root-block now.
-        ir.root_block->BranchTo(CreateRootTerminator());
     }
     return ir.root_block;
 }
@@ -49,6 +45,13 @@
     return ir.flow_nodes.Create<FunctionTerminator>();
 }
 
+Function* Builder::CreateFunction(std::string_view name,
+                                  type::Type* return_type,
+                                  Function::PipelineStage stage,
+                                  std::optional<std::array<uint32_t, 3>> wg_size) {
+    return CreateFunction(ir.symbols.Register(name), return_type, stage, wg_size);
+}
+
 Function* Builder::CreateFunction(Symbol name,
                                   type::Type* return_type,
                                   Function::PipelineStage stage,
@@ -59,50 +62,26 @@
     ir_func->SetStartTarget(CreateBlock());
     ir_func->SetEndTarget(CreateFunctionTerminator());
 
-    // Function is always branching into the Start().target
-    ir_func->StartTarget()->AddInboundBranch(ir_func);
-
     return ir_func;
 }
 
 If* Builder::CreateIf(Value* condition) {
     TINT_ASSERT(IR, condition);
-
-    auto* ir_if = ir.flow_nodes.Create<If>(condition);
-    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->AddInboundBranch(ir_if);
-    ir_if->False().target->AddInboundBranch(ir_if);
-
-    return ir_if;
+    return ir.values.Create<If>(condition, CreateBlock(), CreateBlock(), 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->AddInboundBranch(ir_loop);
-
-    return ir_loop;
+    return ir.values.Create<Loop>(CreateBlock(), CreateBlock(), CreateBlock());
 }
 
 Switch* Builder::CreateSwitch(Value* condition) {
-    auto* ir_switch = ir.flow_nodes.Create<Switch>(condition);
-    ir_switch->Merge().target = CreateBlock();
-    return ir_switch;
+    return ir.values.Create<Switch>(condition, CreateBlock());
 }
 
 Block* Builder::CreateCase(Switch* s, utils::VectorRef<Switch::CaseSelector> selectors) {
-    s->Cases().Push(Switch::Case{selectors, {CreateBlock(), utils::Empty}});
+    s->Cases().Push(Switch::Case{std::move(selectors), CreateBlock()});
 
-    Block* b = s->Cases().Back().Start().target->As<Block>();
-    // Switch branches into the case block
+    Block* b = s->Cases().Back().Start();
     b->AddInboundBranch(s);
     return b;
 }
@@ -238,6 +217,14 @@
     return ir.values.Create<ir::Var>(type);
 }
 
+ir::Branch* Builder::Branch(FlowNode* to, utils::VectorRef<Value*> args) {
+    return ir.values.Create<ir::Branch>(to, args);
+}
+
+ir::Jump* Builder::Jump(FlowNode* to, utils::VectorRef<Value*> args) {
+    return ir.values.Create<ir::Jump>(to, args);
+}
+
 ir::BlockParam* Builder::BlockParam(const type::Type* type) {
     return ir.values.Create<ir::BlockParam>(type);
 }
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index ebd7a87..78bcb9b 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -30,6 +30,7 @@
 #include "src/tint/ir/function_param.h"
 #include "src/tint/ir/function_terminator.h"
 #include "src/tint/ir/if.h"
+#include "src/tint/ir/jump.h"
 #include "src/tint/ir/load.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
@@ -45,6 +46,7 @@
 #include "src/tint/type/f32.h"
 #include "src/tint/type/i32.h"
 #include "src/tint/type/u32.h"
+#include "src/tint/type/vector.h"
 #include "src/tint/type/void.h"
 
 namespace tint::ir {
@@ -73,6 +75,17 @@
     /// @param stage the function stage
     /// @param wg_size the workgroup_size
     /// @returns the flow node
+    Function* CreateFunction(std::string_view name,
+                             type::Type* return_type,
+                             Function::PipelineStage stage = Function::PipelineStage::kUndefined,
+                             std::optional<std::array<uint32_t, 3>> wg_size = {});
+
+    /// Creates a function flow node
+    /// @param name the function name
+    /// @param return_type the function return type
+    /// @param stage the function stage
+    /// @param wg_size the workgroup_size
+    /// @returns the flow node
     Function* CreateFunction(Symbol name,
                              type::Type* return_type,
                              Function::PipelineStage stage = Function::PipelineStage::kUndefined,
@@ -107,6 +120,41 @@
         return ir.constants_arena.Create<T>(std::forward<ARGS>(args)...);
     }
 
+    /// @param v the value
+    /// @returns the constant value
+    const constant::Value* Bool(bool v) {
+        // TODO(dsinclair): Replace when constant::Value is uniqed by the arena.
+        return Constant(create<constant::Scalar<bool>>(ir.types.Get<type::Bool>(), v))->Value();
+    }
+
+    /// @param v the value
+    /// @returns the constant value
+    const constant::Value* U32(uint32_t v) {
+        // TODO(dsinclair): Replace when constant::Value is uniqed by the arena.
+        return Constant(create<constant::Scalar<u32>>(ir.types.Get<type::U32>(), u32(v)))->Value();
+    }
+
+    /// @param v the value
+    /// @returns the constant value
+    const constant::Value* I32(int32_t v) {
+        // TODO(dsinclair): Replace when constant::Value is uniqed by the arena.
+        return Constant(create<constant::Scalar<i32>>(ir.types.Get<type::I32>(), i32(v)))->Value();
+    }
+
+    /// @param v the value
+    /// @returns the constant value
+    const constant::Value* F16(float v) {
+        // TODO(dsinclair): Replace when constant::Value is uniqed by the arena.
+        return Constant(create<constant::Scalar<f16>>(ir.types.Get<type::F16>(), f16(v)))->Value();
+    }
+
+    /// @param v the value
+    /// @returns the constant value
+    const constant::Value* F32(float v) {
+        // TODO(dsinclair): Replace when constant::Value is uniqed by the arena.
+        return Constant(create<constant::Scalar<f32>>(ir.types.Get<type::F32>(), f32(v)))->Value();
+    }
+
     /// Creates a new ir::Constant
     /// @param val the constant value
     /// @returns the new constant
@@ -351,6 +399,18 @@
     /// @returns the instruction
     ir::Var* Declare(const type::Type* type);
 
+    /// Creates a branch declaration
+    /// @param to the node being branched too
+    /// @param args the branch arguments
+    /// @returns the instruction
+    ir::Branch* Branch(FlowNode* to, utils::VectorRef<Value*> args = {});
+
+    /// Creates a jump declaration
+    /// @param to the node being branched too
+    /// @param args the branch arguments
+    /// @returns the instruction
+    ir::Jump* Jump(FlowNode* to, utils::VectorRef<Value*> args = {});
+
     /// Creates a new `BlockParam`
     /// @param type the parameter type
     /// @returns the value
diff --git a/src/tint/ir/builtin.h b/src/tint/ir/builtin.h
index de0e435..ee096bb 100644
--- a/src/tint/ir/builtin.h
+++ b/src/tint/ir/builtin.h
@@ -29,13 +29,8 @@
     /// @param func the builtin function
     /// @param args the conversion arguments
     Builtin(const type::Type* res_type, builtin::Function func, utils::VectorRef<Value*> args);
-    Builtin(const Builtin& inst) = delete;
-    Builtin(Builtin&& inst) = delete;
     ~Builtin() override;
 
-    Builtin& operator=(const Builtin& inst) = delete;
-    Builtin& operator=(Builtin&& inst) = delete;
-
     /// @returns the builtin function
     builtin::Function Func() const { return func_; }
 
diff --git a/src/tint/ir/call.h b/src/tint/ir/call.h
index f4e12f9..c933331 100644
--- a/src/tint/ir/call.h
+++ b/src/tint/ir/call.h
@@ -23,13 +23,8 @@
 /// A Call instruction in the IR.
 class Call : public utils::Castable<Call, Instruction> {
   public:
-    Call(const Call& inst) = delete;
-    Call(Call&& inst) = delete;
     ~Call() override;
 
-    Call& operator=(const Call& inst) = delete;
-    Call& operator=(Call&& inst) = delete;
-
     /// @returns the type of the value
     const type::Type* Type() const override { return result_type_; }
 
diff --git a/src/tint/ir/constant.h b/src/tint/ir/constant.h
index dcf3e5b..2df7250 100644
--- a/src/tint/ir/constant.h
+++ b/src/tint/ir/constant.h
@@ -26,13 +26,8 @@
     /// Constructor
     /// @param val the value stored in the constant
     explicit Constant(const constant::Value* val);
-    Constant(const Constant&) = delete;
-    Constant(Constant&&) = delete;
     ~Constant() override;
 
-    Constant& operator=(const Constant&) = delete;
-    Constant& operator=(Constant&&) = delete;
-
     /// @returns the constants value
     const constant::Value* Value() const { return value_; }
 
diff --git a/src/tint/ir/construct.h b/src/tint/ir/construct.h
index b3ed6b6..f4da78d 100644
--- a/src/tint/ir/construct.h
+++ b/src/tint/ir/construct.h
@@ -27,12 +27,7 @@
     /// @param type the result type
     /// @param args the constructor arguments
     Construct(const type::Type* type, utils::VectorRef<Value*> args);
-    Construct(const Construct& inst) = delete;
-    Construct(Construct&& inst) = delete;
     ~Construct() override;
-
-    Construct& operator=(const Construct& inst) = delete;
-    Construct& operator=(Construct&& inst) = delete;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/convert.h b/src/tint/ir/convert.h
index 9fdbc5c..77778e0 100644
--- a/src/tint/ir/convert.h
+++ b/src/tint/ir/convert.h
@@ -31,13 +31,8 @@
     Convert(const type::Type* result_type,
             const type::Type* from_type,
             utils::VectorRef<Value*> args);
-    Convert(const Convert& inst) = delete;
-    Convert(Convert&& inst) = delete;
     ~Convert() override;
 
-    Convert& operator=(const Convert& inst) = delete;
-    Convert& operator=(Convert&& inst) = delete;
-
     /// @returns the from type
     const type::Type* FromType() const { return from_type_; }
     /// @returns the to type
diff --git a/src/tint/ir/debug.cc b/src/tint/ir/debug.cc
index 85666ff..7e8155d 100644
--- a/src/tint/ir/debug.cc
+++ b/src/tint/ir/debug.cc
@@ -60,81 +60,15 @@
                 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()->To());
 
                 // Dashed lines to merge blocks
-                if (merge_nodes.count(b->Branch().target) != 0) {
+                if (merge_nodes.count(b->Branch()->To()) != 0) {
                     out << " [style=dashed]";
                 }
 
                 out << std::endl;
-                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);
-
-                size_t i = 0;
-                for (const auto& c : s->Cases()) {
-                    out << name_for(c.Start().target)
-                        << R"( [label="case )" + std::to_string(i++) + R"("])" << std::endl;
-                }
-                out << name_for(s) << " -> {";
-                for (const auto& c : s->Cases()) {
-                    if (&c != &(s->Cases().Front())) {
-                        out << ", ";
-                    }
-                    out << name_for(c.Start().target);
-                }
-                out << "}" << std::endl;
-
-                for (const auto& c : s->Cases()) {
-                    Graph(c.Start().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) << " -> {";
-                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 << "}" << std::endl;
-
-                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);
-
-                // 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 << "}" << 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(b->Branch()->To());
             },
             [&](const ir::FunctionTerminator*) {
                 // Already done
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 76af740..8eb0974 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -27,6 +27,7 @@
 #include "src/tint/ir/discard.h"
 #include "src/tint/ir/function_terminator.h"
 #include "src/tint/ir/if.h"
+#include "src/tint/ir/jump.h"
 #include "src/tint/ir/load.h"
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/root_terminator.h"
@@ -41,22 +42,6 @@
 namespace tint::ir {
 namespace {
 
-class ScopedStopNode {
-    static constexpr size_t N = 32;
-
-  public:
-    ScopedStopNode(utils::Hashset<const FlowNode*, N>& stop_nodes, const FlowNode* node)
-        : stop_nodes_(stop_nodes), node_(node) {
-        stop_nodes_.Add(node_);
-    }
-
-    ~ScopedStopNode() { stop_nodes_.Remove(node_); }
-
-  private:
-    utils::Hashset<const FlowNode*, N>& stop_nodes_;
-    const FlowNode* node_;
-};
-
 class ScopedIndent {
   public:
     explicit ScopedIndent(uint32_t& indent) : indent_(indent) { indent_ += 2; }
@@ -103,261 +88,115 @@
     });
 }
 
-void Disassembler::Walk(const FlowNode* node) {
-    if (visited_.Contains(node) || stop_nodes_.Contains(node)) {
-        return;
-    }
-    visited_.Add(node);
-
-    tint::Switch(
-        node,
-        [&](const ir::Function* f) {
-            TINT_SCOPED_ASSIGNMENT(in_function_, true);
-
-            Indent() << "%fn" << IdOf(f) << " = func " << f->Name().Name() << "(";
-            for (auto* p : f->Params()) {
-                if (p != f->Params().Front()) {
-                    out_ << ", ";
-                }
-                out_ << "%" << IdOf(p) << ":" << p->Type()->FriendlyName();
-            }
-            out_ << "):" << f->ReturnType()->FriendlyName();
-
-            if (f->Stage() != Function::PipelineStage::kUndefined) {
-                out_ << " [@" << f->Stage();
-
-                if (f->WorkgroupSize()) {
-                    auto arr = f->WorkgroupSize().value();
-                    out_ << " @workgroup_size(" << arr[0] << ", " << arr[1] << ", " << arr[2]
-                         << ")";
-                }
-
-                if (!f->ReturnAttributes().IsEmpty()) {
-                    out_ << " ra:";
-
-                    for (auto attr : f->ReturnAttributes()) {
-                        out_ << " @" << attr;
-                        if (attr == Function::ReturnAttribute::kLocation) {
-                            out_ << "(" << f->ReturnLocation().value() << ")";
-                        }
-                    }
-                }
-
-                out_ << "]";
-            }
-            out_ << " {" << std::endl;
-
-            {
-                ScopedIndent func_indent(indent_size_);
-                ScopedStopNode scope(stop_nodes_, f->EndTarget());
-                Walk(f->StartTarget());
-            }
-            out_ << "} ";
-            Walk(f->EndTarget());
-        },
-        [&](const ir::Block* b) {
-            // If this block is dead, nothing to do
-            if (!b->HasBranchTarget()) {
-                return;
-            }
-
-            Indent() << "%fn" << IdOf(b) << " = block";
-            if (!b->Params().IsEmpty()) {
-                out_ << " (";
-                for (const auto* p : b->Params()) {
-                    if (p != b->Params().Front()) {
-                        out_ << ", ";
-                    }
-                    EmitValue(p);
-                }
-                out_ << ")";
-            }
-
-            out_ << " {" << std::endl;
-            {
-                ScopedIndent si(indent_size_);
-                EmitBlockInstructions(b);
-            }
-            Indent() << "}";
-
-            std::string suffix = "";
-            if (b->Branch().target->Is<FunctionTerminator>()) {
-                out_ << " -> %func_end";
-                suffix = "return";
-            } else if (b->Branch().target->Is<RootTerminator>()) {
-                // Nothing to do
-            } else {
-                out_ << " -> "
-                     << "%fn" << IdOf(b->Branch().target);
-                suffix = "branch";
-            }
-            if (!b->Branch().args.IsEmpty()) {
-                out_ << " ";
-                for (const auto* v : b->Branch().args) {
-                    if (v != b->Branch().args.Front()) {
-                        out_ << ", ";
-                    }
-                    EmitValue(v);
-                }
-            }
-            if (!suffix.empty()) {
-                out_ << " # " << suffix;
-            }
-            out_ << std::endl;
-
-            if (!b->Branch().target->Is<FunctionTerminator>()) {
-                out_ << std::endl;
-            }
-
-            Walk(b->Branch().target);
-        },
-        [&](const ir::Switch* s) {
-            Indent() << "%fn" << IdOf(s) << " = switch ";
-            EmitValue(s->Condition());
-            out_ << " [";
-            for (const auto& c : s->Cases()) {
-                if (&c != &s->Cases().Front()) {
-                    out_ << ", ";
-                }
-                out_ << "c: (";
-                for (const auto& selector : c.selectors) {
-                    if (&selector != &c.selectors.Front()) {
-                        out_ << " ";
-                    }
-
-                    if (selector.IsDefault()) {
-                        out_ << "default";
-                    } else {
-                        EmitValue(selector.val);
-                    }
-                }
-                out_ << ", %fn" << IdOf(c.Start().target) << ")";
-            }
-            if (s->Merge().target->IsConnected()) {
-                out_ << ", m: %fn" << IdOf(s->Merge().target);
-            }
-            out_ << "]" << std::endl;
-
-            {
-                ScopedIndent switch_indent(indent_size_);
-                ScopedStopNode scope(stop_nodes_, s->Merge().target);
-                for (const auto& c : s->Cases()) {
-                    Indent() << "# case ";
-                    for (const auto& selector : c.selectors) {
-                        if (&selector != &c.selectors.Front()) {
-                            out_ << " ";
-                        }
-
-                        if (selector.IsDefault()) {
-                            out_ << "default";
-                        } else {
-                            EmitValue(selector.val);
-                        }
-                    }
-                    out_ << std::endl;
-                    Walk(c.Start().target);
-                }
-            }
-
-            if (s->Merge().target->IsConnected()) {
-                Indent() << "# switch merge" << std::endl;
-                Walk(s->Merge().target);
-            }
-        },
-        [&](const ir::If* i) {
-            Indent() << "%fn" << IdOf(i) << " = if ";
-            EmitValue(i->Condition());
-
-            bool has_true = i->True().target->HasBranchTarget();
-            bool has_false = i->False().target->HasBranchTarget();
-
-            out_ << " [";
-            if (has_true) {
-                out_ << "t: %fn" << IdOf(i->True().target);
-            }
-            if (has_false) {
-                if (has_true) {
-                    out_ << ", ";
-                }
-                out_ << "f: %fn" << IdOf(i->False().target);
-            }
-            if (i->Merge().target->IsConnected()) {
-                out_ << ", m: %fn" << IdOf(i->Merge().target);
-            }
-            out_ << "]" << std::endl;
-
-            {
-                ScopedIndent if_indent(indent_size_);
-                ScopedStopNode scope(stop_nodes_, i->Merge().target);
-
-                if (has_true) {
-                    Indent() << "# true branch" << std::endl;
-                    Walk(i->True().target);
-                }
-
-                if (has_false) {
-                    Indent() << "# false branch" << std::endl;
-                    Walk(i->False().target);
-                }
-            }
-
-            if (i->Merge().target->IsConnected()) {
-                Indent() << "# if merge" << std::endl;
-                Walk(i->Merge().target);
-            }
-        },
-        [&](const ir::Loop* l) {
-            Indent() << "%fn" << IdOf(l) << " = loop [s: %fn" << IdOf(l->Start().target);
-
-            if (l->Continuing().target->IsConnected()) {
-                out_ << ", c: %fn" << IdOf(l->Continuing().target);
-            }
-            if (l->Merge().target->IsConnected()) {
-                out_ << ", m: %fn" << IdOf(l->Merge().target);
-            }
-            out_ << "]" << std::endl;
-
-            {
-                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);
-                }
-
-                if (l->Continuing().target->IsConnected()) {
-                    Indent() << "# loop continuing" << std::endl;
-                    Walk(l->Continuing().target);
-                }
-            }
-
-            if (l->Merge().target->IsConnected()) {
-                Indent() << "# loop merge" << std::endl;
-                Walk(l->Merge().target);
-            }
-        },
-        [&](const ir::FunctionTerminator*) {
-            TINT_ASSERT(IR, in_function_);
-            Indent() << "%func_end" << std::endl << std::endl;
-        },
-        [&](const ir::RootTerminator*) {
-            TINT_ASSERT(IR, !in_function_);
-            out_ << std::endl;
-        });
-}
-
 std::string Disassembler::Disassemble() {
     if (mod_.root_block) {
-        Walk(mod_.root_block);
+        walk_list_.push_back(mod_.root_block);
+        Walk();
+        TINT_ASSERT(IR, walk_list_.empty());
     }
 
-    for (const auto* func : mod_.functions) {
-        Walk(func);
+    for (auto* func : mod_.functions) {
+        walk_list_.push_back(func);
+        Walk();
+        TINT_ASSERT(IR, walk_list_.empty());
     }
     return out_.str();
 }
 
+void Disassembler::Walk() {
+    utils::Hashset<const FlowNode*, 32> visited_;
+
+    while (!walk_list_.empty()) {
+        const FlowNode* node = walk_list_.front();
+        walk_list_.pop_front();
+
+        if (visited_.Contains(node)) {
+            continue;
+        }
+        visited_.Add(node);
+
+        tint::Switch(
+            node,
+            [&](const ir::Function* f) {
+                in_function_ = true;
+
+                Indent() << "%fn" << IdOf(f) << " = func " << f->Name().Name() << "(";
+                for (auto* p : f->Params()) {
+                    if (p != f->Params().Front()) {
+                        out_ << ", ";
+                    }
+                    out_ << "%" << IdOf(p) << ":" << p->Type()->FriendlyName();
+                }
+                out_ << "):" << f->ReturnType()->FriendlyName();
+
+                if (f->Stage() != Function::PipelineStage::kUndefined) {
+                    out_ << " [@" << f->Stage();
+
+                    if (f->WorkgroupSize()) {
+                        auto arr = f->WorkgroupSize().value();
+                        out_ << " @workgroup_size(" << arr[0] << ", " << arr[1] << ", " << arr[2]
+                             << ")";
+                    }
+
+                    if (!f->ReturnAttributes().IsEmpty()) {
+                        out_ << " ra:";
+
+                        for (auto attr : f->ReturnAttributes()) {
+                            out_ << " @" << attr;
+                            if (attr == Function::ReturnAttribute::kLocation) {
+                                out_ << "(" << f->ReturnLocation().value() << ")";
+                            }
+                        }
+                    }
+
+                    out_ << "]";
+                }
+                out_ << " -> %fn" << IdOf(f->StartTarget()) << std::endl;
+                walk_list_.push_back(f->StartTarget());
+            },
+            [&](const ir::Block* b) {
+                // If this block is dead, nothing to do
+                if (!b->HasBranchTarget()) {
+                    return;
+                }
+
+                Indent() << "%fn" << IdOf(b) << " = block";
+                if (!b->Params().IsEmpty()) {
+                    out_ << " (";
+                    for (auto* p : b->Params()) {
+                        if (p != b->Params().Front()) {
+                            out_ << ", ";
+                        }
+                        EmitValue(p);
+                    }
+                    out_ << ")";
+                }
+
+                out_ << " {" << std::endl;
+                {
+                    ScopedIndent si(indent_size_);
+                    EmitBlockInstructions(b);
+                }
+                Indent() << "}" << std::endl;
+
+                if (!b->Branch()->To()->Is<FunctionTerminator>()) {
+                    out_ << std::endl;
+                }
+
+                walk_list_.push_back(b->Branch()->To());
+            },
+            [&](const ir::FunctionTerminator* t) {
+                TINT_ASSERT(IR, in_function_);
+                Indent() << "%fn" << IdOf(t) << " = func_terminator" << std::endl << std::endl;
+                in_function_ = false;
+            },
+            [&](const ir::RootTerminator* t) {
+                TINT_ASSERT(IR, !in_function_);
+                Indent() << "%fn" << IdOf(t) << " = root_terminator" << std::endl << std::endl;
+            });
+    }
+}
+
 void Disassembler::EmitValueWithType(const Value* val) {
     EmitValue(val);
     if (auto* i = val->As<ir::Instruction>(); i->Type() != nullptr) {
@@ -419,8 +258,12 @@
 
 void Disassembler::EmitInstruction(const Instruction* inst) {
     tint::Switch(
-        inst,  //
-        [&](const ir::Binary* b) { EmitBinary(b); }, [&](const ir::Unary* u) { EmitUnary(u); },
+        inst,                                         //
+        [&](const ir::Switch* s) { EmitSwitch(s); },  //
+        [&](const ir::If* i) { EmitIf(i); },          //
+        [&](const ir::Loop* l) { EmitLoop(l); },      //
+        [&](const ir::Binary* b) { EmitBinary(b); },  //
+        [&](const ir::Unary* u) { EmitUnary(u); },
         [&](const ir::Bitcast* b) {
             EmitValueWithType(b);
             out_ << " = bitcast ";
@@ -468,7 +311,131 @@
                 out_ << ", ";
                 EmitValue(v->Initializer());
             }
-        });
+        },
+        [&](const ir::Branch* b) { EmitBranch(b); },
+        [&](Default) { out_ << "Unknown instruction: " << inst->TypeInfo().name; });
+}
+
+void Disassembler::EmitIf(const If* i) {
+    out_ << "if ";
+    EmitValue(i->Condition());
+
+    bool has_true = i->True()->HasBranchTarget();
+    bool has_false = i->False()->HasBranchTarget();
+
+    out_ << " [";
+    if (has_true) {
+        out_ << "t: %fn" << IdOf(i->True());
+    }
+    if (has_false) {
+        if (has_true) {
+            out_ << ", ";
+        }
+        out_ << "f: %fn" << IdOf(i->False());
+    }
+    if (i->Merge()->IsConnected()) {
+        out_ << ", m: %fn" << IdOf(i->Merge());
+    }
+    out_ << "]";
+
+    if (has_true) {
+        walk_list_.push_back(i->True());
+    }
+    if (has_false) {
+        walk_list_.push_back(i->False());
+    }
+    if (i->Merge()->IsConnected()) {
+        walk_list_.push_back(i->Merge());
+    }
+}
+
+void Disassembler::EmitLoop(const Loop* l) {
+    out_ << "loop [s: %fn" << IdOf(l->Start());
+
+    if (l->Continuing()->IsConnected()) {
+        out_ << ", c: %fn" << IdOf(l->Continuing());
+    }
+    if (l->Merge()->IsConnected()) {
+        out_ << ", m: %fn" << IdOf(l->Merge());
+    }
+    out_ << "]";
+
+    { walk_list_.push_back(l->Start()); }
+
+    if (l->Continuing()->IsConnected()) {
+        walk_list_.push_back(l->Continuing());
+    }
+    if (l->Merge()->IsConnected()) {
+        walk_list_.push_back(l->Merge());
+    }
+}
+
+void Disassembler::EmitSwitch(const Switch* s) {
+    out_ << "switch ";
+    EmitValue(s->Condition());
+    out_ << " [";
+    for (const auto& c : s->Cases()) {
+        if (&c != &s->Cases().Front()) {
+            out_ << ", ";
+        }
+        out_ << "c: (";
+        for (const auto& selector : c.selectors) {
+            if (&selector != &c.selectors.Front()) {
+                out_ << " ";
+            }
+
+            if (selector.IsDefault()) {
+                out_ << "default";
+            } else {
+                EmitValue(selector.val);
+            }
+        }
+        out_ << ", %fn" << IdOf(c.Start()) << ")";
+    }
+    if (s->Merge()->IsConnected()) {
+        out_ << ", m: %fn" << IdOf(s->Merge());
+    }
+    out_ << "]";
+
+    for (auto& c : s->Cases()) {
+        walk_list_.push_back(c.Start());
+    }
+    if (s->Merge()->IsConnected()) {
+        walk_list_.push_back(s->Merge());
+    }
+}
+
+void Disassembler::EmitBranch(const Branch* b) {
+    if (b->Is<Jump>()) {
+        out_ << "jmp ";
+
+        // Stuff the thing we're jumping too into the front of the walk list so it will be emitted
+        // next.
+        walk_list_.push_front(b->To());
+    } else {
+        out_ << "br ";
+    }
+
+    std::string suffix = "";
+    out_ << "%fn" << IdOf(b->To());
+    if (b->To()->Is<FunctionTerminator>()) {
+        suffix = "return";
+    } else if (b->To()->Is<RootTerminator>()) {
+        suffix = "root_end";
+    }
+
+    if (!b->Args().IsEmpty()) {
+        out_ << " ";
+        for (auto* v : b->Args()) {
+            if (v != b->Args().Front()) {
+                out_ << ", ";
+            }
+            EmitValue(v);
+        }
+    }
+    if (!suffix.empty()) {
+        out_ << "  # " << suffix;
+    }
 }
 
 void Disassembler::EmitArgs(const Call* call) {
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index c8953db..7b9e4c5 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -15,15 +15,18 @@
 #ifndef SRC_TINT_IR_DISASSEMBLER_H_
 #define SRC_TINT_IR_DISASSEMBLER_H_
 
+#include <deque>
 #include <string>
 
 #include "src/tint/ir/binary.h"
 #include "src/tint/ir/call.h"
 #include "src/tint/ir/flow_node.h"
+#include "src/tint/ir/if.h"
+#include "src/tint/ir/loop.h"
 #include "src/tint/ir/module.h"
+#include "src/tint/ir/switch.h"
 #include "src/tint/ir/unary.h"
 #include "src/tint/utils/hashmap.h"
-#include "src/tint/utils/hashset.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
@@ -53,18 +56,21 @@
     size_t IdOf(const FlowNode* node);
     std::string_view IdOf(const Value* node);
 
-    void Walk(const FlowNode* node);
+    void Walk();
     void EmitInstruction(const Instruction* inst);
     void EmitValueWithType(const Value* val);
     void EmitValue(const Value* val);
     void EmitArgs(const Call* call);
     void EmitBinary(const Binary* b);
     void EmitUnary(const Unary* b);
+    void EmitBranch(const Branch* b);
+    void EmitSwitch(const Switch* s);
+    void EmitLoop(const Loop* l);
+    void EmitIf(const If* i);
 
     const Module& mod_;
     utils::StringStream out_;
-    utils::Hashset<const FlowNode*, 32> visited_;
-    utils::Hashset<const FlowNode*, 32> stop_nodes_;
+    std::deque<const FlowNode*> walk_list_;
     utils::Hashmap<const FlowNode*, size_t, 32> flow_node_ids_;
     utils::Hashmap<const Value*, std::string, 32> value_ids_;
     uint32_t indent_size_ = 0;
diff --git a/src/tint/ir/discard.h b/src/tint/ir/discard.h
index 2789e57..e87474c 100644
--- a/src/tint/ir/discard.h
+++ b/src/tint/ir/discard.h
@@ -26,12 +26,7 @@
   public:
     /// Constructor
     Discard();
-    Discard(const Discard& inst) = delete;
-    Discard(Discard&& inst) = delete;
     ~Discard() override;
-
-    Discard& operator=(const Discard& inst) = delete;
-    Discard& operator=(Discard&& inst) = delete;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/flow_node.h b/src/tint/ir/flow_node.h
index 289873b..b072964 100644
--- a/src/tint/ir/flow_node.h
+++ b/src/tint/ir/flow_node.h
@@ -18,6 +18,11 @@
 #include "src/tint/utils/castable.h"
 #include "src/tint/utils/vector.h"
 
+// Forward Declarations
+namespace tint::ir {
+class Branch;
+}  // namespace tint::ir
+
 namespace tint::ir {
 
 /// Base class for flow nodes
@@ -26,17 +31,17 @@
     ~FlowNode() override;
 
     /// @returns true if this node has inbound branches and branches out
-    bool IsConnected() const { return HasBranchTarget() && !inbound_branches_.IsEmpty(); }
+    bool IsConnected() const { return HasBranchTarget(); }
 
     /// @returns true if the node has a branch target
     virtual bool HasBranchTarget() const { return false; }
 
     /// @returns the inbound branch list for the flow node
-    utils::VectorRef<FlowNode*> InboundBranches() const { return inbound_branches_; }
+    utils::VectorRef<Branch*> InboundBranches() const { return inbound_branches_; }
 
     /// Adds the given node to the inbound branches
     /// @param node the node to add
-    void AddInboundBranch(FlowNode* node) { inbound_branches_.Push(node); }
+    void AddInboundBranch(Branch* node) { inbound_branches_.Push(node); }
 
   protected:
     /// Constructor
@@ -48,7 +53,7 @@
     ///   - Node is a start node
     ///   - Node is a merge target outside control flow (e.g. an if that returns in both branches)
     ///   - Node is a continue target outside control flow (e.g. a loop that returns)
-    utils::Vector<FlowNode*, 2> inbound_branches_;
+    utils::Vector<Branch*, 2> inbound_branches_;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/from_program.cc b/src/tint/ir/from_program.cc
index 595db58..212efea 100644
--- a/src/tint/ir/from_program.cc
+++ b/src/tint/ir/from_program.cc
@@ -98,19 +98,15 @@
 
 using ResultType = utils::Result<Module, diag::List>;
 
-bool IsConnected(const FlowNode* b) {
+// For an `if` and `switch` block, the merge has a registered incoming branch instruction of the
+// `if` and `switch. So, to determine if the merge is connected to any of the branches that happend
+// in the `if` or `switch` we need a `count` value that is larger then 1.
+bool IsConnected(const FlowNode* b, uint32_t count) {
     // Function is always connected as it's the start.
     if (b->Is<ir::Function>()) {
         return true;
     }
-
-    for (auto* parent : b->InboundBranches()) {
-        if (IsConnected(parent)) {
-            return true;
-        }
-    }
-    // Getting here means all the incoming branches are disconnected.
-    return false;
+    return b->InboundBranches().Length() > count;
 }
 
 /// Impl is the private-implementation of FromProgram().
@@ -145,8 +141,8 @@
         /* dst */ {&builder_.ir.constants_arena},
     };
 
-    /// The stack of flow control blocks.
-    utils::Vector<FlowNode*, 8> flow_stack_;
+    /// The stack of control blocks.
+    utils::Vector<Branch*, 8> control_stack_;
 
     /// The current flow block for expressions.
     Block* current_flow_block_ = nullptr;
@@ -160,15 +156,11 @@
     /// The diagnostic that have been raised.
     diag::List diagnostics_;
 
-    /// Map from ast nodes to flow nodes, used to retrieve the flow node for a given AST node.
-    /// Used for testing purposes.
-    std::unordered_map<const ast::Node*, const FlowNode*> ast_to_flow_;
-
-    class FlowStackScope {
+    class ControlStackScope {
       public:
-        FlowStackScope(Impl* impl, FlowNode* node) : impl_(impl) { impl_->flow_stack_.Push(node); }
+        ControlStackScope(Impl* impl, Branch* b) : impl_(impl) { impl_->control_stack_.Push(b); }
 
-        ~FlowStackScope() { impl_->flow_stack_.Pop(); }
+        ~ControlStackScope() { impl_->control_stack_.Pop(); }
 
       private:
         Impl* impl_;
@@ -178,11 +170,25 @@
         diagnostics_.add_error(tint::diag::System::IR, err, s);
     }
 
+    void JumpTo(FlowNode* node, utils::VectorRef<Value*> args = {}) {
+        TINT_ASSERT(IR, current_flow_block_);
+        TINT_ASSERT(IR, !current_flow_block_->HasBranchTarget());
+
+        current_flow_block_->Instructions().Push(builder_.Jump(node, args));
+        current_flow_block_ = nullptr;
+    }
+    void JumpToIfNeeded(FlowNode* node) {
+        if (!current_flow_block_ || current_flow_block_->HasBranchTarget()) {
+            return;
+        }
+        JumpTo(node);
+    }
+
     void BranchTo(FlowNode* node, utils::VectorRef<Value*> args = {}) {
         TINT_ASSERT(IR, current_flow_block_);
         TINT_ASSERT(IR, !current_flow_block_->HasBranchTarget());
 
-        current_flow_block_->BranchTo(node, args);
+        current_flow_block_->Instructions().Push(builder_.Branch(node, args));
         current_flow_block_ = nullptr;
     }
 
@@ -193,8 +199,8 @@
         BranchTo(node);
     }
 
-    FlowNode* FindEnclosingControl(ControlFlags flags) {
-        for (auto it = flow_stack_.rbegin(); it != flow_stack_.rend(); ++it) {
+    Branch* FindEnclosingControl(ControlFlags flags) {
+        for (auto it = control_stack_.rbegin(); it != control_stack_.rend(); ++it) {
             if ((*it)->Is<Loop>()) {
                 return *it;
             }
@@ -244,6 +250,11 @@
                 });
         }
 
+        // Add the root terminator if needed
+        if (mod.root_block) {
+            mod.root_block->Instructions().Push(builder_.Branch(builder_.CreateRootTerminator()));
+        }
+
         if (diagnostics_.contains_errors()) {
             return ResultType(std::move(diagnostics_));
         }
@@ -253,7 +264,7 @@
 
     void EmitFunction(const ast::Function* ast_func) {
         // The flow stack should have been emptied when the previous function finished building.
-        TINT_ASSERT(IR, flow_stack_.IsEmpty());
+        TINT_ASSERT(IR, control_stack_.IsEmpty());
 
         const auto* sem = program_->Sem().Get(ast_func);
 
@@ -262,8 +273,6 @@
         current_function_ = ir_func;
         builder_.ir.functions.Push(ir_func);
 
-        ast_to_flow_[ast_func] = ir_func;
-
         if (ast_func->IsEntryPoint()) {
             switch (ast_func->PipelineStage()) {
                 case ast::PipelineStage::kVertex:
@@ -343,17 +352,15 @@
         ir_func->SetParams(params);
 
         {
-            FlowStackScope scope(this, ir_func);
-
             current_flow_block_ = ir_func->StartTarget();
             EmitBlock(ast_func->body);
 
-            // 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_->EndTarget());
+            // If the branch target has already been set then a `return` was called. Only set in
+            // the case where `return` wasn't called.
+            JumpToIfNeeded(current_function_->EndTarget());
         }
 
-        TINT_ASSERT(IR, flow_stack_.IsEmpty());
+        TINT_ASSERT(IR, control_stack_.IsEmpty());
         current_flow_block_ = nullptr;
         current_function_ = nullptr;
     }
@@ -362,8 +369,8 @@
         for (auto* s : stmts) {
             EmitStatement(s);
 
-            // If the current flow block has a branch target then the rest of the statements in this
-            // block are dead code. Skip them.
+            // If the current flow block has a branch target then the rest of the statements in
+            // this block are dead code. Skip them.
             if (!current_flow_block_ || current_flow_block_->HasBranchTarget()) {
                 break;
             }
@@ -399,11 +406,11 @@
     }
 
     void EmitAssignment(const ast::AssignmentStatement* stmt) {
-        // If assigning to a phony, just generate the RHS and we're done. Note that, because this
-        // isn't used, a subsequent transform could remove it due to it being dead code. This could
-        // then change the interface for the program (i.e. a global var no longer used). If that
-        // happens we have to either fix this to store to a phony value, or make sure we pull the
-        // interface before doing the dead code elimination.
+        // If assigning to a phony, just generate the RHS and we're done. Note that, because
+        // this isn't used, a subsequent transform could remove it due to it being dead code.
+        // This could then change the interface for the program (i.e. a global var no longer
+        // used). If that happens we have to either fix this to store to a phony value, or make
+        // sure we pull the interface before doing the dead code elimination.
         if (stmt->lhs->Is<ast::PhonyExpression>()) {
             (void)EmitExpression(stmt->rhs);
             return;
@@ -523,8 +530,8 @@
         TINT_DEFER(scopes_.Pop());
 
         // Note, this doesn't need to emit a Block as the current block flow node should be
-        // sufficient as the blocks all get flattened. Each flow control node will inject the basic
-        // blocks it requires.
+        // sufficient as the blocks all get flattened. Each flow control node will inject the
+        // basic blocks it requires.
         EmitStatements(block->statements);
     }
 
@@ -534,50 +541,43 @@
         if (!reg) {
             return;
         }
-        auto* if_node = builder_.CreateIf(reg.Get());
-
-        BranchTo(if_node);
-
-        ast_to_flow_[stmt] = if_node;
+        auto* if_inst = builder_.CreateIf(reg.Get());
+        current_flow_block_->Instructions().Push(if_inst);
 
         {
-            FlowStackScope scope(this, if_node);
+            ControlStackScope scope(this, if_inst);
 
-            current_flow_block_ = if_node->True().target->As<Block>();
+            current_flow_block_ = if_inst->True();
             EmitBlock(stmt->body);
 
             // If the true branch did not execute control flow, then go to the Merge().target
-            BranchToIfNeeded(if_node->Merge().target);
+            BranchToIfNeeded(if_inst->Merge());
 
-            current_flow_block_ = if_node->False().target->As<Block>();
+            current_flow_block_ = if_inst->False();
             if (stmt->else_statement) {
                 EmitStatement(stmt->else_statement);
             }
 
             // If the false branch did not execute control flow, then go to the Merge().target
-            BranchToIfNeeded(if_node->Merge().target);
+            BranchToIfNeeded(if_inst->Merge());
         }
         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->As<Block>();
+        // 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_inst->Merge(), 1)) {
+            current_flow_block_ = if_inst->Merge();
         }
     }
 
     void EmitLoop(const ast::LoopStatement* stmt) {
-        auto* loop_node = builder_.CreateLoop();
-
-        BranchTo(loop_node);
-
-        ast_to_flow_[stmt] = loop_node;
+        auto* loop_inst = builder_.CreateLoop();
+        current_flow_block_->Instructions().Push(loop_inst);
 
         {
-            FlowStackScope scope(this, loop_node);
-
-            current_flow_block_ = loop_node->Start().target->As<Block>();
+            ControlStackScope scope(this, loop_inst);
+            current_flow_block_ = loop_inst->Start();
 
             // The loop doesn't use EmitBlock because it needs the scope stack to not get popped
             // until after the continuing block.
@@ -585,41 +585,39 @@
             TINT_DEFER(scopes_.Pop());
             EmitStatements(stmt->body->statements);
 
-            // The current block didn't `break`, `return` or `continue`, go to the continuing block.
-            BranchToIfNeeded(loop_node->Continuing().target);
+            // The current block didn't `break`, `return` or `continue`, go to the continuing
+            // block.
+            JumpToIfNeeded(loop_inst->Continuing());
 
-            current_flow_block_ = loop_node->Continuing().target->As<Block>();
+            current_flow_block_ = loop_inst->Continuing();
             if (stmt->continuing) {
                 EmitBlock(stmt->continuing);
             }
 
             // Branch back to the start node if the continue target didn't branch out already
-            BranchToIfNeeded(loop_node->Start().target);
+            BranchToIfNeeded(loop_inst->Start());
         }
 
         // 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->As<Block>();
-        if (!IsConnected(loop_node->Merge().target)) {
+        current_flow_block_ = loop_inst->Merge();
+        if (!IsConnected(loop_inst->Merge(), 0)) {
             current_flow_block_ = nullptr;
         }
     }
 
     void EmitWhile(const ast::WhileStatement* stmt) {
-        auto* loop_node = builder_.CreateLoop();
+        auto* loop_inst = builder_.CreateLoop();
+        current_flow_block_->Instructions().Push(loop_inst);
+
         // Continue is always empty, just go back to the start
-        TINT_ASSERT(IR, loop_node->Continuing().target->Is<Block>());
-        loop_node->Continuing().target->As<Block>()->BranchTo(loop_node->Start().target);
-
-        BranchTo(loop_node);
-
-        ast_to_flow_[stmt] = loop_node;
+        loop_inst->Continuing()->Instructions().Push(builder_.Branch(loop_inst->Start()));
 
         {
-            FlowStackScope scope(this, loop_node);
+            ControlStackScope scope(this, loop_inst);
 
-            current_flow_block_ = loop_node->Start().target->As<Block>();
+            current_flow_block_ = loop_inst->Start();
 
             // Emit the while condition into the Start().target of the loop
             auto reg = EmitExpression(stmt->condition);
@@ -628,25 +626,26 @@
             }
 
             // Create an `if (cond) {} else {break;}` control flow
-            auto* if_node = builder_.CreateIf(reg.Get());
-            if_node->True().target->As<Block>()->BranchTo(if_node->Merge().target);
-            if_node->False().target->As<Block>()->BranchTo(loop_node->Merge().target);
+            auto* if_inst = builder_.CreateIf(reg.Get());
+            if_inst->True()->Instructions().Push(builder_.Branch(if_inst->Merge()));
+            if_inst->False()->Instructions().Push(builder_.Branch(loop_inst->Merge()));
+            current_flow_block_->Instructions().Push(if_inst);
 
-            BranchTo(if_node);
-
-            current_flow_block_ = if_node->Merge().target->As<Block>();
+            current_flow_block_ = if_inst->Merge();
             EmitBlock(stmt->body);
 
-            BranchToIfNeeded(loop_node->Continuing().target);
+            JumpToIfNeeded(loop_inst->Continuing());
         }
         // 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->As<Block>();
+        current_flow_block_ = loop_inst->Merge();
     }
 
     void EmitForLoop(const ast::ForLoopStatement* stmt) {
-        auto* loop_node = builder_.CreateLoop();
-        loop_node->Continuing().target->As<Block>()->BranchTo(loop_node->Start().target);
+        auto* loop_inst = builder_.CreateLoop();
+        current_flow_block_->Instructions().Push(loop_inst);
+
+        loop_inst->Continuing()->Instructions().Push(builder_.Branch(loop_inst->Start()));
 
         // Make sure the initializer ends up in a contained scope
         scopes_.Push();
@@ -657,14 +656,10 @@
             EmitStatement(stmt->initializer);
         }
 
-        BranchTo(loop_node);
-
-        ast_to_flow_[stmt] = loop_node;
-
         {
-            FlowStackScope scope(this, loop_node);
+            ControlStackScope scope(this, loop_inst);
 
-            current_flow_block_ = loop_node->Start().target->As<Block>();
+            current_flow_block_ = loop_inst->Start();
 
             if (stmt->condition) {
                 // Emit the condition into the target target of the loop
@@ -674,26 +669,26 @@
                 }
 
                 // Create an `if (cond) {} else {break;}` control flow
-                auto* if_node = builder_.CreateIf(reg.Get());
-                if_node->True().target->As<Block>()->BranchTo(if_node->Merge().target);
-                if_node->False().target->As<Block>()->BranchTo(loop_node->Merge().target);
+                auto* if_inst = builder_.CreateIf(reg.Get());
+                if_inst->True()->Instructions().Push(builder_.Branch(if_inst->Merge()));
+                if_inst->False()->Instructions().Push(builder_.Branch(loop_inst->Merge()));
+                current_flow_block_->Instructions().Push(if_inst);
 
-                BranchTo(if_node);
-                current_flow_block_ = if_node->Merge().target->As<Block>();
+                current_flow_block_ = if_inst->Merge();
             }
 
             EmitBlock(stmt->body);
-            BranchToIfNeeded(loop_node->Continuing().target);
+            JumpToIfNeeded(loop_inst->Continuing());
 
             if (stmt->continuing) {
-                current_flow_block_ = loop_node->Continuing().target->As<Block>();
+                current_flow_block_ = loop_inst->Continuing();
                 EmitStatement(stmt->continuing);
             }
         }
 
         // 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->As<Block>();
+        current_flow_block_ = loop_inst->Merge();
     }
 
     void EmitSwitch(const ast::SwitchStatement* stmt) {
@@ -702,14 +697,11 @@
         if (!reg) {
             return;
         }
-        auto* switch_node = builder_.CreateSwitch(reg.Get());
-
-        BranchTo(switch_node);
-
-        ast_to_flow_[stmt] = switch_node;
+        auto* switch_inst = builder_.CreateSwitch(reg.Get());
+        current_flow_block_->Instructions().Push(switch_inst);
 
         {
-            FlowStackScope scope(this, switch_node);
+            ControlStackScope scope(this, switch_inst);
 
             const auto* sem = program_->Sem().Get(stmt);
             for (const auto* c : sem->Cases()) {
@@ -722,16 +714,16 @@
                     }
                 }
 
-                current_flow_block_ = builder_.CreateCase(switch_node, selectors);
+                current_flow_block_ = builder_.CreateCase(switch_inst, selectors);
                 EmitBlock(c->Body()->Declaration());
 
-                BranchToIfNeeded(switch_node->Merge().target);
+                BranchToIfNeeded(switch_inst->Merge());
             }
         }
         current_flow_block_ = nullptr;
 
-        if (IsConnected(switch_node->Merge().target)) {
-            current_flow_block_ = switch_node->Merge().target->As<Block>();
+        if (IsConnected(switch_inst->Merge(), 1)) {
+            current_flow_block_ = switch_inst->Merge();
         }
     }
 
@@ -753,9 +745,9 @@
         TINT_ASSERT(IR, current_control);
 
         if (auto* c = current_control->As<Loop>()) {
-            BranchTo(c->Merge().target);
+            BranchTo(c->Merge());
         } else if (auto* s = current_control->As<Switch>()) {
-            BranchTo(s->Merge().target);
+            BranchTo(s->Merge());
         } else {
             TINT_UNREACHABLE(IR, diagnostics_);
         }
@@ -766,14 +758,14 @@
         TINT_ASSERT(IR, current_control);
 
         if (auto* c = current_control->As<Loop>()) {
-            BranchTo(c->Continuing().target);
+            BranchTo(c->Continuing());
         } else {
             TINT_UNREACHABLE(IR, diagnostics_);
         }
     }
 
-    // Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so the
-    // code has to continue as before it just predicates writes. If WGSL grows some kind of
+    // Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so
+    // the code has to continue as before it just predicates writes. If WGSL grows some kind of
     // terminating discard that would probably make sense as a FlowNode but would then require
     // figuring out the multi-level exit that is triggered.
     void EmitDiscard(const ast::DiscardStatement*) {
@@ -787,11 +779,8 @@
         if (!reg) {
             return;
         }
-        auto* if_node = builder_.CreateIf(reg.Get());
-
-        BranchTo(if_node);
-
-        ast_to_flow_[stmt] = if_node;
+        auto* if_inst = builder_.CreateIf(reg.Get());
+        current_flow_block_->Instructions().Push(if_inst);
 
         auto* current_control = FindEnclosingControl(ControlFlags::kExcludeSwitch);
         TINT_ASSERT(IR, current_control);
@@ -799,17 +788,17 @@
 
         auto* loop = current_control->As<Loop>();
 
-        current_flow_block_ = if_node->True().target->As<Block>();
-        BranchTo(loop->Merge().target);
+        current_flow_block_ = if_inst->True();
+        BranchTo(loop->Merge());
 
-        current_flow_block_ = if_node->False().target->As<Block>();
-        BranchTo(if_node->Merge().target);
+        current_flow_block_ = if_inst->False();
+        BranchTo(if_inst->Merge());
 
-        current_flow_block_ = if_node->Merge().target->As<Block>();
+        current_flow_block_ = if_inst->Merge();
 
-        // 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.
-        BranchTo(loop->Start().target);
+        // 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.
+        BranchTo(loop->Start());
     }
 
     utils::Result<Value*> EmitExpression(const ast::Expression* expr) {
@@ -845,8 +834,8 @@
             // TODO(dsinclair): Implement
             // },
             [&](const ast::UnaryOpExpression* u) { return EmitUnary(u); },
-            // Note, ast::PhonyExpression is explicitly not handled here as it should never get into
-            // this method. The assignment statement should have filtered it out already.
+            // Note, ast::PhonyExpression is explicitly not handled here as it should never get
+            // into this method. The assignment statement should have filtered it out already.
             [&](Default) {
                 add_error(expr->source,
                           "unknown expression type: " + std::string(expr->TypeInfo().name));
@@ -891,8 +880,8 @@
                 builder_.ir.SetName(val, v->name->symbol.Name());
             },
             [&](const ast::Let* l) {
-                // A `let` doesn't exist as a standalone item in the IR, it's just the result of the
-                // initializer.
+                // A `let` doesn't exist as a standalone item in the IR, it's just the result of
+                // the initializer.
                 auto init = EmitExpression(l->initializer);
                 if (!init) {
                     return;
@@ -911,12 +900,12 @@
             },
             [&](const ast::Const*) {
                 // Skip. This should be handled by const-eval already, so the const will be a
-                // `constant::` value at the usage sites. Can just ignore the `const` variable as it
-                // should never be used.
+                // `constant::` value at the usage sites. Can just ignore the `const` variable
+                // as it should never be used.
                 //
-                // TODO(dsinclair): Probably want to store the const variable somewhere and then in
-                // identifier expression log an error if we ever see a const identifier. Add this
-                // when identifiers and variables are supported.
+                // TODO(dsinclair): Probably want to store the const variable somewhere and then
+                // in identifier expression log an error if we ever see a const identifier. Add
+                // this when identifiers and variables are supported.
             },
             [&](Default) {
                 add_error(var->source, "unknown variable: " + std::string(var->TypeInfo().name));
@@ -953,8 +942,8 @@
         return inst;
     }
 
-    // A short-circut needs special treatment. The short-circuit is decomposed into the relevant if
-    // statements and declarations.
+    // A short-circut needs special treatment. The short-circuit is decomposed into the relevant
+    // if statements and declarations.
     utils::Result<Value*> EmitShortCircuit(const ast::BinaryExpression* expr) {
         switch (expr->op) {
             case ast::BinaryOp::kLogicalAnd:
@@ -972,15 +961,15 @@
             return utils::Failure;
         }
 
-        auto* if_node = builder_.CreateIf(lhs.Get());
-        BranchTo(if_node);
+        auto* if_inst = builder_.CreateIf(lhs.Get());
+        current_flow_block_->Instructions().Push(if_inst);
 
         auto* result = builder_.BlockParam(builder_.ir.types.Get<type::Bool>());
-        if_node->Merge().target->As<Block>()->SetParams(utils::Vector{result});
+        if_inst->Merge()->SetParams(utils::Vector{result});
 
         utils::Result<Value*> rhs;
         {
-            FlowStackScope scope(this, if_node);
+            ControlStackScope scope(this, if_inst);
 
             utils::Vector<Value*, 1> alt_args;
             alt_args.Push(lhs.Get());
@@ -988,19 +977,19 @@
             // If this is an `&&` then we only evaluate the RHS expression in the true block.
             // If this is an `||` then we only evaluate the RHS expression in the false block.
             if (expr->op == ast::BinaryOp::kLogicalAnd) {
-                // If the lhs is false, then that is the result we want to pass to the merge block
-                // as our argument
-                current_flow_block_ = if_node->False().target->As<Block>();
-                BranchTo(if_node->Merge().target, std::move(alt_args));
+                // If the lhs is false, then that is the result we want to pass to the merge
+                // block as our argument
+                current_flow_block_ = if_inst->False();
+                BranchTo(if_inst->Merge(), std::move(alt_args));
 
-                current_flow_block_ = if_node->True().target->As<Block>();
+                current_flow_block_ = if_inst->True();
             } else {
-                // If the lhs is true, then that is the result we want to pass to the merge block
-                // as our argument
-                current_flow_block_ = if_node->True().target->As<Block>();
-                BranchTo(if_node->Merge().target, std::move(alt_args));
+                // If the lhs is true, then that is the result we want to pass to the merge
+                // block as our argument
+                current_flow_block_ = if_inst->True();
+                BranchTo(if_inst->Merge(), std::move(alt_args));
 
-                current_flow_block_ = if_node->False().target->As<Block>();
+                current_flow_block_ = if_inst->False();
             }
 
             rhs = EmitExpression(expr->rhs);
@@ -1010,9 +999,9 @@
             utils::Vector<Value*, 1> args;
             args.Push(rhs.Get());
 
-            BranchTo(if_node->Merge().target, std::move(args));
+            BranchTo(if_inst->Merge(), std::move(args));
         }
-        current_flow_block_ = if_node->Merge().target->As<Block>();
+        current_flow_block_ = if_inst->Merge();
 
         return result;
     }
@@ -1191,67 +1180,6 @@
         }
         return builder_.Constant(cv);
     }
-
-    //    void EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs) {
-    //        for (auto* attr : attrs) {
-    //            EmitAttribute(attr);
-    //        }
-    //    }
-    //
-    //    void EmitAttribute(const ast::Attribute* attr) {
-    //        tint::Switch(  //
-    //            attr,
-    //            [&](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::MustUseAttribute* i) {
-    //              // TODO(dsinclair): Implement
-    //            },
-    //            [&](const ast::IdAttribute*) {
-    //                add_error(attr->source,
-    //                          "found an `Id` attribute. The SubstituteOverrides transform "
-    //                          "must be run before converting to IR");
-    //            },
-    //            [&](const ast::StructMemberSizeAttribute*) {
-    //                TINT_ICE(IR, diagnostics_)
-    //                    << "StructMemberSizeAttribute encountered during IR conversion";
-    //            },
-    //            [&](const ast::StructMemberAlignAttribute*) {
-    //                TINT_ICE(IR, diagnostics_)
-    //                    << "StructMemberAlignAttribute encountered during IR conversion";
-    //            },
-    //            [&](const ast::StrideAttribute* s) {
-    //              // TODO(dsinclair): Implement
-    //            },
-    //            [&](const ast::InternalAttribute *i) {
-    //              // TODO(dsinclair): Implement
-    //            },
-    //            [&](Default) {
-    //                add_error(attr->source, "unknown attribute: " +
-    //                std::string(attr->TypeInfo().name));
-    //            });
-    //    }
 };
 
 }  // namespace
diff --git a/src/tint/ir/from_program_binary_test.cc b/src/tint/ir/from_program_binary_test.cc
index b74a8c8..6e63e40 100644
--- a/src/tint/ir/from_program_binary_test.cc
+++ b/src/tint/ir/from_program_binary_test.cc
@@ -34,17 +34,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = add %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = add %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -59,16 +61,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = add %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = add %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -83,16 +88,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = add %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = add %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -105,17 +113,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = sub %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = sub %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -130,16 +140,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, i32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:i32 = load %v1
-    %3:i32 = sub %2, 1i
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:i32 = load %v1
+  %3:i32 = sub %2, 1i
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -154,16 +167,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = sub %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = sub %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -176,17 +192,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = mul %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = mul %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -201,16 +219,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = mul %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = mul %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -223,17 +244,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = div %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = div %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -248,16 +271,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = div %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = div %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -270,17 +296,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = mod %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = mod %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -295,16 +323,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = mod %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = mod %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -317,17 +348,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = and %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = and %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -342,16 +375,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, bool, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:bool = load %v1
-    %3:bool = and %2, false
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:bool = load %v1
+  %3:bool = and %2, false
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -364,17 +400,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = or %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = or %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -389,16 +427,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, bool, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:bool = load %v1
-    %3:bool = or %2, false
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:bool = load %v1
+  %3:bool = or %2, false
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -411,17 +452,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = xor %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = xor %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -436,16 +479,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = xor %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = xor %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -458,42 +504,42 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
-  %fn2 = block {
-  } -> %func_end true # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool -> %fn2
+%fn2 = block {
+  br %fn3 true  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:bool = call my_func
-  } -> %fn5 # branch
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:bool = call my_func
+  if %1 [t: %fn6, f: %fn7, m: %fn8]
+}
 
-  %fn5 = if %1 [t: %fn6, f: %fn7, m: %fn8]
-    # true branch
-    %fn6 = block {
-    } -> %fn8 false # branch
+%fn6 = block {
+  br %fn8 false
+}
 
-    # false branch
-    %fn7 = block {
-    } -> %fn8 %1 # branch
+%fn7 = block {
+  br %fn8 %1
+}
 
-  # if merge
-  %fn8 = block (%2:bool) {
-  } -> %fn9 # branch
+%fn8 = block (%2:bool) {
+  if %2:bool [t: %fn9, f: %fn10, m: %fn11]
+}
 
-  %fn9 = if %2:bool [t: %fn10, f: %fn11, m: %fn12]
-    # true branch
-    %fn10 = block {
-    } -> %fn12 # branch
+%fn9 = block {
+  br %fn11
+}
 
-    # false branch
-    %fn11 = block {
-    } -> %fn12 # branch
+%fn10 = block {
+  br %fn11
+}
 
-  # if merge
-  %fn12 = block {
-  } -> %func_end # return
-} %func_end
+%fn11 = block {
+  jmp %fn12  # return
+}
+%fn12 = func_terminator
 
 )");
 }
@@ -506,42 +552,42 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
-  %fn2 = block {
-  } -> %func_end true # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool -> %fn2
+%fn2 = block {
+  br %fn3 true  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:bool = call my_func
-  } -> %fn5 # branch
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:bool = call my_func
+  if %1 [t: %fn6, f: %fn7, m: %fn8]
+}
 
-  %fn5 = if %1 [t: %fn6, f: %fn7, m: %fn8]
-    # true branch
-    %fn6 = block {
-    } -> %fn8 %1 # branch
+%fn6 = block {
+  br %fn8 %1
+}
 
-    # false branch
-    %fn7 = block {
-    } -> %fn8 true # branch
+%fn7 = block {
+  br %fn8 true
+}
 
-  # if merge
-  %fn8 = block (%2:bool) {
-  } -> %fn9 # branch
+%fn8 = block (%2:bool) {
+  if %2:bool [t: %fn9, f: %fn10, m: %fn11]
+}
 
-  %fn9 = if %2:bool [t: %fn10, f: %fn11, m: %fn12]
-    # true branch
-    %fn10 = block {
-    } -> %fn12 # branch
+%fn9 = block {
+  br %fn11
+}
 
-    # false branch
-    %fn11 = block {
-    } -> %fn12 # branch
+%fn10 = block {
+  br %fn11
+}
 
-  # if merge
-  %fn12 = block {
-  } -> %func_end # return
-} %func_end
+%fn11 = block {
+  jmp %fn12  # return
+}
+%fn12 = func_terminator
 
 )");
 }
@@ -554,17 +600,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:bool = eq %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:bool = eq %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -577,17 +625,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:bool = neq %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:bool = neq %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -600,17 +650,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:bool = lt %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:bool = lt %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -623,17 +675,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:bool = gt %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:bool = gt %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -646,17 +700,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:bool = lte %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:bool = lte %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -669,17 +725,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:bool = gte %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:bool = gte %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -692,17 +750,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = shiftl %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = shiftl %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -717,16 +777,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = shiftl %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = shiftl %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -739,17 +802,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 0u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 0u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = shiftr %1, 4u
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = shiftr %1, 4u
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -764,16 +829,19 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v1:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:u32 = load %v1
-    %3:u32 = shiftr %2, 1u
-    store %v1, %3
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:u32 = load %v1
+  %3:u32 = shiftr %2, 1u
+  store %v1, %3
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -788,35 +856,36 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():f32 {
-  %fn2 = block {
-  } -> %func_end 0.0f # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():f32 -> %fn2
+%fn2 = block {
+  br %fn3 0.0f  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:f32 = call my_func
-    %2:bool = lt %1, 2.0f
-  } -> %fn5 # branch
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:f32 = call my_func
+  %2:bool = lt %1, 2.0f
+  if %2 [t: %fn6, f: %fn7, m: %fn8]
+}
 
-  %fn5 = if %2 [t: %fn6, f: %fn7, m: %fn8]
-    # true branch
-    %fn6 = block {
-      %3:f32 = call my_func
-      %4:f32 = call my_func
-      %5:f32 = mul 2.29999995231628417969f, %4
-      %6:f32 = div %3, %5
-      %7:bool = gt 2.5f, %6
-    } -> %fn8 %7 # branch
+%fn6 = block {
+  %3:f32 = call my_func
+  %4:f32 = call my_func
+  %5:f32 = mul 2.29999995231628417969f, %4
+  %6:f32 = div %3, %5
+  %7:bool = gt 2.5f, %6
+  br %fn8 %7
+}
 
-    # false branch
-    %fn7 = block {
-    } -> %fn8 %2 # branch
+%fn7 = block {
+  br %fn8 %2
+}
 
-  # if merge
-  %fn8 = block (%tint_symbol:bool) {
-  } -> %func_end # return
-} %func_end
+%fn8 = block (%tint_symbol:bool) {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
 )");
 }
@@ -830,16 +899,18 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func(%p:bool):bool {
-  %fn2 = block {
-  } -> %func_end true # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func(%p:bool):bool -> %fn2
+%fn2 = block {
+  br %fn3 true  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %tint_symbol:bool = call my_func, false
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %tint_symbol:bool = call my_func, false
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/from_program_call_test.cc b/src/tint/ir/from_program_call_test.cc
index 6339c22..2f74508 100644
--- a/src/tint/ir/from_program_call_test.cc
+++ b/src/tint/ir/from_program_call_test.cc
@@ -35,17 +35,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():f32 {
-  %fn2 = block {
-  } -> %func_end 0.0f # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():f32 -> %fn2
+%fn2 = block {
+  br %fn3 0.0f  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:f32 = call my_func
-    %tint_symbol:f32 = bitcast %1
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:f32 = call my_func
+  %tint_symbol:f32 = bitcast %1
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -60,11 +62,12 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func test_function():void [@fragment] {
-  %fn2 = block {
-    discard
-  } -> %func_end # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func test_function():void [@fragment] -> %fn2
+%fn2 = block {
+  discard
+  jmp %fn3  # return
+}
+%fn3 = func_terminator
 
 )");
 }
@@ -77,16 +80,18 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func(%p:f32):void {
-  %fn2 = block {
-  } -> %func_end # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func(%p:f32):void -> %fn2
+%fn2 = block {
+  jmp %fn3  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %2:void = call my_func, 6.0f
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %2:void = call my_func, 6.0f
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -101,15 +106,18 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %i:ptr<private, i32, read_write> = var, 1i
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:i32 = load %i
-    %tint_symbol:f32 = convert i32, %2
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:i32 = load %i
+  %tint_symbol:f32 = convert i32, %2
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -123,8 +131,10 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %i:ptr<private, vec3<f32>, read_write> = var, vec3<f32> 0.0f
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
 )");
 }
@@ -139,15 +149,18 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %i:ptr<private, f32, read_write> = var, 1.0f
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    %2:f32 = load %i
-    %tint_symbol:vec3<f32> = construct 2.0f, 3.0f, %2
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  %2:f32 = load %i
+  %tint_symbol:vec3<f32> = construct 2.0f, 3.0f, %2
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/from_program_materialize_test.cc b/src/tint/ir/from_program_materialize_test.cc
index ba293a7..009d417 100644
--- a/src/tint/ir/from_program_materialize_test.cc
+++ b/src/tint/ir/from_program_materialize_test.cc
@@ -34,10 +34,11 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func test_function():f32 {
-  %fn2 = block {
-  } -> %func_end 2.0f # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func test_function():f32 -> %fn2
+%fn2 = block {
+  br %fn3 2.0f  # return
+}
+%fn3 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/from_program_store_test.cc b/src/tint/ir/from_program_store_test.cc
index 5bb6398..ab0945b 100644
--- a/src/tint/ir/from_program_store_test.cc
+++ b/src/tint/ir/from_program_store_test.cc
@@ -37,14 +37,17 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %a:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    store %a, 4u
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  store %a, 4u
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/from_program_test.cc b/src/tint/ir/from_program_test.cc
index 1049386..197e7e3 100644
--- a/src/tint/ir/from_program_test.cc
+++ b/src/tint/ir/from_program_test.cc
@@ -32,10 +32,10 @@
 /// If multiple flow nodes are found with the type T, then an error is raised and the first is
 /// returned.
 template <typename T>
-const T* FindSingleFlowNode(const Module& mod) {
-    const T* found = nullptr;
+T* FindSingleValue(Module& mod) {
+    T* found = nullptr;
     size_t count = 0;
-    for (auto* node : mod.flow_nodes.Objects()) {
+    for (auto* node : mod.values.Objects()) {
         if (auto* as = node->As<T>()) {
             count++;
             if (!found) {
@@ -44,7 +44,7 @@
         }
     }
     if (count > 1) {
-        ADD_FAILURE() << "FindSingleFlowNode() found " << count << " nodes of type "
+        ADD_FAILURE() << "FindSingleValue() found " << count << " nodes of type "
                       << utils::TypeInfo::Of<T>().name;
     }
     return found;
@@ -66,15 +66,15 @@
     ASSERT_NE(f->StartTarget(), nullptr);
     ASSERT_NE(f->EndTarget(), nullptr);
 
-    EXPECT_EQ(1u, f->StartTarget()->InboundBranches().Length());
     EXPECT_EQ(1u, f->EndTarget()->InboundBranches().Length());
 
     EXPECT_EQ(m->functions[0]->Stage(), Function::PipelineStage::kUndefined);
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f():void {
-  %fn2 = block {
-  } -> %func_end # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f():void -> %fn2
+%fn2 = block {
+  jmp %fn3  # return
+}
+%fn3 = func_terminator
 
 )");
 }
@@ -91,15 +91,15 @@
     ASSERT_NE(f->StartTarget(), nullptr);
     ASSERT_NE(f->EndTarget(), nullptr);
 
-    EXPECT_EQ(1u, f->StartTarget()->InboundBranches().Length());
     EXPECT_EQ(1u, f->EndTarget()->InboundBranches().Length());
 
     EXPECT_EQ(m->functions[0]->Stage(), Function::PipelineStage::kUndefined);
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f(%a:u32):u32 {
-  %fn2 = block {
-  } -> %func_end %a # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f(%a:u32):u32 -> %fn2
+%fn2 = block {
+  br %fn3 %a  # return
+}
+%fn3 = func_terminator
 
 )");
 }
@@ -117,15 +117,15 @@
     ASSERT_NE(f->StartTarget(), nullptr);
     ASSERT_NE(f->EndTarget(), nullptr);
 
-    EXPECT_EQ(1u, f->StartTarget()->InboundBranches().Length());
     EXPECT_EQ(1u, f->EndTarget()->InboundBranches().Length());
 
     EXPECT_EQ(m->functions[0]->Stage(), Function::PipelineStage::kUndefined);
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f(%a:u32, %b:i32, %c:bool):void {
-  %fn2 = block {
-  } -> %func_end # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f(%a:u32, %b:i32, %c:bool):void -> %fn2
+%fn2 = block {
+  jmp %fn3  # return
+}
+%fn3 = func_terminator
 
 )");
 }
@@ -144,42 +144,38 @@
     auto* ast_if = If(true, Block(), Else(Block()));
     WrapInFunction(ast_if);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
-    ASSERT_NE(flow->True().target, nullptr);
-    ASSERT_NE(flow->False().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::If>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->False()->InboundBranches().Length());
+    EXPECT_EQ(3u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  if true [t: %fn3, f: %fn4, m: %fn5]
+}
 
-  %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
-    # true branch
-    %fn4 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  br %fn5
+}
 
-    # false branch
-    %fn5 = block {
-    } -> %fn6 # branch
+%fn4 = block {
+  br %fn5
+}
 
-  # if merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn5 = block {
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -188,41 +184,37 @@
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(ast_if);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
-    ASSERT_NE(flow->True().target, nullptr);
-    ASSERT_NE(flow->False().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::If>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(2u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  if true [t: %fn3, f: %fn4, m: %fn5]
+}
 
-  %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
-    # true branch
-    %fn4 = block {
-    } -> %func_end # return
-    # false branch
-    %fn5 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  br %fn6  # return
+}
+%fn4 = block {
+  br %fn5
+}
 
-  # if merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn5 = block {
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -231,41 +223,37 @@
     auto* ast_if = If(true, Block(), Else(Block(Return())));
     WrapInFunction(ast_if);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
-    ASSERT_NE(flow->True().target, nullptr);
-    ASSERT_NE(flow->False().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::If>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(2u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  if true [t: %fn3, f: %fn4, m: %fn5]
+}
 
-  %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
-    # true branch
-    %fn4 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  br %fn5
+}
 
-    # false branch
-    %fn5 = block {
-    } -> %func_end # return
-  # if merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn4 = block {
+  br %fn6  # return
+}
+%fn5 = block {
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -274,37 +262,33 @@
     auto* ast_if = If(true, Block(Return()), Else(Block(Return())));
     WrapInFunction(ast_if);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
-    ASSERT_NE(flow->True().target, nullptr);
-    ASSERT_NE(flow->False().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::If>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(0u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->False()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(2u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  if true [t: %fn3, f: %fn4]
+}
 
-  %fn3 = if true [t: %fn4, f: %fn5]
-    # true branch
-    %fn4 = block {
-    } -> %func_end # return
-    # false branch
-    %fn5 = block {
-    } -> %func_end # return
-} %func_end
+%fn3 = block {
+  br %fn5  # return
+}
+%fn4 = block {
+  br %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -314,47 +298,46 @@
     auto* ast_if = If(true, Block(ast_loop));
     WrapInFunction(ast_if);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
-    ASSERT_NE(if_flow->True().target, nullptr);
-    ASSERT_NE(if_flow->False().target, nullptr);
-    ASSERT_NE(if_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* if_flow = FindSingleValue<ir::If>(m);
+    ASSERT_NE(if_flow, nullptr);
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
     ASSERT_NE(loop_flow, nullptr);
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  if true [t: %fn3, f: %fn4, m: %fn5]
+}
 
-  %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
-    # true branch
-    %fn4 = block {
-    } -> %fn7 # branch
+%fn3 = block {
+  loop [s: %fn6, c: %fn7, m: %fn8]
+}
 
-    %fn7 = loop [s: %fn8, m: %fn9]
-      # loop start
-      %fn8 = block {
-      } -> %fn9 # branch
+%fn4 = block {
+  br %fn5
+}
 
-    # loop merge
-    %fn9 = block {
-    } -> %fn6 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-    # false branch
-    %fn5 = block {
-    } -> %fn6 # branch
+%fn6 = block {
+  br %fn8
+}
 
-  # if merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn7 = block {
+  br %fn6
+}
+
+%fn8 = block {
+  br %fn5
+}
 
 )");
 }
@@ -363,38 +346,38 @@
     auto* ast_loop = Loop(Block(Break()));
     WrapInFunction(ast_loop);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(flow->Start().target, nullptr);
-    ASSERT_NE(flow->Continuing().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Loop>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(0u, flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, m: %fn5]
-    # loop start
-    %fn4 = block {
-    } -> %fn5 # branch
+%fn3 = block {
+  br %fn5
+}
 
-  # loop merge
-  %fn5 = block {
-  } -> %func_end # return
-} %func_end
+%fn4 = block {
+  br %fn3
+}
+
+%fn5 = block {
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -404,64 +387,55 @@
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
 
-    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
-    ASSERT_NE(if_flow->True().target, nullptr);
-    ASSERT_NE(if_flow->False().target, nullptr);
-    ASSERT_NE(if_flow->Merge().target, nullptr);
+    auto* if_flow = FindSingleValue<ir::If>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, loop_flow->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, if_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
-    # loop start
-    %fn4 = block {
-    } -> %fn7 # branch
+%fn3 = block {
+  if true [t: %fn6, f: %fn7, m: %fn8]
+}
 
-    %fn7 = if true [t: %fn8, f: %fn9, m: %fn10]
-      # true branch
-      %fn8 = block {
-      } -> %fn6 # branch
+%fn4 = block {
+  br %fn3
+}
 
-      # false branch
-      %fn9 = block {
-      } -> %fn10 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-    # if merge
-    %fn10 = block {
-    } -> %fn5 # branch
+%fn6 = block {
+  br %fn5
+}
 
-    # loop continuing
-    %fn5 = block {
-    } -> %fn4 # branch
+%fn7 = block {
+  br %fn8
+}
 
-  # loop merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn8 = block {
+  br %fn4
+}
 
 )");
 }
@@ -471,64 +445,54 @@
     auto* ast_loop = Loop(Block(), Block(ast_break_if));
     WrapInFunction(ast_loop);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
+    auto* break_if_flow = FindSingleValue<ir::If>(m);
 
-    auto* break_if_flow = FindSingleFlowNode<ir::If>(m.Get());
-    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];
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
-
-    EXPECT_EQ(1u, loop_flow->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, break_if_flow->InboundBranches().Length());
-    EXPECT_EQ(1u, break_if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, break_if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, break_if_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, break_if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, break_if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, break_if_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
-    # loop start
-    %fn4 = block {
-    } -> %fn5 # branch
+%fn3 = block {
+  jmp %fn4
+}
 
-    # loop continuing
-    %fn5 = block {
-    } -> %fn7 # branch
+%fn4 = block {
+  if true [t: %fn6, f: %fn7, m: %fn8]
+}
 
-    %fn7 = if true [t: %fn8, f: %fn9, m: %fn10]
-      # true branch
-      %fn8 = block {
-      } -> %fn6 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-      # false branch
-      %fn9 = block {
-      } -> %fn10 # branch
+%fn6 = block {
+  br %fn5
+}
 
-    # if merge
-    %fn10 = block {
-    } -> %fn4 # branch
+%fn7 = block {
+  br %fn8
+}
 
-  # loop merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn8 = block {
+  br %fn3
+}
 
 )");
 }
@@ -539,40 +503,40 @@
     auto* ast_loop = Loop(Block(a), Block(ast_break_if));
     WrapInFunction(ast_loop);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    auto m = res.Move();
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
-    # loop start
-    %fn4 = block {
-    } -> %fn5 # branch
+%fn3 = block {
+  jmp %fn4
+}
 
-    # loop continuing
-    %fn5 = block {
-    } -> %fn7 # branch
+%fn4 = block {
+  if true [t: %fn6, f: %fn7, m: %fn8]
+}
 
-    %fn7 = if true [t: %fn8, f: %fn9, m: %fn10]
-      # true branch
-      %fn8 = block {
-      } -> %fn6 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-      # false branch
-      %fn9 = block {
-      } -> %fn10 # branch
+%fn6 = block {
+  br %fn5
+}
 
-    # if merge
-    %fn10 = block {
-    } -> %fn4 # branch
+%fn7 = block {
+  br %fn8
+}
 
-  # loop merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn8 = block {
+  br %fn3
+}
 
 )");
 }
@@ -582,60 +546,50 @@
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
+    auto* if_flow = FindSingleValue<ir::If>(m);
 
-    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
-    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];
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
-
-    EXPECT_EQ(1u, loop_flow->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(0u, loop_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(0u, loop_flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, if_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4]
+}
 
-  %fn3 = loop [s: %fn4, c: %fn5]
-    # loop start
-    %fn4 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  if true [t: %fn5, f: %fn6, m: %fn7]
+}
 
-    %fn6 = if true [t: %fn7, f: %fn8, m: %fn9]
-      # true branch
-      %fn7 = block {
-      } -> %func_end # return
-      # false branch
-      %fn8 = block {
-      } -> %fn9 # branch
+%fn4 = block {
+  br %fn3
+}
 
-    # if merge
-    %fn9 = block {
-    } -> %fn5 # branch
+%fn5 = block {
+  br %fn8  # return
+}
+%fn6 = block {
+  br %fn7
+}
 
-    # loop continuing
-    %fn5 = block {
-    } -> %fn4 # branch
+%fn7 = block {
+  br %fn4
+}
 
-} %func_end
+%fn8 = func_terminator
 
 )");
 }
@@ -644,34 +598,34 @@
     auto* ast_loop = Loop(Block(Return(), Continue()));
     WrapInFunction(ast_loop, If(true, Block(Return())));
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, loop_flow->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, loop_flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(0u, loop_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(0u, loop_flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(0u, loop_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4]
+}
 
-  %fn3 = loop [s: %fn4]
-    # loop start
-    %fn4 = block {
-    } -> %func_end # return
-} %func_end
+%fn3 = block {
+  br %fn5  # return
+}
+%fn4 = block {
+  br %fn3
+}
+
+%fn5 = func_terminator
 
 )");
 }
@@ -689,41 +643,61 @@
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(Block(ast_loop, ast_if));
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
 
-    auto* break_if_flow = FindSingleFlowNode<ir::If>(m.Get());
-    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];
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    EXPECT_EQ(2u, loop_flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(0u, loop_flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, loop_flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(3u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(1u, loop_flow->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, loop_flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
-    // This is 1 because only the loop branch happens. The subsequent if return is dead code.
-    EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+%fn3 = block {
+  br %fn6  # return
+}
+%fn4 = block {
+  if true [t: %fn7, f: %fn8, m: %fn9]
+}
 
-  %fn3 = loop [s: %fn4]
-    # loop start
-    %fn4 = block {
-    } -> %func_end # return
-} %func_end
+%fn5 = block {
+  if true [t: %fn10, f: %fn11, m: %fn12]
+}
 
+%fn6 = func_terminator
+
+%fn7 = block {
+  br %fn5
+}
+
+%fn8 = block {
+  br %fn9
+}
+
+%fn9 = block {
+  br %fn3
+}
+
+%fn10 = block {
+  br %fn6  # return
+}
+%fn11 = block {
+  br %fn12
+}
+
+%fn12 = block {
+  jmp %fn6  # return
+}
 )");
 }
 
@@ -732,56 +706,50 @@
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(loop_flow->Start().target, nullptr);
-    ASSERT_NE(loop_flow->Continuing().target, nullptr);
-    ASSERT_NE(loop_flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* loop_flow = FindSingleValue<ir::Loop>(m);
+    auto* if_flow = FindSingleValue<ir::If>(m);
 
-    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
-    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];
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
-
-    EXPECT_EQ(1u, loop_flow->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, loop_flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(0u, if_flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(0u, loop_flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(2u, loop_flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, m: %fn5]
-    # loop start
-    %fn4 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  if true [t: %fn6, f: %fn7]
+}
 
-    %fn6 = if true [t: %fn7, f: %fn8]
-      # true branch
-      %fn7 = block {
-      } -> %fn5 # branch
+%fn4 = block {
+  br %fn3
+}
 
-      # false branch
-      %fn8 = block {
-      } -> %fn5 # branch
+%fn5 = block {
+  jmp %fn8  # return
+}
+%fn8 = func_terminator
 
-  # loop merge
-  %fn5 = block {
-  } -> %func_end # return
-} %func_end
+%fn6 = block {
+  br %fn5
+}
+
+%fn7 = block {
+  br %fn5
+}
 
 )");
 }
@@ -803,206 +771,108 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_EQ(1u, m->functions.Length());
-
-    auto block_exit = [&](const ir::FlowNode* node) -> const ir::FlowNode* {
-        if (auto* block = As<ir::Block>(node)) {
-            return block->Branch().target;
-        }
-        return nullptr;
-    };
-
-    auto* loop_flow_a = As<ir::Loop>(m->functions[0]->StartTarget()->Branch().target);
-    ASSERT_NE(loop_flow_a, 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* loop_flow_b = As<ir::Loop>(block_exit(loop_flow_a->Start().target));
-    ASSERT_NE(loop_flow_b, 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* if_flow_a = As<ir::If>(block_exit(loop_flow_b->Start().target));
-    ASSERT_NE(if_flow_a, 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* if_flow_b = As<ir::If>(block_exit(if_flow_a->Merge().target));
-    ASSERT_NE(if_flow_b, 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* loop_flow_c = As<ir::Loop>(block_exit(loop_flow_b->Continuing().target));
-    ASSERT_NE(loop_flow_c, 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* loop_flow_d = As<ir::Loop>(block_exit(loop_flow_c->Merge().target));
-    ASSERT_NE(loop_flow_d, 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* if_flow_c = As<ir::If>(block_exit(loop_flow_d->Continuing().target));
-    ASSERT_NE(if_flow_c, 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* if_flow_d = As<ir::If>(block_exit(loop_flow_b->Merge().target));
-    ASSERT_NE(if_flow_d, 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->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow_a->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_a->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_a->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_b->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow_b->Start().target->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow_b->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_b->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_c->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow_c->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, loop_flow_c->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_c->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_d->InboundBranches().Length());
-    EXPECT_EQ(2u, loop_flow_d->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_d->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, loop_flow_d->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_a->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_a->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_a->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_a->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_b->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_b->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_b->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_b->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_c->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_c->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_c->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_c->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_d->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_d->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_d->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow_d->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, func->StartTarget()->InboundBranches().Length());
-    EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
-
     EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
-    # loop start
-    %fn4 = block {
-    } -> %fn7 # branch
+%fn3 = block {
+  loop [s: %fn6, c: %fn7, m: %fn8]
+}
 
-    %fn7 = loop [s: %fn8, c: %fn9, m: %fn10]
-      # loop start
-      %fn8 = block {
-      } -> %fn11 # branch
+%fn4 = block {
+  br %fn3
+}
 
-      %fn11 = if true [t: %fn12, f: %fn13, m: %fn14]
-        # true branch
-        %fn12 = block {
-        } -> %fn10 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-        # false branch
-        %fn13 = block {
-        } -> %fn14 # branch
+%fn6 = block {
+  if true [t: %fn10, f: %fn11, m: %fn12]
+}
 
-      # if merge
-      %fn14 = block {
-      } -> %fn15 # branch
+%fn7 = block {
+  loop [s: %fn13, c: %fn14, m: %fn15]
+}
 
-      %fn15 = if true [t: %fn16, f: %fn17, m: %fn18]
-        # true branch
-        %fn16 = block {
-        } -> %fn9 # branch
+%fn8 = block {
+  if true [t: %fn16, f: %fn17, m: %fn18]
+}
 
-        # false branch
-        %fn17 = block {
-        } -> %fn18 # branch
+%fn10 = block {
+  br %fn8
+}
 
-      # if merge
-      %fn18 = block {
-      } -> %fn9 # branch
+%fn11 = block {
+  br %fn12
+}
 
-      # loop continuing
-      %fn9 = block {
-      } -> %fn19 # branch
+%fn12 = block {
+  if true [t: %fn19, f: %fn20, m: %fn21]
+}
 
-      %fn19 = loop [s: %fn20, m: %fn21]
-        # loop start
-        %fn20 = block {
-        } -> %fn21 # branch
+%fn13 = block {
+  br %fn15
+}
 
-      # loop merge
-      %fn21 = block {
-      } -> %fn22 # branch
+%fn14 = block {
+  br %fn13
+}
 
-      %fn22 = loop [s: %fn23, c: %fn24, m: %fn25]
-        # loop start
-        %fn23 = block {
-        } -> %fn24 # branch
+%fn15 = block {
+  loop [s: %fn22, c: %fn23, m: %fn24]
+}
 
-        # loop continuing
-        %fn24 = block {
-        } -> %fn26 # branch
+%fn16 = block {
+  br %fn5
+}
 
-        %fn26 = if true [t: %fn27, f: %fn28, m: %fn29]
-          # true branch
-          %fn27 = block {
-          } -> %fn25 # branch
+%fn17 = block {
+  br %fn18
+}
 
-          # false branch
-          %fn28 = block {
-          } -> %fn29 # branch
+%fn18 = block {
+  jmp %fn4
+}
 
-        # if merge
-        %fn29 = block {
-        } -> %fn23 # branch
+%fn19 = block {
+  br %fn7
+}
 
-      # loop merge
-      %fn25 = block {
-      } -> %fn8 # branch
+%fn20 = block {
+  br %fn21
+}
 
-    # loop merge
-    %fn10 = block {
-    } -> %fn30 # branch
+%fn21 = block {
+  jmp %fn7
+}
 
-    %fn30 = if true [t: %fn31, f: %fn32, m: %fn33]
-      # true branch
-      %fn31 = block {
-      } -> %fn6 # branch
+%fn22 = block {
+  jmp %fn23
+}
 
-      # false branch
-      %fn32 = block {
-      } -> %fn33 # branch
+%fn23 = block {
+  if true [t: %fn25, f: %fn26, m: %fn27]
+}
 
-    # if merge
-    %fn33 = block {
-    } -> %fn5 # branch
+%fn24 = block {
+  br %fn6
+}
 
-    # loop continuing
-    %fn5 = block {
-    } -> %fn4 # branch
+%fn25 = block {
+  br %fn24
+}
 
-  # loop merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn26 = block {
+  br %fn27
+}
+
+%fn27 = block {
+  br %fn22
+}
 
 )");
 }
@@ -1011,64 +881,57 @@
     auto* ast_while = While(false, Block());
     WrapInFunction(ast_while);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(flow->Start().target, nullptr);
-    ASSERT_NE(flow->Continuing().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Loop>(m);
 
-    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_NE(flow->Start()->Branch(), nullptr);
+    ASSERT_TRUE(flow->Start()->Branch()->Is<ir::If>());
+    auto* if_flow = flow->Start()->Branch()->As<ir::If>();
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, if_flow->Merge()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
-    # loop start
-    %fn4 = block {
-    } -> %fn7 # branch
+%fn3 = block {
+  if false [t: %fn6, f: %fn7, m: %fn8]
+}
 
-    %fn7 = if false [t: %fn8, f: %fn9, m: %fn10]
-      # true branch
-      %fn8 = block {
-      } -> %fn10 # branch
+%fn4 = block {
+  br %fn3
+}
 
-      # false branch
-      %fn9 = block {
-      } -> %fn6 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-    # if merge
-    %fn10 = block {
-    } -> %fn5 # branch
+%fn6 = block {
+  br %fn8
+}
 
-    # loop continuing
-    %fn5 = block {
-    } -> %fn4 # branch
+%fn7 = block {
+  br %fn5
+}
 
-  # loop merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn8 = block {
+  jmp %fn4
+}
 
 )");
 }
@@ -1077,60 +940,57 @@
     auto* ast_while = While(true, Block(Return()));
     WrapInFunction(ast_while);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(flow->Start().target, nullptr);
-    ASSERT_NE(flow->Continuing().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Loop>(m);
 
-    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_NE(flow->Start()->Branch(), nullptr);
+    ASSERT_TRUE(flow->Start()->Branch()->Is<ir::If>());
+    auto* if_flow = flow->Start()->Branch()->As<ir::If>();
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     EXPECT_EQ(2u, func->EndTarget()->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(0u, flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, if_flow->Merge()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, m: %fn5]
-    # loop start
-    %fn4 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  if true [t: %fn6, f: %fn7, m: %fn8]
+}
 
-    %fn6 = if true [t: %fn7, f: %fn8, m: %fn9]
-      # true branch
-      %fn7 = block {
-      } -> %fn9 # branch
+%fn4 = block {
+  br %fn3
+}
 
-      # false branch
-      %fn8 = block {
-      } -> %fn5 # branch
+%fn5 = block {
+  jmp %fn9  # return
+}
+%fn9 = func_terminator
 
-    # if merge
-    %fn9 = block {
-    } -> %func_end # return
-  # loop merge
-  %fn5 = block {
-  } -> %func_end # return
-} %func_end
+%fn6 = block {
+  br %fn8
+}
 
+%fn7 = block {
+  br %fn5
+}
+
+%fn8 = block {
+  br %fn9  # return
+}
 )");
 }
 
@@ -1151,71 +1011,66 @@
     auto* ast_for = For(Decl(Var("i", ty.i32())), LessThan("i", 10_a), Increment("i"), Block());
     WrapInFunction(ast_for);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(flow->Start().target, nullptr);
-    ASSERT_NE(flow->Continuing().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Loop>(m);
 
-    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_NE(flow->Start()->Branch(), nullptr);
+    ASSERT_TRUE(flow->Start()->Branch()->Is<ir::If>());
+    auto* if_flow = flow->Start()->Branch()->As<ir::If>();
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->True().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->False().target->InboundBranches().Length());
-    EXPECT_EQ(1u, if_flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Merge()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->True()->InboundBranches().Length());
+    EXPECT_EQ(1u, if_flow->False()->InboundBranches().Length());
+    EXPECT_EQ(2u, if_flow->Merge()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()), R"()");
+    EXPECT_EQ(Disassemble(m), R"()");
 }
 
 TEST_F(IR_BuilderImplTest, For_NoInitCondOrContinuing) {
     auto* ast_for = For(nullptr, nullptr, nullptr, Block(Break()));
     WrapInFunction(ast_for);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
-    ASSERT_NE(flow->Start().target, nullptr);
-    ASSERT_NE(flow->Continuing().target, nullptr);
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Loop>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, flow->Continuing().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Start()->InboundBranches().Length());
+    EXPECT_EQ(0u, flow->Continuing()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  loop [s: %fn3, c: %fn4, m: %fn5]
+}
 
-  %fn3 = loop [s: %fn4, m: %fn5]
-    # loop start
-    %fn4 = block {
-    } -> %fn5 # branch
+%fn3 = block {
+  br %fn5
+}
 
-  # loop merge
-  %fn5 = block {
-  } -> %func_end # return
-} %func_end
+%fn4 = block {
+  br %fn3
+}
+
+%fn5 = block {
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -1227,14 +1082,14 @@
 
     WrapInFunction(ast_switch);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Switch>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     auto cases = flow->Cases();
     ASSERT_EQ(3u, cases.Length());
@@ -1252,35 +1107,34 @@
     ASSERT_EQ(1u, cases[2].selectors.Length());
     EXPECT_TRUE(cases[2].selectors[0].IsDefault());
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[0].Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[1].Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[2].Start().target->InboundBranches().Length());
-    EXPECT_EQ(3u, flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[0].Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[1].Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[2].Start()->InboundBranches().Length());
+    EXPECT_EQ(4u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  switch 1i [c: (0i, %fn3), c: (1i, %fn4), c: (default, %fn5), m: %fn6]
+}
 
-  %fn3 = switch 1i [c: (0i, %fn4), c: (1i, %fn5), c: (default, %fn6), m: %fn7]
-    # case 0i
-    %fn4 = block {
-    } -> %fn7 # branch
+%fn3 = block {
+  br %fn6
+}
 
-    # case 1i
-    %fn5 = block {
-    } -> %fn7 # branch
+%fn4 = block {
+  br %fn6
+}
 
-    # case default
-    %fn6 = block {
-    } -> %fn7 # branch
+%fn5 = block {
+  br %fn6
+}
 
-  # switch merge
-  %fn7 = block {
-  } -> %func_end # return
-} %func_end
+%fn6 = block {
+  jmp %fn7  # return
+}
+%fn7 = func_terminator
 
 )");
 }
@@ -1293,14 +1147,14 @@
 
     WrapInFunction(ast_switch);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Switch>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     auto cases = flow->Cases();
     ASSERT_EQ(1u, cases.Length());
@@ -1315,25 +1169,24 @@
 
     EXPECT_TRUE(cases[0].selectors[2].IsDefault());
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[0].Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[0].Start()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  switch 1i [c: (0i 1i default, %fn3), m: %fn4]
+}
 
-  %fn3 = switch 1i [c: (0i 1i default, %fn4), m: %fn5]
-    # case 0i 1i default
-    %fn4 = block {
-    } -> %fn5 # branch
+%fn3 = block {
+  br %fn4
+}
 
-  # switch merge
-  %fn5 = block {
-  } -> %func_end # return
-} %func_end
+%fn4 = block {
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -1342,39 +1195,38 @@
     auto* ast_switch = Switch(1_i, utils::Vector{DefaultCase(Block())});
     WrapInFunction(ast_switch);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Switch>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     auto cases = flow->Cases();
     ASSERT_EQ(1u, cases.Length());
     ASSERT_EQ(1u, cases[0].selectors.Length());
     EXPECT_TRUE(cases[0].selectors[0].IsDefault());
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[0].Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[0].Start()->InboundBranches().Length());
+    EXPECT_EQ(2u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  switch 1i [c: (default, %fn3), m: %fn4]
+}
 
-  %fn3 = switch 1i [c: (default, %fn4), m: %fn5]
-    # case default
-    %fn4 = block {
-    } -> %fn5 # branch
+%fn3 = block {
+  br %fn4
+}
 
-  # switch merge
-  %fn5 = block {
-  } -> %func_end # return
-} %func_end
+%fn4 = block {
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -1385,14 +1237,14 @@
                                                  DefaultCase(Block())});
     WrapInFunction(ast_switch);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto m = res.Move();
+    auto* flow = FindSingleValue<ir::Switch>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     auto cases = flow->Cases();
     ASSERT_EQ(2u, cases.Length());
@@ -1404,31 +1256,30 @@
     ASSERT_EQ(1u, cases[1].selectors.Length());
     EXPECT_TRUE(cases[1].selectors[0].IsDefault());
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[0].Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[1].Start().target->InboundBranches().Length());
-    EXPECT_EQ(2u, flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[0].Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[1].Start()->InboundBranches().Length());
+    EXPECT_EQ(3u, flow->Merge()->InboundBranches().Length());
     // This is 1 because the if is dead-code eliminated and the return doesn't happen.
     EXPECT_EQ(1u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  switch 1i [c: (0i, %fn3), c: (default, %fn4), m: %fn5]
+}
 
-  %fn3 = switch 1i [c: (0i, %fn4), c: (default, %fn5), m: %fn6]
-    # case 0i
-    %fn4 = block {
-    } -> %fn6 # branch
+%fn3 = block {
+  br %fn5
+}
 
-    # case default
-    %fn5 = block {
-    } -> %fn6 # branch
+%fn4 = block {
+  br %fn5
+}
 
-  # switch merge
-  %fn6 = block {
-  } -> %func_end # return
-} %func_end
+%fn5 = block {
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -1440,16 +1291,16 @@
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(ast_switch, ast_if);
 
-    auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    auto res = Build();
+    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
 
-    ASSERT_EQ(FindSingleFlowNode<ir::If>(m.Get()), nullptr);
+    auto m = res.Move();
+    ASSERT_EQ(FindSingleValue<ir::If>(m), nullptr);
 
-    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
-    ASSERT_NE(flow->Merge().target, nullptr);
+    auto* flow = FindSingleValue<ir::Switch>(m);
 
-    ASSERT_EQ(1u, m->functions.Length());
-    auto* func = m->functions[0];
+    ASSERT_EQ(1u, m.functions.Length());
+    auto* func = m.functions[0];
 
     auto cases = flow->Cases();
     ASSERT_EQ(2u, cases.Length());
@@ -1461,25 +1312,24 @@
     ASSERT_EQ(1u, cases[1].selectors.Length());
     EXPECT_TRUE(cases[1].selectors[0].IsDefault());
 
-    EXPECT_EQ(1u, flow->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[0].Start().target->InboundBranches().Length());
-    EXPECT_EQ(1u, cases[1].Start().target->InboundBranches().Length());
-    EXPECT_EQ(0u, flow->Merge().target->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[0].Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, cases[1].Start()->InboundBranches().Length());
+    EXPECT_EQ(1u, flow->Merge()->InboundBranches().Length());
     EXPECT_EQ(2u, func->EndTarget()->InboundBranches().Length());
 
-    EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %fn3 # branch
+    EXPECT_EQ(Disassemble(m),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  switch 1i [c: (0i, %fn3), c: (default, %fn4)]
+}
 
-  %fn3 = switch 1i [c: (0i, %fn4), c: (default, %fn5)]
-    # case 0i
-    %fn4 = block {
-    } -> %func_end # return
-    # case default
-    %fn5 = block {
-    } -> %func_end # return
-} %func_end
+%fn3 = block {
+  br %fn5  # return
+}
+%fn4 = block {
+  br %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -1492,16 +1342,18 @@
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
     EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func b():i32 {
-  %fn2 = block {
-  } -> %func_end 1i # return
-} %func_end
+              R"(%fn1 = func b():i32 -> %fn2
+%fn2 = block {
+  br %fn3 1i  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:i32 = call b
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:i32 = call b
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/from_program_unary_test.cc b/src/tint/ir/from_program_unary_test.cc
index 2afd2a2..a47acf6 100644
--- a/src/tint/ir/from_program_unary_test.cc
+++ b/src/tint/ir/from_program_unary_test.cc
@@ -34,17 +34,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
-  %fn2 = block {
-  } -> %func_end false # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool -> %fn2
+%fn2 = block {
+  br %fn3 false  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:bool = call my_func
-    %tint_symbol:bool = eq %1, false
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:bool = call my_func
+  %tint_symbol:bool = eq %1, false
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -57,17 +59,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
-  %fn2 = block {
-  } -> %func_end 1u # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 -> %fn2
+%fn2 = block {
+  br %fn3 1u  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:u32 = call my_func
-    %tint_symbol:u32 = complement %1
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:u32 = call my_func
+  %tint_symbol:u32 = complement %1
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -80,17 +84,19 @@
     auto m = Build();
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():i32 {
-  %fn2 = block {
-  } -> %func_end 1i # return
-} %func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():i32 -> %fn2
+%fn2 = block {
+  br %fn3 1i  # return
+}
+%fn3 = func_terminator
 
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn4 = block {
-    %1:i32 = call my_func
-    %tint_symbol:i32 = negation %1
-  } -> %func_end # return
-} %func_end
+%fn4 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn5
+%fn5 = block {
+  %1:i32 = call my_func
+  %tint_symbol:i32 = negation %1
+  jmp %fn6  # return
+}
+%fn6 = func_terminator
 
 )");
 }
@@ -106,13 +112,16 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v2:ptr<private, i32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
@@ -130,14 +139,17 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %v3:ptr<private, i32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn3 = block {
-    store %v3, 42i
-  } -> %func_end # return
-} %func_end
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn4
+%fn4 = block {
+  store %v3, 42i
+  jmp %fn5  # return
+}
+%fn5 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/from_program_var_test.cc b/src/tint/ir/from_program_var_test.cc
index e235f88..2e49265 100644
--- a/src/tint/ir/from_program_var_test.cc
+++ b/src/tint/ir/from_program_var_test.cc
@@ -34,8 +34,10 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %a:ptr<private, u32, read_write> = var
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
 )");
 }
@@ -49,8 +51,10 @@
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
   %a:ptr<private, u32, read_write> = var, 2u
+  br %fn2  # root_end
 }
 
+%fn2 = root_terminator
 
 )");
 }
@@ -63,11 +67,12 @@
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
     EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-    %a:ptr<function, u32, read_write> = var
-  } -> %func_end # return
-} %func_end
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  %a:ptr<function, u32, read_write> = var
+  jmp %fn3  # return
+}
+%fn3 = func_terminator
 
 )");
 }
@@ -81,11 +86,12 @@
     ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
     EXPECT_EQ(Disassemble(m.Get()),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-    %a:ptr<function, u32, read_write> = var, 2u
-  } -> %func_end # return
-} %func_end
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  %a:ptr<function, u32, read_write> = var, 2u
+  jmp %fn3  # return
+}
+%fn3 = func_terminator
 
 )");
 }
diff --git a/src/tint/ir/function.h b/src/tint/ir/function.h
index 86465aa..e966e14 100644
--- a/src/tint/ir/function.h
+++ b/src/tint/ir/function.h
@@ -72,13 +72,8 @@
              type::Type* rt,
              PipelineStage stage = PipelineStage::kUndefined,
              std::optional<std::array<uint32_t, 3>> wg_size = {});
-    Function(Function&&) = delete;
-    Function(const Function&) = delete;
     ~Function() override;
 
-    Function& operator=(Function&&) = delete;
-    Function& operator=(const Function&) = delete;
-
     /// @returns the function name
     Symbol Name() const { return name_; }
 
diff --git a/src/tint/ir/function_param.h b/src/tint/ir/function_param.h
index 1bbb812..2da0584 100644
--- a/src/tint/ir/function_param.h
+++ b/src/tint/ir/function_param.h
@@ -26,13 +26,8 @@
     /// Constructor
     /// @param type the type of the var
     explicit FunctionParam(const type::Type* type);
-    FunctionParam(const FunctionParam& inst) = delete;
-    FunctionParam(FunctionParam&& inst) = delete;
     ~FunctionParam() override;
 
-    FunctionParam& operator=(const FunctionParam& inst) = delete;
-    FunctionParam& operator=(FunctionParam&& inst) = delete;
-
     /// @returns the type of the var
     const type::Type* Type() const override { return type_; }
 
diff --git a/src/tint/ir/if.cc b/src/tint/ir/if.cc
index d235b5e..a89a51f 100644
--- a/src/tint/ir/if.cc
+++ b/src/tint/ir/if.cc
@@ -18,7 +18,16 @@
 
 namespace tint::ir {
 
-If::If(Value* cond) : Base(), condition_(cond) {}
+If::If(Value* cond, Block* t, Block* f, Block* m)
+    : Base(m), condition_(cond), true_(t), false_(f), merge_(m) {
+    TINT_ASSERT(IR, true_);
+    TINT_ASSERT(IR, false_);
+    TINT_ASSERT(IR, merge_);
+
+    condition_->AddUsage(this);
+    true_->AddInboundBranch(this);
+    false_->AddInboundBranch(this);
+}
 
 If::~If() = default;
 
diff --git a/src/tint/ir/if.h b/src/tint/ir/if.h
index d02c32a..ad4db1d 100644
--- a/src/tint/ir/if.h
+++ b/src/tint/ir/if.h
@@ -15,8 +15,8 @@
 #ifndef SRC_TINT_IR_IF_H_
 #define SRC_TINT_IR_IF_H_
 
+#include "src/tint/ir/block.h"
 #include "src/tint/ir/branch.h"
-#include "src/tint/ir/flow_node.h"
 #include "src/tint/ir/value.h"
 
 // Forward declarations
@@ -26,42 +26,42 @@
 
 namespace tint::ir {
 
-/// A flow node representing an if statement.
-class If : public utils::Castable<If, FlowNode> {
+/// An if instruction
+class If : public utils::Castable<If, Branch> {
   public:
     /// Constructor
     /// @param cond the if condition
-    explicit If(Value* cond);
-    If(const If&) = delete;
-    If(If&&) = delete;
+    /// @param t the true block
+    /// @param f the false block
+    /// @param m the merge block
+    explicit If(Value* cond, Block* t, Block* f, Block* m);
     ~If() override;
 
-    If& operator=(const If&) = delete;
-    If& operator=(If&&) = delete;
-
     /// @returns the if condition
     const Value* Condition() const { return condition_; }
+    /// @returns the if condition
+    Value* Condition() { return condition_; }
 
     /// @returns the true branch block
-    const Branch& True() const { return true_; }
+    const Block* True() const { return true_; }
     /// @returns the true branch block
-    Branch& True() { return true_; }
+    Block* True() { return true_; }
 
     /// @returns the false branch block
-    const Branch& False() const { return false_; }
+    const Block* False() const { return false_; }
     /// @returns the false branch block
-    Branch& False() { return false_; }
+    Block* False() { return false_; }
 
     /// @returns the merge branch block
-    const Branch& Merge() const { return merge_; }
+    const Block* Merge() const { return merge_; }
     /// @returns the merge branch block
-    Branch& Merge() { return merge_; }
+    Block* Merge() { return merge_; }
 
   private:
-    Branch true_ = {};
-    Branch false_ = {};
-    Branch merge_ = {};
-    Value* condition_;
+    Value* condition_ = nullptr;
+    Block* true_ = nullptr;
+    Block* false_ = nullptr;
+    Block* merge_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 8b52f01..c3c18cd 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -23,14 +23,9 @@
 /// An instruction in the IR.
 class Instruction : public utils::Castable<Instruction, Value> {
   public:
-    Instruction(const Instruction& inst) = delete;
-    Instruction(Instruction&& inst) = delete;
     /// Destructor
     ~Instruction() override;
 
-    Instruction& operator=(const Instruction& inst) = delete;
-    Instruction& operator=(Instruction&& inst) = delete;
-
   protected:
     /// Constructor
     Instruction();
diff --git a/src/tint/ir/jump.cc b/src/tint/ir/jump.cc
new file mode 100644
index 0000000..cda2f06
--- /dev/null
+++ b/src/tint/ir/jump.cc
@@ -0,0 +1,25 @@
+// 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.
+
+#include "src/tint/ir/jump.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Jump);
+
+namespace tint::ir {
+
+Jump::Jump(FlowNode* to, utils::VectorRef<Value*> args) : Base(to, args) {}
+
+Jump::~Jump() = default;
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/jump.h b/src/tint/ir/jump.h
new file mode 100644
index 0000000..3159755
--- /dev/null
+++ b/src/tint/ir/jump.h
@@ -0,0 +1,37 @@
+// 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_JUMP_H_
+#define SRC_TINT_IR_JUMP_H_
+
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/branch.h"
+#include "src/tint/ir/value.h"
+#include "src/tint/utils/castable.h"
+
+namespace tint::ir {
+
+/// A jump instruction. A jump is walk continuing.
+class Jump : public utils::Castable<Jump, Branch> {
+  public:
+    /// Constructor
+    /// @param to the block to branch too
+    /// @param args the branch arguments
+    explicit Jump(FlowNode* to, utils::VectorRef<Value*> args = {});
+    ~Jump() override;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_JUMP_H_
diff --git a/src/tint/ir/load.h b/src/tint/ir/load.h
index e1a365f..bee65be 100644
--- a/src/tint/ir/load.h
+++ b/src/tint/ir/load.h
@@ -27,13 +27,8 @@
     /// @param type the result type
     /// @param from the value being loaded from
     Load(const type::Type* type, Value* from);
-    Load(const Load& inst) = delete;
-    Load(Load&& inst) = delete;
     ~Load() override;
 
-    Load& operator=(const Load& inst) = delete;
-    Load& operator=(Load&& inst) = delete;
-
     /// @returns the type of the value
     const type::Type* Type() const override { return result_type_; }
 
diff --git a/src/tint/ir/loop.cc b/src/tint/ir/loop.cc
index 9a1af45..0bbb710 100644
--- a/src/tint/ir/loop.cc
+++ b/src/tint/ir/loop.cc
@@ -18,7 +18,11 @@
 
 namespace tint::ir {
 
-Loop::Loop() : Base() {}
+Loop::Loop(Block* s, Block* c, Block* m) : Base(s), start_(s), continuing_(c), merge_(m) {
+    TINT_ASSERT(IR, start_);
+    TINT_ASSERT(IR, continuing_);
+    TINT_ASSERT(IR, merge_);
+}
 
 Loop::~Loop() = default;
 
diff --git a/src/tint/ir/loop.h b/src/tint/ir/loop.h
index 590f49c..954a64a 100644
--- a/src/tint/ir/loop.h
+++ b/src/tint/ir/loop.h
@@ -17,41 +17,38 @@
 
 #include "src/tint/ir/block.h"
 #include "src/tint/ir/branch.h"
-#include "src/tint/ir/flow_node.h"
 
 namespace tint::ir {
 
 /// Flow node describing a loop.
-class Loop : public utils::Castable<Loop, FlowNode> {
+class Loop : public utils::Castable<Loop, Branch> {
   public:
     /// Constructor
-    Loop();
-    Loop(const Loop&) = delete;
-    Loop(Loop&&) = delete;
+    /// @param s the start block
+    /// @param c the continuing block
+    /// @param m the merge block
+    Loop(Block* s, Block* c, Block* m);
     ~Loop() override;
 
-    Loop& operator=(const Loop&) = delete;
-    Loop& operator=(Loop&&) = delete;
-
     /// @returns the switch start branch
-    const Branch& Start() const { return start_; }
+    const Block* Start() const { return start_; }
     /// @returns the switch start branch
-    Branch& Start() { return start_; }
+    Block* Start() { return start_; }
 
     /// @returns the switch continuing branch
-    const Branch& Continuing() const { return continuing_; }
+    const Block* Continuing() const { return continuing_; }
     /// @returns the switch continuing branch
-    Branch& Continuing() { return continuing_; }
+    Block* Continuing() { return continuing_; }
 
     /// @returns the switch merge branch
-    const Branch& Merge() const { return merge_; }
+    const Block* Merge() const { return merge_; }
     /// @returns the switch merge branch
-    Branch& Merge() { return merge_; }
+    Block* Merge() { return merge_; }
 
   private:
-    Branch start_ = {};
-    Branch continuing_ = {};
-    Branch merge_ = {};
+    Block* start_ = nullptr;
+    Block* continuing_ = nullptr;
+    Block* merge_ = nullptr;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/root_terminator.h b/src/tint/ir/root_terminator.h
index c6338ba..361aa6d 100644
--- a/src/tint/ir/root_terminator.h
+++ b/src/tint/ir/root_terminator.h
@@ -25,12 +25,7 @@
   public:
     /// Constructor
     RootTerminator();
-    RootTerminator(const RootTerminator&) = delete;
-    RootTerminator(RootTerminator&&) = delete;
     ~RootTerminator() override;
-
-    RootTerminator& operator=(const RootTerminator&) = delete;
-    RootTerminator& operator=(RootTerminator&&) = delete;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/store.h b/src/tint/ir/store.h
index af5377d..374fe54 100644
--- a/src/tint/ir/store.h
+++ b/src/tint/ir/store.h
@@ -27,13 +27,8 @@
     /// @param to the value to store too
     /// @param from the value being stored from
     Store(Value* to, Value* from);
-    Store(const Store& inst) = delete;
-    Store(Store&& inst) = delete;
     ~Store() override;
 
-    Store& operator=(const Store& inst) = delete;
-    Store& operator=(Store&& inst) = delete;
-
     /// @returns the value being stored too
     Value* To() const { return to_; }
 
diff --git a/src/tint/ir/switch.cc b/src/tint/ir/switch.cc
index 3bccc83..a28666a 100644
--- a/src/tint/ir/switch.cc
+++ b/src/tint/ir/switch.cc
@@ -18,7 +18,11 @@
 
 namespace tint::ir {
 
-Switch::Switch(Value* cond) : Base(), condition_(cond) {}
+Switch::Switch(Value* cond, Block* m) : Base(m), condition_(cond), merge_(m) {
+    TINT_ASSERT(IR, condition_);
+    TINT_ASSERT(IR, merge_);
+    condition_->AddUsage(this);
+}
 
 Switch::~Switch() = default;
 
diff --git a/src/tint/ir/switch.h b/src/tint/ir/switch.h
index 2e977ec..588fee9 100644
--- a/src/tint/ir/switch.h
+++ b/src/tint/ir/switch.h
@@ -18,13 +18,12 @@
 #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"
 
 namespace tint::ir {
 
 /// Flow node representing a switch statement
-class Switch : public utils::Castable<Switch, FlowNode> {
+class Switch : public utils::Castable<Switch, Branch> {
   public:
     /// A case selector
     struct CaseSelector {
@@ -40,28 +39,24 @@
         /// The case selector for this node
         utils::Vector<CaseSelector, 4> selectors;
         /// The start block for the case block.
-        Branch start = {};
+        Block* start = nullptr;
 
         /// @returns the case start target
-        const Branch& Start() const { return start; }
+        const Block* Start() const { return start; }
         /// @returns the case start target
-        Branch& Start() { return start; }
+        Block* Start() { return start; }
     };
 
     /// Constructor
     /// @param cond the condition
-    explicit Switch(Value* cond);
-    Switch(const Switch&) = delete;
-    Switch(Switch&&) = delete;
+    /// @param m the merge block
+    explicit Switch(Value* cond, Block* m);
     ~Switch() override;
 
-    Switch& operator=(const Switch&) = delete;
-    Switch& operator=(Switch&&) = delete;
-
     /// @returns the switch merge branch
-    const Branch& Merge() const { return merge_; }
+    const Block* Merge() const { return merge_; }
     /// @returns the switch merge branch
-    Branch& Merge() { return merge_; }
+    Block* Merge() { return merge_; }
 
     /// @returns the switch cases
     utils::VectorRef<Case> Cases() const { return cases_; }
@@ -70,11 +65,13 @@
 
     /// @returns the condition
     const Value* Condition() const { return condition_; }
+    /// @returns the condition
+    Value* Condition() { return condition_; }
 
   private:
-    Branch merge_ = {};
+    Value* condition_ = nullptr;
+    Block* merge_ = nullptr;
     utils::Vector<Case, 4> cases_;
-    Value* condition_;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/to_program.cc b/src/tint/ir/to_program.cc
index 9be189c..c1cb9f9 100644
--- a/src/tint/ir/to_program.cc
+++ b/src/tint/ir/to_program.cc
@@ -23,6 +23,7 @@
 #include "src/tint/ir/function_terminator.h"
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/instruction.h"
+#include "src/tint/ir/jump.h"
 #include "src/tint/ir/load.h"
 #include "src/tint/ir/module.h"
 #include "src/tint/ir/store.h"
@@ -108,25 +109,26 @@
                       std::move(ret_attrs));
     }
 
-    const ast::BlockStatement* FlowNodeGraph(ir::FlowNode* start_node,
-                                             ir::FlowNode* stop_at = nullptr) {
+    const ast::BlockStatement* FlowNodeGraph(const ir::Block* start_node) {
         // TODO(crbug.com/tint/1902): Check if the block is dead
         utils::Vector<const ast::Statement*,
                       decltype(ast::BlockStatement::statements)::static_length>
             stmts;
 
-        ir::Branch root_branch{start_node, {}};
-        const ir::Branch* branch = &root_branch;
+        const ir::FlowNode* block = start_node;
 
         // TODO(crbug.com/tint/1902): Handle block arguments.
 
-        while (branch->target != stop_at) {
-            enum Status { kContinue, kStop, kError };
-            Status status = tint::Switch(
-                branch->target,
+        while (block) {
+            TINT_ASSERT(IR, block->HasBranchTarget());
 
-                [&](const ir::Block* block) {
-                    for (const auto* inst : block->Instructions()) {
+            enum Status { kContinue, kStop, kError };
+
+            Status status = tint::Switch(
+                block,
+
+                [&](const ir::Block* blk) {
+                    for (auto* inst : blk->Instructions()) {
                         auto stmt = Stmt(inst);
                         if (TINT_UNLIKELY(!stmt)) {
                             return kError;
@@ -135,43 +137,27 @@
                             stmts.Push(s);
                         }
                     }
-                    branch = &block->Branch();
-                    return kContinue;
-                },
-
-                [&](const ir::If* if_) {
-                    auto* stmt = If(if_);
-                    if (TINT_UNLIKELY(!stmt)) {
-                        return kError;
-                    }
-                    stmts.Push(stmt);
-                    branch = &if_->Merge();
-                    return branch->target->InboundBranches().IsEmpty() ? kStop : kContinue;
-                },
-
-                [&](const ir::Switch* switch_) {
-                    auto* stmt = Switch(switch_);
-                    if (TINT_UNLIKELY(!stmt)) {
-                        return kError;
-                    }
-                    stmts.Push(stmt);
-                    branch = &switch_->Merge();
-                    return branch->target->InboundBranches().IsEmpty() ? kStop : kContinue;
-                },
-
-                [&](const ir::FunctionTerminator*) {
-                    auto res = FunctionTerminator(branch);
-                    if (TINT_UNLIKELY(!res)) {
-                        return kError;
-                    }
-                    if (auto* stmt = res.Get()) {
-                        stmts.Push(stmt);
+                    if (blk->Branch()->Is<Jump>() && blk->Branch()->To()->Is<Block>()) {
+                        block = blk->Branch()->To()->As<Block>();
+                        return kContinue;
+                    } else if (auto* if_ = blk->Branch()->As<ir::If>()) {
+                        if (if_->Merge()->HasBranchTarget()) {
+                            block = if_->Merge();
+                            return kContinue;
+                        }
+                    } else if (auto* switch_ = blk->Branch()->As<ir::Switch>()) {
+                        if (switch_->Merge()->HasBranchTarget()) {
+                            block = switch_->Merge();
+                            return kContinue;
+                        }
                     }
                     return kStop;
                 },
 
+                [&](const ir::FunctionTerminator*) { return kStop; },
+
                 [&](Default) {
-                    UNHANDLED_CASE(branch->target);
+                    UNHANDLED_CASE(block);
                     return kError;
                 });
 
@@ -188,26 +174,24 @@
 
     const ast::IfStatement* If(const ir::If* i) {
         SCOPED_NESTING();
-
         auto* cond = Expr(i->Condition());
-        auto* t = FlowNodeGraph(i->True().target, i->Merge().target);
+        auto* t = FlowNodeGraph(i->True());
         if (TINT_UNLIKELY(!t)) {
             return nullptr;
         }
 
-        if (!IsEmpty(i->False().target, i->Merge().target)) {
-            // If the else target is an if flow node with the same Merge().target as this if, then
-            // emit an 'else if' instead of a block statement for the else.
-            if (auto* else_if = As<ir::If>(NextNonEmptyNode(i->False().target));
-                else_if &&
-                NextNonEmptyNode(i->Merge().target) == NextNonEmptyNode(else_if->Merge().target)) {
-                auto* f = If(else_if);
+        if (!IsEmpty(i->False(), i->Merge())) {
+            // If the else target is an `if` which has a merge target that just bounces to the outer
+            // if merge target then emit an 'else if' instead of a block statement for the else.
+            if (auto* inst = i->False()->Instructions().Front()->As<ir::If>();
+                inst && inst->Merge()->IsTrampoline(i->Merge())) {
+                auto* f = If(inst);
                 if (!f) {
                     return nullptr;
                 }
                 return b.If(cond, t, b.Else(f));
             } else {
-                auto* f = FlowNodeGraph(i->False().target, i->Merge().target);
+                auto* f = FlowNodeGraph(i->False());
                 if (!f) {
                     return nullptr;
                 }
@@ -226,11 +210,11 @@
             return nullptr;
         }
 
-        auto cases = utils::Transform<1>(
+        auto cases = utils::Transform<2>(
             s->Cases(),  //
-            [&](const ir::Switch::Case& c) -> const tint::ast::CaseStatement* {
+            [&](const ir::Switch::Case c) -> const tint::ast::CaseStatement* {
                 SCOPED_NESTING();
-                auto* body = FlowNodeGraph(c.start.target, s->Merge().target);
+                auto* body = FlowNodeGraph(c.start);
                 if (!body) {
                     return nullptr;
                 }
@@ -261,26 +245,27 @@
     }
 
     utils::Result<const ast::ReturnStatement*> FunctionTerminator(const ir::Branch* branch) {
-        if (branch->args.IsEmpty()) {
+        if (branch->Args().IsEmpty()) {
             // Branch to function terminator has no arguments.
-            // If this block is nested withing some control flow, then we must emit a
-            // 'return' statement, otherwise we've just naturally reached the end of the
-            // function where the 'return' is redundant.
+            // If this block is nested withing some control flow, then we must
+            // emit a 'return' statement, otherwise we've just naturally reached
+            // the end of the function where the 'return' is redundant.
             if (nesting_depth_ > 1) {
                 return b.Return();
             }
             return nullptr;
         }
 
-        // Branch to function terminator has arguments - this is the return value.
-        if (branch->args.Length() != 1) {
-            TINT_ICE(IR, b.Diagnostics())
-                << "expected 1 value for function terminator (return value), got "
-                << branch->args.Length();
+        // Branch to function terminator has arguments - this is the return
+        // value.
+        if (branch->Args().Length() != 1) {
+            TINT_ICE(IR, b.Diagnostics()) << "expected 1 value for function "
+                                             "terminator (return value), got "
+                                          << branch->Args().Length();
             return utils::Failure;
         }
 
-        auto* val = Expr(branch->args.Front());
+        auto* val = Expr(branch->Args().Front());
         if (TINT_UNLIKELY(!val)) {
             return utils::Failure;
         }
@@ -289,36 +274,16 @@
     }
 
     /// @return true if there are no instructions between @p node and and @p stop_at
-    bool IsEmpty(const ir::FlowNode* node, const ir::FlowNode* stop_at) {
-        while (node != stop_at) {
-            if (auto* block = node->As<ir::Block>()) {
-                if (!block->Instructions().IsEmpty()) {
-                    return false;
-                }
-                node = block->Branch().target;
-            } else {
-                return false;
-            }
+    bool IsEmpty(const ir::Block* node, const ir::FlowNode* stop_at) {
+        if (node->Instructions().IsEmpty()) {
+            return true;
         }
-        return true;
-    }
-
-    /// @return the next flow node that isn't an empty block
-    const ir::FlowNode* NextNonEmptyNode(const ir::FlowNode* node) {
-        while (node) {
-            if (auto* block = node->As<ir::Block>()) {
-                for (const auto* inst : block->Instructions()) {
-                    // Load instructions will be inlined, so ignore them.
-                    if (!inst->Is<ir::Load>()) {
-                        return node;
-                    }
-                }
-                node = block->Branch().target;
-            } else {
-                return node;
-            }
+        if (auto* br = node->Instructions().Front()->As<Branch>()) {
+            return br->To() == stop_at;
         }
-        return nullptr;
+        // TODO(dsinclair): This should possibly walk over Jump instructions that
+        // just jump to empty blocks if we want to be comprehensive.
+        return false;
     }
 
     utils::Result<const ast::Statement*> Stmt(const ir::Instruction* inst) {
@@ -328,6 +293,14 @@
             [&](const ir::Var* i) { return Var(i); },        //
             [&](const ir::Load*) { return nullptr; },
             [&](const ir::Store* i) { return Store(i); },  //
+            [&](const ir::If* if_) { return If(if_); },
+            [&](const ir::Switch* switch_) { return Switch(switch_); },
+            [&](const ir::Branch* branch) {
+                if (branch->To()->Is<ir::FunctionTerminator>()) {
+                    return utils::Result<const ast::Statement*>{FunctionTerminator(branch)};
+                }
+                return utils::Result<const ast::Statement*>{nullptr};
+            },
             [&](Default) {
                 UNHANDLED_CASE(inst);
                 return utils::Failure;
diff --git a/src/tint/ir/to_program_roundtrip_test.cc b/src/tint/ir/to_program_roundtrip_test.cc
index 8f26792..c8af503 100644
--- a/src/tint/ir/to_program_roundtrip_test.cc
+++ b/src/tint/ir/to_program_roundtrip_test.cc
@@ -229,10 +229,9 @@
 
 fn f() {
   var cond_a : bool = true;
-  var cond_b : bool = true;
   if (cond_a) {
     a();
-  } else if (cond_b) {
+  } else if (false) {
     b();
   }
   c();
diff --git a/src/tint/ir/transform/add_empty_entry_point.cc b/src/tint/ir/transform/add_empty_entry_point.cc
index 6788d7c..f40c60e 100644
--- a/src/tint/ir/transform/add_empty_entry_point.cc
+++ b/src/tint/ir/transform/add_empty_entry_point.cc
@@ -38,7 +38,7 @@
     auto* ep =
         builder.CreateFunction(ir->symbols.New("unused_entry_point"), ir->types.Get<type::Void>(),
                                Function::PipelineStage::kCompute, std::array{1u, 1u, 1u});
-    ep->StartTarget()->BranchTo(ep->EndTarget());
+    ep->StartTarget()->SetInstructions(utils::Vector{builder.Branch(ep->EndTarget())});
     ir->functions.Push(ep);
 }
 
diff --git a/src/tint/ir/transform/add_empty_entry_point_test.cc b/src/tint/ir/transform/add_empty_entry_point_test.cc
index a363d83..c86c09c 100644
--- a/src/tint/ir/transform/add_empty_entry_point_test.cc
+++ b/src/tint/ir/transform/add_empty_entry_point_test.cc
@@ -25,10 +25,11 @@
 
 TEST_F(IR_AddEmptyEntryPointTest, EmptyModule) {
     auto* expect = R"(
-%fn1 = func unused_entry_point():void [@compute @workgroup_size(1, 1, 1)] {
-  %fn2 = block {
-  } -> %func_end # return
-} %func_end
+%fn1 = func unused_entry_point():void [@compute @workgroup_size(1, 1, 1)] -> %fn2
+%fn2 = block {
+  br %fn3  # return
+}
+%fn3 = func_terminator
 
 )";
 
@@ -38,16 +39,17 @@
 }
 
 TEST_F(IR_AddEmptyEntryPointTest, ExistingEntryPoint) {
-    auto* ep = b.CreateFunction(mod.symbols.New("main"), mod.types.Get<type::Void>(),
-                                Function::PipelineStage::kFragment);
-    ep->StartTarget()->BranchTo(ep->EndTarget());
+    auto* ep =
+        b.CreateFunction("main", mod.types.Get<type::Void>(), Function::PipelineStage::kFragment);
+    ep->StartTarget()->SetInstructions(utils::Vector{b.Branch(ep->EndTarget())});
     mod.functions.Push(ep);
 
     auto* expect = R"(
-%fn1 = func main():void [@fragment] {
-  %fn2 = block {
-  } -> %func_end # return
-} %func_end
+%fn1 = func main():void [@fragment] -> %fn2
+%fn2 = block {
+  br %fn3  # return
+}
+%fn3 = func_terminator
 
 )";
 
diff --git a/src/tint/ir/unary.h b/src/tint/ir/unary.h
index 98ad9b8..698413d 100644
--- a/src/tint/ir/unary.h
+++ b/src/tint/ir/unary.h
@@ -34,18 +34,15 @@
     /// @param result_type the result type
     /// @param val the input value for the instruction
     Unary(enum Kind kind, const type::Type* result_type, Value* val);
-    Unary(const Unary& inst) = delete;
-    Unary(Unary&& inst) = delete;
     ~Unary() override;
 
-    Unary& operator=(const Unary& inst) = delete;
-    Unary& operator=(Unary&& inst) = delete;
-
     /// @returns the type of the value
     const type::Type* Type() const override { return result_type_; }
 
     /// @returns the value for the instruction
     const Value* Val() const { return val_; }
+    /// @returns the value for the instruction
+    Value* Val() { return val_; }
 
     /// @returns the kind of unary instruction
     enum Kind Kind() const { return kind_; }
diff --git a/src/tint/ir/unary_test.cc b/src/tint/ir/unary_test.cc
index 6cefed3..bb0b4f2 100644
--- a/src/tint/ir/unary_test.cc
+++ b/src/tint/ir/unary_test.cc
@@ -26,7 +26,7 @@
 TEST_F(IR_InstructionTest, CreateComplement) {
     Module mod;
     Builder b{mod};
-    const auto* inst = b.Complement(b.ir.types.Get<type::I32>(), b.Constant(4_i));
+    auto* inst = b.Complement(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Unary>());
     EXPECT_EQ(inst->Kind(), Unary::Kind::kComplement);
@@ -40,7 +40,7 @@
 TEST_F(IR_InstructionTest, CreateNegation) {
     Module mod;
     Builder b{mod};
-    const auto* inst = b.Negation(b.ir.types.Get<type::I32>(), b.Constant(4_i));
+    auto* inst = b.Negation(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Unary>());
     EXPECT_EQ(inst->Kind(), Unary::Kind::kNegation);
@@ -54,7 +54,7 @@
 TEST_F(IR_InstructionTest, Unary_Usage) {
     Module mod;
     Builder b{mod};
-    const auto* inst = b.Negation(b.ir.types.Get<type::I32>(), b.Constant(4_i));
+    auto* inst = b.Negation(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     EXPECT_EQ(inst->Kind(), Unary::Kind::kNegation);
 
diff --git a/src/tint/ir/user_call.h b/src/tint/ir/user_call.h
index ba52e20..9e2e4c9 100644
--- a/src/tint/ir/user_call.h
+++ b/src/tint/ir/user_call.h
@@ -29,13 +29,8 @@
     /// @param name the function name
     /// @param args the function arguments
     UserCall(const type::Type* type, Symbol name, utils::VectorRef<Value*> args);
-    UserCall(const UserCall& inst) = delete;
-    UserCall(UserCall&& inst) = delete;
     ~UserCall() override;
 
-    UserCall& operator=(const UserCall& inst) = delete;
-    UserCall& operator=(UserCall&& inst) = delete;
-
     /// @returns the called function name
     Symbol Name() const { return name_; }
 
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index 4b7810b..b090ad8 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -32,12 +32,6 @@
     /// Destructor
     ~Value() override;
 
-    Value(const Value&) = delete;
-    Value(Value&&) = delete;
-
-    Value& operator=(const Value&) = delete;
-    Value& operator=(Value&&) = delete;
-
     /// Adds an instruction which uses this value.
     /// @param inst the instruction
     void AddUsage(const Instruction* inst) { uses_.Add(inst); }
diff --git a/src/tint/ir/var.h b/src/tint/ir/var.h
index 67ccf0d..8561e5c 100644
--- a/src/tint/ir/var.h
+++ b/src/tint/ir/var.h
@@ -28,13 +28,8 @@
     /// Constructor
     /// @param type the type of the var
     explicit Var(const type::Type* type);
-    Var(const Var& inst) = delete;
-    Var(Var&& inst) = delete;
     ~Var() override;
 
-    Var& operator=(const Var& inst) = delete;
-    Var& operator=(Var&& inst) = delete;
-
     /// @returns the type of the var
     const type::Type* Type() const override { return type_; }
 
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
index bb5d222..d0268c6 100644
--- a/src/tint/reader/spirv/function.cc
+++ b/src/tint/reader/spirv/function.cc
@@ -2844,7 +2844,7 @@
     const std::string guard_name = block_info.flow_guard_name;
     if (!guard_name.empty()) {
         // Declare the guard variable just before the "if", initialized to true.
-        auto* guard_var = builder_.Var(guard_name, builder_.ty.bool_(), MakeTrue(Source{}));
+        auto* guard_var = builder_.Var(guard_name, MakeTrue(Source{}));
         auto* guard_decl = create<ast::VariableDeclStatement>(Source{}, guard_var);
         AddStatement(guard_decl);
     }
@@ -3448,7 +3448,7 @@
 
     expr = AddressOfIfNeeded(expr, &inst);
     expr.type = RemapPointerProperties(expr.type, inst.result_id());
-    auto* let = parser_impl_.MakeLet(inst.result_id(), expr.type, expr.expr);
+    auto* let = parser_impl_.MakeLet(inst.result_id(), expr.expr);
     if (!let) {
         return false;
     }
@@ -6212,7 +6212,7 @@
     //   variable with the %src_vector contents, then write the component,
     //   and then make a let-declaration that reads the value out:
     //
-    //    var temp : type = src_vector;
+    //    var temp = src_vector;
     //    temp[index] = component;
     //    let result : type = temp;
     //
@@ -6238,8 +6238,7 @@
         // API in parser_impl_.
         var_name = namer_.MakeDerivedName(original_value_name);
 
-        auto* temp_var = builder_.Var(var_name, type->Build(builder_),
-                                      builtin::AddressSpace::kUndefined, src_vector.expr);
+        auto* temp_var = builder_.Var(var_name, builtin::AddressSpace::kUndefined, src_vector.expr);
 
         AddStatement(builder_.Decl({}, temp_var));
     }
@@ -6278,7 +6277,7 @@
     //   variable with the %composite contents, then write the component,
     //   and then make a let-declaration that reads the value out:
     //
-    //    var temp : type = composite;
+    //    var temp = composite;
     //    temp[index].x = object;
     //    let result : type = temp;
     //
@@ -6308,8 +6307,8 @@
         // It doesn't correspond to a SPIR-V ID, so we don't use the ordinary
         // API in parser_impl_.
         var_name = namer_.MakeDerivedName(original_value_name);
-        auto* temp_var = builder_.Var(var_name, type->Build(builder_),
-                                      builtin::AddressSpace::kUndefined, src_composite.expr);
+        auto* temp_var =
+            builder_.Var(var_name, builtin::AddressSpace::kUndefined, src_composite.expr);
         AddStatement(builder_.Decl({}, temp_var));
     }
 
diff --git a/src/tint/reader/spirv/function_arithmetic_test.cc b/src/tint/reader/spirv/function_arithmetic_test.cc
index 538e410..ed26800 100644
--- a/src/tint/reader/spirv/function_arithmetic_test.cc
+++ b/src/tint/reader/spirv/function_arithmetic_test.cc
@@ -122,7 +122,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : i32 = -(30i);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = -(30i);"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_Int_Uint) {
@@ -139,7 +139,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : i32 = -(bitcast<i32>(10u));"));
+                HasSubstr("let x_1 = -(bitcast<i32>(10u));"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_Uint_Int) {
@@ -156,7 +156,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = bitcast<u32>(-(30i));"));
+                HasSubstr("let x_1 = bitcast<u32>(-(30i));"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_Uint_Uint) {
@@ -173,7 +173,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = bitcast<u32>(-(bitcast<i32>(10u)));"));
+                HasSubstr("let x_1 = bitcast<u32>(-(bitcast<i32>(10u)));"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_SignedVec_SignedVec) {
@@ -189,8 +189,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2i = -(vec2i(30i, 40i));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = -(vec2i(30i, 40i));"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_SignedVec_UnsignedVec) {
@@ -207,7 +206,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2i = -(bitcast<vec2i>(vec2u(10u, 20u)));"));
+                HasSubstr("let x_1 = -(bitcast<vec2i>(vec2u(10u, 20u)));"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_UnsignedVec_SignedVec) {
@@ -224,7 +223,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2u = bitcast<vec2u>(-(vec2i(30i, 40i)));"));
+                HasSubstr("let x_1 = bitcast<vec2u>(-(vec2i(30i, 40i)));"));
 }
 
 TEST_F(SpvUnaryArithTest, SNegate_UnsignedVec_UnsignedVec) {
@@ -240,9 +239,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr(R"(let x_1 : vec2u = bitcast<vec2u>(-(bitcast<vec2i>(vec2u(10u, 20u))));)"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr(R"(let x_1 = bitcast<vec2u>(-(bitcast<vec2i>(vec2u(10u, 20u))));)"));
 }
 
 TEST_F(SpvUnaryArithTest, FNegate_Scalar) {
@@ -258,7 +256,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : f32 = -(50.0f);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = -(50.0f);"));
 }
 
 TEST_F(SpvUnaryArithTest, FNegate_Vector) {
@@ -274,8 +272,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = -(v2float_50_60);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = -(v2float_50_60);"));
 }
 
 struct BinaryData {
@@ -313,8 +310,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     utils::StringStream ss;
-    ss << "let x_1 : " << GetParam().ast_type << " = (" << GetParam().ast_lhs << " "
-       << GetParam().ast_op << " " << GetParam().ast_rhs << ");";
+    ss << "let x_1 = (" << GetParam().ast_lhs << " " << GetParam().ast_op << " "
+       << GetParam().ast_rhs << ");";
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     EXPECT_THAT(got, HasSubstr(ss.str())) << "got:\n" << got << assembly;
@@ -352,7 +349,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     utils::StringStream ss;
-    ss << "let x_1 : " << GetParam().wgsl_type << " = " << GetParam().expected << ";";
+    ss << "let x_1 = " << GetParam().expected << ";";
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     EXPECT_THAT(got, HasSubstr(ss.str())) << "got:\n" << got << assembly;
@@ -547,7 +544,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = bitcast<u32>((30i / 40i));"));
+                HasSubstr("let x_1 = bitcast<u32>((30i / 40i));"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, SDiv_Vector_UnsignedResult) {
@@ -567,9 +564,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr(R"(let x_1 : vec2u = bitcast<vec2u>((vec2i(30i, 40i) / vec2i(40i, 30i)));)"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr(R"(let x_1 = bitcast<vec2u>((vec2i(30i, 40i) / vec2i(40i, 30i)));)"));
 }
 
 INSTANTIATE_TEST_SUITE_P(SpvParserTest_FDiv,
@@ -636,7 +632,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = bitcast<u32>((30i % 40i));"));
+                HasSubstr("let x_1 = bitcast<u32>((30i % 40i));"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, SMod_Vector_UnsignedResult) {
@@ -656,9 +652,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr(R"(let x_1 : vec2u = bitcast<vec2u>((vec2i(30i, 40i) % vec2i(40i, 30i)));)"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr(R"(let x_1 = bitcast<vec2u>((vec2i(30i, 40i) % vec2i(40i, 30i)));)"));
 }
 
 INSTANTIATE_TEST_SUITE_P(SpvParserTest_FRem,
@@ -685,7 +680,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : f32 = (50.0f - (60.0f * floor((50.0f / 60.0f))));"));
+                HasSubstr("let x_1 = (50.0f - (60.0f * floor((50.0f / 60.0f))));"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, FMod_Vector) {
@@ -702,7 +697,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = (v2float_50_60 - (v2float_60_50 * "
+                HasSubstr("let x_1 = (v2float_50_60 - (v2float_60_50 * "
                           "floor((v2float_50_60 / v2float_60_50))));"));
 }
 
@@ -721,8 +716,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : vec2f = (x_1 * x_2);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = (x_1 * x_2);"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, MatrixTimesScalar) {
@@ -740,8 +734,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : mat2x2f = (x_1 * x_2);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = (x_1 * x_2);"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, VectorTimesMatrix) {
@@ -759,8 +752,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : vec2f = (x_1 * x_2);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = (x_1 * x_2);"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, MatrixTimesVector) {
@@ -778,8 +770,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : vec2f = (x_1 * x_2);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = (x_1 * x_2);"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, MatrixTimesMatrix) {
@@ -797,8 +788,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : mat2x2f = (x_1 * x_2);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = (x_1 * x_2);"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, Dot) {
@@ -816,8 +806,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_3 : f32 = dot(x_1, x_2);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_3 = dot(x_1, x_2);"));
 }
 
 TEST_F(SpvBinaryArithTestBasic, OuterProduct) {
@@ -838,7 +827,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr("let x_3 : mat2x3f = mat2x3f("
+    EXPECT_THAT(got, HasSubstr("let x_3 = mat2x3f("
                                "vec3f((x_2.x * x_1.x), (x_2.x * x_1.y), (x_2.x * x_1.z)), "
                                "vec3f((x_2.y * x_1.x), (x_2.y * x_1.y), (x_2.y * x_1.z)));"))
         << got;
@@ -888,7 +877,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_2 : " + arg.ast_type + " = " + builtin.wgsl + "(x_1);"));
+                HasSubstr("let x_2 = " + builtin.wgsl + "(x_1);"));
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -920,7 +909,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
-    const auto* expected = "let x_2 : mat2x2f = transpose(x_1);";
+    const auto* expected = "let x_2 = transpose(x_1);";
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
     EXPECT_THAT(got, HasSubstr(expected)) << got;
@@ -942,7 +931,7 @@
     // Note, in the AST dump mat_2_3 means 2 rows and 3 columns.
     // So the column vectors have 2 elements.
     // That is,   %m3v2float is __mat_2_3f32.
-    const auto* expected = "let x_2 : mat3x2f = transpose(x_1);";
+    const auto* expected = "let x_2 = transpose(x_1);";
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
     EXPECT_THAT(got, HasSubstr(expected)) << got;
@@ -961,7 +950,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
-    const auto* expected = "let x_2 : mat2x3f = transpose(x_1);";
+    const auto* expected = "let x_2 = transpose(x_1);";
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
     EXPECT_THAT(got, HasSubstr(expected)) << got;
diff --git a/src/tint/reader/spirv/function_bit_test.cc b/src/tint/reader/spirv/function_bit_test.cc
index 741126b..71c3d45 100644
--- a/src/tint/reader/spirv/function_bit_test.cc
+++ b/src/tint/reader/spirv/function_bit_test.cc
@@ -129,8 +129,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     utils::StringStream ss;
-    ss << "let x_1 : " << GetParam().ast_type << " = (" << GetParam().ast_lhs << " "
-       << GetParam().ast_op << " " << GetParam().ast_rhs << ");";
+    ss << "let x_1 = (" << GetParam().ast_lhs << " " << GetParam().ast_op << " "
+       << GetParam().ast_rhs << ");";
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(ss.str())) << assembly;
 }
@@ -167,7 +167,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error() << assembly;
     utils::StringStream ss;
-    ss << "let x_1 : " << GetParam().wgsl_type << " = " << GetParam().expected << ";\nreturn;\n";
+    ss << "let x_1 = " << GetParam().expected << ";\nreturn;\n";
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     EXPECT_THAT(got, HasSubstr(ss.str())) << "got:\n" << got << assembly;
@@ -445,7 +445,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = ~(30i);"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = ~(30i);"));
 }
 
 TEST_F(SpvUnaryBitTest, Not_Int_Uint) {
@@ -462,7 +462,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = bitcast<i32>(~(10u));"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<i32>(~(10u));"));
 }
 
 TEST_F(SpvUnaryBitTest, Not_Uint_Int) {
@@ -479,7 +479,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = bitcast<u32>(~(30i));"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<u32>(~(30i));"));
 }
 
 TEST_F(SpvUnaryBitTest, Not_Uint_Uint) {
@@ -496,7 +496,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = ~(10u);"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = ~(10u);"));
 }
 
 TEST_F(SpvUnaryBitTest, Not_SignedVec_SignedVec) {
@@ -513,7 +513,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = ~(vec2i(30i, 40i));"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = ~(vec2i(30i, 40i));"));
 }
 
 TEST_F(SpvUnaryBitTest, Not_SignedVec_UnsignedVec) {
@@ -530,7 +530,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = bitcast<vec2i>(~(vec2u(10u, 20u)));"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<vec2i>(~(vec2u(10u, 20u)));"));
 }
 
 TEST_F(SpvUnaryBitTest, Not_UnsignedVec_SignedVec) {
@@ -547,7 +547,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = bitcast<vec2u>(~(vec2i(30i, 40i)));"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<vec2u>(~(vec2i(30i, 40i)));"));
 }
 TEST_F(SpvUnaryBitTest, Not_UnsignedVec_UnsignedVec) {
     const auto assembly = SimplePreamble() + R"(
@@ -563,7 +563,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = ~(vec2u(10u, 20u));"));
+    EXPECT_THAT(body, HasSubstr("let x_1 = ~(vec2u(10u, 20u));"));
 }
 
 std::string BitTestPreamble() {
@@ -604,7 +604,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = countOneBits(u1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = countOneBits(u1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_Uint_Int) {
@@ -619,7 +619,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = bitcast<u32>(countOneBits(i1));")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<u32>(countOneBits(i1));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_Int_Uint) {
@@ -634,7 +634,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = bitcast<i32>(countOneBits(u1));")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<i32>(countOneBits(u1));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_Int_Int) {
@@ -649,7 +649,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = countOneBits(i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = countOneBits(i1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_UintVector_UintVector) {
@@ -664,7 +664,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = countOneBits(v2u1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = countOneBits(v2u1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_UintVector_IntVector) {
@@ -679,7 +679,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = bitcast<vec2u>(countOneBits(v2i1));")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<vec2u>(countOneBits(v2i1));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_IntVector_UintVector) {
@@ -694,7 +694,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = bitcast<vec2i>(countOneBits(v2u1));")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<vec2i>(countOneBits(v2u1));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitCount_IntVector_IntVector) {
@@ -709,7 +709,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = countOneBits(v2i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = countOneBits(v2i1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitReverse_Uint_Uint) {
@@ -724,7 +724,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = reverseBits(u1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = reverseBits(u1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitReverse_Uint_Int) {
@@ -763,7 +763,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = reverseBits(i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = reverseBits(i1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitReverse_UintVector_UintVector) {
@@ -778,7 +778,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = reverseBits(v2u1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = reverseBits(v2u1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, BitReverse_UintVector_IntVector) {
@@ -817,7 +817,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = reverseBits(v2i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = reverseBits(v2i1);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, InsertBits_Int) {
@@ -832,7 +832,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = insertBits(30i, 40i, 10u, 20u);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = insertBits(30i, 40i, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, InsertBits_Int_SignedOffsetAndCount) {
@@ -847,8 +847,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = insertBits(30i, 40i, u32(10i), u32(20i));"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = insertBits(30i, 40i, u32(10i), u32(20i));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, InsertBits_IntVector) {
@@ -863,8 +862,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr(R"(let x_1 : vec2i = insertBits(x_28, vec2i(40i, 30i), 10u, 20u);)"))
+    EXPECT_THAT(body, HasSubstr(R"(let x_1 = insertBits(x_28, vec2i(40i, 30i), 10u, 20u);)"))
         << body;
 }
 
@@ -880,9 +878,8 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr(R"(let x_1 : vec2i = insertBits(x_28, vec2i(40i, 30i), u32(10i), u32(20i));)"))
+    EXPECT_THAT(body,
+                HasSubstr(R"(let x_1 = insertBits(x_28, vec2i(40i, 30i), u32(10i), u32(20i));)"))
         << body;
 }
 
@@ -898,7 +895,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = insertBits(20u, 10u, 10u, 20u);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = insertBits(20u, 10u, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, InsertBits_Uint_SignedOffsetAndCount) {
@@ -913,8 +910,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = insertBits(20u, 10u, u32(10i), u32(20i));"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = insertBits(20u, 10u, u32(10i), u32(20i));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, InsertBits_UintVector) {
@@ -929,8 +925,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr(R"(let x_1 : vec2u = insertBits(x_26, vec2u(20u, 10u), 10u, 20u);)"))
+    EXPECT_THAT(body, HasSubstr(R"(let x_1 = insertBits(x_26, vec2u(20u, 10u), 10u, 20u);)"))
         << body;
 }
 
@@ -946,9 +941,8 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr(R"(let x_1 : vec2u = insertBits(x_26, vec2u(20u, 10u), u32(10i), u32(20i));)"))
+    EXPECT_THAT(body,
+                HasSubstr(R"(let x_1 = insertBits(x_26, vec2u(20u, 10u), u32(10i), u32(20i));)"))
         << body;
 }
 
@@ -964,7 +958,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = extractBits(30i, 10u, 20u);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(30i, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_Int_SignedOffsetAndCount) {
@@ -979,7 +973,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = extractBits(30i, u32(10i), u32(20i));")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(30i, u32(10i), u32(20i));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_IntVector) {
@@ -994,7 +988,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = extractBits(x_28, 10u, 20u);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(x_28, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_IntVector_SignedOffsetAndCount) {
@@ -1009,8 +1003,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = extractBits(x_28, u32(10i), u32(20i));"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(x_28, u32(10i), u32(20i));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_Uint) {
@@ -1025,7 +1018,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = extractBits(20u, 10u, 20u);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(20u, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_Uint_SignedOffsetAndCount) {
@@ -1040,7 +1033,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = extractBits(20u, u32(10i), u32(20i));")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(20u, u32(10i), u32(20i));")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_UintVector) {
@@ -1055,7 +1048,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = extractBits(x_26, 10u, 20u);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(x_26, 10u, 20u);")) << body;
 }
 
 TEST_F(SpvUnaryBitTest, ExtractBits_UintVector_SignedOffsetAndCount) {
@@ -1070,8 +1063,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = extractBits(x_26, u32(10i), u32(20i));"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = extractBits(x_26, u32(10i), u32(20i));")) << body;
 }
 
 }  // namespace
diff --git a/src/tint/reader/spirv/function_call_test.cc b/src/tint/reader/spirv/function_call_test.cc
index 145a6fb..c15274f 100644
--- a/src/tint/reader/spirv/function_call_test.cc
+++ b/src/tint/reader/spirv/function_call_test.cc
@@ -136,7 +136,7 @@
         f50 = fe.ast_body();
     }
     auto program = p->program();
-    EXPECT_THAT(test::ToString(program, f100), HasSubstr("let x_1 : u32 = x_50();\nreturn;"));
+    EXPECT_THAT(test::ToString(program, f100), HasSubstr("let x_1 = x_50();\nreturn;"));
     EXPECT_THAT(test::ToString(program, f50), HasSubstr("return 42u;"));
 }
 
@@ -178,7 +178,7 @@
     }
     auto program = p->program();
     EXPECT_EQ(test::ToString(program, f100), R"(var x_10 : u32;
-let x_1 : u32 = x_50();
+let x_1 = x_50();
 x_10 = x_1;
 x_10 = x_1;
 return;
@@ -217,7 +217,7 @@
 }
 
 fn x_100_1() {
-  let x_1 : u32 = x_50(42u, 84u);
+  let x_1 = x_50(42u, 84u);
   return;
 }
 
diff --git a/src/tint/reader/spirv/function_cfg_test.cc b/src/tint/reader/spirv/function_cfg_test.cc
index 36d085a..b57633d 100644
--- a/src/tint/reader/spirv/function_cfg_test.cc
+++ b/src/tint/reader/spirv/function_cfg_test.cc
@@ -7192,7 +7192,7 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var_1 = 1u;
-var guard10 : bool = true;
+var guard10 = true;
 if (false) {
   var_1 = 2u;
   if (true) {
@@ -7249,7 +7249,7 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var_1 = 1u;
-var guard10 : bool = true;
+var guard10 = true;
 if (false) {
   var_1 = 2u;
   guard10 = false;
@@ -7320,7 +7320,7 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var_1 = 1u;
-var guard10 : bool = true;
+var guard10 = true;
 if (false) {
   var_1 = 2u;
   if (true) {
diff --git a/src/tint/reader/spirv/function_composite_test.cc b/src/tint/reader/spirv/function_composite_test.cc
index cb8bc42..442d6fc 100644
--- a/src/tint/reader/spirv/function_composite_test.cc
+++ b/src/tint/reader/spirv/function_composite_test.cc
@@ -95,10 +95,9 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr(R"(let x_1 : vec2u = vec2u(10u, 20u);
-let x_2 : vec2i = vec2i(30i, 40i);
-let x_3 : vec2f = vec2f(50.0f, 60.0f);
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_1 = vec2u(10u, 20u);
+let x_2 = vec2i(30i, 40i);
+let x_3 = vec2f(50.0f, 60.0f);
 )"));
 }
 
@@ -115,7 +114,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : mat3x2f = mat3x2f("
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = mat3x2f("
                                                                   "vec2f(50.0f, 60.0f), "
                                                                   "vec2f(60.0f, 50.0f), "
                                                                   "vec2f(70.0f, 70.0f));"));
@@ -135,7 +134,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : array<u32, 5u> = array<u32, 5u>(10u, 20u, 3u, 4u, 5u);"));
+                HasSubstr("let x_1 = array<u32, 5u>(10u, 20u, 3u, 4u, 5u);"));
 }
 
 TEST_F(SpvParserTest_Composite_Construct, Struct) {
@@ -152,7 +151,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : S = S(vec2f(50.0f, 60.0f), 5u, 30i);"));
+                HasSubstr("let x_1 = S(vec2f(50.0f, 60.0f), 5u, 30i);"));
 }
 
 TEST_F(SpvParserTest_Composite_Construct, ConstantComposite_Struct_NoDeduplication) {
@@ -177,8 +176,8 @@
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
     const auto expected = std::string(
-        R"(let x_2 : S_1 = S_1(10u);
-let x_3 : S_2 = S_2(10u);
+        R"(let x_2 = S_1(10u);
+let x_3 = S_2(10u);
 return;
 )");
     EXPECT_EQ(got, expected) << got;
@@ -200,7 +199,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : f32 = vec2f(50.0f, 60.0f).y;"));
+                HasSubstr("let x_1 = vec2f(50.0f, 60.0f).y;"));
 }
 
 TEST_F(SpvParserTest_CompositeExtract, Vector_IndexTooBigError) {
@@ -237,7 +236,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 : vec2f = x_1[2u];"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 = x_1[2u];"));
 }
 
 TEST_F(SpvParserTest_CompositeExtract, Matrix_IndexTooBigError) {
@@ -278,7 +277,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 : f32 = x_1[2u].y;"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 = x_1[2u].y;"));
 }
 
 TEST_F(SpvParserTest_CompositeExtract, Array) {
@@ -298,7 +297,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 : u32 = x_1[3u];"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 = x_1[3u];"));
 }
 
 TEST_F(SpvParserTest_CompositeExtract, RuntimeArray_IsError) {
@@ -338,7 +337,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 : i32 = x_1.field2;"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 = x_1.field2;"));
 }
 
 TEST_F(SpvParserTest_CompositeExtract, Struct_DifferOnlyInMemberName) {
@@ -378,9 +377,9 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto got = fe.ast_body();
     auto program = p->program();
-    EXPECT_THAT(test::ToString(program, got), HasSubstr("let x_2 : u32 = x_1.algo;"))
+    EXPECT_THAT(test::ToString(program, got), HasSubstr("let x_2 = x_1.algo;"))
         << test::ToString(program, got);
-    EXPECT_THAT(test::ToString(program, got), HasSubstr("let x_4 : u32 = x_3.rithm;"))
+    EXPECT_THAT(test::ToString(program, got), HasSubstr("let x_4 = x_3.rithm;"))
         << test::ToString(program, got);
     p->SkipDumpingPending("crbug.com/tint/863");
 }
@@ -426,7 +425,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_2 : f32 = x_1.field1[2u][0u].y;"));
+                HasSubstr("let x_2 = x_1.field1[2u][0u].y;"));
 }
 
 using SpvParserTest_CompositeInsert = SpvParserTest;
@@ -446,9 +445,9 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     const auto* expected =
-        R"(var x_1_1 : vec2f = vec2f(50.0f, 60.0f);
+        R"(var x_1_1 = vec2f(50.0f, 60.0f);
 x_1_1.y = 70.0f;
-let x_1 : vec2f = x_1_1;
+let x_1 = x_1_1;
 return;
 )";
     EXPECT_EQ(got, expected);
@@ -489,9 +488,9 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr(R"(var x_2_1 : mat3x2f = x_1;
+    EXPECT_THAT(body_str, HasSubstr(R"(var x_2_1 = x_1;
 x_2_1[2u] = vec2f(50.0f, 60.0f);
-let x_2 : mat3x2f = x_2_1;
+let x_2 = x_2_1;
 )")) << body_str;
 }
 
@@ -534,9 +533,9 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr(R"(var x_2_1 : mat3x2f = x_1;
+    EXPECT_THAT(body_str, HasSubstr(R"(var x_2_1 = x_1;
 x_2_1[2u] = vec2f(50.0f, 60.0f);
-let x_2 : mat3x2f = x_2_1;
+let x_2 = x_2_1;
 return;
 )")) << body_str;
 }
@@ -559,9 +558,9 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr(R"(var x_2_1 : array<u32, 5u> = x_1;
+    EXPECT_THAT(body_str, HasSubstr(R"(var x_2_1 = x_1;
 x_2_1[3u] = 20u;
-let x_2 : array<u32, 5u> = x_2_1;
+let x_2 = x_2_1;
 )")) << body_str;
 }
 
@@ -604,10 +603,10 @@
     auto ast_body = fe.ast_body();
     auto body_str = test::ToString(p->program(), ast_body);
     EXPECT_THAT(body_str, HasSubstr(R"(var x_36 : S;
-let x_1 : S = x_36;
-var x_2_1 : S = x_1;
+let x_1 = x_36;
+var x_2_1 = x_1;
 x_2_1.field2 = 30i;
-let x_2 : S = x_2_1;
+let x_2 = x_2_1;
 )")) << body_str;
 }
 
@@ -654,14 +653,14 @@
     const auto got = test::ToString(p->program(), ast_body);
     const std::string expected = R"(var var0 : S;
 var var1 : S_1;
-let x_1 : S = var0;
-var x_2_1 : S = x_1;
+let x_1 = var0;
+var x_2_1 = x_1;
 x_2_1.algo = 10u;
-let x_2 : S = x_2_1;
-let x_3 : S_1 = var1;
-var x_4_1 : S_1 = x_3;
+let x_2 = x_2_1;
+let x_3 = var1;
+var x_4_1 = x_3;
 x_4_1.rithm = 11u;
-let x_4 : S_1 = x_4_1;
+let x_4 = x_4_1;
 return;
 )";
     EXPECT_EQ(got, expected) << got;
@@ -709,10 +708,10 @@
     auto ast_body = fe.ast_body();
     auto body_str = test::ToString(p->program(), ast_body);
     EXPECT_THAT(body_str, HasSubstr(R"(var x_38 : S_1;
-let x_1 : S_1 = x_38;
-var x_2_1 : S_1 = x_1;
+let x_1 = x_38;
+var x_2_1 = x_1;
 x_2_1.field1[2u][0u].y = 70.0f;
-let x_2 : S_1 = x_2_1;
+let x_2 = x_2_1;
 )")) << body_str;
 }
 
@@ -732,8 +731,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_1 : u32 = 3u;
-let x_2 : u32 = x_1;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_1 = 3u;
+let x_2 = x_1;
 )"));
 }
 
@@ -754,9 +753,9 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr(R"(let x_1 : ptr<function, u32> = &(x_10);
-let x_2 : ptr<function, u32> = x_1;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"( x_10 : u32;
+let x_1 = &(x_10);
+let x_2 = x_1;
 )"));
 }
 
@@ -780,7 +779,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : vec4u = vec4u(x_2.y, x_2.x, x_1.y, x_1.x);"));
+                HasSubstr("let x_10 = vec4u(x_2.y, x_2.x, x_1.y, x_1.x);"));
 }
 
 TEST_F(SpvParserTest_VectorShuffle, ConstantOperands_UseBoth) {
@@ -797,7 +796,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 : vec4u = vec4u("
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = vec4u("
                                                                   "vec2u(4u, 3u).y, "
                                                                   "vec2u(4u, 3u).x, "
                                                                   "vec2u(3u, 4u).y, "
@@ -819,8 +818,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : vec2u = vec2u(0u, x_1.y);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_10 = vec2u(0u, x_1.y);"));
 }
 
 TEST_F(SpvParserTest_VectorShuffle, FunctionScopeOperands_MixedInputOperandSizes) {
@@ -842,7 +840,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_10 : vec2u = vec2u(x_1.y, x_3.z);"));
+                HasSubstr("let x_10 = vec2u(x_1.y, x_3.z);"));
 }
 
 TEST_F(SpvParserTest_VectorShuffle, IndexTooBig_IsError) {
@@ -880,7 +878,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr("let x_10 : u32 = x_1[x_2];")) << got;
+    EXPECT_THAT(got, HasSubstr("let x_10 = x_1[x_2];")) << got;
 }
 
 TEST_F(SpvParserTest_VectorExtractDynamic, UnsignedIndex) {
@@ -900,7 +898,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr("let x_10 : u32 = x_1[x_2];")) << got;
+    EXPECT_THAT(got, HasSubstr("let x_10 = x_1[x_2];")) << got;
 }
 
 using SpvParserTest_VectorInsertDynamic = SpvParserTest;
@@ -923,9 +921,9 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr(R"(var x_10_1 : vec2u = x_1;
+    EXPECT_THAT(got, HasSubstr(R"(var x_10_1 = x_1;
 x_10_1[x_3] = x_2;
-let x_10 : vec2u = x_10_1;
+let x_10 = x_10_1;
 )")) << got
      << assembly;
 }
diff --git a/src/tint/reader/spirv/function_conversion_test.cc b/src/tint/reader/spirv/function_conversion_test.cc
index 602928a..d8d11fd 100644
--- a/src/tint/reader/spirv/function_conversion_test.cc
+++ b/src/tint/reader/spirv/function_conversion_test.cc
@@ -83,7 +83,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = bitcast<u32>(50.0f);"));
+                HasSubstr("let x_1 = bitcast<u32>(50.0f);"));
 }
 
 TEST_F(SpvUnaryConversionTest, Bitcast_Vector) {
@@ -100,7 +100,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = bitcast<vec2f>(vec2u(10u, 20u));"));
+                HasSubstr("let x_1 = bitcast<vec2f>(vec2u(10u, 20u));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertSToF_BadArg) {
@@ -209,7 +209,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : f32 = f32(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = f32(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertSToF_Scalar_FromUnsigned) {
@@ -227,7 +227,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : f32 = f32(bitcast<i32>(x_30));"));
+                HasSubstr("let x_1 = f32(bitcast<i32>(x_30));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertSToF_Vector_FromSigned) {
@@ -244,8 +244,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = vec2f(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = vec2f(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertSToF_Vector_FromUnsigned) {
@@ -263,7 +262,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = vec2f(bitcast<vec2i>(x_30));"));
+                HasSubstr("let x_1 = vec2f(bitcast<vec2i>(x_30));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertUToF_Scalar_BadArgType) {
@@ -313,7 +312,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : f32 = f32(bitcast<u32>(x_30));"));
+                HasSubstr("let x_1 = f32(bitcast<u32>(x_30));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertUToF_Scalar_FromUnsigned) {
@@ -330,7 +329,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : f32 = f32(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = f32(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertUToF_Vector_FromSigned) {
@@ -348,7 +347,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = vec2f(bitcast<vec2u>(x_30));"));
+                HasSubstr("let x_1 = vec2f(bitcast<vec2u>(x_30));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertUToF_Vector_FromUnsigned) {
@@ -365,8 +364,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2f = vec2f(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = vec2f(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToS_Scalar_BadArgType) {
@@ -415,7 +413,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : i32 = i32(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = i32(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToS_Scalar_ToUnsigned) {
@@ -433,7 +431,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = bitcast<u32>(i32(x_30));"));
+                HasSubstr("let x_1 = bitcast<u32>(i32(x_30));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToS_Vector_ToSigned) {
@@ -450,8 +448,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2i = vec2i(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = vec2i(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToS_Vector_ToUnsigned) {
@@ -469,7 +466,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2u = bitcast<vec2u>(vec2i(x_30));"));
+                HasSubstr("let x_1 = bitcast<vec2u>(vec2i(x_30));"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToU_Scalar_BadArgType) {
@@ -534,7 +531,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : u32 = u32(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = u32(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToU_Vector_ToSigned_IsError) {
@@ -567,8 +564,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2u = vec2u(x_30);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = vec2u(x_30);"));
 }
 
 TEST_F(SpvUnaryConversionTest, ConvertFToU_HoistedValue) {
@@ -609,7 +605,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_82 : u32 = u32(x_600);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_82 = u32(x_600);"));
 }
 
 // TODO(dneto): OpSConvert // only if multiple widths
diff --git a/src/tint/reader/spirv/function_decl_test.cc b/src/tint/reader/spirv/function_decl_test.cc
index 59121a3..b14b457 100644
--- a/src/tint/reader/spirv/function_decl_test.cc
+++ b/src/tint/reader/spirv/function_decl_test.cc
@@ -186,7 +186,7 @@
 
     auto got = test::ToString(p->program());
     std::string expect = R"(fn x_200(x_14 : texture_2d<f32>, x_15 : sampler) {
-  let x_20 : vec4f = textureSample(x_14, x_15, vec2f());
+  let x_20 = textureSample(x_14, x_15, vec2f());
   return;
 }
 )";
@@ -216,7 +216,7 @@
 
     auto got = test::ToString(p->program());
     std::string expect = R"(fn x_200(x_14 : texture_2d<f32>, x_15 : sampler) {
-  let x_20 : vec4f = textureSample(x_14, x_15, vec2f());
+  let x_20 = textureSample(x_14, x_15, vec2f());
   return;
 }
 )";
diff --git a/src/tint/reader/spirv/function_glsl_std_450_test.cc b/src/tint/reader/spirv/function_glsl_std_450_test.cc
index 506c8d4..f480f69 100644
--- a/src/tint/reader/spirv/function_glsl_std_450_test.cc
+++ b/src/tint/reader/spirv/function_glsl_std_450_test.cc
@@ -201,7 +201,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(f1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Float_Floating, Vector) {
@@ -217,7 +217,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(v2f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2f1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Float_FloatingFloating, Scalar) {
@@ -233,7 +233,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(f1, f2);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(f1, f2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Float_FloatingFloating, Vector) {
@@ -249,8 +249,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(v2f1, v2f2);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2f1, v2f2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_Floating, Scalar) {
@@ -266,7 +265,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(f1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_Floating, Vector) {
@@ -282,7 +281,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2f = " + GetParam().wgsl_func + "(v2f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2f1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_FloatingFloating, Scalar) {
@@ -298,7 +297,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(f1, f2);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(f1, f2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_FloatingFloating, Vector) {
@@ -314,8 +313,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2f = " + GetParam().wgsl_func + "(v2f1, v2f2);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2f1, v2f2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_FloatingFloatingFloating, Scalar) {
@@ -331,8 +329,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(f1, f2, f3);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(f1, f2, f3);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_FloatingFloatingFloating, Vector) {
@@ -349,8 +346,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2f = " + GetParam().wgsl_func + "(v2f1, v2f2, v2f3);"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2f1, v2f2, v2f3);"))
         << body;
 }
 
@@ -367,7 +363,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = " + GetParam().wgsl_func + "(f1, i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(f1, i1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Floating_FloatingInting, Vector) {
@@ -384,8 +380,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2f = " + GetParam().wgsl_func + "(v2f1, v2i1);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2f1, v2i1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Float3_Float3Float3, Samples) {
@@ -402,8 +397,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec3f = " + GetParam().wgsl_func + "(v3f1, v3f2);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v3f1, v3f2);")) << body;
 }
 
 INSTANTIATE_TEST_SUITE_P(Samples,
@@ -488,7 +482,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = " + GetParam().wgsl_func + "(i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(i1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Inting_Inting_SignednessCoercing, Scalar_UnsignedArg) {
@@ -505,7 +499,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = " + GetParam().wgsl_func + "(bitcast<i32>(u1));"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(bitcast<i32>(u1));"))
         << body;
 }
 
@@ -523,7 +517,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = bitcast<u32>(" + GetParam().wgsl_func + "(i1));"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<u32>(" + GetParam().wgsl_func + "(i1));"))
         << body;
 }
 
@@ -541,7 +535,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = " + GetParam().wgsl_func + "(v2i1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2i1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Inting_Inting_SignednessCoercing, Vector_UnsignedArg) {
@@ -558,8 +552,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2i = " + GetParam().wgsl_func + "(bitcast<vec2i>(v2u1));"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(bitcast<vec2i>(v2u1));"))
         << body;
 }
 
@@ -577,8 +570,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2u = bitcast<vec2u>(" + GetParam().wgsl_func + "(v2i1));"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = bitcast<vec2u>(" + GetParam().wgsl_func + "(v2i1));"))
         << body;
 }
 
@@ -596,7 +588,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = " + GetParam().wgsl_func + "(i1, i2);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(i1, i2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Inting_IntingInting, Vector) {
@@ -613,8 +605,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2i = " + GetParam().wgsl_func + "(v2i1, v2i2);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2i1, v2i2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Inting_IntingIntingInting, Scalar) {
@@ -631,8 +622,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : i32 = " + GetParam().wgsl_func + "(i1, i2, i3);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(i1, i2, i3);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Inting_IntingIntingInting, Vector) {
@@ -649,8 +639,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2i = " + GetParam().wgsl_func + "(v2i1, v2i2, v2i3);"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2i1, v2i2, v2i3);"))
         << body;
 }
 
@@ -688,7 +677,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = " + GetParam().wgsl_func + "(u1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(u1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Uinting_Uinting, Vector) {
@@ -705,7 +694,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = " + GetParam().wgsl_func + "(v2u1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2u1);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Uinting_UintingUinting, Scalar) {
@@ -721,7 +710,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = " + GetParam().wgsl_func + "(u1, u2);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(u1, u2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Uinting_UintingUinting, Vector) {
@@ -738,8 +727,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2u = " + GetParam().wgsl_func + "(v2u1, v2u2);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2u1, v2u2);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Uinting_UintingUintingUinting, Scalar) {
@@ -755,8 +743,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = " + GetParam().wgsl_func + "(u1, u2, u3);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(u1, u2, u3);")) << body;
 }
 
 TEST_P(SpvParserTest_GlslStd450_Uinting_UintingUintingUinting, Vector) {
@@ -773,8 +760,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : vec2u = " + GetParam().wgsl_func + "(v2u1, v2u2, v2u3);"))
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + GetParam().wgsl_func + "(v2u1, v2u2, v2u3);"))
         << body;
 }
 
@@ -809,7 +795,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : f32 = 1.0f;")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = 1.0f;")) << body;
 }
 
 TEST_F(SpvParserTest, Normalize_Vector2) {
@@ -825,7 +811,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec2f = normalize(v2f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = normalize(v2f1);")) << body;
 }
 
 TEST_F(SpvParserTest, Normalize_Vector3) {
@@ -841,7 +827,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec3f = normalize(v3f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = normalize(v3f1);")) << body;
 }
 
 TEST_F(SpvParserTest, Normalize_Vector4) {
@@ -857,7 +843,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : vec4f = normalize(v4f1);")) << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = normalize(v4f1);")) << body;
 }
 
 // Check that we convert signedness of operands and result type.
@@ -876,9 +862,8 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr(R"(let x_1 : u32 = bitcast<u32>(abs(bitcast<i32>(u1)));)")) << body;
-    EXPECT_THAT(body, HasSubstr(R"(let x_2 : vec2u = bitcast<vec2u>(abs(bitcast<vec2i>(v2u1)));)"))
-        << body;
+    EXPECT_THAT(body, HasSubstr(R"(let x_1 = bitcast<u32>(abs(bitcast<i32>(u1)));)")) << body;
+    EXPECT_THAT(body, HasSubstr(R"(let x_2 = bitcast<vec2u>(abs(bitcast<vec2i>(v2u1)));)")) << body;
 }
 
 TEST_F(SpvParserTest, RectifyOperandsAndResult_SMax) {
@@ -894,14 +879,12 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr(R"(let x_1 : u32 = bitcast<u32>(max(bitcast<i32>(u1), bitcast<i32>(u2)));)"))
+    EXPECT_THAT(body,
+                HasSubstr(R"(let x_1 = bitcast<u32>(max(bitcast<i32>(u1), bitcast<i32>(u2)));)"))
         << body;
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_2 : vec2u = bitcast<vec2u>(max(bitcast<vec2i>(v2u1), bitcast<vec2i>(v2u2)));)"))
+        HasSubstr(R"(let x_2 = bitcast<vec2u>(max(bitcast<vec2i>(v2u1), bitcast<vec2i>(v2u2)));)"))
         << body;
 }
 
@@ -918,14 +901,12 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr(R"(let x_1 : u32 = bitcast<u32>(min(bitcast<i32>(u1), bitcast<i32>(u2)));)"))
+    EXPECT_THAT(body,
+                HasSubstr(R"(let x_1 = bitcast<u32>(min(bitcast<i32>(u1), bitcast<i32>(u2)));)"))
         << body;
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_2 : vec2u = bitcast<vec2u>(min(bitcast<vec2i>(v2u1), bitcast<vec2i>(v2u2)));)"))
+        HasSubstr(R"(let x_2 = bitcast<vec2u>(min(bitcast<vec2i>(v2u1), bitcast<vec2i>(v2u2)));)"))
         << body;
 }
 
@@ -944,13 +925,12 @@
     auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_1 : u32 = bitcast<u32>(clamp(bitcast<i32>(u1), i2, bitcast<i32>(u3)));)"))
+        HasSubstr(R"(let x_1 = bitcast<u32>(clamp(bitcast<i32>(u1), i2, bitcast<i32>(u3)));)"))
         << body;
     EXPECT_THAT(
         body,
         HasSubstr(
-            R"(let x_2 : vec2u = bitcast<vec2u>(clamp(bitcast<vec2i>(v2u1), v2i2, bitcast<vec2i>(v2u3)));)"))
+            R"(let x_2 = bitcast<vec2u>(clamp(bitcast<vec2i>(v2u1), v2i2, bitcast<vec2i>(v2u3)));)"))
         << body;
 }
 
@@ -967,14 +947,12 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr(R"(let x_1 : i32 = bitcast<i32>(max(bitcast<u32>(i1), bitcast<u32>(i2)));)"))
+    EXPECT_THAT(body,
+                HasSubstr(R"(let x_1 = bitcast<i32>(max(bitcast<u32>(i1), bitcast<u32>(i2)));)"))
         << body;
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_2 : vec2i = bitcast<vec2i>(max(bitcast<vec2u>(v2i1), bitcast<vec2u>(v2i2)));)"))
+        HasSubstr(R"(let x_2 = bitcast<vec2i>(max(bitcast<vec2u>(v2i1), bitcast<vec2u>(v2i2)));)"))
         << body;
 }
 
@@ -991,14 +969,12 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(
-        body,
-        HasSubstr(R"(let x_1 : i32 = bitcast<i32>(min(bitcast<u32>(i1), bitcast<u32>(i2)));)"))
+    EXPECT_THAT(body,
+                HasSubstr(R"(let x_1 = bitcast<i32>(min(bitcast<u32>(i1), bitcast<u32>(i2)));)"))
         << body;
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_2 : vec2i = bitcast<vec2i>(min(bitcast<vec2u>(v2i1), bitcast<vec2u>(v2i2)));)"))
+        HasSubstr(R"(let x_2 = bitcast<vec2i>(min(bitcast<vec2u>(v2i1), bitcast<vec2u>(v2i2)));)"))
         << body;
 }
 
@@ -1017,13 +993,12 @@
     auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(
         body,
-        HasSubstr(
-            R"(let x_1 : i32 = bitcast<i32>(clamp(bitcast<u32>(i1), u2, bitcast<u32>(i3)));)"))
+        HasSubstr(R"(let x_1 = bitcast<i32>(clamp(bitcast<u32>(i1), u2, bitcast<u32>(i3)));)"))
         << body;
     EXPECT_THAT(
         body,
         HasSubstr(
-            R"(let x_2 : vec2i = bitcast<vec2i>(clamp(bitcast<vec2u>(v2i1), v2u2, bitcast<vec2u>(v2i3)));)"))
+            R"(let x_2 = bitcast<vec2i>(clamp(bitcast<vec2u>(v2i1), v2u2, bitcast<vec2u>(v2i3)));)"))
         << body;
 }
 
@@ -1048,10 +1023,10 @@
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(body, HasSubstr(R"(
-let x_1 : u32 = bitcast<u32>(firstTrailingBit(i1));
-let x_2 : vec2u = bitcast<vec2u>(firstTrailingBit(v2i1));
-let x_3 : i32 = bitcast<i32>(firstTrailingBit(u1));
-let x_4 : vec2i = bitcast<vec2i>(firstTrailingBit(v2u1));)"))
+let x_1 = bitcast<u32>(firstTrailingBit(i1));
+let x_2 = bitcast<vec2u>(firstTrailingBit(v2i1));
+let x_3 = bitcast<i32>(firstTrailingBit(u1));
+let x_4 = bitcast<vec2i>(firstTrailingBit(v2u1));)"))
         << body;
 }
 
@@ -1094,14 +1069,14 @@
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(body, HasSubstr(R"(
-let x_1 : i32 = firstLeadingBit(i1);
-let x_2 : vec2i = firstLeadingBit(v2i1);
-let x_3 : u32 = bitcast<u32>(firstLeadingBit(i1));
-let x_4 : vec2u = bitcast<vec2u>(firstLeadingBit(v2i1));
-let x_5 : i32 = firstLeadingBit(bitcast<i32>(u1));
-let x_6 : vec2i = firstLeadingBit(bitcast<vec2i>(v2u1));
-let x_7 : u32 = bitcast<u32>(firstLeadingBit(bitcast<i32>(u1)));
-let x_8 : vec2u = bitcast<vec2u>(firstLeadingBit(bitcast<vec2i>(v2u1)));
+let x_1 = firstLeadingBit(i1);
+let x_2 = firstLeadingBit(v2i1);
+let x_3 = bitcast<u32>(firstLeadingBit(i1));
+let x_4 = bitcast<vec2u>(firstLeadingBit(v2i1));
+let x_5 = firstLeadingBit(bitcast<i32>(u1));
+let x_6 = firstLeadingBit(bitcast<vec2i>(v2u1));
+let x_7 = bitcast<u32>(firstLeadingBit(bitcast<i32>(u1)));
+let x_8 = bitcast<vec2u>(firstLeadingBit(bitcast<vec2i>(v2u1)));
 )")) << body;
 }
 
@@ -1144,14 +1119,14 @@
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
     EXPECT_THAT(body, HasSubstr(R"(
-let x_1 : i32 = bitcast<i32>(firstLeadingBit(bitcast<u32>(i1)));
-let x_2 : vec2i = bitcast<vec2i>(firstLeadingBit(bitcast<vec2u>(v2i1)));
-let x_3 : u32 = firstLeadingBit(bitcast<u32>(i1));
-let x_4 : vec2u = firstLeadingBit(bitcast<vec2u>(v2i1));
-let x_5 : i32 = bitcast<i32>(firstLeadingBit(u1));
-let x_6 : vec2i = bitcast<vec2i>(firstLeadingBit(v2u1));
-let x_7 : u32 = firstLeadingBit(u1);
-let x_8 : vec2u = firstLeadingBit(v2u1);
+let x_1 = bitcast<i32>(firstLeadingBit(bitcast<u32>(i1)));
+let x_2 = bitcast<vec2i>(firstLeadingBit(bitcast<vec2u>(v2i1)));
+let x_3 = firstLeadingBit(bitcast<u32>(i1));
+let x_4 = firstLeadingBit(bitcast<vec2u>(v2i1));
+let x_5 = bitcast<i32>(firstLeadingBit(u1));
+let x_6 = bitcast<vec2i>(firstLeadingBit(v2u1));
+let x_7 = firstLeadingBit(u1);
+let x_8 = firstLeadingBit(v2u1);
 )")) << body;
 }
 
@@ -1183,7 +1158,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = " + param.wgsl_func + "(v" +
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + param.wgsl_func + "(v" +
                                 std::to_string(param.vec_size) + "f1);"))
         << body;
 }
@@ -1214,11 +1189,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body,
-                HasSubstr("let x_1 : " + std::string(param.vec_size == 2 ? "vec2f" : "vec4f") +
-
-                          +" = " + param.wgsl_func + "(u1);"))
-        << body;
+    EXPECT_THAT(body, HasSubstr("let x_1 = " + param.wgsl_func + "(u1);")) << body;
 }
 
 INSTANTIATE_TEST_SUITE_P(Samples,
@@ -1242,7 +1213,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    const auto* expected = R"(let x_1 : f32 = refract(vec2f(f1, 0.0f), vec2f(f2, 0.0f), f3).x;)";
+    const auto* expected = R"(let x_1 = refract(vec2f(f1, 0.0f), vec2f(f2, 0.0f), f3).x;)";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1259,7 +1230,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    const auto* expected = R"(let x_1 : vec2f = refract(v2f1, v2f2, f3);)";
+    const auto* expected = R"(let x_1 = refract(v2f1, v2f2, f3);)";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1280,7 +1251,7 @@
     // The %99 sum only has one use.  Ensure it is evaluated only once by
     // making a let-declaration for it, since it is the normal operand to
     // the builtin function, and code generation uses it twice.
-    const auto* expected = R"(let x_1 : f32 = select(-(x_99), x_99, ((f2 * f3) < 0.0f));)";
+    const auto* expected = R"(let x_1 = select(-(x_99), x_99, ((f2 * f3) < 0.0f));)";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1298,7 +1269,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    const auto* expected = R"(let x_1 : vec2f = faceForward(v2f1, v2f2, v2f3);)";
+    const auto* expected = R"(let x_1 = faceForward(v2f1, v2f2, v2f3);)";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1320,7 +1291,7 @@
     // The %99 sum only has one use.  Ensure it is evaluated only once by
     // making a let-declaration for it, since it is the normal operand to
     // the builtin function, and code generation uses it twice.
-    const auto* expected = R"(let x_1 : f32 = (x_98 - (2.0f * (x_99 * (x_99 * x_98))));)";
+    const auto* expected = R"(let x_1 = (x_98 - (2.0f * (x_99 * (x_99 * x_98))));)";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1340,9 +1311,9 @@
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
     const auto* expected = R"(
-let x_98 : vec2f = (v2f1 + v2f1);
-let x_99 : vec2f = (v2f2 + v2f2);
-let x_1 : vec2f = reflect(x_98, x_99);
+let x_98 = (v2f1 + v2f1);
+let x_99 = (v2f2 + v2f2);
+let x_1 = reflect(x_98, x_99);
 )";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
@@ -1361,7 +1332,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    const auto* expected = "let x_1 : f32 = ldexp(f1, i32(u1));";
+    const auto* expected = "let x_1 = ldexp(f1, i32(u1));";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1378,7 +1349,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    const auto* expected = "let x_1 : vec2f = ldexp(v2f1, vec2i(v2u1));";
+    const auto* expected = "let x_1 = ldexp(v2f1, vec2i(v2u1));";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1397,7 +1368,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body = test::ToString(p->program(), ast_body);
-    std::string expected = "let x_1 : f32 = determinant(" + GetParam() + ");";
+    std::string expected = "let x_1 = determinant(" + GetParam() + ");";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
 }
@@ -1420,7 +1391,7 @@
 
     std::string expected =
         "let s = (1.0f / determinant(m2x2f1));\n"
-        "let x_1 : mat2x2f = mat2x2f(vec2f((s * m2x2f1[1u][1u]), (-(s) * "
+        "let x_1 = mat2x2f(vec2f((s * m2x2f1[1u][1u]), (-(s) * "
         "m2x2f1[0u][1u])), vec2f((-(s) * m2x2f1[1u][0u]), (s * m2x2f1[0u][0u])));";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
@@ -1441,7 +1412,7 @@
 
     std::string expected =
         "let s = (1.0f / determinant(m3x3f1));\n"
-        "let x_1 : mat3x3f = (s * mat3x3f(vec3f(((m3x3f1[1u][1u] * m3x3f1[2u][2u]) - "
+        "let x_1 = (s * mat3x3f(vec3f(((m3x3f1[1u][1u] * m3x3f1[2u][2u]) - "
         "(m3x3f1[1u][2u] * m3x3f1[2u][1u])), ((m3x3f1[0u][2u] * m3x3f1[2u][1u]) - (m3x3f1[0u][1u] "
         "* m3x3f1[2u][2u])), ((m3x3f1[0u][1u] * m3x3f1[1u][2u]) - (m3x3f1[0u][2u] * "
         "m3x3f1[1u][1u]))), vec3f(((m3x3f1[1u][2u] * m3x3f1[2u][0u]) - (m3x3f1[1u][0u] * "
@@ -1470,7 +1441,7 @@
 
     std::string expected =
         "let s = (1.0f / determinant(m4x4f1));\n"
-        "let x_1 : mat4x4f = (s * mat4x4f(vec4f((((m4x4f1[1u][1u] * ((m4x4f1[2u][2u] * "
+        "let x_1 = (s * mat4x4f(vec4f((((m4x4f1[1u][1u] * ((m4x4f1[2u][2u] * "
         "m4x4f1[3u][3u]) - (m4x4f1[2u][3u] * m4x4f1[3u][2u]))) - (m4x4f1[1u][2u] * "
         "((m4x4f1[2u][1u] * m4x4f1[3u][3u]) - (m4x4f1[2u][3u] * m4x4f1[3u][1u])))) + "
         "(m4x4f1[1u][3u] * ((m4x4f1[2u][1u] * m4x4f1[3u][2u]) - (m4x4f1[2u][2u] * "
@@ -1551,10 +1522,10 @@
 
     std::string expected =
         "let s = (1.0f / determinant(m2x2f1));\n"
-        "let x_1 : mat2x2f = mat2x2f(vec2f((s * m2x2f1[1u][1u]), (-(s) * "
+        "let x_1 = mat2x2f(vec2f((s * m2x2f1[1u][1u]), (-(s) * "
         "m2x2f1[0u][1u])), vec2f((-(s) * m2x2f1[1u][0u]), (s * m2x2f1[0u][0u])));\n"
         "let s_1 = (1.0f / determinant(m2x2f1));\n"
-        "let x_2 : mat2x2f = mat2x2f(vec2f((s_1 * m2x2f1[1u][1u]), (-(s_1) * "
+        "let x_2 = mat2x2f(vec2f((s_1 * m2x2f1[1u][1u]), (-(s_1) * "
         "m2x2f1[0u][1u])), vec2f((-(s_1) * m2x2f1[1u][0u]), (s_1 * m2x2f1[0u][0u])));";
 
     EXPECT_THAT(body, HasSubstr(expected)) << body;
diff --git a/src/tint/reader/spirv/function_logical_test.cc b/src/tint/reader/spirv/function_logical_test.cc
index 4e62df9..a537154 100644
--- a/src/tint/reader/spirv/function_logical_test.cc
+++ b/src/tint/reader/spirv/function_logical_test.cc
@@ -136,7 +136,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : bool = !(true);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = !(true);"));
 }
 
 TEST_F(SpvUnaryLogicalTest, LogicalNot_Vector) {
@@ -153,7 +153,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = !(vec2<bool>(true, false));"));
+                HasSubstr("let x_1 = !(vec2<bool>(true, false));"));
 }
 
 struct BinaryData {
@@ -190,8 +190,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     utils::StringStream ss;
-    ss << "let x_1 : " << GetParam().ast_type << " = (" << GetParam().ast_lhs << " "
-       << GetParam().ast_op << " " << GetParam().ast_rhs << ");";
+    ss << "let x_1 = (" << GetParam().ast_lhs << " " << GetParam().ast_op << " "
+       << GetParam().ast_rhs << ");";
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(ss.str())) << assembly;
 }
@@ -516,7 +516,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = !((50.0f != 60.0f));"));
+                HasSubstr("let x_1 = !((50.0f != 60.0f));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordEqual_Vector) {
@@ -532,9 +532,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr("let x_1 : vec2<bool> = !((vec2f(50.0f, 60.0f) != vec2f(60.0f, 50.0f)));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 = !((vec2f(50.0f, 60.0f) != vec2f(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordNotEqual_Scalar) {
@@ -551,7 +550,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = !((50.0f == 60.0f));"));
+                HasSubstr("let x_1 = !((50.0f == 60.0f));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordNotEqual_Vector) {
@@ -567,9 +566,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr("let x_1 : vec2<bool> = !((vec2f(50.0f, 60.0f) == vec2f(60.0f, 50.0f)));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 = !((vec2f(50.0f, 60.0f) == vec2f(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordLessThan_Scalar) {
@@ -586,7 +584,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = !((50.0f >= 60.0f));"));
+                HasSubstr("let x_1 = !((50.0f >= 60.0f));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordLessThan_Vector) {
@@ -602,9 +600,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr("let x_1 : vec2<bool> = !((vec2f(50.0f, 60.0f) >= vec2f(60.0f, 50.0f)));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 = !((vec2f(50.0f, 60.0f) >= vec2f(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordLessThanEqual_Scalar) {
@@ -620,8 +617,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = !((50.0f > 60.0f));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = !((50.0f > 60.0f));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordLessThanEqual_Vector) {
@@ -637,9 +633,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr("let x_1 : vec2<bool> = !((vec2f(50.0f, 60.0f) > vec2f(60.0f, 50.0f)));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 = !((vec2f(50.0f, 60.0f) > vec2f(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordGreaterThan_Scalar) {
@@ -656,7 +651,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = !((50.0f <= 60.0f));"));
+                HasSubstr("let x_1 = !((50.0f <= 60.0f));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordGreaterThan_Vector) {
@@ -672,9 +667,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr("let x_1 : vec2<bool> = !((vec2f(50.0f, 60.0f) <= vec2f(60.0f, 50.0f)));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 = !((vec2f(50.0f, 60.0f) <= vec2f(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordGreaterThanEqual_Scalar) {
@@ -690,8 +684,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = !((50.0f < 60.0f));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = !((50.0f < 60.0f));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordGreaterThanEqual_Vector) {
@@ -707,9 +700,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(
-        test::ToString(p->program(), ast_body),
-        HasSubstr("let x_1 : vec2<bool> = !((vec2f(50.0f, 60.0f) < vec2f(60.0f, 50.0f)));"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body),
+                HasSubstr("let x_1 = !((vec2f(50.0f, 60.0f) < vec2f(60.0f, 50.0f)));"));
 }
 
 using SpvLogicalTest = SpvParserTestBase<::testing::Test>;
@@ -728,7 +720,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = select(false, true, true);"));
+                HasSubstr("let x_1 = select(false, true, true);"));
 }
 
 TEST_F(SpvLogicalTest, Select_BoolCond_IntScalarParams) {
@@ -745,7 +737,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : u32 = select(20u, 10u, true);"));
+                HasSubstr("let x_1 = select(20u, 10u, true);"));
 }
 
 TEST_F(SpvLogicalTest, Select_BoolCond_FloatScalarParams) {
@@ -762,7 +754,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : f32 = select(60.0f, 50.0f, true);"));
+                HasSubstr("let x_1 = select(60.0f, 50.0f, true);"));
 }
 
 TEST_F(SpvLogicalTest, Select_BoolCond_VectorParams) {
@@ -781,7 +773,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : vec2u = select("
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = select("
                                                                   "vec2u(20u, 10u), "
                                                                   "vec2u(10u, 20u), "
                                                                   "true);"));
@@ -806,7 +798,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 : vec2u = select("
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = select("
                                                                   "vec2u(20u, 10u), "
                                                                   "vec2u(10u, 20u), "
                                                                   "vec2<bool>(true, false));"));
@@ -826,7 +818,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = any(vec2<bool>(true, false));"));
+                HasSubstr("let x_1 = any(vec2<bool>(true, false));"));
 }
 
 TEST_F(SpvLogicalTest, All) {
@@ -843,7 +835,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = all(vec2<bool>(true, false));"));
+                HasSubstr("let x_1 = all(vec2<bool>(true, false));"));
 }
 
 TEST_F(SpvLogicalTest, IsNan_Scalar) {
@@ -859,8 +851,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = isNan(50.0f);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = isNan(50.0f);"));
 }
 
 TEST_F(SpvLogicalTest, IsNan_Vector) {
@@ -877,7 +868,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = isNan(vec2f(50.0f, 60.0f));"));
+                HasSubstr("let x_1 = isNan(vec2f(50.0f, 60.0f));"));
 }
 
 TEST_F(SpvLogicalTest, IsInf_Scalar) {
@@ -893,8 +884,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : bool = isInf(50.0f);"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_1 = isInf(50.0f);"));
 }
 
 TEST_F(SpvLogicalTest, IsInf_Vector) {
@@ -911,7 +901,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = isInf(vec2f(50.0f, 60.0f));"));
+                HasSubstr("let x_1 = isInf(vec2f(50.0f, 60.0f));"));
 }
 
 // TODO(dneto): Kernel-guarded instructions.
diff --git a/src/tint/reader/spirv/function_memory_test.cc b/src/tint/reader/spirv/function_memory_test.cc
index f7b1862..b75a3bc 100644
--- a/src/tint/reader/spirv/function_memory_test.cc
+++ b/src/tint/reader/spirv/function_memory_test.cc
@@ -159,7 +159,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 : bool = x_1;"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_2 = x_1;"));
 }
 
 TEST_F(SpvParserMemoryTest, EmitStatement_LoadScalar) {
@@ -181,8 +181,9 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_2 : u32 = x_1;
-let x_3 : u32 = x_1;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"( x_1 = 42u;
+let x_2 = x_1;
+let x_3 = x_1;
 )"));
 }
 
@@ -206,7 +207,8 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_2 : u32 = x_1;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"( x_1 = 42u;
+let x_2 = x_1;
 x_1 = x_2;
 x_1 = x_2;
 )"));
@@ -802,7 +804,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModule());
     const auto got = test::ToString(p->program());
     const std::string expected = R"(fn x_200(x_1 : ptr<private, vec2u>) {
-  let x_3 : u32 = (*(x_1)).x;
+  let x_3 = (*(x_1)).x;
   return;
 }
 
@@ -846,7 +848,7 @@
     const auto got = test::ToString(p->program());
     const std::string expected = R"(fn main_1() {
   var x_1 : u32;
-  let x_2 : ptr<function, u32> = &(x_1);
+  let x_2 = &(x_1);
   return;
 }
 
@@ -1018,11 +1020,11 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr(R"(let x_1 : ptr<storage, u32, read_write> = &(myvar.field0);
+    EXPECT_THAT(got, HasSubstr(R"(let x_1 = &(myvar.field0);
 *(x_1) = 0u;
 *(x_1) = 0u;
-let x_2 : ptr<storage, u32, read_write> = &(myvar.field1[1u]);
-let x_3 : u32 = *(x_2);
+let x_2 = &(myvar.field1[1u]);
+let x_3 = *(x_2);
 *(x_2) = 0u;
 )"));
 }
@@ -1054,11 +1056,11 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr(R"(let x_1 : ptr<storage, u32, read> = &(myvar.field0);
+    EXPECT_THAT(got, HasSubstr(R"(let x_1 = &(myvar.field0);
 *(x_1) = 0u;
 *(x_1) = 0u;
-let x_2 : ptr<storage, u32, read> = &(myvar.field1[1u]);
-let x_3 : u32 = *(x_2);
+let x_2 = &(myvar.field1[1u]);
+let x_3 = *(x_2);
 *(x_2) = 0u;
 )")) << got
      << assembly;
@@ -1093,11 +1095,11 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr(R"(let x_1 : ptr<storage, u32, read_write> = &(myvar.field0);
+    EXPECT_THAT(got, HasSubstr(R"(let x_1 = &(myvar.field0);
 *(x_1) = 0u;
 *(x_1) = 0u;
-let x_2 : ptr<storage, u32, read_write> = &(myvar.field1[1u]);
-let x_3 : u32 = *(x_2);
+let x_2 = &(myvar.field1[1u]);
+let x_3 = *(x_2);
 *(x_2) = 0u;
 )")) << got;
 }
@@ -1133,11 +1135,11 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(got, HasSubstr(R"(let x_1 : ptr<storage, u32, read> = &(myvar.field0);
+    EXPECT_THAT(got, HasSubstr(R"(let x_1 = &(myvar.field0);
 *(x_1) = 0u;
 *(x_1) = 0u;
-let x_2 : ptr<storage, u32, read> = &(myvar.field1[1u]);
-let x_3 : u32 = *(x_2);
+let x_2 = &(myvar.field1[1u]);
+let x_3 = *(x_2);
 *(x_2) = 0u;
 )")) << got;
 }
@@ -1216,8 +1218,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr(R"(let x_2 : ptr<storage, u32, read_write> = &(myvar.field1[1u]);
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_2 = &(myvar.field1[1u]);
 *(x_2) = 0u;
 )")) << p->error();
 
@@ -1275,7 +1276,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr("let x_1 : u32 = arrayLength(&(myvar.rtarr));")) << body_str;
+    EXPECT_THAT(body_str, HasSubstr("let x_1 = arrayLength(&(myvar.rtarr));")) << body_str;
 }
 
 TEST_F(SpvParserMemoryTest, ArrayLength_FromCopyObject) {
@@ -1295,8 +1296,8 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr(R"(let x_2 : ptr<storage, S, read_write> = &(myvar);
-let x_1 : u32 = arrayLength(&((*(x_2)).rtarr));
+    EXPECT_THAT(body_str, HasSubstr(R"(let x_2 = &(myvar);
+let x_1 = arrayLength(&((*(x_2)).rtarr));
 )")) << body_str;
 
     p->SkipDumpingPending("crbug.com/tint/1041 track access mode in spirv-reader parser type");
@@ -1319,7 +1320,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr("let x_1 : u32 = arrayLength(&(myvar.rtarr));")) << body_str;
+    EXPECT_THAT(body_str, HasSubstr("let x_1 = arrayLength(&(myvar.rtarr));")) << body_str;
 }
 
 std::string InvalidPointerPreamble() {
diff --git a/src/tint/reader/spirv/function_misc_test.cc b/src/tint/reader/spirv/function_misc_test.cc
index 2c984d2..3ab14e8 100644
--- a/src/tint/reader/spirv/function_misc_test.cc
+++ b/src/tint/reader/spirv/function_misc_test.cc
@@ -72,10 +72,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_11 : bool = false;
-let x_12 : u32 = 0u;
-let x_13 : i32 = 0i;
-let x_14 : f32 = 0.0f;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_11 = false;
+let x_12 = 0u;
+let x_13 = 0i;
+let x_14 = 0.0f;
 return;
 )"));
 }
@@ -102,11 +102,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr(R"(let x_14 : vec2<bool> = vec2<bool>();
-let x_11 : vec2u = vec2u();
-let x_12 : vec2i = vec2i();
-let x_13 : vec2f = vec2f();
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_14 = vec2<bool>();
+let x_11 = vec2u();
+let x_12 = vec2i();
+let x_13 = vec2f();
 )"));
 }
 
@@ -131,10 +130,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_11 : bool = false;
-let x_12 : u32 = 0u;
-let x_13 : i32 = 0i;
-let x_14 : f32 = 0.0f;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_11 = false;
+let x_12 = 0u;
+let x_13 = 0i;
+let x_14 = 0.0f;
 return;
 )"));
 }
@@ -158,9 +157,9 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_11 : vec2u = vec2u();
-let x_12 : vec2i = vec2i();
-let x_13 : vec2f = vec2f();
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(let x_11 = vec2u();
+let x_12 = vec2i();
+let x_13 = vec2f();
 )"));
 }
 
@@ -181,8 +180,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_11 : mat2x2f = mat2x2f();"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_11 = mat2x2f();"));
 }
 
 TEST_F(SpvParserTestMiscInstruction, OpUndef_InFunction_Array) {
@@ -203,8 +201,7 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_11 : array<u32, 2u> = array<u32, 2u>();"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("let x_11 = array<u32, 2u>();"));
 }
 
 TEST_F(SpvParserTestMiscInstruction, OpUndef_InFunction_Struct) {
@@ -225,7 +222,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_11 : S = S(false, 0u, 0i, 0.0f);"));
+                HasSubstr("let x_11 = S(false, 0u, 0i, 0.0f);"));
 }
 
 TEST_F(SpvParserTestMiscInstruction, OpNop) {
diff --git a/src/tint/reader/spirv/function_var_test.cc b/src/tint/reader/spirv/function_var_test.cc
index fd3580b..f9f5ead 100644
--- a/src/tint/reader/spirv/function_var_test.cc
+++ b/src/tint/reader/spirv/function_var_test.cc
@@ -181,11 +181,11 @@
     EXPECT_TRUE(fe.EmitFunctionVariables());
 
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(var a : bool = true;
-var b : bool = false;
-var c : i32 = -1i;
-var d : u32 = 1u;
-var e : f32 = 1.5f;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(var a = true;
+var b = false;
+var c = -1i;
+var d = 1u;
+var e = 1.5f;
 )"));
 }
 
@@ -210,10 +210,10 @@
     EXPECT_TRUE(fe.EmitFunctionVariables());
 
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(var a : bool = false;
-var b : i32 = 0i;
-var c : u32 = 0u;
-var d : f32 = 0.0f;
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(var a = false;
+var b = 0i;
+var c = 0u;
+var d = 0.0f;
 )"));
 }
 
@@ -235,7 +235,7 @@
 
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("var x_200 : vec2f = vec2f(1.5f, 2.0f);"));
+                HasSubstr("var x_200 = vec2f(1.5f, 2.0f);"));
 }
 
 TEST_F(SpvParserFunctionVarTest, EmitFunctionVariables_MatrixInitializer) {
@@ -260,7 +260,7 @@
     EXPECT_TRUE(fe.EmitFunctionVariables());
 
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("var x_200 : mat3x2f = mat3x2f("
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("var x_200 = mat3x2f("
                                                                   "vec2f(1.5f, 2.0f), "
                                                                   "vec2f(2.0f, 3.0f), "
                                                                   "vec2f(3.0f, 4.0f));"));
@@ -284,7 +284,7 @@
 
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("var x_200 : array<u32, 2u> = array<u32, 2u>(1u, 2u);"));
+                HasSubstr("var x_200 = array<u32, 2u>(1u, 2u);"));
 }
 
 TEST_F(SpvParserFunctionVarTest, EmitFunctionVariables_ArrayInitializer_Alias) {
@@ -311,7 +311,7 @@
 
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    const char* expect = "var x_200 : Arr = Arr(1u, 2u);\n";
+    const char* expect = "var x_200 = Arr(1u, 2u);\n";
     EXPECT_EQ(expect, got);
 }
 
@@ -332,8 +332,7 @@
     EXPECT_TRUE(fe.EmitFunctionVariables());
 
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("var x_200 : array<u32, 2u> = array<u32, 2u>();"));
+    EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("var x_200 = array<u32, 2u>();"));
 }
 
 TEST_F(SpvParserFunctionVarTest, EmitFunctionVariables_ArrayInitializer_Alias_Null) {
@@ -360,7 +359,7 @@
 
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("var x_200 : Arr = @stride(16) array<u32, 2u>();"));
+                HasSubstr("var x_200 = @stride(16) array<u32, 2u>();"));
 }
 
 TEST_F(SpvParserFunctionVarTest, EmitFunctionVariables_StructInitializer) {
@@ -382,7 +381,7 @@
 
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("var x_200 : S = S(1u, 1.5f, array<u32, 2u>(1u, 2u));"));
+                HasSubstr("var x_200 = S(1u, 1.5f, array<u32, 2u>(1u, 2u));"));
 }
 
 TEST_F(SpvParserFunctionVarTest, EmitFunctionVariables_StructInitializer_Null) {
@@ -404,7 +403,7 @@
 
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("var x_200 : S = S(0u, 0.0f, array<u32, 2u>());"));
+                HasSubstr("var x_200 = S(0u, 0.0f, array<u32, 2u>());"));
 }
 
 TEST_F(SpvParserFunctionVarTest, EmitFunctionVariables_Decorate_RelaxedPrecision) {
@@ -556,7 +555,7 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var x_25 : u32;
-let x_2 : u32 = (1u + 1u);
+let x_2 = (1u + 1u);
 x_25 = 1u;
 x_25 = x_2;
 x_25 = x_2;
@@ -601,7 +600,7 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var x_25 : u32;
-let x_2 : u32 = (1u + 1u);
+let x_2 = (1u + 1u);
 x_25 = 1u;
 loop {
 
@@ -740,10 +739,10 @@
     // a const definition.
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    auto* expect = R"(let x_1 : u32 = 1u;
+    auto* expect = R"(let x_1 = 1u;
 if (true) {
 }
-let x_3 : u32 = x_1;
+let x_3 = x_1;
 x_200 = x_3;
 return;
 )";
@@ -801,10 +800,10 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(if (true) {
-  let x_1 : u32 = 1u;
+  let x_1 = 1u;
   if (true) {
   }
-  let x_3 : u32 = x_1;
+  let x_3 = x_1;
   x_200 = x_3;
 }
 return;
@@ -857,14 +856,14 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(if (true) {
-  let x_1 : u32 = 1u;
+  let x_1 = 1u;
   switch(1u) {
     case 0u: {
     }
     default: {
     }
   }
-  let x_3 : u32 = x_1;
+  let x_3 = x_1;
   x_200 = x_3;
 }
 return;
@@ -911,8 +910,8 @@
     // a const definition.
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    auto* expect = R"(let x_1 : u32 = 1u;
-let x_2 : u32 = x_1;
+    auto* expect = R"(let x_1 = 1u;
+let x_2 = x_1;
 if (true) {
 }
 return;
@@ -1015,8 +1014,8 @@
     auto* expect = R"(loop {
   var x_2 : u32;
   var x_3 : u32;
-  let x_101 : bool = x_7;
-  let x_102 : bool = x_8;
+  let x_101 = x_7;
+  let x_102 = x_8;
   x_2 = 0u;
   x_3 = 1u;
   if (x_101) {
@@ -1088,8 +1087,8 @@
     auto* expect = R"(loop {
   var x_2 : u32;
   var x_3 : u32;
-  let x_101 : bool = x_7;
-  let x_102 : bool = x_8;
+  let x_101 = x_7;
+  let x_102 = x_8;
   x_2 = 0u;
   x_3 = 1u;
   if (x_101) {
@@ -1164,7 +1163,7 @@
 
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    auto* expect = R"(let x_101 : bool = x_17;
+    auto* expect = R"(let x_101 = x_17;
 loop {
   var x_2 : u32;
   var x_5 : u32;
@@ -1182,7 +1181,7 @@
 
     continuing {
       x_7 = (x_4 + x_6);
-      let x_8 : u32 = x_5;
+      let x_8 = x_5;
       x_2 = x_4;
       x_5 = x_7;
     }
@@ -1244,8 +1243,8 @@
 
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    auto* expect = R"(let x_101 : bool = x_7;
-let x_102 : bool = x_8;
+    auto* expect = R"(let x_101 = x_7;
+let x_102 = x_8;
 loop {
   var x_2 : u32;
   if (x_101) {
@@ -1317,8 +1316,8 @@
 
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
-    auto* expect = R"(let x_101 : bool = x_7;
-let x_102 : bool = x_8;
+    auto* expect = R"(let x_101 = x_7;
+let x_102 = x_8;
 loop {
   var x_2 : u32;
   if (x_101) {
@@ -1380,13 +1379,13 @@
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var x_101 : bool;
-let x_11 : bool = (true & true);
-let x_12 : bool = !(x_11);
+let x_11 = (true & true);
+let x_12 = !(x_11);
 x_101 = x_11;
 if (true) {
   x_101 = x_12;
 }
-let x_102 : bool = x_101;
+let x_102 = x_101;
 return;
 )";
     EXPECT_EQ(expect, got);
@@ -1532,7 +1531,7 @@
     x_101 = x_999;
   }
 }
-let x_1000 : bool = x_101;
+let x_1000 = x_101;
 return;
 )";
     EXPECT_EQ(expect, got);
@@ -1698,7 +1697,7 @@
     x_101 = x_999;
   }
 }
-let x_1000 : bool = x_101;
+let x_1000 = x_101;
 return;
 )";
     EXPECT_EQ(expect, got);
@@ -1737,7 +1736,7 @@
 } else {
   return;
 }
-let x_201 : vec2i = x_200;
+let x_201 = x_200;
 return;
 )";
     auto ast_body = fe.ast_body();
@@ -1780,7 +1779,7 @@
 } else {
   return;
 }
-let x_201 : vec2i = x_200;
+let x_201 = x_200;
 return;
 )";
     EXPECT_EQ(got, expected) << got;
diff --git a/src/tint/reader/spirv/parser_impl.cc b/src/tint/reader/spirv/parser_impl.cc
index 220ccb9..fa9572a 100644
--- a/src/tint/reader/spirv/parser_impl.cc
+++ b/src/tint/reader/spirv/parser_impl.cc
@@ -1610,16 +1610,15 @@
         return nullptr;
     }
 
+    // Use type inference if there is an initializer.
     auto sym = builder_.Symbols().Register(namer_.Name(id));
-    return builder_.Var(Source{}, sym, storage_type->Build(builder_), address_space, access,
-                        initializer, std::move(attrs.list));
+    return builder_.Var(Source{}, sym, initializer ? ast::Type{} : storage_type->Build(builder_),
+                        address_space, access, initializer, std::move(attrs.list));
 }
 
-const ast::Let* ParserImpl::MakeLet(uint32_t id,
-                                    const Type* type,
-                                    const ast::Expression* initializer) {
+const ast::Let* ParserImpl::MakeLet(uint32_t id, const ast::Expression* initializer) {
     auto sym = builder_.Symbols().Register(namer_.Name(id));
-    return builder_.Let(Source{}, sym, type->Build(builder_), initializer, utils::Empty);
+    return builder_.Let(Source{}, sym, initializer, utils::Empty);
 }
 
 const ast::Override* ParserImpl::MakeOverride(uint32_t id,
diff --git a/src/tint/reader/spirv/parser_impl.h b/src/tint/reader/spirv/parser_impl.h
index 9c28aef..028414c 100644
--- a/src/tint/reader/spirv/parser_impl.h
+++ b/src/tint/reader/spirv/parser_impl.h
@@ -446,10 +446,9 @@
 
     /// Creates an AST 'let' node for a SPIR-V ID, including any attached decorations,.
     /// @param id the SPIR-V result ID
-    /// @param type the type of the variable
     /// @param initializer the variable initializer
     /// @returns the AST 'let' node
-    const ast::Let* MakeLet(uint32_t id, const Type* type, const ast::Expression* initializer);
+    const ast::Let* MakeLet(uint32_t id, const ast::Expression* initializer);
 
     /// Creates an AST 'override' node for a SPIR-V ID, including any attached decorations.
     /// @param id the SPIR-V result ID
diff --git a/src/tint/reader/spirv/parser_impl_function_decl_test.cc b/src/tint/reader/spirv/parser_impl_function_decl_test.cc
index e463551..6d793ab 100644
--- a/src/tint/reader/spirv/parser_impl_function_decl_test.cc
+++ b/src/tint/reader/spirv/parser_impl_function_decl_test.cc
@@ -377,12 +377,12 @@
 }
 
 fn branch() -> u32 {
-  let leaf_result : u32 = leaf();
+  let leaf_result = leaf();
   return leaf_result;
 }
 
 fn root() {
-  let branch_result : u32 = branch();
+  let branch_result = branch();
   return;
 }
 )")) << program_ast;
diff --git a/src/tint/reader/spirv/parser_impl_handle_test.cc b/src/tint/reader/spirv/parser_impl_handle_test.cc
index 0447110..206e258 100644
--- a/src/tint/reader/spirv/parser_impl_handle_test.cc
+++ b/src/tint/reader/spirv/parser_impl_handle_test.cc
@@ -1827,8 +1827,31 @@
 @group(0) @binding(1) var x_30 : sampler_comparison;
 )",
                         R"(
-  let x_200 : vec4f = vec4f(textureSample(x_20, x_10, coords12), 0.0f, 0.0f, 0.0f);
-  let x_210 : f32 = textureSampleCompare(x_20, x_30, coords12, 0.20000000298023223877f);
+
+@group(2) @binding(1) var x_20 : texture_depth_2d;
+
+@group(0) @binding(1) var x_30 : sampler_comparison;
+
+fn main_1() {
+  let f1 = 1.0f;
+  let vf12 = vec2f(1.0f, 2.0f);
+  let vf21 = vec2f(2.0f, 1.0f);
+  let vf123 = vec3f(1.0f, 2.0f, 3.0f);
+  let vf1234 = vec4f(1.0f, 2.0f, 3.0f, 4.0f);
+  let i1 = 1i;
+  let vi12 = vec2i(1i, 2i);
+  let vi123 = vec3i(1i, 2i, 3i);
+  let vi1234 = vec4i(1i, 2i, 3i, 4i);
+  let u1 = 1u;
+  let vu12 = vec2u(1u, 2u);
+  let vu123 = vec3u(1u, 2u, 3u);
+  let vu1234 = vec4u(1u, 2u, 3u, 4u);
+  let coords1 = 1.0f;
+  let coords12 = vf12;
+  let coords123 = vf123;
+  let coords1234 = vf1234;
+  let x_200 = vec4f(textureSample(x_20, x_10, coords12), 0.0f, 0.0f, 0.0f);
+  let x_210 = textureSampleCompare(x_20, x_30, coords12, 0.20000000298023223877f);
 )"}));
 
 INSTANTIATE_TEST_SUITE_P(
@@ -2649,20 +2672,20 @@
         // Level of detail is injected for sampled texture
         {"%float 2D 0 0 0 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12",
          R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-         R"(let x_99 : vec4f = textureLoad(x_20, vi12, 0i);)"},
+         R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
         // OpImageFetch with explicit level, on sampled texture
         {"%float 2D 0 0 0 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12 Lod %int_3",
          R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-         R"(let x_99 : vec4f = textureLoad(x_20, vi12, 3i);)"},
+         R"(let x_99 = textureLoad(x_20, vi12, 3i);)"},
         // OpImageFetch with no extra params, on depth texture
         // Level of detail is injected for depth texture
         {"%float 2D 1 0 0 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12",
          R"(@group(2) @binding(1) var x_20 : texture_depth_2d;)",
-         R"(let x_99 : vec4f = vec4f(textureLoad(x_20, vi12, 0i), 0.0f, 0.0f, 0.0f);)"},
+         R"(let x_99 = vec4f(textureLoad(x_20, vi12, 0i), 0.0f, 0.0f, 0.0f);)"},
         // OpImageFetch with extra params, on depth texture
         {"%float 2D 1 0 0 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12 Lod %int_3",
          R"(@group(2) @binding(1) var x_20 : texture_depth_2d;)",
-         R"(let x_99 : vec4f = vec4f(textureLoad(x_20, vi12, 3i), 0.0f, 0.0f, 0.0f);)"}}));
+         R"(let x_99 = vec4f(textureLoad(x_20, vi12, 3i), 0.0f, 0.0f, 0.0f);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageFetch_Depth,
@@ -2675,7 +2698,7 @@
         // ImageFetch on depth image.
         {"%float 2D 1 0 0 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12 ",
          R"(@group(2) @binding(1) var x_20 : texture_depth_2d;)",
-         R"(let x_99 : vec4f = vec4f(textureLoad(x_20, vi12, 0i), 0.0f, 0.0f, 0.0f);)"}}));
+         R"(let x_99 = vec4f(textureLoad(x_20, vi12, 0i), 0.0f, 0.0f, 0.0f);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageFetch_DepthMultisampled,
@@ -2688,7 +2711,7 @@
         // ImageFetch on multisampled depth image.
         {"%float 2D 1 0 1 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12 Sample %i1",
          R"(@group(2) @binding(1) var x_20 : texture_depth_multisampled_2d;)",
-         R"(let x_99 : vec4f = vec4f(textureLoad(x_20, vi12, i1), 0.0f, 0.0f, 0.0f);)"}}));
+         R"(let x_99 = vec4f(textureLoad(x_20, vi12, i1), 0.0f, 0.0f, 0.0f);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(ImageFetch_Multisampled,
                          SpvParserHandleTest_ImageAccessTest,
@@ -2703,7 +2726,7 @@
                              {"%float 2D 0 0 1 1 Unknown",
                               "%99 = OpImageFetch %v4float %im %vi12 Sample %i1",
                               R"(@group(2) @binding(1) var x_20 : texture_multisampled_2d<f32>;)",
-                              R"(let x_99 : vec4f = textureLoad(x_20, vi12, i1);)"}}));
+                              R"(let x_99 = textureLoad(x_20, vi12, i1);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(ImageFetch_Multisampled_ConvertSampleOperand,
                          SpvParserHandleTest_ImageAccessTest,
@@ -2711,7 +2734,7 @@
                              {"%float 2D 0 0 1 1 Unknown",
                               "%99 = OpImageFetch %v4float %im %vi12 Sample %u1",
                               R"(@group(2) @binding(1) var x_20 : texture_multisampled_2d<f32>;)",
-                              R"(let x_99 : vec4f = textureLoad(x_20, vi12, i32(u1));)"}}));
+                              R"(let x_99 = textureLoad(x_20, vi12, i32(u1));)"}}));
 
 INSTANTIATE_TEST_SUITE_P(ConvertResultSignedness,
                          SpvParserHandleTest_SampledImageAccessTest,
@@ -2733,11 +2756,11 @@
                              // OpImageFetch requires no conversion, float -> v4float
                              {"%float 2D 0 0 0 1 Unknown", "%99 = OpImageFetch %v4float %im %vi12",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                              R"(let x_99 : vec4f = textureLoad(x_20, vi12, 0i);)"},
+                              R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
                              // OpImageFetch requires no conversion, uint -> v4uint
                              {"%uint 2D 0 0 0 1 Unknown", "%99 = OpImageFetch %v4uint %im %vi12",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<u32>;)",
-                              R"(let x_99 : vec4u = textureLoad(x_20, vi12, 0i);)"},
+                              R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
                              // OpImageFetch requires conversion, uint -> v4int
                              // is invalid SPIR-V:
                              // "Expected Image 'Sampled Type' to be the same as Result Type
@@ -2746,7 +2769,7 @@
                              // OpImageFetch requires no conversion, int -> v4int
                              {"%int 2D 0 0 0 1 Unknown", "%99 = OpImageFetch %v4int %im %vi12",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<i32>;)",
-                              R"(let x_99 : vec4i = textureLoad(x_20, vi12, 0i);)"},
+                              R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
                              // OpImageFetch requires conversion, int -> v4uint
                              // is invalid SPIR-V:
                              // "Expected Image 'Sampled Type' to be the same as Result Type
@@ -2759,11 +2782,11 @@
                              // OpImageRead requires no conversion, float -> v4float
                              {"%float 2D 0 0 0 2 Rgba32f", "%99 = OpImageRead %v4float %im %vi12",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                              R"(let x_99 : vec4f = textureLoad(x_20, vi12, 0i);)"},
+                              R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
                              // OpImageRead requires no conversion, uint -> v4uint
                              {"%uint 2D 0 0 0 2 Rgba32ui", "%99 = OpImageRead %v4uint %im %vi12",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<u32>;)",
-                              R"(let x_99 : vec4u = textureLoad(x_20, vi12, 0i);)"},
+                              R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
 
                              // OpImageRead requires conversion, uint -> v4int
                              // is invalid SPIR-V:
@@ -2773,7 +2796,7 @@
                              // OpImageRead requires no conversion, int -> v4int
                              {"%int 2D 0 0 0 2 Rgba32i", "%99 = OpImageRead %v4int %im %vi12",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<i32>;)",
-                              R"(let x_99 : vec4i = textureLoad(x_20, vi12, 0i);)"},
+                              R"(let x_99 = textureLoad(x_20, vi12, 0i);)"},
 
                              // OpImageRead requires conversion, int -> v4uint
                              // is invalid SPIR-V:
@@ -2792,7 +2815,7 @@
                               R"(@group(0) @binding(0) var x_10 : sampler;
 
 @group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                              R"(let x_99 : vec4f = textureSample(x_20, x_10, vf12);)"}}));
+                              R"(let x_99 = textureSample(x_20, x_10, vf12);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(ImageQuerySize_NonArrayed_SignedResult,
                          // ImageQuerySize requires storage image or multisampled
@@ -2806,26 +2829,26 @@
                               "%98 = OpImageRead %v4float %im %i1\n",  // Implicitly mark as
                                                                        // NonWritable
                               R"(@group(2) @binding(1) var x_20 : texture_1d<f32>;)",
-                              R"(let x_99 : i32 = i32(textureDimensions(x_20));)"},
+                              R"(let x_99 = i32(textureDimensions(x_20));)"},
                              // 2D storage image
                              {"%float 2D 0 0 0 2 Rgba32f",
                               "%99 = OpImageQuerySize %v2int %im \n"
                               "%98 = OpImageRead %v4float %im %vi12\n",  // Implicitly mark as
                                                                          // NonWritable
                               R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                              R"(let x_99 : vec2i = vec2i(textureDimensions(x_20))"},
+                              R"(let x_99 = vec2i(textureDimensions(x_20))"},
                              // 3D storage image
                              {"%float 3D 0 0 0 2 Rgba32f",
                               "%99 = OpImageQuerySize %v3int %im \n"
                               "%98 = OpImageRead %v4float %im %vi123\n",  // Implicitly mark as
                                                                           // NonWritable
                               R"(@group(2) @binding(1) var x_20 : texture_3d<f32>;)",
-                              R"(let x_99 : vec3i = vec3i(textureDimensions(x_20));)"},
+                              R"(let x_99 = vec3i(textureDimensions(x_20));)"},
 
                              // Multisampled
                              {"%float 2D 0 0 1 1 Unknown", "%99 = OpImageQuerySize %v2int %im \n",
                               R"(@group(2) @binding(1) var x_20 : texture_multisampled_2d<f32>;)",
-                              R"(let x_99 : vec2i = vec2i(textureDimensions(x_20));)"}}));
+                              R"(let x_99 = vec2i(textureDimensions(x_20));)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageQuerySize_Arrayed_SignedResult,
@@ -2841,7 +2864,7 @@
          "%99 = OpImageQuerySize %v3int %im \n"
          "%98 = OpImageRead %v4float %im %vi123\n",
          R"(@group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-         R"(let x_99 : vec3i = vec3i(vec3u(textureDimensions(x_20), textureNumLayers(x_20)));)"}
+         R"(let x_99 = vec3i(vec3u(textureDimensions(x_20), textureNumLayers(x_20)));)"}
         // 3D array storage image doesn't exist.
 
         // Multisampled array
@@ -2857,32 +2880,32 @@
         // 1D
         {"%float 1D 0 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_1d<f32>;)",
-         R"(let x_99 : i32 = i32(textureDimensions(x_20, i1)))"},
+         R"(let x_99 = i32(textureDimensions(x_20, i1)))"},
 
         // 2D
         {"%float 2D 0 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %v2int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-         R"(let x_99 : vec2i = vec2i(textureDimensions(x_20, i1));)"},
+         R"(let x_99 = vec2i(textureDimensions(x_20, i1));)"},
 
         // 3D
         {"%float 3D 0 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %v3int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_3d<f32>;)",
-         R"(let x_99 : vec3i = vec3i(textureDimensions(x_20, i1));)"},
+         R"(let x_99 = vec3i(textureDimensions(x_20, i1));)"},
 
         // Cube
         {"%float Cube 0 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %v2int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_cube<f32>;)",
-         R"(let x_99 : vec2i = vec2i(textureDimensions(x_20, i1).xy);)"},
+         R"(let x_99 = vec2i(textureDimensions(x_20, i1).xy);)"},
 
         // Depth 2D
         {"%float 2D 1 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %v2int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_depth_2d;)",
-         R"(let x_99 : vec2i = vec2i(textureDimensions(x_20, i1));)"},
+         R"(let x_99 = vec2i(textureDimensions(x_20, i1));)"},
 
         // Depth Cube
         {"%float Cube 1 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %v2int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_depth_cube;)",
-         R"(let x_99 : vec2i = vec2i(textureDimensions(x_20, i1).xy);)"}}));
+         R"(let x_99 = vec2i(textureDimensions(x_20, i1).xy);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageQuerySizeLod_Arrayed_SignedResult_SignedLevel,
@@ -2897,7 +2920,7 @@
         // 2D array
         {"%float 2D 0 1 0 1 Unknown", "%99 = OpImageQuerySizeLod %v3int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-         R"(let x_99 : vec3i = vec3i(vec3u(textureDimensions(x_20, i1), textureNumLayers(x_20)));)"},
+         R"(let x_99 = vec3i(vec3u(textureDimensions(x_20, i1), textureNumLayers(x_20)));)"},
 
         // There is no 3D array
 
@@ -2908,12 +2931,12 @@
         // https://github.com/gpuweb/gpuweb/issues/1345
         {"%float Cube 0 1 0 1 Unknown", "%99 = OpImageQuerySizeLod %v3int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_cube_array<f32>;)",
-         R"(let x_99 : vec3i = vec3i(vec3u(textureDimensions(x_20, i1).xy, textureNumLayers(x_20)));)"},
+         R"(let x_99 = vec3i(vec3u(textureDimensions(x_20, i1).xy, textureNumLayers(x_20)));)"},
 
         // Depth 2D array
         {"%float 2D 1 1 0 1 Unknown", "%99 = OpImageQuerySizeLod %v3int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-         R"(let x_99 : vec3i = vec3i(vec3u(textureDimensions(x_20, i1), textureNumLayers(x_20)));)"},
+         R"(let x_99 = vec3i(vec3u(textureDimensions(x_20, i1), textureNumLayers(x_20)));)"},
 
         // Depth Cube Array
         //
@@ -2922,7 +2945,7 @@
         // https://github.com/gpuweb/gpuweb/issues/1345
         {"%float Cube 1 1 0 1 Unknown", "%99 = OpImageQuerySizeLod %v3int %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_depth_cube_array;)",
-         R"(let x_99 : vec3i = vec3i(vec3u(textureDimensions(x_20, i1).xy, textureNumLayers(x_20)));)"}}));
+         R"(let x_99 = vec3i(vec3u(textureDimensions(x_20, i1).xy, textureNumLayers(x_20)));)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     // textureDimensions accepts both signed and unsigned the level-of-detail values.
@@ -2932,7 +2955,7 @@
 
         {"%float 1D 0 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %int %im %u1\n",
          R"(@group(2) @binding(1) var x_20 : texture_1d<f32>;)",
-         R"(let x_99 : i32 = i32(textureDimensions(x_20, u1));)"}}));
+         R"(let x_99 = i32(textureDimensions(x_20, u1));)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     // When SPIR-V wants the result type to be unsigned, we have to
@@ -2945,7 +2968,7 @@
 
         {"%float 1D 0 0 0 1 Unknown", "%99 = OpImageQuerySizeLod %uint %im %i1\n",
          R"(@group(2) @binding(1) var x_20 : texture_1d<f32>;)",
-         R"(let x_99 : i32 = i32(textureDimensions(x_20, i1));)"}}));
+         R"(let x_99 = i32(textureDimensions(x_20, i1));)"}}));
 
 INSTANTIATE_TEST_SUITE_P(ImageQueryLevels_SignedResult,
                          SpvParserHandleTest_SampledImageAccessTest,
@@ -2958,47 +2981,47 @@
                              // 2D
                              {"%float 2D 0 0 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // 2D array
                              {"%float 2D 0 1 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_2d_array<f32>;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // 3D
                              {"%float 3D 0 0 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_3d<f32>;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // Cube
                              {"%float Cube 0 0 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_cube<f32>;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // Cube array
                              {"%float Cube 0 1 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_cube_array<f32>;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // depth 2d
                              {"%float 2D 1 0 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_depth_2d;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // depth 2d array
                              {"%float 2D 1 1 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // depth cube
                              {"%float Cube 1 0 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_depth_cube;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"},
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"},
 
                              // depth cube array
                              {"%float Cube 1 1 0 1 Unknown", "%99 = OpImageQueryLevels %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_depth_cube_array;)",
-                              R"(let x_99 : i32 = i32(textureNumLevels(x_20));)"}}));
+                              R"(let x_99 = i32(textureNumLevels(x_20));)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     // Spot check that a value conversion is inserted when SPIR-V asks for an unsigned int result.
@@ -3007,7 +3030,7 @@
     ::testing::ValuesIn(std::vector<ImageAccessCase>{
         {"%float 2D 0 0 0 1 Unknown", "%99 = OpImageQueryLevels %uint %im\n",
          R"(@group(2) @binding(1) var x_20 : texture_2d<f32>;)",
-         R"(let x_99 : u32 = textureNumLevels(x_20);)"}}));
+         R"(let x_99 = textureNumLevels(x_20);)"}}));
 
 INSTANTIATE_TEST_SUITE_P(ImageQuerySamples_SignedResult,
                          SpvParserHandleTest_SampledImageAccessTest,
@@ -3015,7 +3038,7 @@
                              // Multsample 2D
                              {"%float 2D 0 0 1 1 Unknown", "%99 = OpImageQuerySamples %int %im\n",
                               R"(@group(2) @binding(1) var x_20 : texture_multisampled_2d<f32>;)",
-                              R"(let x_99 : i32 = i32(textureNumSamples(x_20));)"}
+                              R"(let x_99 = i32(textureNumSamples(x_20));)"}
 
                              // Multisample 2D array
                              // Not in WebGPU
@@ -3029,7 +3052,7 @@
         // Multisample 2D
         {"%float 2D 0 0 1 1 Unknown", "%99 = OpImageQuerySamples %uint %im\n",
          R"(@group(2) @binding(1) var x_20 : texture_multisampled_2d<f32>;)",
-         R"(let x_99 : u32 = textureNumSamples(x_20);)"}
+         R"(let x_99 = textureNumSamples(x_20);)"}
 
         // Multisample 2D array
         // Not in WebGPU
@@ -3815,8 +3838,8 @@
     auto ast_body = fe.ast_body();
     const auto got = test::ToString(p->program(), ast_body);
     auto* expect = R"(var var_1 : vec4f;
-let x_22 : vec4f = textureSample(x_2, x_3, vec2f());
-let x_26 : vec4f = textureSample(x_2, x_3, vec2f());
+let x_22 = textureSample(x_2, x_3, vec2f());
+let x_26 = textureSample(x_2, x_3, vec2f());
 var_1 = (x_22 + x_26);
 return;
 )";
@@ -3890,7 +3913,7 @@
     auto* expect = R"(switch(0i) {
   default: {
     if (true) {
-      let x_24 : vec4f = textureSample(var_im, var_s, vec2f());
+      let x_24 = textureSample(var_im, var_s, vec2f());
     }
   }
 }
@@ -3977,7 +4000,7 @@
 x_900 = 0.0f;
 if (true) {
   if (true) {
-    let x_18 : vec4f = textureSample(x_20, x_10, x_900);
+    let x_18 = textureSample(x_20, x_10, x_900);
   }
 }
 return;
@@ -4064,7 +4087,7 @@
 x_900 = vec2f();
 if (true) {
   if (true) {
-    let x_19 : vec4f = textureSample(x_20, x_10, x_900.x);
+    let x_19 = textureSample(x_20, x_10, x_900.x);
   }
 }
 return;
@@ -4226,7 +4249,7 @@
     x_15 = x_17;
   }
 }
-let x_21 : f32 = select(0.0f, x_14, (x_14 > 1.0f));
+let x_21 = select(0.0f, x_14, (x_14 > 1.0f));
 x_1 = vec4f(x_21, x_21, x_21, x_21);
 return;
 )";
diff --git a/src/tint/reader/spirv/parser_impl_module_var_test.cc b/src/tint/reader/spirv/parser_impl_module_var_test.cc
index 871edf7..6855479 100644
--- a/src/tint/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/tint/reader/spirv/parser_impl_module_var_test.cc
@@ -881,15 +881,15 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr(R"(var<private> x_1 : bool = true;
+    EXPECT_THAT(module_str, HasSubstr(R"(var<private> x_1 = true;
 
-var<private> x_2 : bool = false;
+var<private> x_2 = false;
 
-var<private> x_3 : i32 = -1i;
+var<private> x_3 = -1i;
 
-var<private> x_4 : u32 = 1u;
+var<private> x_4 = 1u;
 
-var<private> x_5 : f32 = 1.5f;
+var<private> x_5 = 1.5f;
 )"));
 }
 
@@ -908,13 +908,13 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr(R"(var<private> x_1 : bool = false;
+    EXPECT_THAT(module_str, HasSubstr(R"(var<private> x_1 = false;
 
-var<private> x_2 : i32 = 0i;
+var<private> x_2 = 0i;
 
-var<private> x_3 : u32 = 0u;
+var<private> x_3 = 0u;
 
-var<private> x_4 : f32 = 0.0f;
+var<private> x_4 = 0.0f;
 )"));
 }
 
@@ -933,13 +933,13 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr(R"(var<private> x_1 : bool = false;
+    EXPECT_THAT(module_str, HasSubstr(R"(var<private> x_1 = false;
 
-var<private> x_2 : i32 = 0i;
+var<private> x_2 = 0i;
 
-var<private> x_3 : u32 = 0u;
+var<private> x_3 = 0u;
 
-var<private> x_4 : f32 = 0.0f;
+var<private> x_4 = 0.0f;
 )"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
@@ -956,7 +956,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2f = vec2f(1.5f, 2.0f);"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2f(1.5f, 2.0f);"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, VectorBoolNullInitializer) {
@@ -968,7 +968,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2<bool> = vec2<bool>();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2<bool>();"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, VectorBoolUndefInitializer) {
@@ -980,7 +980,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2<bool> = vec2<bool>();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2<bool>();"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
     p->DeliberatelyInvalidSpirv();
@@ -995,7 +995,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2u = vec2u();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2u();"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, VectorUintUndefInitializer) {
@@ -1007,7 +1007,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2u = vec2u();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2u();"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
     p->DeliberatelyInvalidSpirv();
@@ -1022,7 +1022,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2i = vec2i();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2i();"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, VectorIntUndefInitializer) {
@@ -1034,7 +1034,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2i = vec2i();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2i();"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
     p->DeliberatelyInvalidSpirv();
@@ -1049,7 +1049,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2f = vec2f();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2f();"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, VectorFloatUndefInitializer) {
@@ -1061,7 +1061,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : vec2f = vec2f();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = vec2f();"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
     p->DeliberatelyInvalidSpirv();
@@ -1082,7 +1082,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : mat3x2f = mat3x2f("
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = mat3x2f("
                                       "vec2f(1.5f, 2.0f), "
                                       "vec2f(2.0f, 3.0f), "
                                       "vec2f(3.0f, 4.0f));"));
@@ -1097,7 +1097,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : mat3x2f = mat3x2f();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = mat3x2f();"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, MatrixUndefInitializer) {
@@ -1109,7 +1109,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : mat3x2f = mat3x2f();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = mat3x2f();"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
     p->DeliberatelyInvalidSpirv();
@@ -1125,8 +1125,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str,
-                HasSubstr("var<private> x_200 : array<u32, 2u> = array<u32, 2u>(1u, 2u);"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = array<u32, 2u>(1u, 2u);"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, ArrayNullInitializer) {
@@ -1138,7 +1137,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : array<u32, 2u> = array<u32, 2u>();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = array<u32, 2u>();"));
 }
 
 TEST_F(SpvModuleScopeVarParserTest, ArrayUndefInitializer) {
@@ -1150,7 +1149,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : array<u32, 2u> = array<u32, 2u>();"));
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = array<u32, 2u>();"));
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
     p->DeliberatelyInvalidSpirv();
@@ -1167,8 +1166,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str,
-                HasSubstr("var<private> x_200 : S = S(1u, 1.5f, array<u32, 2u>(1u, 2u));"))
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = S(1u, 1.5f, array<u32, 2u>(1u, 2u));"))
         << module_str;
 }
 
@@ -1181,7 +1179,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
     EXPECT_TRUE(p->error().empty());
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : S = S(0u, 0.0f, array<u32, 2u>());"))
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = S(0u, 0.0f, array<u32, 2u>());"))
         << module_str;
 }
 
@@ -1195,7 +1193,7 @@
     EXPECT_TRUE(p->error().empty());
 
     const auto module_str = test::ToString(p->program());
-    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 : S = S(0u, 0.0f, array<u32, 2u>());"))
+    EXPECT_THAT(module_str, HasSubstr("var<private> x_200 = S(0u, 0.0f, array<u32, 2u>());"))
         << module_str;
 
     // This example module emits ok, but is not valid SPIR-V in the first place.
@@ -1660,7 +1658,7 @@
         R"(var<private> x_1 : i32;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -1773,7 +1771,7 @@
     const std::string expected = R"(var<private> x_1 : i32;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -1826,7 +1824,7 @@
     const std::string expected = R"(var<private> x_1 : u32;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -1855,8 +1853,8 @@
     const std::string expected = R"(var<private> x_1 : u32;
 
 fn main_1() {
-  let x_11 : ptr<private, u32> = &(x_1);
-  let x_2 : u32 = *(x_11);
+  let x_11 = &(x_1);
+  let x_2 = *(x_11);
   return;
 }
 
@@ -1885,7 +1883,7 @@
     const std::string expected = R"(var<private> x_1 : u32;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -1999,7 +1997,7 @@
     const std::string expected = R"(var<private> x_1 : array<u32, 1u>;
 
 fn main_1() {
-  let x_3 : u32 = x_1[0i];
+  let x_3 = x_1[0i];
   return;
 }
 
@@ -2031,7 +2029,7 @@
     const std::string expected = R"(var<private> x_1 : array<u32, 1u>;
 
 fn main_1() {
-  let x_4 : u32 = x_1[0i];
+  let x_4 = x_1[0i];
   return;
 }
 
@@ -2063,7 +2061,7 @@
     const std::string expected = R"(var<private> x_1 : array<u32, 1u>;
 
 fn main_1() {
-  let x_4 : u32 = x_1[0i];
+  let x_4 = x_1[0i];
   return;
 }
 
@@ -2094,7 +2092,7 @@
     const std::string expected = R"(var<private> x_1 : array<i32, 1u>;
 
 fn main_1() {
-  let x_3 : i32 = x_1[0i];
+  let x_3 = x_1[0i];
   return;
 }
 
@@ -2126,7 +2124,7 @@
     const std::string expected = R"(var<private> x_1 : array<i32, 1u>;
 
 fn main_1() {
-  let x_4 : i32 = x_1[0i];
+  let x_4 = x_1[0i];
   return;
 }
 
@@ -2158,7 +2156,7 @@
     const std::string expected = R"(var<private> x_1 : array<i32, 1u>;
 
 fn main_1() {
-  let x_4 : i32 = x_1[0i];
+  let x_4 = x_1[0i];
   return;
 }
 
@@ -2435,7 +2433,7 @@
 var<private> x_1 : Arr;
 
 fn main_1() {
-  let x_3 : u32 = x_1[0i];
+  let x_3 = x_1[0i];
   return;
 }
 
@@ -2532,7 +2530,7 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -2589,9 +2587,9 @@
 var<private> x_5 : vec4f;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   if (true) {
-    let x_3 : u32 = x_1;
+    let x_3 = x_1;
     if (true) {
     }
   }
@@ -2631,8 +2629,8 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_14 : ptr<private, i32> = &(x_1);
-  let x_2 : i32 = *(x_14);
+  let x_14 = &(x_1);
+  let x_2 = *(x_14);
   return;
 }
 
@@ -2669,7 +2667,7 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -2705,7 +2703,7 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -2742,8 +2740,8 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_14 : ptr<private, u32> = &(x_1);
-  let x_2 : u32 = *(x_14);
+  let x_14 = &(x_1);
+  let x_2 = *(x_14);
   return;
 }
 
@@ -2780,7 +2778,7 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -2864,7 +2862,7 @@
 var<private> position_1 : vec4f;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -2901,8 +2899,8 @@
 var<private> position_1 : vec4f;
 
 fn main_1() {
-  let x_14 : ptr<private, i32> = &(x_1);
-  let x_2 : i32 = *(x_14);
+  let x_14 = &(x_1);
+  let x_2 = *(x_14);
   return;
 }
 
@@ -2939,7 +2937,7 @@
 var<private> position_1 : vec4f;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -2998,7 +2996,7 @@
 var<private> position_1 : vec4f;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -3035,8 +3033,8 @@
 var<private> position_1 : vec4f;
 
 fn main_1() {
-  let x_14 : ptr<private, u32> = &(x_1);
-  let x_2 : u32 = *(x_14);
+  let x_14 = &(x_1);
+  let x_2 = *(x_14);
   return;
 }
 
@@ -3073,7 +3071,7 @@
 var<private> position_1 : vec4f;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -3230,7 +3228,7 @@
     std::string expected = R"(var<private> x_1 : ${wgsl_type};
 
 fn main_1() {
-  let x_2 : ${wgsl_type} = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -3275,8 +3273,8 @@
     std::string expected = R"(var<private> x_1 : ${wgsl_type};
 
 fn main_1() {
-  let x_13 : ptr<private, ${wgsl_type}> = &(x_1);
-  let x_2 : ${wgsl_type} = *(x_13);
+  let x_13 = &(x_1);
+  let x_2 = *(x_13);
   return;
 }
 
@@ -3321,7 +3319,7 @@
     std::string expected = R"(var<private> x_1 : ${wgsl_type};
 
 fn main_1() {
-  let x_2 : ${wgsl_type} = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -3396,7 +3394,7 @@
     std::string expected = R"(var<private> x_1 : ${wgsl_type};
 
 fn main_1() {
-  let x_2 : ${wgsl_component_type} = x_1.y;
+  let x_2 = x_1.y;
   return;
 }
 
@@ -3582,7 +3580,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto got = test::ToString(p->program());
-    const std::string expected = "var<private> x_1 : u32 = 1u;";
+    const std::string expected = "var<private> x_1 = 1u;";
     EXPECT_THAT(got, HasSubstr(expected)) << got;
 }
 
@@ -3602,7 +3600,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto got = test::ToString(p->program());
-    const std::string expected = "var<private> x_1 : array<u32, 1u> = array<u32, 1u>(2u);";
+    const std::string expected = "var<private> x_1 = array<u32, 1u>(2u);";
     EXPECT_THAT(got, HasSubstr(expected)) << got;
 }
 
@@ -3622,7 +3620,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
     EXPECT_TRUE(p->error().empty());
     const auto got = test::ToString(p->program());
-    const std::string expected = "var<private> x_1 : array<i32, 1u> = array<i32, 1u>(14i);";
+    const std::string expected = "var<private> x_1 = array<i32, 1u>(14i);";
     EXPECT_THAT(got, HasSubstr(expected)) << got;
 }
 
@@ -3749,7 +3747,7 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_2 : u32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -3798,7 +3796,7 @@
 var<private> x_4 : vec4f;
 
 fn main_1() {
-  let x_2 : i32 = x_1;
+  let x_2 = x_1;
   return;
 }
 
@@ -3920,7 +3918,7 @@
     EXPECT_TRUE(p->error().empty());
     const auto got = test::ToString(p->program());
     const std::string expected =
-        R"(var<private> x_1 : array<u32, 1u> = array<u32, 1u>();
+        R"(var<private> x_1 = array<u32, 1u>();
 
 fn main_1() {
   return;
@@ -3966,7 +3964,7 @@
     EXPECT_TRUE(p->error().empty());
     const auto got = test::ToString(p->program());
     const std::string expected =
-        R"(var<private> x_1 : array<i32, 1u> = array<i32, 1u>();
+        R"(var<private> x_1 = array<i32, 1u>();
 
 fn main_1() {
   return;
@@ -4009,7 +4007,7 @@
     ASSERT_TRUE(p->Parse()) << p->error() << assembly;
     EXPECT_TRUE(p->error().empty());
     const auto got = test::ToString(p->program());
-    const std::string expected = R"(var<private> x_1 : f32 = 0.0f;
+    const std::string expected = R"(var<private> x_1 = 0.0f;
 
 fn main_1() {
   return;
@@ -4112,7 +4110,7 @@
 
     const auto got = test::ToString(p->program());
     const std::string expected =
-        R"(var<private> gl_Position : vec4f = vec4f(1.0f, 2.0f, 3.0f, 4.0f);
+        R"(var<private> gl_Position = vec4f(1.0f, 2.0f, 3.0f, 4.0f);
 
 fn main_1() {
   return;
diff --git a/src/tint/transform/manager_test.cc b/src/tint/transform/manager_test.cc
index a81f7bf..d1f6333 100644
--- a/src/tint/transform/manager_test.cc
+++ b/src/tint/transform/manager_test.cc
@@ -52,7 +52,7 @@
         ir::Builder builder(*mod);
         auto* func =
             builder.CreateFunction(mod->symbols.New("ir_func"), mod->types.Get<type::Void>());
-        func->StartTarget()->BranchTo(func->EndTarget());
+        func->StartTarget()->SetInstructions(utils::Vector{builder.Branch(func->EndTarget())});
         mod->functions.Push(func);
     }
 };
@@ -70,7 +70,7 @@
     ir::Builder builder(mod);
     auto* func =
         builder.CreateFunction(builder.ir.symbols.New("main"), builder.ir.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    func->StartTarget()->SetInstructions(utils::Vector{builder.Branch(func->EndTarget())});
     builder.ir.functions.Push(func);
     return mod;
 }
diff --git a/src/tint/type/manager.h b/src/tint/type/manager.h
index 0650f1b..4eb48bb 100644
--- a/src/tint/type/manager.h
+++ b/src/tint/type/manager.h
@@ -18,6 +18,7 @@
 #include <utility>
 
 #include "src/tint/type/type.h"
+#include "src/tint/type/vector.h"
 #include "src/tint/utils/hash.h"
 #include "src/tint/utils/unique_allocator.h"
 
@@ -84,6 +85,23 @@
         return types_.Find<TYPE>(std::forward<ARGS>(args)...);
     }
 
+    /// @param inner the inner type
+    /// @param size the vector size
+    /// @returns the vector type
+    type::Type* vec(type::Type* inner, uint32_t size) { return Get<type::Vector>(inner, size); }
+
+    /// @param inner the inner type
+    /// @returns the vector type
+    type::Type* vec2(type::Type* inner) { return vec(inner, 2); }
+
+    /// @param inner the inner type
+    /// @returns the vector type
+    type::Type* vec3(type::Type* inner) { return vec(inner, 3); }
+
+    /// @param inner the inner type
+    /// @returns the vector type
+    type::Type* vec4(type::Type* inner) { return vec(inner, 4); }
+
     /// @returns an iterator to the beginning of the types
     TypeIterator begin() const { return types_.begin(); }
     /// @returns an iterator to the end of the types
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.cc b/src/tint/writer/spirv/ir/generator_impl_ir.cc
index bd9a735..44bead9 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.cc
@@ -292,8 +292,15 @@
         current_function_.push_inst(spv::Op::OpLabel, {Label(block)});
     }
 
+    // If there are no instructions in the block, it's a dead end, so we shouldn't be able to get
+    // here to begin with.
+    if (block->Instructions().IsEmpty()) {
+        current_function_.push_inst(spv::Op::OpUnreachable, {});
+        return;
+    }
+
     // Emit the instructions.
-    for (const auto* inst : block->Instructions()) {
+    for (auto* inst : block->Instructions()) {
         auto result = Switch(
             inst,  //
             [&](const ir::Binary* b) { return EmitBinary(b); },
@@ -303,6 +310,14 @@
                 return 0u;
             },
             [&](const ir::Var* v) { return EmitVar(v); },
+            [&](const ir::If* i) {
+                EmitIf(i);
+                return 0u;
+            },
+            [&](const ir::Branch* b) {
+                EmitBranch(b);
+                return 0u;
+            },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_)
                     << "unimplemented instruction: " << inst->TypeInfo().name;
@@ -310,46 +325,42 @@
             });
         instructions_.Add(inst, result);
     }
+}
 
-    // Handle the branch at the end of the block.
+void GeneratorImplIr::EmitBranch(const ir::Branch* b) {
     Switch(
-        block->Branch().target,
-        [&](const ir::Block* b) { current_function_.push_inst(spv::Op::OpBranch, {Label(b)}); },
-        [&](const ir::If* i) { EmitIf(i); },
+        b->To(),
+        [&](const ir::Block* blk) { current_function_.push_inst(spv::Op::OpBranch, {Label(blk)}); },
         [&](const ir::FunctionTerminator*) {
             // TODO(jrprice): Handle the return value, which will be a branch argument.
-            if (!block->Branch().args.IsEmpty()) {
+            if (!b->Args().IsEmpty()) {
                 TINT_ICE(Writer, diagnostics_) << "unimplemented return value";
             }
             current_function_.push_inst(spv::Op::OpReturn, {});
         },
         [&](Default) {
-            if (!block->Branch().target) {
-                // A block may not have an outward branch (e.g. an unreachable merge block).
-                current_function_.push_inst(spv::Op::OpUnreachable, {});
-            } else {
-                TINT_ICE(Writer, diagnostics_)
-                    << "unimplemented branch target: " << block->Branch().target->TypeInfo().name;
-            }
+            // A block may not have an outward branch (e.g. an unreachable merge
+            // block).
+            current_function_.push_inst(spv::Op::OpUnreachable, {});
         });
 }
 
 void GeneratorImplIr::EmitIf(const ir::If* i) {
-    auto* merge_block = i->Merge().target->As<ir::Block>();
-    auto* true_block = i->True().target->As<ir::Block>();
-    auto* false_block = i->False().target->As<ir::Block>();
+    auto* merge_block = i->Merge();
+    auto* true_block = i->True();
+    auto* false_block = i->False();
 
     // Generate labels for the blocks. We emit the true or false block if it:
-    // 1. contains instructions, or
-    // 2. branches somewhere other then the Merge().target.
+    // 1. contains instructions other then the branch, or
+    // 2. branches somewhere other then the Merge().
     // Otherwise we skip them and branch straight to the merge block.
     uint32_t merge_label = Label(merge_block);
     uint32_t true_label = merge_label;
     uint32_t false_label = merge_label;
-    if (!true_block->Instructions().IsEmpty() || true_block->Branch().target != merge_block) {
+    if (true_block->Instructions().Length() > 1 || true_block->Branch()->To() != merge_block) {
         true_label = Label(true_block);
     }
-    if (!false_block->Instructions().IsEmpty() || false_block->Branch().target != merge_block) {
+    if (false_block->Instructions().Length() > 1 || false_block->Branch()->To() != merge_block) {
         false_label = Label(false_block);
     }
 
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir.h b/src/tint/writer/spirv/ir/generator_impl_ir.h
index b72201f..ccd09ad 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir.h
+++ b/src/tint/writer/spirv/ir/generator_impl_ir.h
@@ -30,6 +30,7 @@
 namespace tint::ir {
 class Binary;
 class Block;
+class Branch;
 class If;
 class Function;
 class Load;
@@ -121,6 +122,10 @@
     /// @returns the result ID of the instruction
     uint32_t EmitVar(const ir::Var* var);
 
+    /// Emit a branch instruction.
+    /// @param b the branch instruction to emit
+    void EmitBranch(const ir::Branch* b);
+
   private:
     /// Get the result ID of the constant `constant`, emitting its instruction if necessary.
     /// @param constant the constant to get the ID for
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc
index e9231f3..8792239 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_binary_test.cc
@@ -20,11 +20,10 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, Binary_Add_I32) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     func->StartTarget()->SetInstructions(
-        utils::Vector{b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i))});
+        utils::Vector{b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -42,11 +41,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Add_U32) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     func->StartTarget()->SetInstructions(
-        utils::Vector{b.Add(mod.types.Get<type::U32>(), b.Constant(1_u), b.Constant(2_u))});
+        utils::Vector{b.Add(mod.types.Get<type::U32>(), b.Constant(1_u), b.Constant(2_u)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -64,11 +62,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Add_F32) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     func->StartTarget()->SetInstructions(
-        utils::Vector{b.Add(mod.types.Get<type::F32>(), b.Constant(1_f), b.Constant(2_f))});
+        utils::Vector{b.Add(mod.types.Get<type::F32>(), b.Constant(1_f), b.Constant(2_f)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -86,11 +83,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Sub_I32) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     func->StartTarget()->SetInstructions(
-        utils::Vector{b.Subtract(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i))});
+        utils::Vector{b.Subtract(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -108,11 +104,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Sub_U32) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     func->StartTarget()->SetInstructions(
-        utils::Vector{b.Subtract(mod.types.Get<type::U32>(), b.Constant(1_u), b.Constant(2_u))});
+        utils::Vector{b.Subtract(mod.types.Get<type::U32>(), b.Constant(1_u), b.Constant(2_u)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -130,11 +125,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Sub_F32) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     func->StartTarget()->SetInstructions(
-        utils::Vector{b.Subtract(mod.types.Get<type::F32>(), b.Constant(1_f), b.Constant(2_f))});
+        utils::Vector{b.Subtract(mod.types.Get<type::F32>(), b.Constant(1_f), b.Constant(2_f)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -152,18 +146,15 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Sub_Vec2i) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
-    auto* lhs = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(mod.types.Get<type::I32>(), 2u),
-        utils::Vector{b.Constant(42_i)->Value(), b.Constant(-1_i)->Value()}, false, false);
-    auto* rhs = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(mod.types.Get<type::I32>(), 2u),
-        utils::Vector{b.Constant(0_i)->Value(), b.Constant(-43_i)->Value()}, false, false);
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
+    auto* lhs = b.create<constant::Composite>(mod.types.vec2(mod.types.Get<type::I32>()),
+                                              utils::Vector{b.I32(42), b.I32(-1)}, false, false);
+    auto* rhs = b.create<constant::Composite>(mod.types.vec2(mod.types.Get<type::I32>()),
+                                              utils::Vector{b.I32(0), b.I32(-43)}, false, false);
     func->StartTarget()->SetInstructions(
         utils::Vector{b.Subtract(mod.types.Get<type::Vector>(mod.types.Get<type::I32>(), 2u),
-                                 b.Constant(lhs), b.Constant(rhs))});
+                                 b.Constant(lhs), b.Constant(rhs)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -186,22 +177,17 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Sub_Vec4f) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
-    auto* lhs = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(mod.types.Get<type::F32>(), 4u),
-        utils::Vector{b.Constant(42_f)->Value(), b.Constant(-1_f)->Value(),
-                      b.Constant(0_f)->Value(), b.Constant(1.25_f)->Value()},
-        false, false);
-    auto* rhs = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(mod.types.Get<type::F32>(), 4u),
-        utils::Vector{b.Constant(0_f)->Value(), b.Constant(1.25_f)->Value(),
-                      b.Constant(-42_f)->Value(), b.Constant(1_f)->Value()},
-        false, false);
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
+    auto* lhs = b.create<constant::Composite>(
+        mod.types.vec4(mod.types.Get<type::F32>()),
+        utils::Vector{b.F32(42), b.F32(-1), b.F32(0), b.F32(1.25)}, false, false);
+    auto* rhs = b.create<constant::Composite>(
+        mod.types.vec4(mod.types.Get<type::F32>()),
+        utils::Vector{b.F32(0), b.F32(1.25), b.F32(-42), b.F32(1)}, false, false);
     func->StartTarget()->SetInstructions(
         utils::Vector{b.Subtract(mod.types.Get<type::Vector>(mod.types.Get<type::F32>(), 4u),
-                                 b.Constant(lhs), b.Constant(rhs))});
+                                 b.Constant(lhs), b.Constant(rhs)),
+                      b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -226,11 +212,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Chain) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
-
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
     auto* a = b.Subtract(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i));
-    func->StartTarget()->SetInstructions(utils::Vector{a, b.Add(mod.types.Get<type::I32>(), a, a)});
+    func->StartTarget()->SetInstructions(
+        utils::Vector{a, b.Add(mod.types.Get<type::I32>(), a, a), b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_constant_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_constant_test.cc
index 6ab48aa..75775ff 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_constant_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_constant_test.cc
@@ -63,11 +63,10 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_Vec4Bool) {
-    auto* t = b.Constant(true);
-    auto* f = b.Constant(false);
-    auto* v = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(mod.types.Get<type::Bool>(), 4u),
-        utils::Vector{t->Value(), f->Value(), f->Value(), t->Value()}, false, true);
+    auto* v = b.create<constant::Composite>(
+        mod.types.vec4(mod.types.Get<type::Bool>()),
+        utils::Vector{b.Bool(true), b.Bool(false), b.Bool(false), b.Bool(true)}, false, true);
+
     generator_.Constant(b.Constant(v));
     EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeBool
 %2 = OpTypeVector %3 4
@@ -78,12 +77,8 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_Vec2i) {
-    auto* i = mod.types.Get<type::I32>();
-    auto* i_42 = b.Constant(i32(42));
-    auto* i_n1 = b.Constant(i32(-1));
-    auto* v = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(i, 2u), utils::Vector{i_42->Value(), i_n1->Value()}, false,
-        false);
+    auto* v = b.create<constant::Composite>(mod.types.vec2(mod.types.Get<type::I32>()),
+                                            utils::Vector{b.I32(42), b.I32(-1)}, false, false);
     generator_.Constant(b.Constant(v));
     EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeInt 32 1
 %2 = OpTypeVector %3 2
@@ -94,13 +89,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_Vec3u) {
-    auto* u = mod.types.Get<type::U32>();
-    auto* u_42 = b.Constant(u32(42));
-    auto* u_0 = b.Constant(u32(0));
-    auto* u_4b = b.Constant(u32(4000000000));
-    auto* v = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(u, 3u),
-        utils::Vector{u_42->Value(), u_0->Value(), u_4b->Value()}, false, true);
+    auto* v = b.create<constant::Composite>(mod.types.vec3(mod.types.Get<type::U32>()),
+                                            utils::Vector{b.U32(42), b.U32(0), b.U32(4000000000)},
+                                            false, true);
     generator_.Constant(b.Constant(v));
     EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeInt 32 0
 %2 = OpTypeVector %3 3
@@ -112,14 +103,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_Vec4f) {
-    auto* f = mod.types.Get<type::F32>();
-    auto* f_42 = b.Constant(f32(42));
-    auto* f_0 = b.Constant(f32(0));
-    auto* f_q = b.Constant(f32(0.25));
-    auto* f_n1 = b.Constant(f32(-1));
-    auto* v = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(f, 4u),
-        utils::Vector{f_42->Value(), f_0->Value(), f_q->Value(), f_n1->Value()}, false, true);
+    auto* v = b.create<constant::Composite>(
+        mod.types.vec4(mod.types.Get<type::F32>()),
+        utils::Vector{b.F32(42), b.F32(0), b.F32(0.25), b.F32(-1)}, false, true);
     generator_.Constant(b.Constant(v));
     EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeFloat 32
 %2 = OpTypeVector %3 4
@@ -132,12 +118,8 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_Vec2h) {
-    auto* h = mod.types.Get<type::F16>();
-    auto* h_42 = b.Constant(f16(42));
-    auto* h_q = b.Constant(f16(0.25));
-    auto* v = mod.constants_arena.Create<constant::Composite>(
-        mod.types.Get<type::Vector>(h, 2u), utils::Vector{h_42->Value(), h_q->Value()}, false,
-        false);
+    auto* v = b.create<constant::Composite>(mod.types.vec2(mod.types.Get<type::F16>()),
+                                            utils::Vector{b.F16(42), b.F16(0.25)}, false, false);
     generator_.Constant(b.Constant(v));
     EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeFloat 16
 %2 = OpTypeVector %3 2
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc
index d2af246..7221963 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_function_test.cc
@@ -18,8 +18,8 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, Function_Empty) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
+    func->StartTarget()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -34,8 +34,8 @@
 
 // Test that we do not emit the same function type more than once.
 TEST_F(SpvGeneratorImplTest, Function_DeduplicateType) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
+    func->StartTarget()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     generator_.EmitFunction(func);
@@ -46,9 +46,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Compute) {
-    auto* func = b.CreateFunction(mod.symbols.Register("main"), mod.types.Get<type::Void>(),
+    auto* func = b.CreateFunction("main", mod.types.Get<type::Void>(),
                                   ir::Function::PipelineStage::kCompute, {{32, 4, 1}});
-    func->StartTarget()->BranchTo(func->EndTarget());
+    func->StartTarget()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpEntryPoint GLCompute %1 "main"
@@ -64,9 +64,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Fragment) {
-    auto* func = b.CreateFunction(mod.symbols.Register("main"), mod.types.Get<type::Void>(),
+    auto* func = b.CreateFunction("main", mod.types.Get<type::Void>(),
                                   ir::Function::PipelineStage::kFragment);
-    func->StartTarget()->BranchTo(func->EndTarget());
+    func->StartTarget()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpEntryPoint Fragment %1 "main"
@@ -82,9 +82,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Vertex) {
-    auto* func = b.CreateFunction(mod.symbols.Register("main"), mod.types.Get<type::Void>(),
-                                  ir::Function::PipelineStage::kVertex);
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func =
+        b.CreateFunction("main", mod.types.Get<type::Void>(), ir::Function::PipelineStage::kVertex);
+    func->StartTarget()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpEntryPoint Vertex %1 "main"
@@ -99,17 +99,17 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Multiple) {
-    auto* f1 = b.CreateFunction(mod.symbols.Register("main1"), mod.types.Get<type::Void>(),
+    auto* f1 = b.CreateFunction("main1", mod.types.Get<type::Void>(),
                                 ir::Function::PipelineStage::kCompute, {{32, 4, 1}});
-    f1->StartTarget()->BranchTo(f1->EndTarget());
+    f1->StartTarget()->SetInstructions(utils::Vector{b.Branch(f1->EndTarget())});
 
-    auto* f2 = b.CreateFunction(mod.symbols.Register("main2"), mod.types.Get<type::Void>(),
+    auto* f2 = b.CreateFunction("main2", mod.types.Get<type::Void>(),
                                 ir::Function::PipelineStage::kCompute, {{8, 2, 16}});
-    f2->StartTarget()->BranchTo(f2->EndTarget());
+    f2->StartTarget()->SetInstructions(utils::Vector{b.Branch(f2->EndTarget())});
 
-    auto* f3 = b.CreateFunction(mod.symbols.Register("main3"), mod.types.Get<type::Void>(),
+    auto* f3 = b.CreateFunction("main3", mod.types.Get<type::Void>(),
                                 ir::Function::PipelineStage::kFragment);
-    f3->StartTarget()->BranchTo(f3->EndTarget());
+    f3->StartTarget()->SetInstructions(utils::Vector{b.Branch(f3->EndTarget())});
 
     generator_.EmitFunction(f1);
     generator_.EmitFunction(f2);
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
index fa8cc6b..dc312d1 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_if_test.cc
@@ -20,14 +20,14 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, If_TrueEmpty_FalseEmpty) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* i = b.CreateIf(b.Constant(true));
-    i->True().target->As<ir::Block>()->BranchTo(i->Merge().target);
-    i->False().target->As<ir::Block>()->BranchTo(i->Merge().target);
-    i->Merge().target->As<ir::Block>()->BranchTo(func->EndTarget());
+    i->True()->SetInstructions(utils::Vector{b.Branch(i->Merge())});
+    i->False()->SetInstructions(utils::Vector{b.Branch(i->Merge())});
+    i->Merge()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
-    func->StartTarget()->BranchTo(i);
+    func->StartTarget()->SetInstructions(utils::Vector{i});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -46,18 +46,17 @@
 }
 
 TEST_F(SpvGeneratorImplTest, If_FalseEmpty) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* i = b.CreateIf(b.Constant(true));
-    i->False().target->As<ir::Block>()->BranchTo(i->Merge().target);
-    i->Merge().target->As<ir::Block>()->BranchTo(func->EndTarget());
+    i->False()->SetInstructions(utils::Vector{b.Branch(i->Merge())});
+    i->Merge()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
-    auto* true_block = i->True().target->As<ir::Block>();
-    true_block->SetInstructions(
-        utils::Vector{b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(1_i))});
-    true_block->BranchTo(i->Merge().target);
+    auto* true_block = i->True();
+    true_block->SetInstructions(utils::Vector{
+        b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(1_i)), b.Branch(i->Merge())});
 
-    func->StartTarget()->BranchTo(i);
+    func->StartTarget()->SetInstructions(utils::Vector{i});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -81,18 +80,17 @@
 }
 
 TEST_F(SpvGeneratorImplTest, If_TrueEmpty) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* i = b.CreateIf(b.Constant(true));
-    i->True().target->As<ir::Block>()->BranchTo(i->Merge().target);
-    i->Merge().target->As<ir::Block>()->BranchTo(func->EndTarget());
+    i->True()->SetInstructions(utils::Vector{b.Branch(i->Merge())});
+    i->Merge()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
-    auto* false_block = i->False().target->As<ir::Block>();
-    false_block->SetInstructions(
-        utils::Vector{b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(1_i))});
-    false_block->BranchTo(i->Merge().target);
+    auto* false_block = i->False();
+    false_block->SetInstructions(utils::Vector{
+        b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(1_i)), b.Branch(i->Merge())});
 
-    func->StartTarget()->BranchTo(i);
+    func->StartTarget()->SetInstructions(utils::Vector{i});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -116,14 +114,13 @@
 }
 
 TEST_F(SpvGeneratorImplTest, If_BothBranchesReturn) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* i = b.CreateIf(b.Constant(true));
-    i->True().target->As<ir::Block>()->BranchTo(func->EndTarget());
-    i->False().target->As<ir::Block>()->BranchTo(func->EndTarget());
-    i->Merge().target->As<ir::Block>()->BranchTo(nullptr);
+    i->True()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
+    i->False()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
-    func->StartTarget()->BranchTo(i);
+    func->StartTarget()->SetInstructions(utils::Vector{i});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
diff --git a/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc b/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc
index 20e5167..6c9fbc3 100644
--- a/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc
+++ b/src/tint/writer/spirv/ir/generator_impl_ir_var_test.cc
@@ -21,12 +21,11 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, FunctionVar_NoInit) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* ty = mod.types.Get<type::Pointer>(
         mod.types.Get<type::I32>(), builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
-    func->StartTarget()->SetInstructions(utils::Vector{b.Declare(ty)});
+    func->StartTarget()->SetInstructions(utils::Vector{b.Declare(ty), b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -43,15 +42,14 @@
 }
 
 TEST_F(SpvGeneratorImplTest, FunctionVar_WithInit) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* ty = mod.types.Get<type::Pointer>(
         mod.types.Get<type::I32>(), builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
     auto* v = b.Declare(ty);
     v->SetInitializer(b.Constant(42_i));
 
-    func->StartTarget()->SetInstructions(utils::Vector{v});
+    func->StartTarget()->SetInstructions(utils::Vector{v, b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -70,13 +68,12 @@
 }
 
 TEST_F(SpvGeneratorImplTest, FunctionVar_Name) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* ty = mod.types.Get<type::Pointer>(
         mod.types.Get<type::I32>(), builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
     auto* v = b.Declare(ty);
-    func->StartTarget()->SetInstructions(utils::Vector{v});
+    func->StartTarget()->SetInstructions(utils::Vector{v, b.Branch(func->EndTarget())});
     mod.SetName(v, "myvar");
 
     generator_.EmitFunction(func);
@@ -95,8 +92,7 @@
 }
 
 TEST_F(SpvGeneratorImplTest, FunctionVar_DeclInsideBlock) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* ty = mod.types.Get<type::Pointer>(
         mod.types.Get<type::I32>(), builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
@@ -104,14 +100,11 @@
     v->SetInitializer(b.Constant(42_i));
 
     auto* i = b.CreateIf(b.Constant(true));
-    i->False().target->As<ir::Block>()->BranchTo(func->EndTarget());
-    i->Merge().target->As<ir::Block>()->BranchTo(func->EndTarget());
+    i->True()->SetInstructions(utils::Vector{v, b.Branch(i->Merge())});
+    i->False()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
+    i->Merge()->SetInstructions(utils::Vector{b.Branch(func->EndTarget())});
 
-    auto* true_block = i->True().target->As<ir::Block>();
-    true_block->SetInstructions(utils::Vector{v});
-    true_block->BranchTo(i->Merge().target);
-
-    func->StartTarget()->BranchTo(i);
+    func->StartTarget()->SetInstructions(utils::Vector{i});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -139,14 +132,13 @@
 }
 
 TEST_F(SpvGeneratorImplTest, FunctionVar_Load) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* store_ty = mod.types.Get<type::I32>();
     auto* ty = mod.types.Get<type::Pointer>(store_ty, builtin::AddressSpace::kFunction,
                                             builtin::Access::kReadWrite);
     auto* v = b.Declare(ty);
-    func->StartTarget()->SetInstructions(utils::Vector{v, b.Load(v)});
+    func->StartTarget()->SetInstructions(utils::Vector{v, b.Load(v), b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -164,13 +156,13 @@
 }
 
 TEST_F(SpvGeneratorImplTest, FunctionVar_Store) {
-    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
-    func->StartTarget()->BranchTo(func->EndTarget());
+    auto* func = b.CreateFunction("foo", mod.types.Get<type::Void>());
 
     auto* ty = mod.types.Get<type::Pointer>(
         mod.types.Get<type::I32>(), builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
     auto* v = b.Declare(ty);
-    func->StartTarget()->SetInstructions(utils::Vector{v, b.Store(v, b.Constant(42_i))});
+    func->StartTarget()->SetInstructions(
+        utils::Vector{v, b.Store(v, b.Constant(42_i)), b.Branch(func->EndTarget())});
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"