[ir] Add ability to clone in the IR.

Add support for cloning in the IR.

This CL is based off of https://dawn-review.googlesource.com/c/dawn/+/152400

Bug: tint:1718
Change-Id: I8fce30872ad2c02c59363d2cd8b5771917d97342
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/152221
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/lang/core/ir/BUILD.bazel b/src/tint/lang/core/ir/BUILD.bazel
index 7d7188c..195dbfd 100644
--- a/src/tint/lang/core/ir/BUILD.bazel
+++ b/src/tint/lang/core/ir/BUILD.bazel
@@ -35,6 +35,7 @@
     "builder.cc",
     "builtin_call.cc",
     "call.cc",
+    "clone_context.cc",
     "constant.cc",
     "construct.cc",
     "continue.cc",
@@ -85,6 +86,7 @@
     "builder.h",
     "builtin_call.h",
     "call.h",
+    "clone_context.h",
     "constant.h",
     "construct.h",
     "continue.h",
@@ -188,7 +190,9 @@
     "store_vector_element_test.cc",
     "switch_test.cc",
     "swizzle_test.cc",
+    "terminate_invocation_test.cc",
     "unary_test.cc",
+    "unreachable_test.cc",
     "user_call_test.cc",
     "validator_test.cc",
     "value_test.cc",
diff --git a/src/tint/lang/core/ir/BUILD.cmake b/src/tint/lang/core/ir/BUILD.cmake
index 3e21f9e..8af569e 100644
--- a/src/tint/lang/core/ir/BUILD.cmake
+++ b/src/tint/lang/core/ir/BUILD.cmake
@@ -46,6 +46,8 @@
   lang/core/ir/builtin_call.h
   lang/core/ir/call.cc
   lang/core/ir/call.h
+  lang/core/ir/clone_context.cc
+  lang/core/ir/clone_context.h
   lang/core/ir/constant.cc
   lang/core/ir/constant.h
   lang/core/ir/construct.cc
@@ -188,7 +190,9 @@
   lang/core/ir/store_vector_element_test.cc
   lang/core/ir/switch_test.cc
   lang/core/ir/swizzle_test.cc
+  lang/core/ir/terminate_invocation_test.cc
   lang/core/ir/unary_test.cc
+  lang/core/ir/unreachable_test.cc
   lang/core/ir/user_call_test.cc
   lang/core/ir/validator_test.cc
   lang/core/ir/value_test.cc
diff --git a/src/tint/lang/core/ir/BUILD.gn b/src/tint/lang/core/ir/BUILD.gn
index 4a844c9..661272f 100644
--- a/src/tint/lang/core/ir/BUILD.gn
+++ b/src/tint/lang/core/ir/BUILD.gn
@@ -49,6 +49,8 @@
     "builtin_call.h",
     "call.cc",
     "call.h",
+    "clone_context.cc",
+    "clone_context.h",
     "constant.cc",
     "constant.h",
     "construct.cc",
@@ -189,7 +191,9 @@
       "store_vector_element_test.cc",
       "switch_test.cc",
       "swizzle_test.cc",
+      "terminate_invocation_test.cc",
       "unary_test.cc",
+      "unreachable_test.cc",
       "user_call_test.cc",
       "validator_test.cc",
       "value_test.cc",
diff --git a/src/tint/lang/core/ir/access.cc b/src/tint/lang/core/ir/access.cc
index 34051ac..73682b9 100644
--- a/src/tint/lang/core/ir/access.cc
+++ b/src/tint/lang/core/ir/access.cc
@@ -16,6 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Access);
 
 namespace tint::core::ir {
@@ -28,6 +31,13 @@
 }
 
 Access::~Access() = default;
+
+Access* Access::Clone(CloneContext& ctx) {
+    auto new_result = ctx.Clone(Result());
+    auto new_obj = ctx.Clone(Object());
+    auto new_indices = ctx.Clone<Access::kDefaultNumOperands>(Indices());
+    return ctx.ir.instructions.Create<Access>(new_result, new_obj, new_indices);
+}
 //! @endcond
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/access.h b/src/tint/lang/core/ir/access.h
index 4c76121..0221f50 100644
--- a/src/tint/lang/core/ir/access.h
+++ b/src/tint/lang/core/ir/access.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// An access instruction in the IR.
-class Access : public Castable<Access, OperandInstruction<3, 1>> {
+class Access final : public Castable<Access, OperandInstruction<3, 1>> {
   public:
     /// The offset in Operands() for the object being accessed
     static constexpr size_t kObjectOperandOffset = 0;
@@ -38,6 +38,9 @@
     Access(InstructionResult* result, Value* object, VectorRef<Value*> indices);
     ~Access() override;
 
+    /// @copydoc Instruction::Clone()
+    Access* Clone(CloneContext& ctx) override;
+
     /// @returns the object used for the access
     Value* Object() { return operands_[kObjectOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/access_test.cc b/src/tint/lang/core/ir/access_test.cc
index 8b3c310..7d9c4d6 100644
--- a/src/tint/lang/core/ir/access_test.cc
+++ b/src/tint/lang/core/ir/access_test.cc
@@ -18,7 +18,8 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/lang/core/ir/ir_helper_test.h"
 
-using namespace tint::core::fluent_types;  // NOLINT
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
 
 namespace tint::core::ir {
 namespace {
@@ -60,5 +61,43 @@
         "");
 }
 
+TEST_F(IR_AccessTest, Clone) {
+    auto* type = ty.ptr<function, i32>();
+    auto* var = b.Var(type);
+    auto* idx1 = b.Constant(u32(1));
+    auto* idx2 = b.Constant(u32(2));
+    auto* a = b.Access(type, var, idx1, idx2);
+
+    auto* new_a = clone_ctx.Clone(a);
+
+    EXPECT_NE(a, new_a);
+
+    EXPECT_NE(a->Result(), new_a->Result());
+    EXPECT_EQ(type, new_a->Result()->Type());
+
+    EXPECT_NE(nullptr, new_a->Object());
+    EXPECT_NE(a->Object(), new_a->Object());
+
+    auto indices = new_a->Indices();
+    EXPECT_EQ(2u, indices.Length());
+
+    auto* val0 = indices[0]->As<Constant>()->Value();
+    EXPECT_EQ(1_u, val0->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto* val1 = indices[1]->As<Constant>()->Value();
+    EXPECT_EQ(2_u, val1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_AccessTest, CloneNoIndices) {
+    auto* type = ty.ptr<function, i32>();
+    auto* var = b.Var(type);
+    auto* a = b.Access(type, var);
+
+    auto* new_a = clone_ctx.Clone(a);
+
+    auto indices = new_a->Indices();
+    EXPECT_EQ(0u, indices.Length());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/binary.cc b/src/tint/lang/core/ir/binary.cc
index 1b1bdcf..251d587 100644
--- a/src/tint/lang/core/ir/binary.cc
+++ b/src/tint/lang/core/ir/binary.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/binary.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Binary);
 
 namespace tint::core::ir {
@@ -26,6 +29,13 @@
 
 Binary::~Binary() = default;
 
+Binary* Binary::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_lhs = ctx.Clone(LHS());
+    auto* new_rhs = ctx.Clone(RHS());
+    return ctx.ir.instructions.Create<Binary>(new_result, kind_, new_lhs, new_rhs);
+}
+
 std::string_view ToString(enum Binary::Kind kind) {
     switch (kind) {
         case Binary::Kind::kAdd:
diff --git a/src/tint/lang/core/ir/binary.h b/src/tint/lang/core/ir/binary.h
index 53ddf1a..3f4c79a 100644
--- a/src/tint/lang/core/ir/binary.h
+++ b/src/tint/lang/core/ir/binary.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A binary instruction in the IR.
-class Binary : public Castable<Binary, OperandInstruction<2, 1>> {
+class Binary final : public Castable<Binary, OperandInstruction<2, 1>> {
   public:
     /// The offset in Operands() for the LHS
     static constexpr size_t kLhsOperandOffset = 0;
@@ -62,6 +62,9 @@
     Binary(InstructionResult* result, enum Kind kind, Value* lhs, Value* rhs);
     ~Binary() override;
 
+    /// @copydoc Instruction::Clone()
+    Binary* Clone(CloneContext& ctx) override;
+
     /// @returns the kind of the binary instruction
     enum Kind Kind() { return kind_; }
 
diff --git a/src/tint/lang/core/ir/binary_test.cc b/src/tint/lang/core/ir/binary_test.cc
index 721bf45..bf8553f 100644
--- a/src/tint/lang/core/ir/binary_test.cc
+++ b/src/tint/lang/core/ir/binary_test.cc
@@ -374,5 +374,26 @@
     EXPECT_THAT(rhs_b->Usages(), testing::UnorderedElementsAre(Usage{inst, 1u}));
 }
 
+TEST_F(IR_BinaryTest, Clone) {
+    auto* lhs = b.Constant(2_i);
+    auto* rhs = b.Constant(4_i);
+    auto* inst = b.And(mod.Types().i32(), lhs, rhs);
+
+    auto* c = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, c);
+
+    EXPECT_EQ(mod.Types().i32(), c->Result()->Type());
+    EXPECT_EQ(Binary::Kind::kAnd, c->Kind());
+
+    auto new_lhs = c->LHS()->As<Constant>()->Value();
+    ASSERT_TRUE(new_lhs->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(2_i, new_lhs->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+
+    auto new_rhs = c->RHS()->As<Constant>()->Value();
+    ASSERT_TRUE(new_rhs->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, new_rhs->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/bitcast.cc b/src/tint/lang/core/ir/bitcast.cc
index cdaf695..f1fc737 100644
--- a/src/tint/lang/core/ir/bitcast.cc
+++ b/src/tint/lang/core/ir/bitcast.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/bitcast.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Bitcast);
 
 namespace tint::core::ir {
@@ -25,4 +28,10 @@
 
 Bitcast::~Bitcast() = default;
 
+Bitcast* Bitcast::Clone(CloneContext& ctx) {
+    auto* new_res = ctx.Clone(Result());
+    auto* new_val = ctx.Clone(Val());
+    return ctx.ir.instructions.Create<Bitcast>(new_res, new_val);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/bitcast.h b/src/tint/lang/core/ir/bitcast.h
index d5f7425..3c35280 100644
--- a/src/tint/lang/core/ir/bitcast.h
+++ b/src/tint/lang/core/ir/bitcast.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A bitcast instruction in the IR.
-class Bitcast : public Castable<Bitcast, Call> {
+class Bitcast final : public Castable<Bitcast, Call> {
   public:
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
@@ -34,6 +34,9 @@
     Bitcast(InstructionResult* result, Value* val);
     ~Bitcast() override;
 
+    /// @copydoc Instruction::Clone()
+    Bitcast* Clone(CloneContext& ctx) override;
+
     /// @returns the operand value
     Value* Val() { return operands_[kValueOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/bitcast_test.cc b/src/tint/lang/core/ir/bitcast_test.cc
index 02b190d..b6d14ff 100644
--- a/src/tint/lang/core/ir/bitcast_test.cc
+++ b/src/tint/lang/core/ir/bitcast_test.cc
@@ -70,5 +70,19 @@
         "");
 }
 
+TEST_F(IR_BitcastTest, Clone) {
+    auto* inst = b.Bitcast(mod.Types().i32(), 4_i);
+
+    auto* n = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, n);
+
+    EXPECT_EQ(mod.Types().i32(), n->Result()->Type());
+
+    auto new_val = n->Val()->As<Constant>()->Value();
+    ASSERT_TRUE(new_val->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, new_val->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/block.cc b/src/tint/lang/core/ir/block.cc
index f49c9d0..00654b9 100644
--- a/src/tint/lang/core/ir/block.cc
+++ b/src/tint/lang/core/ir/block.cc
@@ -13,6 +13,10 @@
 // limitations under the License.
 
 #include "src/tint/lang/core/ir/block.h"
+
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/control_instruction.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Block);
@@ -23,6 +27,30 @@
 
 Block::~Block() = default;
 
+Block* Block::Clone(CloneContext&) {
+    TINT_UNREACHABLE() << "blocks must be cloned with CloneInto";
+    return nullptr;
+}
+
+void Block::CloneInto(CloneContext& ctx, Block* out) {
+    // Note, the `parent_` is not cloned here. Doing so can end up in infinite loops as we try to
+    // clone a control instruction and the blocks inside of it. The `parent_` pointer should be set
+    // by the control instructions constructor.
+
+    for (auto* inst_in : *this) {
+        auto* inst_out = inst_in->Clone(ctx);
+        auto results_out = inst_out->Results();
+        auto results_in = inst_in->Results();
+        TINT_ASSERT(results_out.Length() == results_in.Length());
+
+        size_t len = results_out.Length();
+        for (size_t i = 0; i < len; ++i) {
+            ctx.Replace(results_in[i], results_out[i]);
+        }
+        out->Append(inst_out);
+    }
+}
+
 Instruction* Block::Prepend(Instruction* inst) {
     TINT_ASSERT_OR_RETURN_VALUE(inst, inst);
     TINT_ASSERT_OR_RETURN_VALUE(inst->Block() == nullptr, inst);
diff --git a/src/tint/lang/core/ir/block.h b/src/tint/lang/core/ir/block.h
index 1badce2..a01e8a0 100644
--- a/src/tint/lang/core/ir/block.h
+++ b/src/tint/lang/core/ir/block.h
@@ -36,6 +36,15 @@
     Block();
     ~Block() override;
 
+    /// @param ctx the CloneContext used to clone this block
+    /// @returns a clone of this block
+    virtual Block* Clone(CloneContext& ctx);
+
+    /// Clones the block contents into the given block
+    /// @param ctx the CloneContext used to clone
+    /// @param out the block to clone into
+    virtual void CloneInto(CloneContext& ctx, Block* out);
+
     /// @returns true if this is block has a terminator instruction
     bool HasTerminator() {
         return instructions_.last != nullptr && instructions_.last->Is<ir::Terminator>();
diff --git a/src/tint/lang/core/ir/block_param.cc b/src/tint/lang/core/ir/block_param.cc
index b2a992e..1993417 100644
--- a/src/tint/lang/core/ir/block_param.cc
+++ b/src/tint/lang/core/ir/block_param.cc
@@ -13,6 +13,9 @@
 // limitations under the License.
 
 #include "src/tint/lang/core/ir/block_param.h"
+
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::BlockParam);
@@ -25,4 +28,14 @@
 
 BlockParam::~BlockParam() = default;
 
+BlockParam* BlockParam::Clone(CloneContext& ctx) {
+    auto* new_bp = ctx.ir.values.Create<BlockParam>(type_);
+
+    auto name = ctx.ir.NameOf(this);
+    if (name.IsValid()) {
+        ctx.ir.SetName(new_bp, ctx.ir.NameOf(this).Name());
+    }
+    return new_bp;
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/block_param.h b/src/tint/lang/core/ir/block_param.h
index 315aba7..778a6f2 100644
--- a/src/tint/lang/core/ir/block_param.h
+++ b/src/tint/lang/core/ir/block_param.h
@@ -31,6 +31,9 @@
     /// @returns the type of the var
     const core::type::Type* Type() override { return type_; }
 
+    /// @copydoc Instruction::Clone()
+    BlockParam* Clone(CloneContext& ctx) override;
+
   private:
     /// the result type of the instruction
     const core::type::Type* type_ = nullptr;
diff --git a/src/tint/lang/core/ir/block_param_test.cc b/src/tint/lang/core/ir/block_param_test.cc
index 8677c83..ca29c98 100644
--- a/src/tint/lang/core/ir/block_param_test.cc
+++ b/src/tint/lang/core/ir/block_param_test.cc
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/core/ir/block_param.h"
+#include <string>
+
 #include "gtest/gtest-spi.h"
+#include "src/tint/lang/core/ir/block_param.h"
 #include "src/tint/lang/core/ir/ir_helper_test.h"
 
 namespace tint::core::ir {
@@ -32,5 +34,23 @@
         "");
 }
 
+TEST_F(IR_BlockParamTest, Clone) {
+    auto* inst = b.BlockParam(mod.Types().i32());
+
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, new_inst);
+    EXPECT_EQ(mod.Types().i32(), new_inst->Type());
+}
+
+TEST_F(IR_BlockParamTest, CloneWithName) {
+    auto* inst = b.BlockParam("p", mod.Types().i32());
+
+    auto* new_inst = clone_ctx.Clone(inst);
+    EXPECT_EQ(mod.Types().i32(), new_inst->Type());
+
+    EXPECT_EQ(std::string("p"), mod.NameOf(new_inst).Name());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/break_if.cc b/src/tint/lang/core/ir/break_if.cc
index 56ce784..bf4a6c3 100644
--- a/src/tint/lang/core/ir/break_if.cc
+++ b/src/tint/lang/core/ir/break_if.cc
@@ -17,7 +17,9 @@
 #include <utility>
 
 #include "src/tint/lang/core/ir/block.h"
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/loop.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -38,4 +40,11 @@
 
 BreakIf::~BreakIf() = default;
 
+BreakIf* BreakIf::Clone(CloneContext& ctx) {
+    auto* new_loop = ctx.Clone(loop_);
+    auto* new_cond = ctx.Clone(Condition());
+    auto new_args = ctx.Clone<BreakIf::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<BreakIf>(new_cond, new_loop, new_args);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/break_if.h b/src/tint/lang/core/ir/break_if.h
index 1f73c2f..e672b90 100644
--- a/src/tint/lang/core/ir/break_if.h
+++ b/src/tint/lang/core/ir/break_if.h
@@ -29,7 +29,7 @@
 namespace tint::core::ir {
 
 /// A break-if iteration instruction.
-class BreakIf : public Castable<BreakIf, Terminator> {
+class BreakIf final : public Castable<BreakIf, Terminator> {
   public:
     /// The offset in Operands() for the condition
     static constexpr size_t kConditionOperandOffset = 0;
@@ -44,6 +44,9 @@
     BreakIf(Value* condition, ir::Loop* loop, VectorRef<Value*> args = tint::Empty);
     ~BreakIf() override;
 
+    /// @copydoc Instruction::Clone()
+    BreakIf* Clone(CloneContext& ctx) override;
+
     /// @returns the MultiInBlock arguments
     tint::Slice<Value* const> Args() override {
         return operands_.Slice().Offset(kArgsOperandOffset);
diff --git a/src/tint/lang/core/ir/break_if_test.cc b/src/tint/lang/core/ir/break_if_test.cc
index 644e2dd7..18e5d2d 100644
--- a/src/tint/lang/core/ir/break_if_test.cc
+++ b/src/tint/lang/core/ir/break_if_test.cc
@@ -58,5 +58,49 @@
         "");
 }
 
+TEST_F(IR_BreakIfTest, Clone) {
+    auto* loop = b.Loop();
+    auto* cond = b.Constant(true);
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+
+    auto* brk = b.BreakIf(loop, cond, arg1, arg2);
+
+    auto* new_loop = clone_ctx.Clone(loop);
+    clone_ctx.Replace(loop, new_loop);
+
+    auto* new_brk = clone_ctx.Clone(brk);
+
+    EXPECT_NE(brk, new_brk);
+
+    EXPECT_EQ(new_loop, new_brk->Loop());
+
+    auto args = new_brk->Args();
+    EXPECT_EQ(2u, args.Length());
+
+    auto new_cond = new_brk->Condition()->As<Constant>()->Value();
+    ASSERT_TRUE(new_cond->Is<core::constant::Scalar<bool>>());
+    EXPECT_TRUE(new_cond->As<core::constant::Scalar<bool>>()->ValueAs<bool>());
+
+    auto new_arg0 = args[0]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg0->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(1_u, new_arg0->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto new_arg1 = args[1]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg1->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(2_u, new_arg1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_BreakIfTest, CloneNoArgs) {
+    auto* loop = b.Loop();
+    auto* cond = b.Constant(true);
+
+    auto* brk = b.BreakIf(loop, cond);
+    auto* new_brk = clone_ctx.Clone(brk);
+
+    auto args = new_brk->Args();
+    EXPECT_EQ(0u, args.Length());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 90608d2..7088ee0 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -793,7 +793,6 @@
             return nullptr;
         }
         auto* let = Append(ir.instructions.Create<ir::Let>(InstructionResult(val->Type()), val));
-        ir.SetName(let, name);
         ir.SetName(let->Result(), name);
         return let;
     }
diff --git a/src/tint/lang/core/ir/builtin_call.h b/src/tint/lang/core/ir/builtin_call.h
index f152f7d..fe35766 100644
--- a/src/tint/lang/core/ir/builtin_call.h
+++ b/src/tint/lang/core/ir/builtin_call.h
@@ -21,7 +21,7 @@
 
 namespace tint::core::ir {
 
-/// A builtin call instruction in the IR.
+/// The base class for builtin call instructions in the IR.
 class BuiltinCall : public Castable<BuiltinCall, Call> {
   public:
     /// The base offset in Operands() for the args
diff --git a/src/tint/lang/core/ir/clone_context.cc b/src/tint/lang/core/ir/clone_context.cc
new file mode 100644
index 0000000..d5faa58
--- /dev/null
+++ b/src/tint/lang/core/ir/clone_context.cc
@@ -0,0 +1,24 @@
+// 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/lang/core/ir/clone_context.h"
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/let.h"
+
+namespace tint::core::ir {
+
+CloneContext::CloneContext(Module& module) : ir(module) {}
+
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/clone_context.h b/src/tint/lang/core/ir/clone_context.h
new file mode 100644
index 0000000..b5c375b
--- /dev/null
+++ b/src/tint/lang/core/ir/clone_context.h
@@ -0,0 +1,111 @@
+// 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_LANG_CORE_IR_CLONE_CONTEXT_H_
+#define SRC_TINT_LANG_CORE_IR_CLONE_CONTEXT_H_
+
+#include "src/tint/utils/containers/hashmap.h"
+#include "src/tint/utils/containers/transform.h"
+#include "src/tint/utils/traits/traits.h"
+
+namespace tint::core::ir {
+class Block;
+class Instruction;
+class Module;
+class Value;
+}  // namespace tint::core::ir
+
+namespace tint::core::ir {
+
+/// Constant in the IR.
+class CloneContext {
+  public:
+    /// @param module the IR module
+    explicit CloneContext(Module& module);
+
+    /// The IR module
+    Module& ir;
+
+    /// Performs a clone of @p what.
+    /// @param what the item to clone
+    /// @return the cloned item
+    template <typename T>
+    T* Clone(T* what) {
+        if (auto replacement = replacements_.Get(what)) {
+            auto* cast = As<T>((*replacement)());
+            TINT_ASSERT(cast);
+            return cast;
+        }
+        auto* result = what->Clone(*this)->template As<T>();
+        Replace(what, result);
+        return result;
+    }
+
+    /// Performs a clone of all the elements in @p what.
+    /// @param what the elements to clone
+    /// @return the cloned elements
+    template <size_t N, typename T>
+    Vector<T*, N> Clone(Slice<T* const> what) {
+        return Transform<N>(what, [&](T* const p) { return Clone(p); });
+    }
+
+    /// Performs a clone of all the elements in @p what.
+    /// @param what the elements to clone
+    /// @return the cloned elements
+    template <size_t N, typename T>
+    Vector<T*, N> Clone(Slice<T*> what) {
+        return Transform<N>(what, [&](T* p) { return Clone(p); });
+    }
+
+    /// Performs a clone of all the elements in @p what.
+    /// @param what the elements to clone
+    /// @return the cloned elements
+    template <size_t N, typename T>
+    Vector<T*, N> Clone(Vector<T*, N> what) {
+        return Transform(what, [&](T* p) { return Clone(p); });
+    }
+
+    /// Registers the replacement of `what` with `with`
+    /// @param what the value or instruction to replace
+    /// @param with either a pointer to a replacement instruction, or a function with the signature
+    /// `T*(T*)` used to build the replacement
+    template <typename WHAT, typename WITH>
+    void Replace(WHAT* what, WITH&& with) {
+        using T = std::decay_t<WHAT>;
+        using F = std::decay_t<WITH>;
+
+        constexpr bool T_is_value = traits::IsTypeOrDerived<T, Value>;
+        constexpr bool T_is_instruction = traits::IsTypeOrDerived<T, Instruction>;
+        static_assert(T_is_value || T_is_instruction);
+
+        constexpr bool F_is_pointer = std::is_pointer_v<F>;
+        constexpr bool F_is_function = std::is_function_v<F>;
+        static_assert(F_is_pointer || F_is_function);
+
+        if constexpr (F_is_pointer) {
+            replacements_.Add(what, [with]() { return with; });
+        } else if constexpr (F_is_function) {
+            static_assert(std::is_same_v<traits::ParameterType<F, 0>, T*>);
+            static_assert(std::is_same_v<traits::ReturnType<F>, T*>);
+            replacements_.Add(what, [what, with]() { return with(what); });
+        }
+    }
+
+  private:
+    Hashmap<CastableBase*, std::function<CastableBase*()>, 8> replacements_;
+};
+
+}  // namespace tint::core::ir
+
+#endif  // SRC_TINT_LANG_CORE_IR_CLONE_CONTEXT_H_
diff --git a/src/tint/lang/core/ir/constant.cc b/src/tint/lang/core/ir/constant.cc
index 2510a39..a9cc837 100644
--- a/src/tint/lang/core/ir/constant.cc
+++ b/src/tint/lang/core/ir/constant.cc
@@ -25,4 +25,8 @@
 
 Constant::~Constant() = default;
 
+Constant* Constant::Clone(CloneContext&) {
+    return this;  // Constants are immutable so can just return ourselves.
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/constant.h b/src/tint/lang/core/ir/constant.h
index a316e24..ebf584b 100644
--- a/src/tint/lang/core/ir/constant.h
+++ b/src/tint/lang/core/ir/constant.h
@@ -34,6 +34,9 @@
     /// @returns the type of the constant
     const core::type::Type* Type() override { return value_->Type(); }
 
+    /// @copydoc Value::Clone()
+    Constant* Clone(CloneContext& ctx) override;
+
   private:
     const core::constant::Value* const value_ = nullptr;
 };
diff --git a/src/tint/lang/core/ir/constant_test.cc b/src/tint/lang/core/ir/constant_test.cc
index 73b4282..1bdcfb9 100644
--- a/src/tint/lang/core/ir/constant_test.cc
+++ b/src/tint/lang/core/ir/constant_test.cc
@@ -113,5 +113,12 @@
         "");
 }
 
+TEST_F(IR_ConstantTest, Clone) {
+    auto* c = b.Constant(2_u);
+    auto* new_c = clone_ctx.Clone(c);
+
+    EXPECT_EQ(c, new_c);
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/construct.cc b/src/tint/lang/core/ir/construct.cc
index bbdb1db..f9ae628 100644
--- a/src/tint/lang/core/ir/construct.cc
+++ b/src/tint/lang/core/ir/construct.cc
@@ -16,6 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Construct);
 
 namespace tint::core::ir {
@@ -27,4 +30,10 @@
 
 Construct::~Construct() = default;
 
+Construct* Construct::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto new_args = ctx.Clone<Construct::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<Construct>(new_result, new_args);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/construct.h b/src/tint/lang/core/ir/construct.h
index 61c9a76..29fe22c 100644
--- a/src/tint/lang/core/ir/construct.h
+++ b/src/tint/lang/core/ir/construct.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A constructor instruction in the IR.
-class Construct : public Castable<Construct, Call> {
+class Construct final : public Castable<Construct, Call> {
   public:
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
@@ -34,6 +34,9 @@
     explicit Construct(InstructionResult* result, VectorRef<Value*> args = tint::Empty);
     ~Construct() override;
 
+    /// @copydoc Instruction::Clone()
+    Construct* Clone(CloneContext& ctx) override;
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() override { return "construct"; }
 };
diff --git a/src/tint/lang/core/ir/construct_test.cc b/src/tint/lang/core/ir/construct_test.cc
index 044fb87..0340892 100644
--- a/src/tint/lang/core/ir/construct_test.cc
+++ b/src/tint/lang/core/ir/construct_test.cc
@@ -54,5 +54,35 @@
         "");
 }
 
+TEST_F(IR_ConstructTest, Clone) {
+    auto* arg1 = b.Constant(true);
+    auto* arg2 = b.Constant(false);
+    auto* c = b.Construct(mod.Types().f32(), arg1, arg2);
+
+    auto* new_c = clone_ctx.Clone(c);
+
+    EXPECT_NE(c, new_c);
+    EXPECT_NE(c->Result(), new_c->Result());
+    EXPECT_EQ(mod.Types().f32(), new_c->Result()->Type());
+
+    auto args = new_c->Args();
+    EXPECT_EQ(2u, args.Length());
+
+    auto* val0 = args[0]->As<Constant>()->Value();
+    EXPECT_TRUE(val0->As<core::constant::Scalar<bool>>()->ValueAs<bool>());
+
+    auto* val1 = args[1]->As<Constant>()->Value();
+    EXPECT_FALSE(val1->As<core::constant::Scalar<bool>>()->ValueAs<bool>());
+}
+
+TEST_F(IR_ConstructTest, CloneEmpty) {
+    auto* c = b.Construct(mod.Types().f32());
+
+    auto* new_c = clone_ctx.Clone(c);
+    EXPECT_NE(c->Result(), new_c->Result());
+    EXPECT_EQ(mod.Types().f32(), new_c->Result()->Type());
+    EXPECT_TRUE(new_c->Args().IsEmpty());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/continue.cc b/src/tint/lang/core/ir/continue.cc
index 6fb585b..7ddb1a3 100644
--- a/src/tint/lang/core/ir/continue.cc
+++ b/src/tint/lang/core/ir/continue.cc
@@ -17,7 +17,9 @@
 #include <utility>
 
 #include "src/tint/lang/core/ir/block.h"
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/loop.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -37,4 +39,11 @@
 
 Continue::~Continue() = default;
 
+Continue* Continue::Clone(CloneContext& ctx) {
+    auto* new_loop = ctx.Clone(Loop());
+    auto new_args = ctx.Clone<Continue::kDefaultNumOperands>(Args());
+
+    return ctx.ir.instructions.Create<Continue>(new_loop, new_args);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/continue.h b/src/tint/lang/core/ir/continue.h
index 1c393e1..b4a0178 100644
--- a/src/tint/lang/core/ir/continue.h
+++ b/src/tint/lang/core/ir/continue.h
@@ -28,7 +28,7 @@
 namespace tint::core::ir {
 
 /// A continue instruction.
-class Continue : public Castable<Continue, Terminator> {
+class Continue final : public Castable<Continue, Terminator> {
   public:
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
@@ -39,6 +39,9 @@
     explicit Continue(ir::Loop* loop, VectorRef<Value*> args = tint::Empty);
     ~Continue() override;
 
+    /// @copydoc Instruction::Clone()
+    Continue* Clone(CloneContext& ctx) override;
+
     /// @returns the loop owning the continue block
     ir::Loop* Loop() { return loop_; }
 
diff --git a/src/tint/lang/core/ir/continue_test.cc b/src/tint/lang/core/ir/continue_test.cc
index 0dfe024..88c25ec 100644
--- a/src/tint/lang/core/ir/continue_test.cc
+++ b/src/tint/lang/core/ir/continue_test.cc
@@ -56,5 +56,38 @@
         "");
 }
 
+TEST_F(IR_ContinueTest, Clone) {
+    auto* loop = b.Loop();
+    auto* cont = b.Continue(loop);
+
+    auto* new_loop = clone_ctx.Clone(loop);
+    clone_ctx.Replace(loop, new_loop);
+
+    auto* new_c = clone_ctx.Clone(cont);
+
+    EXPECT_NE(cont, new_c);
+    EXPECT_EQ(new_loop, new_c->Loop());
+    EXPECT_TRUE(new_c->Args().IsEmpty());
+}
+
+TEST_F(IR_ContinueTest, CloneWithArgs) {
+    auto* loop = b.Loop();
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+
+    auto* cont = b.Continue(loop, arg1, arg2);
+
+    auto* new_c = clone_ctx.Clone(cont);
+
+    auto args = new_c->Args();
+    EXPECT_EQ(2u, args.Length());
+
+    auto* val0 = args[0]->As<Constant>()->Value();
+    EXPECT_EQ(1_u, val0->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto* val1 = args[1]->As<Constant>()->Value();
+    EXPECT_EQ(2_u, val1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/convert.cc b/src/tint/lang/core/ir/convert.cc
index 3f3a70b..c9a5696 100644
--- a/src/tint/lang/core/ir/convert.cc
+++ b/src/tint/lang/core/ir/convert.cc
@@ -16,6 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Convert);
 
 namespace tint::core::ir {
@@ -27,4 +30,10 @@
 
 Convert::~Convert() = default;
 
+Convert* Convert::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_val = ctx.Clone(Args()[0]);
+    return ctx.ir.instructions.Create<Convert>(new_result, new_val);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/convert.h b/src/tint/lang/core/ir/convert.h
index 1c58228..6699c18 100644
--- a/src/tint/lang/core/ir/convert.h
+++ b/src/tint/lang/core/ir/convert.h
@@ -24,7 +24,7 @@
 namespace tint::core::ir {
 
 /// A value conversion instruction in the IR.
-class Convert : public Castable<Convert, Call> {
+class Convert final : public Castable<Convert, Call> {
   public:
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
@@ -35,6 +35,9 @@
     Convert(InstructionResult* result, Value* value);
     ~Convert() override;
 
+    /// @copydoc Instruction::Clone()
+    Convert* Clone(CloneContext& ctx) override;
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() override { return "convert"; }
 };
diff --git a/src/tint/lang/core/ir/convert_test.cc b/src/tint/lang/core/ir/convert_test.cc
index d239dcc..3732b8f 100644
--- a/src/tint/lang/core/ir/convert_test.cc
+++ b/src/tint/lang/core/ir/convert_test.cc
@@ -41,5 +41,21 @@
     EXPECT_EQ(c->Result()->Source(), c);
 }
 
+TEST_F(IR_ConvertTest, Clone) {
+    auto* c = b.Convert(mod.Types().f32(), 1_u);
+
+    auto* new_c = clone_ctx.Clone(c);
+
+    EXPECT_NE(c, new_c);
+    EXPECT_NE(c->Result(), new_c->Result());
+    EXPECT_EQ(mod.Types().f32(), new_c->Result()->Type());
+
+    auto args = new_c->Args();
+    EXPECT_EQ(1u, args.Length());
+
+    auto* val0 = args[0]->As<Constant>()->Value();
+    EXPECT_EQ(1_u, val0->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/core_builtin_call.cc b/src/tint/lang/core/ir/core_builtin_call.cc
index 68a49c8..ada7835 100644
--- a/src/tint/lang/core/ir/core_builtin_call.cc
+++ b/src/tint/lang/core/ir/core_builtin_call.cc
@@ -16,6 +16,8 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::CoreBuiltinCall);
@@ -32,4 +34,10 @@
 
 CoreBuiltinCall::~CoreBuiltinCall() = default;
 
+CoreBuiltinCall* CoreBuiltinCall::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto new_args = ctx.Clone<CoreBuiltinCall::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<CoreBuiltinCall>(new_result, func_, new_args);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/core_builtin_call.h b/src/tint/lang/core/ir/core_builtin_call.h
index bc1907b..c887b4b 100644
--- a/src/tint/lang/core/ir/core_builtin_call.h
+++ b/src/tint/lang/core/ir/core_builtin_call.h
@@ -26,7 +26,7 @@
 namespace tint::core::ir {
 
 /// A core builtin call instruction in the IR.
-class CoreBuiltinCall : public Castable<CoreBuiltinCall, BuiltinCall> {
+class CoreBuiltinCall final : public Castable<CoreBuiltinCall, BuiltinCall> {
   public:
     /// Constructor
     /// @param result the result value
@@ -37,6 +37,9 @@
                     VectorRef<Value*> args = tint::Empty);
     ~CoreBuiltinCall() override;
 
+    /// @copydoc Instruction::Clone()
+    CoreBuiltinCall* Clone(CloneContext& ctx) override;
+
     /// @returns the builtin function
     core::Function Func() { return func_; }
 
diff --git a/src/tint/lang/core/ir/core_builtin_call_test.cc b/src/tint/lang/core/ir/core_builtin_call_test.cc
index 1a201e4..e05e564 100644
--- a/src/tint/lang/core/ir/core_builtin_call_test.cc
+++ b/src/tint/lang/core/ir/core_builtin_call_test.cc
@@ -73,5 +73,39 @@
         "");
 }
 
+TEST_F(IR_CoreBuiltinCallTest, Clone) {
+    auto* builtin = b.Call(mod.Types().f32(), core::Function::kAbs, 1_u, 2_u);
+
+    auto* new_b = clone_ctx.Clone(builtin);
+
+    EXPECT_NE(builtin, new_b);
+    EXPECT_NE(builtin->Result(), new_b->Result());
+    EXPECT_EQ(mod.Types().f32(), new_b->Result()->Type());
+
+    EXPECT_EQ(core::Function::kAbs, new_b->Func());
+
+    auto args = new_b->Args();
+    EXPECT_EQ(2u, args.Length());
+
+    auto* val0 = args[0]->As<Constant>()->Value();
+    EXPECT_EQ(1_u, val0->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto* val1 = args[1]->As<Constant>()->Value();
+    EXPECT_EQ(2_u, val1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_CoreBuiltinCallTest, CloneNoArgs) {
+    auto* builtin = b.Call(mod.Types().f32(), core::Function::kAbs);
+
+    auto* new_b = clone_ctx.Clone(builtin);
+    EXPECT_NE(builtin->Result(), new_b->Result());
+    EXPECT_EQ(mod.Types().f32(), new_b->Result()->Type());
+
+    EXPECT_EQ(core::Function::kAbs, new_b->Func());
+
+    auto args = new_b->Args();
+    EXPECT_TRUE(args.IsEmpty());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/discard.cc b/src/tint/lang/core/ir/discard.cc
index a4aaa19..7112f01 100644
--- a/src/tint/lang/core/ir/discard.cc
+++ b/src/tint/lang/core/ir/discard.cc
@@ -13,7 +13,9 @@
 // limitations under the License.
 
 #include "src/tint/lang/core/ir/discard.h"
-#include "src/tint/lang/core/type/void.h"
+
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Discard);
 
@@ -23,4 +25,8 @@
 
 Discard::~Discard() = default;
 
+Discard* Discard::Clone(CloneContext& ctx) {
+    return ctx.ir.instructions.Create<Discard>();
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/discard.h b/src/tint/lang/core/ir/discard.h
index b98c753..b9d3ee9 100644
--- a/src/tint/lang/core/ir/discard.h
+++ b/src/tint/lang/core/ir/discard.h
@@ -23,12 +23,15 @@
 namespace tint::core::ir {
 
 /// A discard instruction in the IR.
-class Discard : public Castable<Discard, Call> {
+class Discard final : public Castable<Discard, Call> {
   public:
     /// Constructor
     Discard();
     ~Discard() override;
 
+    /// @copydoc Instruction::Clone()
+    Discard* Clone(CloneContext& ctx) override;
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() override { return "discard"; }
 };
diff --git a/src/tint/lang/core/ir/discard_test.cc b/src/tint/lang/core/ir/discard_test.cc
index 2ebb80b..cb3bf9be 100644
--- a/src/tint/lang/core/ir/discard_test.cc
+++ b/src/tint/lang/core/ir/discard_test.cc
@@ -34,5 +34,13 @@
     EXPECT_FALSE(inst->HasMultiResults());
 }
 
+TEST_F(IR_DiscardTest, Clone) {
+    auto* d = b.Discard();
+    auto* new_d = clone_ctx.Clone(d);
+
+    EXPECT_NE(d, new_d);
+    EXPECT_NE(nullptr, new_d);
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_if.cc b/src/tint/lang/core/ir/exit_if.cc
index 641df8c..8bb6539 100644
--- a/src/tint/lang/core/ir/exit_if.cc
+++ b/src/tint/lang/core/ir/exit_if.cc
@@ -16,7 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/if.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::ExitIf);
@@ -30,6 +32,12 @@
 
 ExitIf::~ExitIf() = default;
 
+ExitIf* ExitIf::Clone(CloneContext& ctx) {
+    auto* new_if = ctx.Clone(If());
+    auto new_args = ctx.Clone<ExitIf::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<ExitIf>(new_if, new_args);
+}
+
 void ExitIf::SetIf(ir::If* i) {
     SetControlInstruction(i);
 }
diff --git a/src/tint/lang/core/ir/exit_if.h b/src/tint/lang/core/ir/exit_if.h
index 31e6d55..57950ee 100644
--- a/src/tint/lang/core/ir/exit_if.h
+++ b/src/tint/lang/core/ir/exit_if.h
@@ -28,7 +28,7 @@
 namespace tint::core::ir {
 
 /// A exit if instruction.
-class ExitIf : public Castable<ExitIf, Exit> {
+class ExitIf final : public Castable<ExitIf, Exit> {
   public:
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
@@ -39,6 +39,9 @@
     explicit ExitIf(ir::If* i, VectorRef<Value*> args = tint::Empty);
     ~ExitIf() override;
 
+    /// @copydoc Instruction::Clone()
+    ExitIf* Clone(CloneContext& ctx) override;
+
     /// Re-associates the exit with the given if instruction
     /// @param i the new If to exit from
     void SetIf(ir::If* i);
diff --git a/src/tint/lang/core/ir/exit_if_test.cc b/src/tint/lang/core/ir/exit_if_test.cc
index a74ae52..46b70e1 100644
--- a/src/tint/lang/core/ir/exit_if_test.cc
+++ b/src/tint/lang/core/ir/exit_if_test.cc
@@ -53,5 +53,40 @@
     EXPECT_FALSE(exit->Alive());
 }
 
+TEST_F(IR_ExitIfTest, Clone) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* if_ = b.If(true);
+    auto* e = b.ExitIf(if_, arg1, arg2);
+
+    auto* new_if = clone_ctx.Clone(if_);
+    auto* new_exit = clone_ctx.Clone(e);
+
+    EXPECT_NE(e, new_exit);
+    EXPECT_EQ(new_if, new_exit->If());
+
+    auto args = new_exit->Args();
+    ASSERT_EQ(2u, args.Length());
+
+    auto new_arg1 = args[0]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg1->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(1_u, new_arg1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto new_arg2 = args[1]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg2->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(2_u, new_arg2->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_ExitIfTest, CloneNoArgs) {
+    auto* if_ = b.If(true);
+    auto* e = b.ExitIf(if_);
+
+    auto* new_if = clone_ctx.Clone(if_);
+    auto* new_exit = clone_ctx.Clone(e);
+
+    EXPECT_EQ(new_if, new_exit->If());
+    EXPECT_TRUE(new_exit->Args().IsEmpty());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_loop.cc b/src/tint/lang/core/ir/exit_loop.cc
index 674ce88..8e8d504 100644
--- a/src/tint/lang/core/ir/exit_loop.cc
+++ b/src/tint/lang/core/ir/exit_loop.cc
@@ -17,7 +17,9 @@
 #include <utility>
 
 #include "src/tint/lang/core/ir/block.h"
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/loop.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::ExitLoop);
@@ -31,6 +33,12 @@
 
 ExitLoop::~ExitLoop() = default;
 
+ExitLoop* ExitLoop::Clone(CloneContext& ctx) {
+    auto* new_loop = ctx.Clone(Loop());
+    auto new_args = ctx.Clone<ExitLoop::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<ExitLoop>(new_loop, new_args);
+}
+
 void ExitLoop::SetLoop(ir::Loop* l) {
     SetControlInstruction(l);
 }
diff --git a/src/tint/lang/core/ir/exit_loop.h b/src/tint/lang/core/ir/exit_loop.h
index 6e173df..e50c7f7 100644
--- a/src/tint/lang/core/ir/exit_loop.h
+++ b/src/tint/lang/core/ir/exit_loop.h
@@ -28,7 +28,7 @@
 namespace tint::core::ir {
 
 /// A exit loop instruction.
-class ExitLoop : public Castable<ExitLoop, Exit> {
+class ExitLoop final : public Castable<ExitLoop, Exit> {
   public:
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
@@ -39,6 +39,9 @@
     explicit ExitLoop(ir::Loop* loop, VectorRef<Value*> args = tint::Empty);
     ~ExitLoop() override;
 
+    /// @copydoc Instruction::Clone()
+    ExitLoop* Clone(CloneContext& ctx) override;
+
     /// Re-associates the exit with the given loop instruction
     /// @param l the new loop to exit from
     void SetLoop(ir::Loop* l);
diff --git a/src/tint/lang/core/ir/exit_loop_test.cc b/src/tint/lang/core/ir/exit_loop_test.cc
index 0853d6b1..79ce43a 100644
--- a/src/tint/lang/core/ir/exit_loop_test.cc
+++ b/src/tint/lang/core/ir/exit_loop_test.cc
@@ -43,5 +43,40 @@
     EXPECT_FALSE(exit->Alive());
 }
 
+TEST_F(IR_ExitLoopTest, Clone) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* loop = b.Loop();
+    auto* e = b.ExitLoop(loop, arg1, arg2);
+
+    auto* new_loop = clone_ctx.Clone(loop);
+    auto* new_exit = clone_ctx.Clone(e);
+
+    EXPECT_NE(e, new_exit);
+    EXPECT_EQ(new_loop, new_exit->Loop());
+
+    auto args = new_exit->Args();
+    ASSERT_EQ(2u, args.Length());
+
+    auto new_arg1 = args[0]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg1->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(1_u, new_arg1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto new_arg2 = args[1]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg2->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(2_u, new_arg2->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_ExitLoopTest, CloneNoArgs) {
+    auto* loop = b.Loop();
+    auto* e = b.ExitLoop(loop);
+
+    auto* new_loop = clone_ctx.Clone(loop);
+    auto* new_exit = clone_ctx.Clone(e);
+
+    EXPECT_EQ(new_loop, new_exit->Loop());
+    EXPECT_TRUE(new_exit->Args().IsEmpty());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/exit_switch.cc b/src/tint/lang/core/ir/exit_switch.cc
index 7a70073..2216c8f 100644
--- a/src/tint/lang/core/ir/exit_switch.cc
+++ b/src/tint/lang/core/ir/exit_switch.cc
@@ -16,6 +16,8 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/lang/core/ir/switch.h"
 
@@ -30,6 +32,12 @@
 
 ExitSwitch::~ExitSwitch() = default;
 
+ExitSwitch* ExitSwitch::Clone(CloneContext& ctx) {
+    auto* new_switch = ctx.Clone(Switch());
+    auto new_args = ctx.Clone<ExitSwitch::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<ExitSwitch>(new_switch, new_args);
+}
+
 void ExitSwitch::SetSwitch(ir::Switch* s) {
     SetControlInstruction(s);
 }
diff --git a/src/tint/lang/core/ir/exit_switch.h b/src/tint/lang/core/ir/exit_switch.h
index 45a2733..c73ad6b 100644
--- a/src/tint/lang/core/ir/exit_switch.h
+++ b/src/tint/lang/core/ir/exit_switch.h
@@ -28,7 +28,7 @@
 namespace tint::core::ir {
 
 /// A exit switch instruction.
-class ExitSwitch : public Castable<ExitSwitch, Exit> {
+class ExitSwitch final : public Castable<ExitSwitch, Exit> {
   public:
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
@@ -39,6 +39,9 @@
     explicit ExitSwitch(ir::Switch* sw, VectorRef<Value*> args = tint::Empty);
     ~ExitSwitch() override;
 
+    /// @copydoc Instruction::Clone()
+    ExitSwitch* Clone(CloneContext& ctx) override;
+
     /// Re-associates the exit with the given switch instruction
     /// @param s the new switch to exit from
     void SetSwitch(ir::Switch* s);
diff --git a/src/tint/lang/core/ir/exit_switch_test.cc b/src/tint/lang/core/ir/exit_switch_test.cc
index d476cda..23f2f9c 100644
--- a/src/tint/lang/core/ir/exit_switch_test.cc
+++ b/src/tint/lang/core/ir/exit_switch_test.cc
@@ -53,5 +53,40 @@
     EXPECT_FALSE(exit->Alive());
 }
 
+TEST_F(IR_ExitSwitchTest, Clone) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+    auto* switch_ = b.Switch(true);
+    auto* e = b.ExitSwitch(switch_, arg1, arg2);
+
+    auto* new_switch = clone_ctx.Clone(switch_);
+    auto* new_exit = clone_ctx.Clone(e);
+
+    EXPECT_NE(e, new_exit);
+    EXPECT_EQ(new_switch, new_exit->Switch());
+
+    auto args = new_exit->Args();
+    ASSERT_EQ(2u, args.Length());
+
+    auto new_arg1 = args[0]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg1->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(1_u, new_arg1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto new_arg2 = args[1]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg2->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(2_u, new_arg2->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_ExitSwitchTest, CloneNoArgs) {
+    auto* switch_ = b.Switch(true);
+    auto* e = b.ExitSwitch(switch_);
+
+    auto* new_switch = clone_ctx.Clone(switch_);
+    auto* new_exit = clone_ctx.Clone(e);
+
+    EXPECT_EQ(new_switch, new_exit->Switch());
+    EXPECT_TRUE(new_exit->Args().IsEmpty());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/function.cc b/src/tint/lang/core/ir/function.cc
index 28a23f2..57309c0 100644
--- a/src/tint/lang/core/ir/function.cc
+++ b/src/tint/lang/core/ir/function.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/core/ir/function.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/containers/predicates.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -32,6 +34,22 @@
 
 Function::~Function() = default;
 
+Function* Function::Clone(CloneContext& ctx) {
+    auto* new_func = ctx.ir.values.Create<Function>(return_.type, pipeline_stage_, workgroup_size_);
+    new_func->block_ = ctx.ir.blocks.Create<ir::Block>();
+    new_func->params_ = ctx.Clone<1>(params_.Slice());
+    new_func->return_.builtin = return_.builtin;
+    new_func->return_.location = return_.location;
+    new_func->return_.invariant = return_.invariant;
+
+    ctx.Replace(this, new_func);
+    block_->CloneInto(ctx, new_func->block_);
+
+    ctx.ir.SetName(new_func, ctx.ir.NameOf(this).Name());
+    ctx.ir.functions.Push(new_func);
+    return new_func;
+}
+
 void Function::SetParams(VectorRef<FunctionParam*> params) {
     params_ = std::move(params);
     TINT_ASSERT(!params_.Any(IsNull));
diff --git a/src/tint/lang/core/ir/function.h b/src/tint/lang/core/ir/function.h
index c24db41..34a9276 100644
--- a/src/tint/lang/core/ir/function.h
+++ b/src/tint/lang/core/ir/function.h
@@ -67,6 +67,9 @@
              std::optional<std::array<uint32_t, 3>> wg_size = {});
     ~Function() override;
 
+    /// @copydoc Instruction::Clone()
+    Function* Clone(CloneContext& ctx) override;
+
     /// Sets the function stage
     /// @param stage the stage to set
     void SetStage(PipelineStage stage) { pipeline_stage_ = stage; }
diff --git a/src/tint/lang/core/ir/function_param.cc b/src/tint/lang/core/ir/function_param.cc
index 877364e..8d667ee 100644
--- a/src/tint/lang/core/ir/function_param.cc
+++ b/src/tint/lang/core/ir/function_param.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/core/ir/function_param.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::FunctionParam);
@@ -58,4 +60,18 @@
     return "<unknown>";
 }
 
+FunctionParam* FunctionParam::Clone(CloneContext& ctx) {
+    auto* out = ctx.ir.values.Create<FunctionParam>(type_);
+    out->builtin_ = builtin_;
+    out->location_ = location_;
+    out->binding_point_ = binding_point_;
+    out->invariant_ = invariant_;
+
+    auto name = ctx.ir.NameOf(this);
+    if (name.IsValid()) {
+        ctx.ir.SetName(out, name);
+    }
+    return out;
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/function_param.h b/src/tint/lang/core/ir/function_param.h
index d9bf75c..f3b83b7 100644
--- a/src/tint/lang/core/ir/function_param.h
+++ b/src/tint/lang/core/ir/function_param.h
@@ -67,6 +67,9 @@
     /// @returns the type of the var
     const core::type::Type* Type() override { return type_; }
 
+    /// @copydoc Value::Clone()
+    FunctionParam* Clone(CloneContext& ctx) override;
+
     /// Sets the builtin information. Note, it is currently an error if the builtin is already set.
     /// @param val the builtin to set
     void SetBuiltin(FunctionParam::Builtin val) {
diff --git a/src/tint/lang/core/ir/function_param_test.cc b/src/tint/lang/core/ir/function_param_test.cc
index 1ca35d7..eb1b4c1 100644
--- a/src/tint/lang/core/ir/function_param_test.cc
+++ b/src/tint/lang/core/ir/function_param_test.cc
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/core/ir/function_param.h"
+#include <string>
+
 #include "gtest/gtest-spi.h"
+#include "src/tint/lang/core/ir/function_param.h"
 #include "src/tint/lang/core/ir/ir_helper_test.h"
 
 namespace tint::core::ir {
@@ -44,5 +46,53 @@
         "");
 }
 
+TEST_F(IR_FunctionParamTest, CloneEmpty) {
+    auto* fp = b.FunctionParam(mod.Types().f32());
+
+    auto* new_fp = clone_ctx.Clone(fp);
+    EXPECT_EQ(new_fp->Type(), mod.Types().f32());
+    EXPECT_FALSE(new_fp->Builtin().has_value());
+    EXPECT_FALSE(new_fp->Location().has_value());
+    EXPECT_FALSE(new_fp->BindingPoint().has_value());
+    EXPECT_FALSE(new_fp->Invariant());
+}
+
+TEST_F(IR_FunctionParamTest, Clone) {
+    auto* fp = b.FunctionParam(mod.Types().f32());
+    fp->SetBuiltin(FunctionParam::Builtin::kVertexIndex);
+    fp->SetLocation(
+        1, Interpolation{core::InterpolationType::kFlat, core::InterpolationSampling::kCentroid});
+    fp->SetInvariant(true);
+    fp->SetBindingPoint(1, 2);
+
+    auto* new_fp = clone_ctx.Clone(fp);
+
+    EXPECT_NE(fp, new_fp);
+    EXPECT_EQ(new_fp->Type(), mod.Types().f32());
+
+    EXPECT_TRUE(new_fp->Builtin().has_value());
+    EXPECT_EQ(FunctionParam::Builtin::kVertexIndex, new_fp->Builtin().value());
+
+    EXPECT_TRUE(new_fp->Location().has_value());
+    auto loc = new_fp->Location();
+    EXPECT_EQ(1u, loc->value);
+    EXPECT_EQ(core::InterpolationType::kFlat, loc->interpolation->type);
+    EXPECT_EQ(core::InterpolationSampling::kCentroid, loc->interpolation->sampling);
+
+    EXPECT_TRUE(new_fp->BindingPoint().has_value());
+    auto bp = new_fp->BindingPoint();
+    EXPECT_EQ(1u, bp->group);
+    EXPECT_EQ(2u, bp->binding);
+
+    EXPECT_TRUE(new_fp->Invariant());
+}
+
+TEST_F(IR_FunctionParamTest, CloneWithName) {
+    auto* fp = b.FunctionParam("fp", mod.Types().f32());
+    auto* new_fp = clone_ctx.Clone(fp);
+
+    EXPECT_EQ(std::string("fp"), mod.NameOf(new_fp).Name());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/function_test.cc b/src/tint/lang/core/ir/function_test.cc
index 862c9ab..6739793 100644
--- a/src/tint/lang/core/ir/function_test.cc
+++ b/src/tint/lang/core/ir/function_test.cc
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "src/tint/lang/core/ir/function.h"
+#include <string>
+
 #include "gtest/gtest-spi.h"
+#include "src/tint/lang/core/ir/function.h"
 #include "src/tint/lang/core/ir/ir_helper_test.h"
 
 namespace tint::core::ir {
@@ -66,5 +68,61 @@
         "");
 }
 
+TEST_F(IR_FunctionTest, Clone) {
+    auto* f =
+        b.Function("my_func", mod.Types().i32(), Function::PipelineStage::kCompute, {{2, 3, 4}});
+    f->SetReturnBuiltin(Function::ReturnBuiltin::kFragDepth);
+    f->SetReturnLocation(
+        1, Interpolation{core::InterpolationType::kFlat, core::InterpolationSampling::kCentroid});
+    f->SetReturnInvariant(true);
+
+    auto* param1 = b.FunctionParam("a", mod.Types().i32());
+    auto* param2 = b.FunctionParam("b", mod.Types().f32());
+    f->SetParams({param1, param2});
+
+    auto* new_param1 = clone_ctx.Clone(param1);
+    auto* new_param2 = clone_ctx.Clone(param2);
+    auto* new_f = clone_ctx.Clone(f);
+
+    EXPECT_NE(f, new_f);
+    EXPECT_EQ(std::string("my_func"), mod.NameOf(new_f).Name());
+
+    EXPECT_EQ(Function::PipelineStage::kCompute, new_f->Stage());
+    EXPECT_TRUE(new_f->WorkgroupSize().has_value());
+    auto wg = new_f->WorkgroupSize().value();
+    EXPECT_EQ(2u, wg[0]);
+    EXPECT_EQ(3u, wg[1]);
+    EXPECT_EQ(4u, wg[2]);
+
+    EXPECT_EQ(mod.Types().i32(), new_f->ReturnType());
+
+    EXPECT_TRUE(new_f->ReturnBuiltin().has_value());
+    EXPECT_EQ(Function::ReturnBuiltin::kFragDepth, new_f->ReturnBuiltin().value());
+
+    EXPECT_TRUE(new_f->ReturnLocation().has_value());
+    auto loc = new_f->ReturnLocation().value();
+    EXPECT_EQ(1u, loc.value);
+    EXPECT_EQ(core::InterpolationType::kFlat, loc.interpolation->type);
+    EXPECT_EQ(core::InterpolationSampling::kCentroid, loc.interpolation->sampling);
+
+    EXPECT_TRUE(new_f->ReturnInvariant());
+
+    EXPECT_EQ(2u, new_f->Params().Length());
+    EXPECT_EQ(new_param1, new_f->Params()[0]);
+    EXPECT_EQ(new_param2, new_f->Params()[1]);
+
+    EXPECT_EQ(new_f, mod.functions.Back());
+}
+
+TEST_F(IR_FunctionTest, CloneWithExits) {
+    auto* f = b.Function("my_func", mod.Types().void_());
+    b.Append(f->Block(), [&] { b.Return(f); });
+
+    auto* new_f = clone_ctx.Clone(f);
+    EXPECT_EQ(1u, new_f->Block()->Length());
+    EXPECT_TRUE(new_f->Block()->Front()->Is<Return>());
+    EXPECT_EQ(new_f, new_f->Block()->Front()->As<Return>()->Func());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/if.cc b/src/tint/lang/core/ir/if.cc
index a57adf5..6368db2 100644
--- a/src/tint/lang/core/ir/if.cc
+++ b/src/tint/lang/core/ir/if.cc
@@ -16,6 +16,8 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::If);
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -46,4 +48,17 @@
     }
 }
 
+If* If::Clone(CloneContext& ctx) {
+    auto* new_cond = ctx.Clone(Condition());
+    auto* new_true = ctx.ir.blocks.Create<ir::Block>();
+    auto* new_false = ctx.ir.blocks.Create<ir::Block>();
+
+    auto* new_if = ctx.ir.instructions.Create<If>(new_cond, new_true, new_false);
+    ctx.Replace(this, new_if);
+
+    true_->CloneInto(ctx, new_true);
+    false_->CloneInto(ctx, new_false);
+    return new_if;
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/if.h b/src/tint/lang/core/ir/if.h
index aa68508..d4f031b 100644
--- a/src/tint/lang/core/ir/if.h
+++ b/src/tint/lang/core/ir/if.h
@@ -42,7 +42,7 @@
 ///                    ▼
 ///                   out
 /// ```
-class If : public Castable<If, ControlInstruction> {
+class If final : public Castable<If, ControlInstruction> {
   public:
     /// The index of the condition operand
     static constexpr size_t kConditionOperandOffset = 0;
@@ -54,6 +54,9 @@
     If(Value* cond, ir::Block* t, ir::Block* f);
     ~If() override;
 
+    /// @copydoc Instruction::Clone()
+    If* Clone(CloneContext& ctx) override;
+
     /// @copydoc ControlInstruction::ForeachBlock
     void ForeachBlock(const std::function<void(ir::Block*)>& cb) override;
 
diff --git a/src/tint/lang/core/ir/if_test.cc b/src/tint/lang/core/ir/if_test.cc
index 8ecb742..01a8538 100644
--- a/src/tint/lang/core/ir/if_test.cc
+++ b/src/tint/lang/core/ir/if_test.cc
@@ -63,5 +63,34 @@
         "");
 }
 
+TEST_F(IR_IfTest, Clone) {
+    auto* if_ = b.If(b.Constant(true));
+    auto* new_if = clone_ctx.Clone(if_);
+
+    EXPECT_NE(if_, new_if);
+
+    auto new_cond = new_if->Condition()->As<Constant>()->Value();
+    ASSERT_TRUE(new_cond->Is<core::constant::Scalar<bool>>());
+    EXPECT_TRUE(new_cond->As<core::constant::Scalar<bool>>()->ValueAs<bool>());
+
+    EXPECT_NE(nullptr, new_if->True());
+    EXPECT_NE(nullptr, new_if->False());
+    EXPECT_NE(if_->True(), new_if->True());
+    EXPECT_NE(if_->False(), new_if->False());
+}
+
+TEST_F(IR_IfTest, CloneWithExits) {
+    If* new_if = nullptr;
+    {
+        auto* if_ = b.If(true);
+        b.Append(if_->True(), [&] { b.ExitIf(if_); });
+        new_if = clone_ctx.Clone(if_);
+    }
+
+    ASSERT_EQ(1u, new_if->True()->Length());
+    EXPECT_TRUE(new_if->True()->Front()->Is<ExitIf>());
+    EXPECT_EQ(new_if, new_if->True()->Front()->As<ExitIf>()->If());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/instruction.h b/src/tint/lang/core/ir/instruction.h
index d2503d2..7a6f790 100644
--- a/src/tint/lang/core/ir/instruction.h
+++ b/src/tint/lang/core/ir/instruction.h
@@ -25,6 +25,7 @@
 // Forward declarations
 namespace tint::core::ir {
 class Block;
+class CloneContext;
 }  // namespace tint::core::ir
 
 namespace tint::core::ir {
@@ -62,6 +63,10 @@
     /// @returns the friendly name for the instruction
     virtual std::string FriendlyName() = 0;
 
+    /// @param ctx the CloneContext used to clone this instruction
+    /// @returns a clone of this instruction
+    virtual Instruction* Clone(CloneContext& ctx) = 0;
+
     /// @returns true if the Instruction has not been destroyed with Destroy()
     bool Alive() const { return !flags_.Contains(Flag::kDead); }
 
diff --git a/src/tint/lang/core/ir/instruction_result.cc b/src/tint/lang/core/ir/instruction_result.cc
index cc78b33..01e8d61 100644
--- a/src/tint/lang/core/ir/instruction_result.cc
+++ b/src/tint/lang/core/ir/instruction_result.cc
@@ -14,8 +14,10 @@
 
 #include "src/tint/lang/core/ir/instruction_result.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/constant.h"
 #include "src/tint/lang/core/ir/instruction.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::InstructionResult);
@@ -33,4 +35,10 @@
     Base::Destroy();
 }
 
+InstructionResult* InstructionResult::Clone(CloneContext& ctx) {
+    // Do not clone the `Source`. It will be set when this result is placed in the new parent
+    // instruction.
+    return ctx.ir.values.Create<InstructionResult>(type_);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/instruction_result.h b/src/tint/lang/core/ir/instruction_result.h
index d139a3c..c3b747c 100644
--- a/src/tint/lang/core/ir/instruction_result.h
+++ b/src/tint/lang/core/ir/instruction_result.h
@@ -36,6 +36,9 @@
     /// @returns the type of the value
     const core::type::Type* Type() override { return type_; }
 
+    /// @copydoc Value::Clone()
+    InstructionResult* Clone(CloneContext& ctx) override;
+
     /// Sets the type of the value to @p type
     /// @param type the new type of the value
     void SetType(const core::type::Type* type) { type_ = type; }
diff --git a/src/tint/lang/core/ir/instruction_result_test.cc b/src/tint/lang/core/ir/instruction_result_test.cc
index 68951b7..0991ea3 100644
--- a/src/tint/lang/core/ir/instruction_result_test.cc
+++ b/src/tint/lang/core/ir/instruction_result_test.cc
@@ -35,5 +35,14 @@
         "");
 }
 
+TEST_F(IR_InstructionResultTest, Clone) {
+    auto* val = b.Add(mod.Types().i32(), 1_i, 2_i)->Result();
+    auto* new_res = clone_ctx.Clone(val);
+
+    EXPECT_NE(val, new_res);
+    EXPECT_EQ(nullptr, new_res->Source());
+    EXPECT_EQ(mod.Types().i32(), new_res->Type());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/ir_helper_test.h b/src/tint/lang/core/ir/ir_helper_test.h
index 9e13af9..8d749c2 100644
--- a/src/tint/lang/core/ir/ir_helper_test.h
+++ b/src/tint/lang/core/ir/ir_helper_test.h
@@ -17,6 +17,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/module.h"
 
 namespace tint::core::ir {
@@ -34,6 +35,9 @@
     Builder b{mod};
     /// The type manager
     core::type::Manager& ty{mod.Types()};
+
+    /// CloneContext
+    CloneContext clone_ctx{mod};
 };
 
 using IRTestHelper = IRTestHelperBase<testing::Test>;
diff --git a/src/tint/lang/core/ir/let.cc b/src/tint/lang/core/ir/let.cc
index 4e7236a..b843ee5 100644
--- a/src/tint/lang/core/ir/let.cc
+++ b/src/tint/lang/core/ir/let.cc
@@ -13,6 +13,9 @@
 // limitations under the License.
 
 #include "src/tint/lang/core/ir/let.h"
+
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/store.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Let);
@@ -26,4 +29,15 @@
 
 Let::~Let() = default;
 
+Let* Let::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_val = ctx.Clone(Value());
+    auto* new_let = ctx.ir.instructions.Create<Let>(new_result, new_val);
+
+    auto name = ctx.ir.NameOf(this);
+    ctx.ir.SetName(new_let, name.Name());
+
+    return new_let;
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/let.h b/src/tint/lang/core/ir/let.h
index 5c0355b..4077534 100644
--- a/src/tint/lang/core/ir/let.h
+++ b/src/tint/lang/core/ir/let.h
@@ -22,7 +22,7 @@
 namespace tint::core::ir {
 
 /// A no-op instruction in the IR, used to position and name a value
-class Let : public Castable<Let, OperandInstruction<1, 1>> {
+class Let final : public Castable<Let, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
@@ -33,6 +33,9 @@
     Let(InstructionResult* result, Value* value);
     ~Let() override;
 
+    /// @copydoc Instruction::Clone()
+    Let* Clone(CloneContext& ctx) override;
+
     /// @returns the value
     ir::Value* Value() { return operands_[kValueOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/let_test.cc b/src/tint/lang/core/ir/let_test.cc
index b0f716c..6769734 100644
--- a/src/tint/lang/core/ir/let_test.cc
+++ b/src/tint/lang/core/ir/let_test.cc
@@ -49,5 +49,23 @@
     EXPECT_EQ(let->Result()->Type(), value->Type());
 }
 
+TEST_F(IR_LetTest, Clone) {
+    auto* value = b.Constant(4_f);
+    auto* let = b.Let("l", value);
+
+    auto* new_let = clone_ctx.Clone(let);
+
+    EXPECT_NE(let, new_let);
+    EXPECT_NE(nullptr, new_let->Result());
+    EXPECT_NE(let->Result(), new_let->Result());
+
+    auto new_val = new_let->Value()->As<Constant>()->Value();
+    ASSERT_TRUE(new_val->Is<core::constant::Scalar<f32>>());
+    EXPECT_FLOAT_EQ(4_f, new_val->As<core::constant::Scalar<f32>>()->ValueAs<f32>());
+
+    EXPECT_EQ(std::string("l"), mod.NameOf(new_let).Name());
+    EXPECT_EQ(std::string("l"), mod.NameOf(new_let->Result()).Name());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load.cc b/src/tint/lang/core/ir/load.cc
index d60e151..361643b 100644
--- a/src/tint/lang/core/ir/load.cc
+++ b/src/tint/lang/core/ir/load.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/core/ir/load.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -33,4 +35,10 @@
 
 Load::~Load() = default;
 
+Load* Load::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_from = ctx.Clone(From());
+    return ctx.ir.instructions.Create<Load>(new_result, new_from);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load.h b/src/tint/lang/core/ir/load.h
index 55fb983..1bf1ca1 100644
--- a/src/tint/lang/core/ir/load.h
+++ b/src/tint/lang/core/ir/load.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A load instruction in the IR.
-class Load : public Castable<Load, OperandInstruction<1, 1>> {
+class Load final : public Castable<Load, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the from value
     static constexpr size_t kFromOperandOffset = 0;
@@ -35,6 +35,9 @@
 
     ~Load() override;
 
+    /// @copydoc Instruction::Clone()
+    Load* Clone(CloneContext& ctx) override;
+
     /// @returns the value being loaded from
     Value* From() { return operands_[kFromOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/load_test.cc b/src/tint/lang/core/ir/load_test.cc
index fe4c6fe..1a07476 100644
--- a/src/tint/lang/core/ir/load_test.cc
+++ b/src/tint/lang/core/ir/load_test.cc
@@ -69,5 +69,19 @@
         "");
 }
 
+TEST_F(IR_LoadTest, Clone) {
+    auto* var = b.Var(ty.ptr<function, i32>());
+    auto* inst = b.Load(var);
+
+    auto* new_var = clone_ctx.Clone(var);
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, new_inst);
+    EXPECT_NE(nullptr, new_inst->Result());
+    EXPECT_NE(inst->Result(), new_inst->Result());
+
+    EXPECT_EQ(new_var->Result(), new_inst->From());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load_vector_element.cc b/src/tint/lang/core/ir/load_vector_element.cc
index e0969b9..aebf4d5 100644
--- a/src/tint/lang/core/ir/load_vector_element.cc
+++ b/src/tint/lang/core/ir/load_vector_element.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/load_vector_element.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::LoadVectorElement);
 
 namespace tint::core::ir {
@@ -28,4 +31,11 @@
 
 LoadVectorElement::~LoadVectorElement() = default;
 
+LoadVectorElement* LoadVectorElement::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_from = ctx.Clone(From());
+    auto* new_index = ctx.Clone(Index());
+    return ctx.ir.instructions.Create<LoadVectorElement>(new_result, new_from, new_index);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/load_vector_element.h b/src/tint/lang/core/ir/load_vector_element.h
index cd7e206..c141a0b 100644
--- a/src/tint/lang/core/ir/load_vector_element.h
+++ b/src/tint/lang/core/ir/load_vector_element.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A load instruction for a single vector element in the IR.
-class LoadVectorElement : public Castable<LoadVectorElement, OperandInstruction<3, 0>> {
+class LoadVectorElement final : public Castable<LoadVectorElement, OperandInstruction<3, 0>> {
   public:
     /// The offset in Operands() for the `from` value
     static constexpr size_t kFromOperandOffset = 0;
@@ -38,6 +38,9 @@
     LoadVectorElement(InstructionResult* result, ir::Value* from, ir::Value* index);
     ~LoadVectorElement() override;
 
+    /// @copydoc Instruction::Clone()
+    LoadVectorElement* Clone(CloneContext& ctx) override;
+
     /// @returns the vector pointer value
     ir::Value* From() { return operands_[kFromOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/load_vector_element_test.cc b/src/tint/lang/core/ir/load_vector_element_test.cc
index 35a755d..744cf2b 100644
--- a/src/tint/lang/core/ir/load_vector_element_test.cc
+++ b/src/tint/lang/core/ir/load_vector_element_test.cc
@@ -58,5 +58,23 @@
     EXPECT_FALSE(inst->HasMultiResults());
 }
 
+TEST_F(IR_LoadVectorElementTest, Clone) {
+    auto* from = b.Var(ty.ptr<private_, vec3<i32>>());
+    auto* inst = b.LoadVectorElement(from, 2_i);
+
+    auto* new_from = clone_ctx.Clone(from);
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, new_inst);
+    EXPECT_NE(nullptr, new_inst->Result());
+    EXPECT_NE(inst->Result(), new_inst->Result());
+
+    EXPECT_EQ(new_from->Result(), new_inst->From());
+
+    auto new_idx = new_inst->Index()->As<Constant>()->Value();
+    ASSERT_TRUE(new_idx->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(2_i, new_idx->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/loop.cc b/src/tint/lang/core/ir/loop.cc
index 5112718..ada2516 100644
--- a/src/tint/lang/core/ir/loop.cc
+++ b/src/tint/lang/core/ir/loop.cc
@@ -16,6 +16,8 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -42,6 +44,21 @@
 
 Loop::~Loop() = default;
 
+Loop* Loop::Clone(CloneContext& ctx) {
+    auto* new_init = ctx.ir.blocks.Create<MultiInBlock>();
+    auto* new_body = ctx.ir.blocks.Create<MultiInBlock>();
+    auto* new_continuing = ctx.ir.blocks.Create<MultiInBlock>();
+
+    auto* new_loop = ctx.ir.instructions.Create<Loop>(new_init, new_body, new_continuing);
+    ctx.Replace(this, new_loop);
+
+    initializer_->CloneInto(ctx, new_init);
+    body_->CloneInto(ctx, new_body);
+    continuing_->CloneInto(ctx, new_continuing);
+
+    return new_loop;
+}
+
 void Loop::ForeachBlock(const std::function<void(ir::Block*)>& cb) {
     if (initializer_) {
         cb(initializer_);
diff --git a/src/tint/lang/core/ir/loop.h b/src/tint/lang/core/ir/loop.h
index 689b9e2..5e97226 100644
--- a/src/tint/lang/core/ir/loop.h
+++ b/src/tint/lang/core/ir/loop.h
@@ -56,7 +56,7 @@
 ///                     out
 ///
 /// ```
-class Loop : public Castable<Loop, ControlInstruction> {
+class Loop final : public Castable<Loop, ControlInstruction> {
   public:
     /// Constructor
     /// @param i the initializer block
@@ -65,6 +65,9 @@
     Loop(ir::Block* i, ir::MultiInBlock* b, ir::MultiInBlock* c);
     ~Loop() override;
 
+    /// @copydoc Instruction::Clone()
+    Loop* Clone(CloneContext& ctx) override;
+
     /// @copydoc ControlInstruction::ForeachBlock
     void ForeachBlock(const std::function<void(ir::Block*)>& cb) override;
 
diff --git a/src/tint/lang/core/ir/loop_test.cc b/src/tint/lang/core/ir/loop_test.cc
index 1a4beb5..89699f0 100644
--- a/src/tint/lang/core/ir/loop_test.cc
+++ b/src/tint/lang/core/ir/loop_test.cc
@@ -65,5 +65,57 @@
         "");
 }
 
+TEST_F(IR_LoopTest, Clone) {
+    auto* loop = b.Loop();
+    auto* new_loop = clone_ctx.Clone(loop);
+
+    EXPECT_NE(loop, new_loop);
+    EXPECT_FALSE(new_loop->HasResults());
+    EXPECT_EQ(0u, new_loop->Exits().Count());
+    EXPECT_NE(nullptr, new_loop->Initializer());
+    EXPECT_NE(loop->Initializer(), new_loop->Initializer());
+
+    EXPECT_NE(nullptr, new_loop->Body());
+    EXPECT_NE(loop->Body(), new_loop->Body());
+
+    EXPECT_NE(nullptr, new_loop->Continuing());
+    EXPECT_NE(loop->Continuing(), new_loop->Continuing());
+}
+
+TEST_F(IR_LoopTest, CloneWithExits) {
+    Loop* new_loop = nullptr;
+    {
+        auto* loop = b.Loop();
+        b.Append(loop->Body(), [&] {
+            auto* if_ = b.If(true);
+            b.Append(if_->True(), [&] { b.Continue(loop); });
+            b.Append(if_->False(), [&] { b.ExitLoop(loop); });
+            b.Append(loop->Continuing(), [&] { b.BreakIf(loop, false); });
+
+            b.NextIteration(loop);
+        });
+        new_loop = clone_ctx.Clone(loop);
+    }
+
+    ASSERT_EQ(2u, new_loop->Body()->Length());
+    EXPECT_TRUE(new_loop->Body()->Front()->Is<If>());
+
+    auto* new_if = new_loop->Body()->Front()->As<If>();
+    ASSERT_EQ(1u, new_if->True()->Length());
+    EXPECT_TRUE(new_if->True()->Front()->Is<Continue>());
+    EXPECT_EQ(new_loop, new_if->True()->Front()->As<Continue>()->Loop());
+
+    ASSERT_EQ(1u, new_if->False()->Length());
+    EXPECT_TRUE(new_if->False()->Front()->Is<ExitLoop>());
+    EXPECT_EQ(new_loop, new_if->False()->Front()->As<ExitLoop>()->Loop());
+
+    ASSERT_EQ(1u, new_loop->Continuing()->Length());
+    EXPECT_TRUE(new_loop->Continuing()->Front()->Is<BreakIf>());
+    EXPECT_EQ(new_loop, new_loop->Continuing()->Front()->As<BreakIf>()->Loop());
+
+    EXPECT_TRUE(new_loop->Body()->Back()->Is<NextIteration>());
+    EXPECT_EQ(new_loop, new_loop->Body()->Back()->As<NextIteration>()->Loop());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/multi_in_block.cc b/src/tint/lang/core/ir/multi_in_block.cc
index 6916512..676691b 100644
--- a/src/tint/lang/core/ir/multi_in_block.cc
+++ b/src/tint/lang/core/ir/multi_in_block.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/multi_in_block.h"
 
+#include "src/tint/lang/core/ir/block_param.h"
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/containers/predicates.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -25,6 +28,19 @@
 
 MultiInBlock::~MultiInBlock() = default;
 
+MultiInBlock* MultiInBlock::Clone(CloneContext&) {
+    TINT_UNREACHABLE() << "blocks must be cloned with CloneInto";
+    return nullptr;
+}
+
+void MultiInBlock::CloneInto(CloneContext& ctx, Block* out) {
+    TINT_ASSERT(out->Is<MultiInBlock>());
+
+    auto new_params = ctx.Clone(params_);
+    out->As<MultiInBlock>()->SetParams(new_params);
+    Block::CloneInto(ctx, out);
+}
+
 void MultiInBlock::SetParams(VectorRef<BlockParam*> params) {
     params_ = std::move(params);
 }
diff --git a/src/tint/lang/core/ir/multi_in_block.h b/src/tint/lang/core/ir/multi_in_block.h
index 1fecb6d..5934dca 100644
--- a/src/tint/lang/core/ir/multi_in_block.h
+++ b/src/tint/lang/core/ir/multi_in_block.h
@@ -35,6 +35,12 @@
     MultiInBlock();
     ~MultiInBlock() override;
 
+    /// @copydoc Block::Clone()
+    MultiInBlock* Clone(CloneContext& ctx) override;
+
+    /// @copydoc Block::CloneInto()
+    void CloneInto(CloneContext& ctx, Block* out) override;
+
     /// Sets the params to the block
     /// @param params the params for the block
     void SetParams(VectorRef<BlockParam*> params);
diff --git a/src/tint/lang/core/ir/multi_in_block_test.cc b/src/tint/lang/core/ir/multi_in_block_test.cc
index f2145af..2e7552e 100644
--- a/src/tint/lang/core/ir/multi_in_block_test.cc
+++ b/src/tint/lang/core/ir/multi_in_block_test.cc
@@ -35,5 +35,43 @@
         "");
 }
 
+TEST_F(IR_MultiInBlockTest, CloneInto) {
+    auto* loop = b.Loop();
+
+    auto* blk = b.MultiInBlock();
+    auto* add = b.Add(mod.Types().i32(), 1_i, 2_i);
+    blk->Append(add);
+    blk->SetParams({b.BlockParam(mod.Types().i32()), b.BlockParam(mod.Types().f32())});
+    blk->SetParent(loop);
+
+    auto* terminate = b.TerminateInvocation();
+    blk->AddInboundSiblingBranch(terminate);
+
+    auto* new_blk = b.MultiInBlock();
+    blk->CloneInto(clone_ctx, new_blk);
+
+    EXPECT_EQ(0u, new_blk->InboundSiblingBranches().Length());
+
+    EXPECT_EQ(2u, new_blk->Params().Length());
+    EXPECT_EQ(mod.Types().i32(), new_blk->Params()[0]->Type());
+    EXPECT_EQ(mod.Types().f32(), new_blk->Params()[1]->Type());
+
+    EXPECT_EQ(nullptr, new_blk->Parent());
+
+    EXPECT_EQ(1u, new_blk->Length());
+    EXPECT_NE(add, new_blk->Front());
+    EXPECT_TRUE(new_blk->Front()->Is<Binary>());
+    EXPECT_EQ(Binary::Kind::kAdd, new_blk->Front()->As<Binary>()->Kind());
+}
+
+TEST_F(IR_MultiInBlockTest, CloneEmpty) {
+    auto* blk = b.MultiInBlock();
+    auto* new_blk = b.MultiInBlock();
+    blk->CloneInto(clone_ctx, new_blk);
+
+    EXPECT_EQ(0u, new_blk->InboundSiblingBranches().Length());
+    EXPECT_EQ(0u, new_blk->Params().Length());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/next_iteration.cc b/src/tint/lang/core/ir/next_iteration.cc
index 1aeb9f4..1c3fe65 100644
--- a/src/tint/lang/core/ir/next_iteration.cc
+++ b/src/tint/lang/core/ir/next_iteration.cc
@@ -16,7 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/loop.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -37,4 +39,10 @@
 
 NextIteration::~NextIteration() = default;
 
+NextIteration* NextIteration::Clone(CloneContext& ctx) {
+    auto* new_loop = ctx.Clone(loop_);
+    auto new_args = ctx.Clone<NextIteration::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<NextIteration>(new_loop, new_args);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/next_iteration.h b/src/tint/lang/core/ir/next_iteration.h
index 87de579..f845dee 100644
--- a/src/tint/lang/core/ir/next_iteration.h
+++ b/src/tint/lang/core/ir/next_iteration.h
@@ -28,7 +28,7 @@
 namespace tint::core::ir {
 
 /// A next iteration instruction.
-class NextIteration : public Castable<NextIteration, Terminator> {
+class NextIteration final : public Castable<NextIteration, Terminator> {
   public:
     /// The base offset in Operands() for the args
     static constexpr size_t kArgsOperandOffset = 0;
@@ -39,6 +39,9 @@
     explicit NextIteration(ir::Loop* loop, VectorRef<Value*> args = tint::Empty);
     ~NextIteration() override;
 
+    /// @copydoc Instruction::Clone()
+    NextIteration* Clone(CloneContext& ctx) override;
+
     /// @returns the loop being iterated
     ir::Loop* Loop() { return loop_; }
 
diff --git a/src/tint/lang/core/ir/next_iteration_test.cc b/src/tint/lang/core/ir/next_iteration_test.cc
index 3d81331..f1efc9e 100644
--- a/src/tint/lang/core/ir/next_iteration_test.cc
+++ b/src/tint/lang/core/ir/next_iteration_test.cc
@@ -39,5 +39,38 @@
     EXPECT_FALSE(inst->HasMultiResults());
 }
 
+TEST_F(IR_NextIterationTest, Clone) {
+    auto* arg1 = b.Constant(1_u);
+    auto* arg2 = b.Constant(2_u);
+
+    auto* loop = b.Loop();
+    auto* inst = b.NextIteration(loop, arg1, arg2);
+
+    auto* new_loop = clone_ctx.Clone(loop);
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, new_inst);
+    EXPECT_EQ(new_loop, new_inst->Loop());
+
+    auto args = new_inst->Args();
+    EXPECT_EQ(2u, args.Length());
+
+    auto* val0 = args[0]->As<Constant>()->Value();
+    EXPECT_EQ(1_u, val0->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto* val1 = args[1]->As<Constant>()->Value();
+    EXPECT_EQ(2_u, val1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_NextIterationTest, CloneNoArgs) {
+    auto* loop = b.Loop();
+    auto* inst = b.NextIteration(loop);
+
+    auto* new_loop = clone_ctx.Clone(loop);
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_EQ(new_loop, new_inst->Loop());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/operand_instruction.h b/src/tint/lang/core/ir/operand_instruction.h
index 49c2c85..980c696 100644
--- a/src/tint/lang/core/ir/operand_instruction.h
+++ b/src/tint/lang/core/ir/operand_instruction.h
@@ -134,6 +134,9 @@
     Vector<ir::Value*, N> operands_;
     /// The results of this instruction.
     Vector<ir::InstructionResult*, R> results_;
+
+    /// The default number of operands
+    static constexpr size_t kDefaultNumOperands = N;
 };
 
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/return.cc b/src/tint/lang/core/ir/return.cc
index ab6e841..49b5939 100644
--- a/src/tint/lang/core/ir/return.cc
+++ b/src/tint/lang/core/ir/return.cc
@@ -16,7 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
 #include "src/tint/lang/core/ir/function.h"
+#include "src/tint/lang/core/ir/module.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Return);
 
@@ -33,6 +35,12 @@
 
 Return::~Return() = default;
 
+Return* Return::Clone(CloneContext& ctx) {
+    auto* new_func = ctx.Clone(Func());
+    auto new_val = Value() ? ctx.Clone(Value()) : nullptr;
+    return ctx.ir.instructions.Create<Return>(new_func, new_val);
+}
+
 Function* Return::Func() const {
     return tint::As<Function>(operands_[kFunctionOperandOffset]);
 }
diff --git a/src/tint/lang/core/ir/return.h b/src/tint/lang/core/ir/return.h
index f5b8c8a..0f7c434 100644
--- a/src/tint/lang/core/ir/return.h
+++ b/src/tint/lang/core/ir/return.h
@@ -28,7 +28,7 @@
 namespace tint::core::ir {
 
 /// A return instruction.
-class Return : public Castable<Return, Terminator> {
+class Return final : public Castable<Return, Terminator> {
   public:
     /// The offset in Operands() for the function being returned
     static constexpr size_t kFunctionOperandOffset = 0;
@@ -47,6 +47,9 @@
 
     ~Return() override;
 
+    /// @copydoc Instruction::Clone()
+    Return* Clone(CloneContext& ctx) override;
+
     /// @returns the function being returned
     Function* Func() const;
 
diff --git a/src/tint/lang/core/ir/return_test.cc b/src/tint/lang/core/ir/return_test.cc
index f08c4b5..dc68fd6 100644
--- a/src/tint/lang/core/ir/return_test.cc
+++ b/src/tint/lang/core/ir/return_test.cc
@@ -62,5 +62,33 @@
     }
 }
 
+TEST_F(IR_ReturnTest, Clone) {
+    auto* func = b.Function("func", ty.i32());
+    auto* ret = b.Return(func, b.Constant(1_i));
+
+    auto* new_func = clone_ctx.Clone(func);
+    auto* new_ret = clone_ctx.Clone(ret);
+
+    EXPECT_NE(ret, new_ret);
+    EXPECT_EQ(new_func, new_ret->Func());
+
+    EXPECT_EQ(1u, new_ret->Args().Length());
+
+    auto new_val = new_ret->Value()->As<Constant>()->Value();
+    ASSERT_TRUE(new_val->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(1_i, new_val->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
+TEST_F(IR_ReturnTest, CloneWithoutArgs) {
+    auto* func = b.Function("func", ty.i32());
+    auto* ret = b.Return(func);
+
+    auto* new_func = clone_ctx.Clone(func);
+    auto* new_ret = clone_ctx.Clone(ret);
+
+    EXPECT_EQ(new_func, new_ret->Func());
+    EXPECT_EQ(nullptr, new_ret->Value());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store.cc b/src/tint/lang/core/ir/store.cc
index c333ede..8088087 100644
--- a/src/tint/lang/core/ir/store.cc
+++ b/src/tint/lang/core/ir/store.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/store.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Store);
 
 namespace tint::core::ir {
@@ -27,4 +30,10 @@
 
 Store::~Store() = default;
 
+Store* Store::Clone(CloneContext& ctx) {
+    auto* new_to = ctx.Clone(To());
+    auto* new_from = ctx.Clone(From());
+    return ctx.ir.instructions.Create<Store>(new_to, new_from);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store.h b/src/tint/lang/core/ir/store.h
index 924a6b8..3c5071e 100644
--- a/src/tint/lang/core/ir/store.h
+++ b/src/tint/lang/core/ir/store.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A store instruction in the IR.
-class Store : public Castable<Store, OperandInstruction<2, 0>> {
+class Store final : public Castable<Store, OperandInstruction<2, 0>> {
   public:
     /// The offset in Operands() for the `to` value
     static constexpr size_t kToOperandOffset = 0;
@@ -37,6 +37,9 @@
     Store(Value* to, Value* from);
     ~Store() override;
 
+    /// @copydoc Instruction::Clone()
+    Store* Clone(CloneContext& ctx) override;
+
     /// @returns the value being stored too
     Value* To() { return operands_[kToOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/store_test.cc b/src/tint/lang/core/ir/store_test.cc
index f13049b..c5677f0 100644
--- a/src/tint/lang/core/ir/store_test.cc
+++ b/src/tint/lang/core/ir/store_test.cc
@@ -58,5 +58,20 @@
     EXPECT_FALSE(inst->HasMultiResults());
 }
 
+TEST_F(IR_StoreTest, Clone) {
+    auto* v = b.Var("a", mod.Types().ptr<private_, i32>());
+    auto* s = b.Store(v, b.Constant(1_i));
+
+    auto* new_v = clone_ctx.Clone(v);
+    auto* new_s = clone_ctx.Clone(s);
+
+    EXPECT_NE(s, new_s);
+    EXPECT_EQ(new_v->Result(), new_s->To());
+
+    auto new_from = new_s->From()->As<Constant>()->Value();
+    ASSERT_TRUE(new_from->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(1_i, new_from->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store_vector_element.cc b/src/tint/lang/core/ir/store_vector_element.cc
index c133211..7d4a45d 100644
--- a/src/tint/lang/core/ir/store_vector_element.cc
+++ b/src/tint/lang/core/ir/store_vector_element.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/store_vector_element.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::StoreVectorElement);
 
 namespace tint::core::ir {
@@ -28,4 +31,11 @@
 
 StoreVectorElement::~StoreVectorElement() = default;
 
+StoreVectorElement* StoreVectorElement::Clone(CloneContext& ctx) {
+    auto* new_to = ctx.Clone(To());
+    auto* new_idx = ctx.Clone(Index());
+    auto* new_val = ctx.Clone(Value());
+    return ctx.ir.instructions.Create<StoreVectorElement>(new_to, new_idx, new_val);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/store_vector_element.h b/src/tint/lang/core/ir/store_vector_element.h
index a4b31a8..5f89adb 100644
--- a/src/tint/lang/core/ir/store_vector_element.h
+++ b/src/tint/lang/core/ir/store_vector_element.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A store instruction for a single vector element in the IR.
-class StoreVectorElement : public Castable<StoreVectorElement, OperandInstruction<3, 0>> {
+class StoreVectorElement final : public Castable<StoreVectorElement, OperandInstruction<3, 0>> {
   public:
     /// The offset in Operands() for the `to` value
     static constexpr size_t kToOperandOffset = 0;
@@ -41,6 +41,9 @@
     StoreVectorElement(ir::Value* to, ir::Value* index, ir::Value* value);
     ~StoreVectorElement() override;
 
+    /// @copydoc Instruction::Clone()
+    StoreVectorElement* Clone(CloneContext& ctx) override;
+
     /// @returns the vector pointer value
     ir::Value* To() { return operands_[kToOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/store_vector_element_test.cc b/src/tint/lang/core/ir/store_vector_element_test.cc
index 03e147e..28f417e 100644
--- a/src/tint/lang/core/ir/store_vector_element_test.cc
+++ b/src/tint/lang/core/ir/store_vector_element_test.cc
@@ -66,5 +66,24 @@
     EXPECT_FALSE(inst->HasMultiResults());
 }
 
+TEST_F(IR_StoreVectorElementTest, Clone) {
+    auto* to = b.Var(ty.ptr<private_, vec3<i32>>());
+    auto* inst = b.StoreVectorElement(to, 2_i, 4_i);
+
+    auto* new_to = clone_ctx.Clone(to);
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, new_inst);
+    EXPECT_EQ(new_to->Result(), new_inst->To());
+
+    auto new_idx = new_inst->Index()->As<Constant>()->Value();
+    ASSERT_TRUE(new_idx->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(2_i, new_idx->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+
+    auto new_val = new_inst->Value()->As<Constant>()->Value();
+    ASSERT_TRUE(new_val->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, new_val->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/switch.cc b/src/tint/lang/core/ir/switch.cc
index df353ab..d260921 100644
--- a/src/tint/lang/core/ir/switch.cc
+++ b/src/tint/lang/core/ir/switch.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/core/ir/switch.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Switch);
@@ -34,4 +36,25 @@
     }
 }
 
+Switch* Switch::Clone(CloneContext& ctx) {
+    auto* new_cond = ctx.Clone(Condition());
+    auto* new_switch = ctx.ir.instructions.Create<Switch>(new_cond);
+    ctx.Replace(this, new_switch);
+
+    new_switch->cases_.Reserve(cases_.Length());
+    for (const auto& cse : cases_) {
+        Switch::Case new_case{};
+        new_case.block = ctx.ir.blocks.Create<ir::Block>();
+        cse.block->CloneInto(ctx, new_case.block);
+
+        new_case.selectors.Reserve(cse.selectors.Length());
+        for (const auto& sel : cse.selectors) {
+            auto* new_val = sel.val ? ctx.Clone(sel.val) : nullptr;
+            new_case.selectors.Push(Switch::CaseSelector{new_val});
+        }
+        new_switch->cases_.Push(new_case);
+    }
+    return new_switch;
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/switch.h b/src/tint/lang/core/ir/switch.h
index 1b450ac..44f0e6a 100644
--- a/src/tint/lang/core/ir/switch.h
+++ b/src/tint/lang/core/ir/switch.h
@@ -43,7 +43,7 @@
 ///                            ▼
 ///                           out
 /// ```
-class Switch : public Castable<Switch, ControlInstruction> {
+class Switch final : public Castable<Switch, ControlInstruction> {
   public:
     /// The offset in Operands() for the condition
     static constexpr size_t kConditionOperandOffset = 0;
@@ -73,6 +73,9 @@
     explicit Switch(Value* cond);
     ~Switch() override;
 
+    /// @copydoc Instruction::Clone()
+    Switch* Clone(CloneContext& ctx) override;
+
     /// @copydoc ControlInstruction::ForeachBlock
     void ForeachBlock(const std::function<void(ir::Block*)>& cb) override;
 
diff --git a/src/tint/lang/core/ir/switch_test.cc b/src/tint/lang/core/ir/switch_test.cc
index 5d1442c..83cc223 100644
--- a/src/tint/lang/core/ir/switch_test.cc
+++ b/src/tint/lang/core/ir/switch_test.cc
@@ -22,6 +22,7 @@
 namespace {
 
 using namespace tint::core::number_suffixes;  // NOLINT
+
 using IR_SwitchTest = IRTestHelper;
 
 TEST_F(IR_SwitchTest, Usage) {
@@ -43,5 +44,62 @@
     EXPECT_THAT(switch_->Cases().Front().Block()->Parent(), switch_);
 }
 
+TEST_F(IR_SwitchTest, Clone) {
+    auto* switch_ = b.Switch(1_i);
+    switch_->Cases().Push(
+        Switch::Case{{Switch::CaseSelector{}, Switch::CaseSelector{b.Constant(2_i)}}, b.Block()});
+    switch_->Cases().Push(Switch::Case{{Switch::CaseSelector{b.Constant(3_i)}}, b.Block()});
+
+    auto* new_switch = clone_ctx.Clone(switch_);
+
+    EXPECT_NE(switch_, new_switch);
+
+    auto new_cond = new_switch->Condition()->As<Constant>()->Value();
+    ASSERT_TRUE(new_cond->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(1_i, new_cond->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+
+    auto& cases = new_switch->Cases();
+    ASSERT_EQ(2u, cases.Length());
+
+    {
+        auto& case1 = cases[0];
+        EXPECT_NE(nullptr, case1.block);
+        EXPECT_NE(switch_->Cases()[0].block, case1.block);
+
+        ASSERT_EQ(2u, case1.selectors.Length());
+        EXPECT_EQ(nullptr, case1.selectors[0].val);
+        auto val = case1.selectors[1].val->Value();
+        ASSERT_TRUE(val->Is<core::constant::Scalar<i32>>());
+        EXPECT_EQ(2_i, val->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+    }
+
+    {
+        auto& case2 = cases[1];
+        EXPECT_NE(nullptr, case2.block);
+        EXPECT_NE(switch_->Cases()[1].block, case2.block);
+
+        ASSERT_EQ(1u, case2.selectors.Length());
+        auto val = case2.selectors[0].val->Value();
+        ASSERT_TRUE(val->Is<core::constant::Scalar<i32>>());
+        EXPECT_EQ(3_i, val->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+    }
+}
+
+TEST_F(IR_SwitchTest, CloneWithExits) {
+    Switch* new_switch = nullptr;
+    {
+        auto* switch_ = b.Switch(1_i);
+
+        auto* blk = b.Block();
+        b.Append(blk, [&] { b.ExitSwitch(switch_); });
+        switch_->Cases().Push(Switch::Case{{Switch::CaseSelector{b.Constant(3_i)}}, blk});
+        new_switch = clone_ctx.Clone(switch_);
+    }
+
+    auto& case_ = new_switch->Cases().Front();
+    ASSERT_TRUE(case_.block->Front()->Is<ExitSwitch>());
+    EXPECT_EQ(new_switch, case_.block->Front()->As<ExitSwitch>()->Switch());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/swizzle.cc b/src/tint/lang/core/ir/swizzle.cc
index 3789021..0180d48 100644
--- a/src/tint/lang/core/ir/swizzle.cc
+++ b/src/tint/lang/core/ir/swizzle.cc
@@ -16,6 +16,8 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/utils/ice/ice.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Swizzle);
@@ -37,4 +39,10 @@
 
 Swizzle::~Swizzle() = default;
 
+Swizzle* Swizzle::Clone(CloneContext& ctx) {
+    auto* result = ctx.Clone(Result());
+    auto* new_obj = ctx.Clone(Object());
+    return ctx.ir.instructions.Create<Swizzle>(result, new_obj, indices_);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/swizzle.h b/src/tint/lang/core/ir/swizzle.h
index 48a838e..880e5c6 100644
--- a/src/tint/lang/core/ir/swizzle.h
+++ b/src/tint/lang/core/ir/swizzle.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A swizzle instruction in the IR.
-class Swizzle : public Castable<Swizzle, OperandInstruction<1, 1>> {
+class Swizzle final : public Castable<Swizzle, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the object being swizzled
     static constexpr size_t kObjectOperandOffset = 0;
@@ -35,6 +35,9 @@
     Swizzle(InstructionResult* result, Value* object, VectorRef<uint32_t> indices);
     ~Swizzle() override;
 
+    /// @copydoc Instruction::Clone()
+    Swizzle* Clone(CloneContext& ctx) override;
+
     /// @returns the object used for the access
     Value* Object() { return operands_[kObjectOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/swizzle_test.cc b/src/tint/lang/core/ir/swizzle_test.cc
index 3f435ef..09eb7e9 100644
--- a/src/tint/lang/core/ir/swizzle_test.cc
+++ b/src/tint/lang/core/ir/swizzle_test.cc
@@ -86,5 +86,22 @@
         "");
 }
 
+TEST_F(IR_SwizzleTest, Clone) {
+    auto* var = b.Var(ty.ptr<function, i32>());
+    auto* s = b.Swizzle(mod.Types().i32(), var, {2u});
+
+    auto* new_var = clone_ctx.Clone(var);
+    auto* new_s = clone_ctx.Clone(s);
+
+    EXPECT_NE(s, new_s);
+    EXPECT_NE(nullptr, new_s->Result());
+    EXPECT_NE(s->Result(), new_s->Result());
+
+    EXPECT_EQ(new_var->Result(), new_s->Object());
+
+    EXPECT_EQ(1u, new_s->Indices().Length());
+    EXPECT_EQ(2u, new_s->Indices().Front());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/terminate_invocation.cc b/src/tint/lang/core/ir/terminate_invocation.cc
index a7ab28e..9fa5b54 100644
--- a/src/tint/lang/core/ir/terminate_invocation.cc
+++ b/src/tint/lang/core/ir/terminate_invocation.cc
@@ -14,10 +14,17 @@
 
 #include "src/tint/lang/core/ir/terminate_invocation.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::TerminateInvocation);
 
 namespace tint::core::ir {
 
 TerminateInvocation::~TerminateInvocation() = default;
 
+TerminateInvocation* TerminateInvocation::Clone(CloneContext& ctx) {
+    return ctx.ir.instructions.Create<TerminateInvocation>();
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/terminate_invocation.h b/src/tint/lang/core/ir/terminate_invocation.h
index 8edf03e..097145c 100644
--- a/src/tint/lang/core/ir/terminate_invocation.h
+++ b/src/tint/lang/core/ir/terminate_invocation.h
@@ -22,10 +22,13 @@
 namespace tint::core::ir {
 
 /// An terminate invocation instruction in the IR.
-class TerminateInvocation : public Castable<TerminateInvocation, Terminator> {
+class TerminateInvocation final : public Castable<TerminateInvocation, Terminator> {
   public:
     ~TerminateInvocation() override;
 
+    /// @copydoc Instruction::Clone()
+    TerminateInvocation* Clone(CloneContext& ctx) override;
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() override { return "terminate_invocation"; }
 };
diff --git a/src/tint/lang/core/ir/terminate_invocation_test.cc b/src/tint/lang/core/ir/terminate_invocation_test.cc
new file mode 100644
index 0000000..459aae0
--- /dev/null
+++ b/src/tint/lang/core/ir/terminate_invocation_test.cc
@@ -0,0 +1,34 @@
+// 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/lang/core/ir/terminate_invocation.h"
+#include "gtest/gtest.h"
+#include "src/tint/lang/core/ir/ir_helper_test.h"
+
+namespace tint::core::ir {
+namespace {
+
+using IR_TerminateInvocationTest = IRTestHelper;
+
+TEST_F(IR_TerminateInvocationTest, Clone) {
+    auto* ti = b.TerminateInvocation();
+    auto* new_ti = clone_ctx.Clone(ti);
+
+    EXPECT_NE(ti, new_ti);
+    EXPECT_NE(nullptr, new_ti);
+    EXPECT_NE(ti, new_ti);
+}
+
+}  // namespace
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unary.cc b/src/tint/lang/core/ir/unary.cc
index d6ce129..69b6857 100644
--- a/src/tint/lang/core/ir/unary.cc
+++ b/src/tint/lang/core/ir/unary.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/lang/core/ir/unary.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Unary);
 
 namespace tint::core::ir {
@@ -25,4 +28,10 @@
 
 Unary::~Unary() = default;
 
+Unary* Unary::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_val = ctx.Clone(Val());
+    return ctx.ir.instructions.Create<Unary>(new_result, kind_, new_val);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unary.h b/src/tint/lang/core/ir/unary.h
index 2e55cc3..15fdfa3 100644
--- a/src/tint/lang/core/ir/unary.h
+++ b/src/tint/lang/core/ir/unary.h
@@ -23,7 +23,7 @@
 namespace tint::core::ir {
 
 /// A unary instruction in the IR.
-class Unary : public Castable<Unary, OperandInstruction<1, 1>> {
+class Unary final : public Castable<Unary, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the value
     static constexpr size_t kValueOperandOffset = 0;
@@ -41,6 +41,9 @@
     Unary(InstructionResult* result, enum Kind kind, Value* val);
     ~Unary() override;
 
+    /// @copydoc Instruction::Clone()
+    Unary* Clone(CloneContext& ctx) override;
+
     /// @returns the value for the instruction
     Value* Val() { return operands_[kValueOperandOffset]; }
 
diff --git a/src/tint/lang/core/ir/unary_test.cc b/src/tint/lang/core/ir/unary_test.cc
index 95d30cf..ab90266 100644
--- a/src/tint/lang/core/ir/unary_test.cc
+++ b/src/tint/lang/core/ir/unary_test.cc
@@ -78,5 +78,20 @@
         "");
 }
 
+TEST_F(IR_UnaryTest, Clone) {
+    auto* inst = b.Complement(mod.Types().i32(), 4_i);
+    auto* new_inst = clone_ctx.Clone(inst);
+
+    EXPECT_NE(inst, new_inst);
+    EXPECT_NE(nullptr, new_inst->Result());
+    EXPECT_NE(inst->Result(), new_inst->Result());
+
+    EXPECT_EQ(Unary::Kind::kComplement, new_inst->Kind());
+
+    auto new_val = new_inst->Val()->As<Constant>()->Value();
+    ASSERT_TRUE(new_val->Is<core::constant::Scalar<i32>>());
+    EXPECT_EQ(4_i, new_val->As<core::constant::Scalar<i32>>()->ValueAs<i32>());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unreachable.cc b/src/tint/lang/core/ir/unreachable.cc
index 0011530..da73d70 100644
--- a/src/tint/lang/core/ir/unreachable.cc
+++ b/src/tint/lang/core/ir/unreachable.cc
@@ -14,10 +14,17 @@
 
 #include "src/tint/lang/core/ir/unreachable.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::Unreachable);
 
 namespace tint::core::ir {
 
 Unreachable::~Unreachable() = default;
 
+Unreachable* Unreachable::Clone(CloneContext& ctx) {
+    return ctx.ir.instructions.Create<Unreachable>();
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/unreachable.h b/src/tint/lang/core/ir/unreachable.h
index fd92538..9a9d264 100644
--- a/src/tint/lang/core/ir/unreachable.h
+++ b/src/tint/lang/core/ir/unreachable.h
@@ -22,10 +22,13 @@
 namespace tint::core::ir {
 
 /// An unreachable instruction in the IR.
-class Unreachable : public Castable<Unreachable, Terminator> {
+class Unreachable final : public Castable<Unreachable, Terminator> {
   public:
     ~Unreachable() override;
 
+    /// @copydoc Instruction::Clone()
+    Unreachable* Clone(CloneContext& ctx) override;
+
     /// @returns the friendly name for the instruction
     std::string FriendlyName() override { return "unreachable"; }
 };
diff --git a/src/tint/lang/core/ir/unreachable_test.cc b/src/tint/lang/core/ir/unreachable_test.cc
new file mode 100644
index 0000000..5e172bd
--- /dev/null
+++ b/src/tint/lang/core/ir/unreachable_test.cc
@@ -0,0 +1,46 @@
+// 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 "gtest/gtest-spi.h"
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/instruction.h"
+#include "src/tint/lang/core/ir/ir_helper_test.h"
+
+namespace tint::core::ir {
+namespace {
+
+using IR_UnreachableTest = IRTestHelper;
+
+TEST_F(IR_UnreachableTest, Unreachable) {
+    auto* inst = b.Unreachable();
+    ASSERT_TRUE(inst->Is<ir::Unreachable>());
+}
+
+TEST_F(IR_UnreachableTest, Result) {
+    auto* inst = b.Unreachable();
+
+    EXPECT_FALSE(inst->HasResults());
+    EXPECT_FALSE(inst->HasMultiResults());
+}
+
+TEST_F(IR_UnreachableTest, Clone) {
+    auto* d = b.Unreachable();
+    auto* new_d = clone_ctx.Clone(d);
+
+    EXPECT_NE(d, new_d);
+    EXPECT_NE(nullptr, new_d);
+}
+
+}  // namespace
+}  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/user_call.cc b/src/tint/lang/core/ir/user_call.cc
index 1a41472..0f20129 100644
--- a/src/tint/lang/core/ir/user_call.cc
+++ b/src/tint/lang/core/ir/user_call.cc
@@ -16,6 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+
 TINT_INSTANTIATE_TYPEINFO(tint::core::ir::UserCall);
 
 namespace tint::core::ir {
@@ -28,4 +31,11 @@
 
 UserCall::~UserCall() = default;
 
+UserCall* UserCall::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_target = ctx.Clone(Target());
+    auto new_args = ctx.Clone<UserCall::kDefaultNumOperands>(Args());
+    return ctx.ir.instructions.Create<UserCall>(new_result, new_target, new_args);
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/user_call.h b/src/tint/lang/core/ir/user_call.h
index f6155ab..f6688e2 100644
--- a/src/tint/lang/core/ir/user_call.h
+++ b/src/tint/lang/core/ir/user_call.h
@@ -24,7 +24,7 @@
 namespace tint::core::ir {
 
 /// A user call instruction in the IR.
-class UserCall : public Castable<UserCall, Call> {
+class UserCall final : public Castable<UserCall, Call> {
   public:
     /// The offset in Operands() for the function being called
     static constexpr size_t kFunctionOperandOffset = 0;
@@ -39,6 +39,9 @@
     UserCall(InstructionResult* result, Function* func, VectorRef<Value*> args);
     ~UserCall() override;
 
+    /// @copydoc Instruction::Clone()
+    UserCall* Clone(CloneContext& ctx) override;
+
     /// @returns the call arguments
     tint::Slice<Value*> Args() override { return operands_.Slice().Offset(kArgsOperandOffset); }
 
diff --git a/src/tint/lang/core/ir/user_call_test.cc b/src/tint/lang/core/ir/user_call_test.cc
index 51f99eb..efbe64c 100644
--- a/src/tint/lang/core/ir/user_call_test.cc
+++ b/src/tint/lang/core/ir/user_call_test.cc
@@ -56,5 +56,39 @@
         "");
 }
 
+TEST_F(IR_UserCallTest, Clone) {
+    auto* func = b.Function("myfunc", mod.Types().void_());
+    auto* e = b.Call(mod.Types().void_(), func, Vector{b.Constant(1_u), b.Constant(2_u)});
+
+    auto* new_func = clone_ctx.Clone(func);
+    auto* new_e = clone_ctx.Clone(e);
+
+    EXPECT_NE(e, new_e);
+    EXPECT_NE(nullptr, new_e->Result());
+    EXPECT_NE(e->Result(), new_e->Result());
+
+    EXPECT_EQ(new_func, new_e->Target());
+
+    auto args = new_e->Args();
+    EXPECT_EQ(2u, args.Length());
+
+    auto new_arg1 = args[0]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg1->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(1_u, new_arg1->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+
+    auto new_arg2 = args[1]->As<Constant>()->Value();
+    ASSERT_TRUE(new_arg2->Is<core::constant::Scalar<u32>>());
+    EXPECT_EQ(2_u, new_arg2->As<core::constant::Scalar<u32>>()->ValueAs<u32>());
+}
+
+TEST_F(IR_UserCallTest, CloneWithoutArgs) {
+    auto* func = b.Function("myfunc", mod.Types().void_());
+    auto* e = b.Call(mod.Types().void_(), func);
+
+    auto* new_e = clone_ctx.Clone(e);
+
+    EXPECT_EQ(0u, new_e->Args().Length());
+}
+
 }  // namespace
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/value.h b/src/tint/lang/core/ir/value.h
index 6393bc9..176ad0e 100644
--- a/src/tint/lang/core/ir/value.h
+++ b/src/tint/lang/core/ir/value.h
@@ -21,6 +21,7 @@
 
 // Forward declarations
 namespace tint::core::ir {
+class CloneContext;
 class Instruction;
 }  // namespace tint::core::ir
 
@@ -63,6 +64,10 @@
     /// The Value must not be in use by any instruction.
     virtual void Destroy();
 
+    /// @param ctx the CloneContext used to clone this value
+    /// @returns a clone of this value
+    virtual Value* Clone(CloneContext& ctx) = 0;
+
     /// @returns true if the Value has not been destroyed with Destroy()
     bool Alive() const { return !flags_.Contains(Flag::kDead); }
 
diff --git a/src/tint/lang/core/ir/var.cc b/src/tint/lang/core/ir/var.cc
index 501d6af..3ea21f9 100644
--- a/src/tint/lang/core/ir/var.cc
+++ b/src/tint/lang/core/ir/var.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/lang/core/ir/var.h"
 
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/store.h"
 #include "src/tint/lang/core/type/pointer.h"
 #include "src/tint/utils/ice/ice.h"
@@ -34,6 +36,20 @@
 
 Var::~Var() = default;
 
+Var* Var::Clone(CloneContext& ctx) {
+    auto* new_result = ctx.Clone(Result());
+    auto* new_var = ctx.ir.instructions.Create<Var>(new_result);
+
+    new_var->binding_point_ = binding_point_;
+    new_var->attributes_ = attributes_;
+
+    auto name = ctx.ir.NameOf(this);
+    if (name.IsValid()) {
+        ctx.ir.SetName(new_var, name.Name());
+    }
+    return new_var;
+}
+
 void Var::SetInitializer(Value* initializer) {
     SetOperand(Var::kInitializerOperandOffset, initializer);
 }
diff --git a/src/tint/lang/core/ir/var.h b/src/tint/lang/core/ir/var.h
index 384f7b1..01e8c38 100644
--- a/src/tint/lang/core/ir/var.h
+++ b/src/tint/lang/core/ir/var.h
@@ -40,7 +40,7 @@
 };
 
 /// A var instruction in the IR.
-class Var : public Castable<Var, OperandInstruction<1, 1>> {
+class Var final : public Castable<Var, OperandInstruction<1, 1>> {
   public:
     /// The offset in Operands() for the initializer
     static constexpr size_t kInitializerOperandOffset = 0;
@@ -50,6 +50,9 @@
     explicit Var(InstructionResult* result);
     ~Var() override;
 
+    /// @copydoc Instruction::Clone()
+    Var* Clone(CloneContext& ctx) override;
+
     /// Sets the var initializer
     /// @param initializer the initializer
     void SetInitializer(Value* initializer);
diff --git a/src/tint/lang/core/ir/var_test.cc b/src/tint/lang/core/ir/var_test.cc
index 24fb70e..c147dc3 100644
--- a/src/tint/lang/core/ir/var_test.cc
+++ b/src/tint/lang/core/ir/var_test.cc
@@ -58,5 +58,54 @@
     EXPECT_TRUE(init->Usages().IsEmpty());
 }
 
+TEST_F(IR_VarTest, Clone) {
+    auto* v = b.Var(mod.Types().ptr(core::AddressSpace::kFunction, mod.Types().f32()));
+    v->SetInitializer(b.Constant(4_f));
+    v->SetBindingPoint(1, 2);
+    v->SetAttributes(IOAttributes{
+        3, 4, core::BuiltinValue::kFragDepth,
+        Interpolation{core::InterpolationType::kFlat, core::InterpolationSampling::kCentroid},
+        true});
+
+    auto* new_v = clone_ctx.Clone(v);
+
+    EXPECT_NE(v, new_v);
+    ASSERT_NE(nullptr, new_v->Result());
+    EXPECT_NE(v->Result(), new_v->Result());
+    EXPECT_EQ(new_v->Result()->Type(),
+              mod.Types().ptr(core::AddressSpace::kFunction, mod.Types().f32()));
+
+    auto new_val = v->Initializer()->As<Constant>()->Value();
+    ASSERT_TRUE(new_val->Is<core::constant::Scalar<f32>>());
+    EXPECT_FLOAT_EQ(4_f, new_val->As<core::constant::Scalar<f32>>()->ValueAs<f32>());
+
+    EXPECT_TRUE(new_v->BindingPoint().has_value());
+    EXPECT_EQ(1u, new_v->BindingPoint()->group);
+    EXPECT_EQ(2u, new_v->BindingPoint()->binding);
+
+    auto& attrs = new_v->Attributes();
+    EXPECT_TRUE(attrs.location.has_value());
+    EXPECT_EQ(3u, attrs.location.value());
+
+    EXPECT_TRUE(attrs.index.has_value());
+    EXPECT_EQ(4u, attrs.index.value());
+
+    EXPECT_TRUE(attrs.builtin.has_value());
+    EXPECT_EQ(core::BuiltinValue::kFragDepth, attrs.builtin.value());
+
+    EXPECT_TRUE(attrs.interpolation.has_value());
+    EXPECT_EQ(core::InterpolationType::kFlat, attrs.interpolation->type);
+    EXPECT_EQ(core::InterpolationSampling::kCentroid, attrs.interpolation->sampling);
+
+    EXPECT_TRUE(attrs.invariant);
+}
+
+TEST_F(IR_VarTest, CloneWithName) {
+    auto* v = b.Var("v", mod.Types().ptr(core::AddressSpace::kFunction, mod.Types().f32()));
+    auto* new_v = clone_ctx.Clone(v);
+
+    EXPECT_EQ(std::string("v"), mod.NameOf(new_v).Name());
+}
+
 }  // namespace
 }  // namespace tint::core::ir