[tint][wgsl] Split CloneContext into two

Split into ast::CloneContext and program::CloneContext

program::CloneContext wraps the ast::CloneContext.

The purpose of this is to break the cyclic dependency between AST <->
Program.

Change-Id: If52962b074c862cf681ca8367f2ed2bf8478c3dd
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/143386
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 87452d3..1b8d7b3 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -287,7 +287,7 @@
 
 libtint_source_set("libtint_program_src") {
   sources = [
-    "lang/wgsl/ast/clone_context.cc",
+    "lang/wgsl/program/clone_context.cc",
     "lang/wgsl/program/program.cc",
     "lang/wgsl/program/program_builder.cc",
     "lang/wgsl/resolver/const_eval.cc",
@@ -637,6 +637,7 @@
     "lang/wgsl/ast/call_statement.cc",
     "lang/wgsl/ast/case_selector.cc",
     "lang/wgsl/ast/case_statement.cc",
+    "lang/wgsl/ast/clone_context.cc",
     "lang/wgsl/ast/compound_assignment_statement.cc",
     "lang/wgsl/ast/const.cc",
     "lang/wgsl/ast/const_assert.cc",
@@ -1588,6 +1589,7 @@
       "lang/wgsl/ast/call_statement_test.cc",
       "lang/wgsl/ast/case_selector_test.cc",
       "lang/wgsl/ast/case_statement_test.cc",
+      "lang/wgsl/ast/clone_context_test.cc",
       "lang/wgsl/ast/compound_assignment_statement_test.cc",
       "lang/wgsl/ast/const_assert_test.cc",
       "lang/wgsl/ast/continue_statement_test.cc",
@@ -2439,7 +2441,7 @@
 
   tint_unittests_source_set("tint_unittests_core_src") {
     sources = [
-      "lang/wgsl/ast/clone_context_test.cc",
+      "lang/wgsl/program/clone_context_test.cc",
       "lang/wgsl/program/program_builder_test.cc",
       "lang/wgsl/program/program_test.cc",
     ]
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index ab97c32..6d17a41 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -443,6 +443,8 @@
   lang/wgsl/inspector/resource_binding.h
   lang/wgsl/inspector/scalar.cc
   lang/wgsl/inspector/scalar.h
+  lang/wgsl/program/clone_context.cc
+  lang/wgsl/program/clone_context.h
   lang/wgsl/program/program.cc
   lang/wgsl/program/program.h
   lang/wgsl/program/program_builder.cc
@@ -960,7 +962,6 @@
 ################################################################################
 if(TINT_BUILD_TESTS)
   list(APPEND TINT_TEST_SRCS
-    lang/wgsl/ast/clone_context_test.cc
     lang/core/builtin/number_test.cc
     lang/core/constant/composite_test.cc
     lang/core/constant/manager_test.cc
@@ -1006,6 +1007,7 @@
     lang/wgsl/ast/call_statement_test.cc
     lang/wgsl/ast/case_selector_test.cc
     lang/wgsl/ast/case_statement_test.cc
+    lang/wgsl/ast/clone_context_test.cc
     lang/wgsl/ast/compound_assignment_statement_test.cc
     lang/wgsl/ast/const_assert_test.cc
     lang/wgsl/ast/continue_statement_test.cc
@@ -1056,6 +1058,7 @@
     lang/wgsl/helpers/append_vector_test.cc
     lang/wgsl/helpers/check_supported_extensions_test.cc
     lang/wgsl/helpers/flatten_bindings_test.cc
+    lang/wgsl/program/clone_context_test.cc
     lang/wgsl/program/program_builder_test.cc
     lang/wgsl/program/program_test.cc
     lang/wgsl/sem/builtin_test.cc
diff --git a/src/tint/fuzzers/shuffle_transform.cc b/src/tint/fuzzers/shuffle_transform.cc
index 97a9ec3..f014a46 100644
--- a/src/tint/fuzzers/shuffle_transform.cc
+++ b/src/tint/fuzzers/shuffle_transform.cc
@@ -17,6 +17,7 @@
 #include <random>
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 namespace tint::fuzzers {
@@ -27,7 +28,7 @@
                                                                const ast::transform::DataMap&,
                                                                ast::transform::DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto decls = src->AST().GlobalDeclarations();
     auto rng = std::mt19937_64{seed_};
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation.h
index 5f741ef..0c39e82 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutation.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation.h
@@ -21,7 +21,7 @@
 #include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.h"
 
-#include "src/tint/lang/wgsl/ast/clone_context.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program.h"
 
 namespace tint::fuzzers::ast_fuzzer {
@@ -50,7 +50,7 @@
     /// @brief Applies this mutation to the `clone_context`.
     ///
     /// Precondition: `IsApplicable` must return `true` when invoked on the same
-    /// `node_id_map` and `clone_context->src` instance of `tint::Program`. A new
+    /// `node_id_map` and `clone_context.src` instance of `tint::Program`. A new
     /// `tint::Program` that arises in `clone_context` must be valid.
     ///
     /// @param node_id_map - the map from `tint::ast::` nodes to their ids.
@@ -60,7 +60,7 @@
     ///     cloned program. This argument cannot be a `nullptr` nor can it point
     ///     to the same object as `node_id_map`.
     virtual void Apply(const NodeIdMap& node_id_map,
-                       tint::CloneContext* clone_context,
+                       tint::program::CloneContext& clone_context,
                        NodeIdMap* new_node_id_map) const = 0;
 
     /// @return a protobuf message for this mutation.
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
index cac7345..8d7591d 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
@@ -355,7 +355,7 @@
 }
 
 void MutationChangeBinaryOperator::Apply(const NodeIdMap& node_id_map,
-                                         CloneContext* clone_context,
+                                         program::CloneContext& clone_context,
                                          NodeIdMap* new_node_id_map) const {
     // Get the node whose operator is to be replaced.
     const auto* binary_expr_node =
@@ -365,94 +365,84 @@
     const ast::BinaryExpression* cloned_replacement;
     switch (static_cast<ast::BinaryOp>(message_.new_operator())) {
         case ast::BinaryOp::kAnd:
-            cloned_replacement =
-                clone_context->dst->And(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->And(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kOr:
-            cloned_replacement =
-                clone_context->dst->Or(clone_context->Clone(binary_expr_node->lhs),
-                                       clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Or(clone_context.Clone(binary_expr_node->lhs),
+                                                       clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kXor:
-            cloned_replacement =
-                clone_context->dst->Xor(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Xor(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kLogicalAnd:
             cloned_replacement =
-                clone_context->dst->LogicalAnd(clone_context->Clone(binary_expr_node->lhs),
-                                               clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->LogicalAnd(clone_context.Clone(binary_expr_node->lhs),
+                                              clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kLogicalOr:
             cloned_replacement =
-                clone_context->dst->LogicalOr(clone_context->Clone(binary_expr_node->lhs),
-                                              clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->LogicalOr(clone_context.Clone(binary_expr_node->lhs),
+                                             clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kEqual:
             cloned_replacement =
-                clone_context->dst->Equal(clone_context->Clone(binary_expr_node->lhs),
-                                          clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->Equal(clone_context.Clone(binary_expr_node->lhs),
+                                         clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kNotEqual:
             cloned_replacement =
-                clone_context->dst->NotEqual(clone_context->Clone(binary_expr_node->lhs),
-                                             clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->NotEqual(clone_context.Clone(binary_expr_node->lhs),
+                                            clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kLessThan:
             cloned_replacement =
-                clone_context->dst->LessThan(clone_context->Clone(binary_expr_node->lhs),
-                                             clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->LessThan(clone_context.Clone(binary_expr_node->lhs),
+                                            clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kGreaterThan:
             cloned_replacement =
-                clone_context->dst->GreaterThan(clone_context->Clone(binary_expr_node->lhs),
-                                                clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->GreaterThan(clone_context.Clone(binary_expr_node->lhs),
+                                               clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kLessThanEqual:
             cloned_replacement =
-                clone_context->dst->LessThanEqual(clone_context->Clone(binary_expr_node->lhs),
-                                                  clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->LessThanEqual(clone_context.Clone(binary_expr_node->lhs),
+                                                 clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kGreaterThanEqual:
             cloned_replacement =
-                clone_context->dst->GreaterThanEqual(clone_context->Clone(binary_expr_node->lhs),
-                                                     clone_context->Clone(binary_expr_node->rhs));
+                clone_context.dst->GreaterThanEqual(clone_context.Clone(binary_expr_node->lhs),
+                                                    clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kShiftLeft:
-            cloned_replacement =
-                clone_context->dst->Shl(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Shl(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kShiftRight:
-            cloned_replacement =
-                clone_context->dst->Shr(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Shr(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kAdd:
-            cloned_replacement =
-                clone_context->dst->Add(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Add(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kSubtract:
-            cloned_replacement =
-                clone_context->dst->Sub(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Sub(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kMultiply:
-            cloned_replacement =
-                clone_context->dst->Mul(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Mul(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kDivide:
-            cloned_replacement =
-                clone_context->dst->Div(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Div(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kModulo:
-            cloned_replacement =
-                clone_context->dst->Mod(clone_context->Clone(binary_expr_node->lhs),
-                                        clone_context->Clone(binary_expr_node->rhs));
+            cloned_replacement = clone_context.dst->Mod(clone_context.Clone(binary_expr_node->lhs),
+                                                        clone_context.Clone(binary_expr_node->rhs));
             break;
         case ast::BinaryOp::kNone:
             cloned_replacement = nullptr;
@@ -460,7 +450,7 @@
     }
     // Set things up so that the original binary expression will be replaced with
     // its clone, and update the id mapping.
-    clone_context->Replace(binary_expr_node, cloned_replacement);
+    clone_context.Replace(binary_expr_node, cloned_replacement);
     new_node_id_map->Add(cloned_replacement, message_.binary_expr_id());
 }
 
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
index 5732778..375d49e 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
@@ -53,7 +53,7 @@
     ///
     /// @copydetails Mutation::Apply
     void Apply(const NodeIdMap& node_id_map,
-               tint::CloneContext* clone_context,
+               tint::program::CloneContext& clone_context,
                NodeIdMap* new_node_id_map) const override;
 
     protobufs::Mutation ToMessage() const override;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.cc
index beae8bd..4b23fc9 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.cc
@@ -65,7 +65,7 @@
 }
 
 void MutationChangeUnaryOperator::Apply(const NodeIdMap& node_id_map,
-                                        tint::CloneContext* clone_context,
+                                        tint::program::CloneContext& clone_context,
                                         NodeIdMap* new_node_id_map) const {
     const auto* unary_expr_node =
         tint::As<ast::UnaryOpExpression>(node_id_map.GetNode(message_.unary_expr_id()));
@@ -74,11 +74,11 @@
     switch (static_cast<ast::UnaryOp>(message_.new_operator())) {
         case ast::UnaryOp::kComplement:
             cloned_replacement =
-                clone_context->dst->Complement(clone_context->Clone(unary_expr_node->expr));
+                clone_context.dst->Complement(clone_context.Clone(unary_expr_node->expr));
             break;
         case ast::UnaryOp::kNegation:
             cloned_replacement =
-                clone_context->dst->Negation(clone_context->Clone(unary_expr_node->expr));
+                clone_context.dst->Negation(clone_context.Clone(unary_expr_node->expr));
             break;
         default:
             cloned_replacement = nullptr;
@@ -86,7 +86,7 @@
     }
     // Set things up so that the original unary expression will be replaced with
     // its clone, and update the id mapping.
-    clone_context->Replace(unary_expr_node, cloned_replacement);
+    clone_context.Replace(unary_expr_node, cloned_replacement);
     new_node_id_map->Add(cloned_replacement, message_.unary_expr_id());
 }
 
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.h
index 8baf194..4682075 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.h
@@ -54,7 +54,7 @@
     ///
     /// @copydetails Mutation::Apply
     void Apply(const NodeIdMap& node_id_map,
-               tint::CloneContext* clone_context,
+               tint::program::CloneContext& clone_context,
                NodeIdMap* new_node_id_map) const override;
 
     protobufs::Mutation ToMessage() const override;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc
index b3cec9c..6af5f46 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc
@@ -66,31 +66,31 @@
 }
 
 void MutationDeleteStatement::Apply(const NodeIdMap& node_id_map,
-                                    tint::CloneContext* clone_context,
+                                    tint::program::CloneContext& clone_context,
                                     NodeIdMap* /* unused */) const {
     const auto* statement_node =
         tint::As<ast::Statement>(node_id_map.GetNode(message_.statement_id()));
     const auto* statement_sem_node =
-        tint::As<sem::Statement>(clone_context->src->Sem().Get(statement_node));
+        tint::As<sem::Statement>(clone_context.src->Sem().Get(statement_node));
     const auto* sem_parent = statement_sem_node->Parent();
 
     if (tint::Is<sem::IfStatement>(sem_parent) &&
         tint::As<ast::IfStatement>(sem_parent->Declaration())->else_statement == statement_node) {
         // Remove the "else" part of an if statement.
-        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+        clone_context.Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
     } else if (tint::Is<sem::ForLoopStatement>(sem_parent) &&
                tint::As<ast::ForLoopStatement>(sem_parent->Declaration())->initializer ==
                    statement_node) {
         // Remove the initializer of a for loop.
-        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+        clone_context.Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
     } else if (tint::Is<sem::ForLoopStatement>(sem_parent) &&
                tint::As<ast::ForLoopStatement>(sem_parent->Declaration())->continuing ==
                    statement_node) {
         // Remove the "continuing" statement of a for loop.
-        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+        clone_context.Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
     } else if (tint::Is<sem::LoopContinuingBlockStatement>(statement_sem_node)) {
         // Remove the "continuing" block of a loop.
-        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+        clone_context.Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
     } else if (tint::Is<ast::CaseStatement>(statement_node)) {
         // Remove a case statement from its enclosing switch statement.
         const auto& case_statement_list =
@@ -98,7 +98,7 @@
         assert(std::find(case_statement_list->begin(), case_statement_list->end(),
                          statement_node) != case_statement_list->end() &&
                "Statement not found.");
-        clone_context->Remove(*case_statement_list, statement_node);
+        clone_context.Remove(*case_statement_list, statement_node);
     } else if (tint::Is<ast::BlockStatement>(statement_node)) {
         // Remove a block statement from the block that encloses it. A special case is required for
         // this, since a sem::Block has itself as its associated sem::Block, so it is necessary to
@@ -108,7 +108,7 @@
         assert(std::find(statement_list.begin(), statement_list.end(), statement_node) !=
                    statement_list.end() &&
                "Statement not found.");
-        clone_context->Remove(statement_list, statement_node);
+        clone_context.Remove(statement_list, statement_node);
     } else {
         // Remove a non-block statement from the block that encloses it.
         const auto& statement_list =
@@ -116,7 +116,7 @@
         assert(std::find(statement_list.begin(), statement_list.end(), statement_node) !=
                    statement_list.end() &&
                "Statement not found.");
-        clone_context->Remove(statement_list, statement_node);
+        clone_context.Remove(statement_list, statement_node);
     }
 }
 
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h
index b77f822..5c818fc 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h
@@ -52,7 +52,7 @@
     ///
     /// @copydetails Mutation::Apply
     void Apply(const NodeIdMap& node_id_map,
-               tint::CloneContext* clone_context,
+               tint::program::CloneContext& clone_context,
                NodeIdMap* new_node_id_map) const override;
 
     protobufs::Mutation ToMessage() const override;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc
index d752715..1c52ed3 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc
@@ -74,16 +74,15 @@
 }
 
 void MutationReplaceIdentifier::Apply(const NodeIdMap& node_id_map,
-                                      tint::CloneContext* clone_context,
+                                      tint::program::CloneContext& clone_context,
                                       NodeIdMap* new_node_id_map) const {
     const auto* use_node = node_id_map.GetNode(message_.use_id());
     const auto* replacement_var =
         tint::As<ast::Variable>(node_id_map.GetNode(message_.replacement_id()));
 
-    auto* cloned_replacement =
-        clone_context->dst->Expr(clone_context->Clone(use_node->source),
-                                 clone_context->Clone(replacement_var->name->symbol));
-    clone_context->Replace(use_node, cloned_replacement);
+    auto* cloned_replacement = clone_context.dst->Expr(
+        clone_context.Clone(use_node->source), clone_context.Clone(replacement_var->name->symbol));
+    clone_context.Replace(use_node, cloned_replacement);
     new_node_id_map->Add(cloned_replacement, message_.use_id());
 }
 
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h
index 3fe729a..12f5ee9 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h
@@ -57,7 +57,7 @@
     ///
     /// @copydetails Mutation::Apply
     void Apply(const NodeIdMap& node_id_map,
-               tint::CloneContext* clone_context,
+               tint::program::CloneContext& clone_context,
                NodeIdMap* new_node_id_map) const override;
 
     protobufs::Mutation ToMessage() const override;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
index c14fdd3..1fa54ca 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
@@ -74,16 +74,16 @@
 }
 
 void MutationWrapUnaryOperator::Apply(const NodeIdMap& node_id_map,
-                                      tint::CloneContext* clone_context,
+                                      tint::program::CloneContext& clone_context,
                                       NodeIdMap* new_node_id_map) const {
     auto* expression_node =
         tint::As<ast::Expression>(node_id_map.GetNode(message_.expression_id()));
 
-    auto* replacement_expression_node = clone_context->dst->create<ast::UnaryOpExpression>(
+    auto* replacement_expression_node = clone_context.dst->create<ast::UnaryOpExpression>(
         static_cast<ast::UnaryOp>(message_.unary_op_wrapper()),
-        clone_context->Clone(expression_node));
+        clone_context.Clone(expression_node));
 
-    clone_context->Replace(expression_node, replacement_expression_node);
+    clone_context.Replace(expression_node, replacement_expression_node);
 
     new_node_id_map->Add(replacement_expression_node, message_.fresh_id());
 }
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h
index 58c70ee..5dcc204 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h
@@ -59,7 +59,7 @@
     ///
     /// @copydetails Mutation::Apply
     void Apply(const NodeIdMap& node_id_map,
-               tint::CloneContext* clone_context,
+               tint::program::CloneContext& clone_context,
                NodeIdMap* new_node_id_map) const override;
 
     protobufs::Mutation ToMessage() const override;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
index 7fa0118..44f40f3 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -76,17 +76,17 @@
 
     // The mutated `program` will be copied into the `mutated` program builder.
     tint::ProgramBuilder mutated;
-    tint::CloneContext clone_context(&mutated, &program);
+    tint::program::CloneContext clone_context(&mutated, &program);
     NodeIdMap new_node_id_map;
     clone_context.ReplaceAll(
         [&node_id_map, &new_node_id_map, &clone_context](const ast::Node* node) {
             // Make sure all `tint::ast::` nodes' ids are preserved.
-            auto* cloned = tint::As<ast::Node>(node->Clone(&clone_context));
+            auto* cloned = tint::As<ast::Node>(node->Clone(clone_context));
             new_node_id_map.Add(cloned, node_id_map.GetId(node));
             return cloned;
         });
 
-    mutation.Apply(node_id_map, &clone_context, &new_node_id_map);
+    mutation.Apply(node_id_map, clone_context, &new_node_id_map);
     if (mutation_sequence) {
         *mutation_sequence->add_mutation() = mutation.ToMessage();
     }
diff --git a/src/tint/lang/spirv/reader/ast_parser/function.cc b/src/tint/lang/spirv/reader/ast_parser/function.cc
index e18b4ba..60e9967 100644
--- a/src/tint/lang/spirv/reader/ast_parser/function.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/function.cc
@@ -778,7 +778,7 @@
 
 DefInfo::Local::~Local() = default;
 
-ast::Node* StatementBuilder::Clone(CloneContext*) const {
+ast::Node* StatementBuilder::Clone(ast::CloneContext&) const {
     return nullptr;
 }
 
diff --git a/src/tint/lang/spirv/reader/ast_parser/function.h b/src/tint/lang/spirv/reader/ast_parser/function.h
index 5658690..9fc1ede 100644
--- a/src/tint/lang/spirv/reader/ast_parser/function.h
+++ b/src/tint/lang/spirv/reader/ast_parser/function.h
@@ -419,7 +419,7 @@
     virtual const ast::Statement* Build(ProgramBuilder* builder) const = 0;
 
   private:
-    Node* Clone(CloneContext*) const override;
+    Node* Clone(ast::CloneContext&) const override;
 };
 
 /// A FunctionEmitter emits a SPIR-V function onto a Tint AST module.
diff --git a/src/tint/lang/spirv/reader/reader.cc b/src/tint/lang/spirv/reader/reader.cc
index 8604ff3..41408e5 100644
--- a/src/tint/lang/spirv/reader/reader.cc
+++ b/src/tint/lang/spirv/reader/reader.cc
@@ -25,6 +25,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/spirv_atomic.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 
 namespace tint::spirv::reader {
 
@@ -51,7 +52,7 @@
     Program program_with_disjoint_ast(std::move(builder));
 
     ProgramBuilder output;
-    CloneContext(&output, &program_with_disjoint_ast, false).Clone();
+    program::CloneContext(&output, &program_with_disjoint_ast, false).Clone();
     auto program = Program(std::move(output));
     if (!program.IsValid()) {
         return program;
diff --git a/src/tint/lang/wgsl/ast/accessor_expression.cc b/src/tint/lang/wgsl/ast/accessor_expression.cc
index 67c3728..5512f0d 100644
--- a/src/tint/lang/wgsl/ast/accessor_expression.cc
+++ b/src/tint/lang/wgsl/ast/accessor_expression.cc
@@ -14,8 +14,6 @@
 
 #include "src/tint/lang/wgsl/ast/accessor_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
-
 TINT_INSTANTIATE_TYPEINFO(tint::ast::AccessorExpression);
 
 namespace tint::ast {
diff --git a/src/tint/lang/wgsl/ast/alias.cc b/src/tint/lang/wgsl/ast/alias.cc
index 79c2374..67efb93 100644
--- a/src/tint/lang/wgsl/ast/alias.cc
+++ b/src/tint/lang/wgsl/ast/alias.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/alias.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Alias);
 
@@ -27,12 +28,12 @@
 
 Alias::~Alias() = default;
 
-const Alias* Alias::Clone(CloneContext* ctx) const {
+const Alias* Alias::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto sym = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    return ctx->dst->create<Alias>(src, sym, ty);
+    auto src = ctx.Clone(source);
+    auto sym = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    return ctx.dst->create<Alias>(src, sym, ty);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/alias.h b/src/tint/lang/wgsl/ast/alias.h
index 73ebdc7..4474d5b 100644
--- a/src/tint/lang/wgsl/ast/alias.h
+++ b/src/tint/lang/wgsl/ast/alias.h
@@ -39,7 +39,7 @@
     /// Clones this type and all transitive types using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned type
-    const Alias* Clone(CloneContext* ctx) const override;
+    const Alias* Clone(CloneContext& ctx) const override;
 
     /// the alias type
     const Type type;
diff --git a/src/tint/lang/wgsl/ast/assignment_statement.cc b/src/tint/lang/wgsl/ast/assignment_statement.cc
index 39307eb..17d3efd 100644
--- a/src/tint/lang/wgsl/ast/assignment_statement.cc
+++ b/src/tint/lang/wgsl/ast/assignment_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::AssignmentStatement);
 
@@ -34,12 +35,12 @@
 
 AssignmentStatement::~AssignmentStatement() = default;
 
-const AssignmentStatement* AssignmentStatement::Clone(CloneContext* ctx) const {
+const AssignmentStatement* AssignmentStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* l = ctx->Clone(lhs);
-    auto* r = ctx->Clone(rhs);
-    return ctx->dst->create<AssignmentStatement>(src, l, r);
+    auto src = ctx.Clone(source);
+    auto* l = ctx.Clone(lhs);
+    auto* r = ctx.Clone(rhs);
+    return ctx.dst->create<AssignmentStatement>(src, l, r);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/assignment_statement.h b/src/tint/lang/wgsl/ast/assignment_statement.h
index 7b2c865..7e6fc96 100644
--- a/src/tint/lang/wgsl/ast/assignment_statement.h
+++ b/src/tint/lang/wgsl/ast/assignment_statement.h
@@ -42,7 +42,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const AssignmentStatement* Clone(CloneContext* ctx) const override;
+    const AssignmentStatement* Clone(CloneContext& ctx) const override;
 
     /// left side expression
     const Expression* const lhs;
diff --git a/src/tint/lang/wgsl/ast/attribute.h b/src/tint/lang/wgsl/ast/attribute.h
index 91d4e64..daf18c0 100644
--- a/src/tint/lang/wgsl/ast/attribute.h
+++ b/src/tint/lang/wgsl/ast/attribute.h
@@ -16,9 +16,9 @@
 #define SRC_TINT_LANG_WGSL_AST_ATTRIBUTE_H_
 
 #include <string>
-#include <vector>
 
 #include "src/tint/lang/wgsl/ast/node.h"
+#include "src/tint/utils/containers/vector.h"
 
 namespace tint::ast {
 
diff --git a/src/tint/lang/wgsl/ast/binary_expression.cc b/src/tint/lang/wgsl/ast/binary_expression.cc
index 4a58f1c..adae2e3 100644
--- a/src/tint/lang/wgsl/ast/binary_expression.cc
+++ b/src/tint/lang/wgsl/ast/binary_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/binary_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BinaryExpression);
 
@@ -36,12 +37,12 @@
 
 BinaryExpression::~BinaryExpression() = default;
 
-const BinaryExpression* BinaryExpression::Clone(CloneContext* ctx) const {
+const BinaryExpression* BinaryExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* l = ctx->Clone(lhs);
-    auto* r = ctx->Clone(rhs);
-    return ctx->dst->create<BinaryExpression>(src, op, l, r);
+    auto src = ctx.Clone(source);
+    auto* l = ctx.Clone(lhs);
+    auto* r = ctx.Clone(rhs);
+    return ctx.dst->create<BinaryExpression>(src, op, l, r);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/binary_expression.h b/src/tint/lang/wgsl/ast/binary_expression.h
index 2b9cd2c..81eae37 100644
--- a/src/tint/lang/wgsl/ast/binary_expression.h
+++ b/src/tint/lang/wgsl/ast/binary_expression.h
@@ -113,7 +113,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BinaryExpression* Clone(CloneContext* ctx) const override;
+    const BinaryExpression* Clone(CloneContext& ctx) const override;
 
     /// the binary op type
     const BinaryOp op;
diff --git a/src/tint/lang/wgsl/ast/binding_attribute.cc b/src/tint/lang/wgsl/ast/binding_attribute.cc
index 662672e..2f1a79c 100644
--- a/src/tint/lang/wgsl/ast/binding_attribute.cc
+++ b/src/tint/lang/wgsl/ast/binding_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BindingAttribute);
 
@@ -34,11 +35,11 @@
     return "binding";
 }
 
-const BindingAttribute* BindingAttribute::Clone(CloneContext* ctx) const {
+const BindingAttribute* BindingAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* expr_ = ctx->Clone(expr);
-    return ctx->dst->create<BindingAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto* expr_ = ctx.Clone(expr);
+    return ctx.dst->create<BindingAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/binding_attribute.h b/src/tint/lang/wgsl/ast/binding_attribute.h
index 76f16bc..d029666 100644
--- a/src/tint/lang/wgsl/ast/binding_attribute.h
+++ b/src/tint/lang/wgsl/ast/binding_attribute.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BindingAttribute* Clone(CloneContext* ctx) const override;
+    const BindingAttribute* Clone(CloneContext& ctx) const override;
 
     /// the binding expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/bitcast_expression.cc b/src/tint/lang/wgsl/ast/bitcast_expression.cc
index f943eca..2d19308 100644
--- a/src/tint/lang/wgsl/ast/bitcast_expression.cc
+++ b/src/tint/lang/wgsl/ast/bitcast_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BitcastExpression);
 
@@ -33,12 +34,12 @@
 
 BitcastExpression::~BitcastExpression() = default;
 
-const BitcastExpression* BitcastExpression::Clone(CloneContext* ctx) const {
+const BitcastExpression* BitcastExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto t = ctx->Clone(type);
-    auto* e = ctx->Clone(expr);
-    return ctx->dst->create<BitcastExpression>(src, t, e);
+    auto src = ctx.Clone(source);
+    auto t = ctx.Clone(type);
+    auto* e = ctx.Clone(expr);
+    return ctx.dst->create<BitcastExpression>(src, t, e);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/bitcast_expression.h b/src/tint/lang/wgsl/ast/bitcast_expression.h
index c13addf..0274a25 100644
--- a/src/tint/lang/wgsl/ast/bitcast_expression.h
+++ b/src/tint/lang/wgsl/ast/bitcast_expression.h
@@ -42,7 +42,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BitcastExpression* Clone(CloneContext* ctx) const override;
+    const BitcastExpression* Clone(CloneContext& ctx) const override;
 
     /// the target cast type
     const Type type;
diff --git a/src/tint/lang/wgsl/ast/block_statement.cc b/src/tint/lang/wgsl/ast/block_statement.cc
index a49d07e..f0950fc 100644
--- a/src/tint/lang/wgsl/ast/block_statement.cc
+++ b/src/tint/lang/wgsl/ast/block_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/block_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BlockStatement);
 
@@ -38,12 +39,12 @@
 
 BlockStatement::~BlockStatement() = default;
 
-const BlockStatement* BlockStatement::Clone(CloneContext* ctx) const {
+const BlockStatement* BlockStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto stmts = ctx->Clone(statements);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<BlockStatement>(src, std::move(stmts), std::move(attrs));
+    auto src = ctx.Clone(source);
+    auto stmts = ctx.Clone(statements);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<BlockStatement>(src, std::move(stmts), std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/block_statement.h b/src/tint/lang/wgsl/ast/block_statement.h
index 6a0449f..cc1d81a 100644
--- a/src/tint/lang/wgsl/ast/block_statement.h
+++ b/src/tint/lang/wgsl/ast/block_statement.h
@@ -18,6 +18,8 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/statement.h"
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/diagnostic/source.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -54,7 +56,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BlockStatement* Clone(CloneContext* ctx) const override;
+    const BlockStatement* Clone(CloneContext& ctx) const override;
 
     /// the statement list
     const tint::Vector<const Statement*, 8> statements;
diff --git a/src/tint/lang/wgsl/ast/bool_literal_expression.cc b/src/tint/lang/wgsl/ast/bool_literal_expression.cc
index 668b37f..ee7779c 100644
--- a/src/tint/lang/wgsl/ast/bool_literal_expression.cc
+++ b/src/tint/lang/wgsl/ast/bool_literal_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BoolLiteralExpression);
 
@@ -28,10 +29,10 @@
 
 BoolLiteralExpression::~BoolLiteralExpression() = default;
 
-const BoolLiteralExpression* BoolLiteralExpression::Clone(CloneContext* ctx) const {
+const BoolLiteralExpression* BoolLiteralExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<BoolLiteralExpression>(src, value);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<BoolLiteralExpression>(src, value);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/bool_literal_expression.h b/src/tint/lang/wgsl/ast/bool_literal_expression.h
index ec9ca98..38a202d 100644
--- a/src/tint/lang/wgsl/ast/bool_literal_expression.h
+++ b/src/tint/lang/wgsl/ast/bool_literal_expression.h
@@ -36,7 +36,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BoolLiteralExpression* Clone(CloneContext* ctx) const override;
+    const BoolLiteralExpression* Clone(CloneContext& ctx) const override;
 
     /// The boolean literal value
     const bool value;
diff --git a/src/tint/lang/wgsl/ast/break_if_statement.cc b/src/tint/lang/wgsl/ast/break_if_statement.cc
index 19431ef..a7817c0 100644
--- a/src/tint/lang/wgsl/ast/break_if_statement.cc
+++ b/src/tint/lang/wgsl/ast/break_if_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BreakIfStatement);
 
@@ -31,11 +32,11 @@
 
 BreakIfStatement::~BreakIfStatement() = default;
 
-const BreakIfStatement* BreakIfStatement::Clone(CloneContext* ctx) const {
+const BreakIfStatement* BreakIfStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* cond = ctx->Clone(condition);
-    return ctx->dst->create<BreakIfStatement>(src, cond);
+    auto src = ctx.Clone(source);
+    auto* cond = ctx.Clone(condition);
+    return ctx.dst->create<BreakIfStatement>(src, cond);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/break_if_statement.h b/src/tint/lang/wgsl/ast/break_if_statement.h
index 7be1806..41d5d7c 100644
--- a/src/tint/lang/wgsl/ast/break_if_statement.h
+++ b/src/tint/lang/wgsl/ast/break_if_statement.h
@@ -38,7 +38,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BreakIfStatement* Clone(CloneContext* ctx) const override;
+    const BreakIfStatement* Clone(CloneContext& ctx) const override;
 
     /// The if condition or nullptr if none set
     const Expression* const condition;
diff --git a/src/tint/lang/wgsl/ast/break_statement.cc b/src/tint/lang/wgsl/ast/break_statement.cc
index 51bfd95..06810e4 100644
--- a/src/tint/lang/wgsl/ast/break_statement.cc
+++ b/src/tint/lang/wgsl/ast/break_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/break_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BreakStatement);
 
@@ -25,10 +26,10 @@
 
 BreakStatement::~BreakStatement() = default;
 
-const BreakStatement* BreakStatement::Clone(CloneContext* ctx) const {
+const BreakStatement* BreakStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<BreakStatement>(src);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<BreakStatement>(src);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/break_statement.h b/src/tint/lang/wgsl/ast/break_statement.h
index 12a71df..c9dd005 100644
--- a/src/tint/lang/wgsl/ast/break_statement.h
+++ b/src/tint/lang/wgsl/ast/break_statement.h
@@ -35,7 +35,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BreakStatement* Clone(CloneContext* ctx) const override;
+    const BreakStatement* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index 7897a78..beca149 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -88,6 +88,7 @@
 #include "src/tint/lang/wgsl/ast/return_statement.h"
 #include "src/tint/lang/wgsl/ast/stage_attribute.h"
 #include "src/tint/lang/wgsl/ast/stride_attribute.h"
+#include "src/tint/lang/wgsl/ast/struct.h"
 #include "src/tint/lang/wgsl/ast/struct_member_align_attribute.h"
 #include "src/tint/lang/wgsl/ast/struct_member_offset_attribute.h"
 #include "src/tint/lang/wgsl/ast/struct_member_size_attribute.h"
@@ -99,7 +100,6 @@
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/lang/wgsl/ast/while_statement.h"
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
-#include "src/tint/lang/wgsl/program/program.h"
 #include "src/tint/utils/generation_id.h"
 #include "src/tint/utils/text/string.h"
 
@@ -108,10 +108,8 @@
 #endif
 
 // Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
 namespace tint::ast {
+class CloneContext;
 class VariableDeclStatement;
 }  // namespace tint::ast
 
@@ -3534,12 +3532,6 @@
 };
 //! @endcond
 
-/// @param builder the Builder
-/// @returns the GenerationID of the Builder
-inline GenerationID GenerationIDOf(const Builder* builder) {
-    return builder->ID();
-}
-
 // Primary template for metafunction that evaluates to true iff T can be wrapped in a statement.
 template <typename T, typename /*  = void */>
 struct CanWrapInStatement : std::false_type {};
@@ -3553,4 +3545,14 @@
 
 }  // namespace tint::ast
 
+namespace tint {
+
+/// @param builder the Builder
+/// @returns the GenerationID of the ast::Builder
+inline GenerationID GenerationIDOf(const ast::Builder* builder) {
+    return builder->ID();
+}
+
+}  // namespace tint
+
 #endif  // SRC_TINT_LANG_WGSL_AST_BUILDER_H_
diff --git a/src/tint/lang/wgsl/ast/builtin_attribute.cc b/src/tint/lang/wgsl/ast/builtin_attribute.cc
index 22602ef..3da6380 100644
--- a/src/tint/lang/wgsl/ast/builtin_attribute.cc
+++ b/src/tint/lang/wgsl/ast/builtin_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::BuiltinAttribute);
 
@@ -36,11 +37,11 @@
     return "builtin";
 }
 
-const BuiltinAttribute* BuiltinAttribute::Clone(CloneContext* ctx) const {
+const BuiltinAttribute* BuiltinAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto b = ctx->Clone(builtin);
-    return ctx->dst->create<BuiltinAttribute>(src, b);
+    auto src = ctx.Clone(source);
+    auto b = ctx.Clone(builtin);
+    return ctx.dst->create<BuiltinAttribute>(src, b);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/builtin_attribute.h b/src/tint/lang/wgsl/ast/builtin_attribute.h
index 28e72d0..a55876f 100644
--- a/src/tint/lang/wgsl/ast/builtin_attribute.h
+++ b/src/tint/lang/wgsl/ast/builtin_attribute.h
@@ -44,7 +44,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const BuiltinAttribute* Clone(CloneContext* ctx) const override;
+    const BuiltinAttribute* Clone(CloneContext& ctx) const override;
 
     /// The builtin value
     const Expression* const builtin;
diff --git a/src/tint/lang/wgsl/ast/builtin_texture_helper_test.cc b/src/tint/lang/wgsl/ast/builtin_texture_helper_test.cc
index cb7b4eb..31063df 100644
--- a/src/tint/lang/wgsl/ast/builtin_texture_helper_test.cc
+++ b/src/tint/lang/wgsl/ast/builtin_texture_helper_test.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/core/type/multisampled_texture.h"
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
 
 namespace tint::ast::test {
 namespace {
diff --git a/src/tint/lang/wgsl/ast/builtin_texture_helper_test.h b/src/tint/lang/wgsl/ast/builtin_texture_helper_test.h
index 1c17867..de1cab4 100644
--- a/src/tint/lang/wgsl/ast/builtin_texture_helper_test.h
+++ b/src/tint/lang/wgsl/ast/builtin_texture_helper_test.h
@@ -21,7 +21,7 @@
 #include "src/tint/lang/core/builtin/texel_format.h"
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
 
 namespace tint::ast::test {
 
diff --git a/src/tint/lang/wgsl/ast/call_expression.cc b/src/tint/lang/wgsl/ast/call_expression.cc
index 7541e82..e3aec51 100644
--- a/src/tint/lang/wgsl/ast/call_expression.cc
+++ b/src/tint/lang/wgsl/ast/call_expression.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::CallExpression);
 
@@ -38,12 +39,12 @@
 
 CallExpression::~CallExpression() = default;
 
-const CallExpression* CallExpression::Clone(CloneContext* ctx) const {
+const CallExpression* CallExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto p = ctx->Clone(args);
-    auto t = ctx->Clone(target);
-    return ctx->dst->create<CallExpression>(src, t, std::move(p));
+    auto src = ctx.Clone(source);
+    auto p = ctx.Clone(args);
+    auto t = ctx.Clone(target);
+    return ctx.dst->create<CallExpression>(src, t, std::move(p));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/call_expression.h b/src/tint/lang/wgsl/ast/call_expression.h
index ada8dd1..07a3e5a 100644
--- a/src/tint/lang/wgsl/ast/call_expression.h
+++ b/src/tint/lang/wgsl/ast/call_expression.h
@@ -16,6 +16,7 @@
 #define SRC_TINT_LANG_WGSL_AST_CALL_EXPRESSION_H_
 
 #include "src/tint/lang/wgsl/ast/expression.h"
+#include "src/tint/utils/containers/vector.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -50,7 +51,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const CallExpression* Clone(CloneContext* ctx) const override;
+    const CallExpression* Clone(CloneContext& ctx) const override;
 
     /// The target function or type
     const IdentifierExpression* target;
diff --git a/src/tint/lang/wgsl/ast/call_statement.cc b/src/tint/lang/wgsl/ast/call_statement.cc
index abd8a038..9fca156 100644
--- a/src/tint/lang/wgsl/ast/call_statement.cc
+++ b/src/tint/lang/wgsl/ast/call_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::CallStatement);
 
@@ -31,11 +32,11 @@
 
 CallStatement::~CallStatement() = default;
 
-const CallStatement* CallStatement::Clone(CloneContext* ctx) const {
+const CallStatement* CallStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* call = ctx->Clone(expr);
-    return ctx->dst->create<CallStatement>(src, call);
+    auto src = ctx.Clone(source);
+    auto* call = ctx.Clone(expr);
+    return ctx.dst->create<CallStatement>(src, call);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/call_statement.h b/src/tint/lang/wgsl/ast/call_statement.h
index e74cb93..5203fe1 100644
--- a/src/tint/lang/wgsl/ast/call_statement.h
+++ b/src/tint/lang/wgsl/ast/call_statement.h
@@ -37,7 +37,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const CallStatement* Clone(CloneContext* ctx) const override;
+    const CallStatement* Clone(CloneContext& ctx) const override;
 
     /// The call expression
     const CallExpression* const expr;
diff --git a/src/tint/lang/wgsl/ast/case_selector.cc b/src/tint/lang/wgsl/ast/case_selector.cc
index 58a5172..bc3b8b6 100644
--- a/src/tint/lang/wgsl/ast/case_selector.cc
+++ b/src/tint/lang/wgsl/ast/case_selector.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::CaseSelector);
 
@@ -27,11 +28,11 @@
 
 CaseSelector::~CaseSelector() = default;
 
-const CaseSelector* CaseSelector::Clone(CloneContext* ctx) const {
+const CaseSelector* CaseSelector::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto ex = ctx->Clone(expr);
-    return ctx->dst->create<CaseSelector>(src, ex);
+    auto src = ctx.Clone(source);
+    auto ex = ctx.Clone(expr);
+    return ctx.dst->create<CaseSelector>(src, ex);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/case_selector.h b/src/tint/lang/wgsl/ast/case_selector.h
index 8019ff1..fb845ff 100644
--- a/src/tint/lang/wgsl/ast/case_selector.h
+++ b/src/tint/lang/wgsl/ast/case_selector.h
@@ -41,7 +41,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const CaseSelector* Clone(CloneContext* ctx) const override;
+    const CaseSelector* Clone(CloneContext& ctx) const override;
 
     /// The selector, nullptr for a default selector
     const Expression* const expr = nullptr;
diff --git a/src/tint/lang/wgsl/ast/case_statement.cc b/src/tint/lang/wgsl/ast/case_statement.cc
index f5f5e1f..bc9cb56 100644
--- a/src/tint/lang/wgsl/ast/case_statement.cc
+++ b/src/tint/lang/wgsl/ast/case_statement.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::CaseStatement);
 
@@ -48,12 +49,12 @@
     return false;
 }
 
-const CaseStatement* CaseStatement::Clone(CloneContext* ctx) const {
+const CaseStatement* CaseStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto sel = ctx->Clone(selectors);
-    auto* b = ctx->Clone(body);
-    return ctx->dst->create<CaseStatement>(src, std::move(sel), b);
+    auto src = ctx.Clone(source);
+    auto sel = ctx.Clone(selectors);
+    auto* b = ctx.Clone(body);
+    return ctx.dst->create<CaseStatement>(src, std::move(sel), b);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/case_statement.h b/src/tint/lang/wgsl/ast/case_statement.h
index 2f50f67..c0d6426 100644
--- a/src/tint/lang/wgsl/ast/case_statement.h
+++ b/src/tint/lang/wgsl/ast/case_statement.h
@@ -44,7 +44,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const CaseStatement* Clone(CloneContext* ctx) const override;
+    const CaseStatement* Clone(CloneContext& ctx) const override;
 
     /// @returns true if this item contains a default selector
     bool ContainsDefault() const;
diff --git a/src/tint/lang/wgsl/ast/clone_context.cc b/src/tint/lang/wgsl/ast/clone_context.cc
index 5bc57ab..8830ba8 100644
--- a/src/tint/lang/wgsl/ast/clone_context.cc
+++ b/src/tint/lang/wgsl/ast/clone_context.cc
@@ -16,35 +16,16 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/utils/containers/map.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::Cloneable);
+namespace tint::ast {
 
-namespace tint {
-
-Cloneable::Cloneable() = default;
-Cloneable::Cloneable(Cloneable&&) = default;
-Cloneable::~Cloneable() = default;
-
-CloneContext::CloneContext(ProgramBuilder* to, Program const* from, bool auto_clone_symbols)
-    : dst(to), src(from) {
-    if (auto_clone_symbols) {
-        // Almost all transforms will want to clone all symbols before doing any
-        // work, to avoid any newly created symbols clashing with existing symbols
-        // in the source program and causing them to be renamed.
-        from->Symbols().Foreach([&](Symbol s) { Clone(s); });
-    }
-}
-
-CloneContext::CloneContext(ProgramBuilder* builder) : CloneContext(builder, nullptr, false) {}
+CloneContext::CloneContext(ast::Builder* to, GenerationID from) : dst(to), src_id(from) {}
 
 CloneContext::~CloneContext() = default;
 
 Symbol CloneContext::Clone(Symbol s) {
-    if (!src) {
-        return s;  // In-place clone
-    }
     return cloned_symbols_.GetOrCreate(s, [&]() -> Symbol {
         if (symbol_transform_) {
             return symbol_transform_(s);
@@ -53,10 +34,6 @@
     });
 }
 
-void CloneContext::Clone() {
-    dst->AST().Copy(this, &src->AST());
-}
-
 ast::FunctionList CloneContext::Clone(const ast::FunctionList& v) {
     ast::FunctionList out;
     out.Reserve(v.Length());
@@ -70,22 +47,22 @@
     return {Clone(ty.expr)};
 }
 
-const tint::Cloneable* CloneContext::CloneCloneable(const Cloneable* object) {
+const ast::Node* CloneContext::CloneNode(const ast::Node* node) {
     // If the input is nullptr, there's nothing to clone - just return nullptr.
-    if (object == nullptr) {
+    if (node == nullptr) {
         return nullptr;
     }
 
-    // Was Replace() called for this object?
-    if (auto fn = replacements_.Find(object)) {
+    // Was Replace() called for this node?
+    if (auto fn = replacements_.Find(node)) {
         return (*fn)();
     }
 
     // Attempt to clone using the registered replacer functions.
-    auto& typeinfo = object->TypeInfo();
+    auto& typeinfo = node->TypeInfo();
     for (auto& transform : transforms_) {
         if (typeinfo.Is(transform.typeinfo)) {
-            if (auto* transformed = transform.function(object)) {
+            if (auto* transformed = transform.function(node)) {
                 return transformed;
             }
             break;
@@ -94,10 +71,10 @@
 
     // No transform for this type, or the transform returned nullptr.
     // Clone with T::Clone().
-    return object->Clone(this);
+    return node->Clone(*this);
 }
 
-void CloneContext::CheckedCastFailure(const Cloneable* got, const TypeInfo& expected) {
+void CloneContext::CheckedCastFailure(const ast::Node* got, const TypeInfo& expected) {
     TINT_ICE() << "Cloned object was not of the expected type\n"
                << "got:      " << got->TypeInfo().name << "\n"
                << "expected: " << expected.name;
@@ -111,4 +88,4 @@
 CloneContext::CloneableTransform::CloneableTransform(const CloneableTransform&) = default;
 CloneContext::CloneableTransform::~CloneableTransform() = default;
 
-}  // namespace tint
+}  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/clone_context.h b/src/tint/lang/wgsl/ast/clone_context.h
index 92f6030..47f6ebf 100644
--- a/src/tint/lang/wgsl/ast/clone_context.h
+++ b/src/tint/lang/wgsl/ast/clone_context.h
@@ -21,6 +21,7 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/containers/hashset.h"
 #include "src/tint/utils/containers/vector.h"
@@ -34,42 +35,13 @@
 #include "src/tint/utils/traits/traits.h"
 
 // Forward declarations
-namespace tint {
-class CloneContext;
-class Program;
-class ProgramBuilder;
-}  // namespace tint
 namespace tint::ast {
 class FunctionList;
 class Node;
 struct Type;
 }  // namespace tint::ast
 
-namespace tint {
-
-GenerationID GenerationIDOf(const Program*);
-GenerationID GenerationIDOf(const ProgramBuilder*);
-
-/// Cloneable is the base class for all objects that can be cloned
-class Cloneable : public Castable<Cloneable> {
-  public:
-    /// Constructor
-    Cloneable();
-    /// Move constructor
-    Cloneable(Cloneable&&);
-    /// Destructor
-    ~Cloneable() override;
-
-    /// Performs a deep clone of this object using the CloneContext `ctx`.
-    /// @param ctx the clone context
-    /// @return the newly cloned object
-    virtual const Cloneable* Clone(CloneContext* ctx) const = 0;
-};
-
-/// @returns an invalid GenerationID
-inline GenerationID GenerationIDOf(const Cloneable*) {
-    return GenerationID();
-}
+namespace tint::ast {
 
 /// CloneContext holds the state used while cloning AST nodes.
 class CloneContext {
@@ -85,35 +57,28 @@
     using SymbolTransform = std::function<Symbol(Symbol)>;
 
     /// Constructor for cloning objects from `from` into `to`.
-    /// @param to the target ProgramBuilder to clone into
-    /// @param from the source Program to clone from
-    /// @param auto_clone_symbols clone all symbols in `from` before returning
-    CloneContext(ProgramBuilder* to, Program const* from, bool auto_clone_symbols = true);
-
-    /// Constructor for cloning objects from and to the ProgramBuilder `builder`.
-    /// @param builder the ProgramBuilder
-    explicit CloneContext(ProgramBuilder* builder);
+    /// @param to the target Builder to clone into
+    /// @param from the generation ID of the source program being cloned
+    CloneContext(Builder* to, GenerationID from);
 
     /// Destructor
     ~CloneContext();
 
-    /// Clones the Node or type::Type `a` into the ProgramBuilder #dst if `a` is
-    /// not null. If `a` is null, then Clone() returns null.
+    /// Clones the Node or type::Type @p object into the Builder #dst if @p object is not null. If
+    /// @p object is null, then Clone() returns null.
     ///
-    /// Clone() may use a function registered with ReplaceAll() to create a
-    /// transformed version of the object. See ReplaceAll() for more information.
+    /// Clone() may use a function registered with ReplaceAll() to create a transformed version of
+    /// the object. See ReplaceAll() for more information.
     ///
-    /// If the CloneContext is cloning from a Program to a ProgramBuilder, then
-    /// the Node or type::Type `a` must be owned by the Program #src.
+    /// If the CloneContext is cloning from a Program to a Builder, then the Node or type::Type @p
+    /// object must be owned by the source program.
     ///
-    /// @param object the type deriving from Cloneable to clone
+    /// @param object the type deriving from Node to clone
     /// @return the cloned node
     template <typename T>
     const T* Clone(const T* object) {
-        if (src) {
-            TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, object);
-        }
-        if (auto* cloned = CloneCloneable(object)) {
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, object);
+        if (auto* cloned = CloneNode(object)) {
             auto* out = CheckedCast<T>(cloned);
             TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(dst, out);
             return out;
@@ -121,16 +86,16 @@
         return nullptr;
     }
 
-    /// Clones the Node or type::Type `a` into the ProgramBuilder #dst if `a` is
-    /// not null. If `a` is null, then Clone() returns null.
+    /// Clones the Node or type::Type @p object into the Builder #dst if @p object is not null. If
+    /// @p object is null, then Clone() returns null.
     ///
-    /// Unlike Clone(), this method does not invoke or use any transformations
-    /// registered by ReplaceAll().
+    /// Unlike Clone(), this method does not invoke or use any transformations registered by
+    /// ReplaceAll().
     ///
-    /// If the CloneContext is cloning from a Program to a ProgramBuilder, then
-    /// the Node or type::Type `a` must be owned by the Program #src.
+    /// If the CloneContext is cloning from a Program to a Builder, then the Node or type::Type @p
+    /// object must be owned by the source program.
     ///
-    /// @param a the type deriving from Cloneable to clone
+    /// @param a the type deriving from Node to clone
     /// @return the cloned node
     template <typename T>
     const T* CloneWithoutTransform(const T* a) {
@@ -138,14 +103,12 @@
         if (a == nullptr) {
             return nullptr;
         }
-        if (src) {
-            TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, a);
-        }
-        auto* c = a->Clone(this);
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, a);
+        auto* c = a->Clone(*this);
         return CheckedCast<T>(c);
     }
 
-    /// Clones the ast::Type `ty` into the ProgramBuilder #dst
+    /// Clones the ast::Type `ty` into the Builder #dst
     /// @param ty the AST type.
     /// @return the cloned node
     ast::Type Clone(const ast::Type& ty);
@@ -160,16 +123,16 @@
 
     /// Clones the Symbol `s` into #dst
     ///
-    /// The Symbol `s` must be owned by the Program #src.
+    /// The Symbol `s` must be owned by the source program.
     ///
     /// @param s the Symbol to clone
     /// @return the cloned source
     Symbol Clone(Symbol s);
 
-    /// Clones each of the elements of the vector `v` into the ProgramBuilder
+    /// Clones each of the elements of the vector `v` into the Builder
     /// #dst.
     ///
-    /// All the elements of the vector `v` must be owned by the Program #src.
+    /// All the elements of the vector `v` must be owned by the source program.
     ///
     /// @param v the vector to clone
     /// @return the cloned vector
@@ -183,11 +146,11 @@
         return out;
     }
 
-    /// Clones each of the elements of the vector `v` using the ProgramBuilder
+    /// Clones each of the elements of the vector `v` using the Builder
     /// #dst, inserting any additional elements into the list that were registered
     /// with calls to InsertBefore().
     ///
-    /// All the elements of the vector `v` must be owned by the Program #src.
+    /// All the elements of the vector `v` must be owned by the source program.
     ///
     /// @param v the vector to clone
     /// @return the cloned vector
@@ -202,7 +165,7 @@
     /// inserting any additional elements into the list that were registered with
     /// calls to InsertBefore().
     ///
-    /// All the elements of the vector `from` must be owned by the Program #src.
+    /// All the elements of the vector `from` must be owned by the source program.
     ///
     /// @param from the vector to clone
     /// @param to the cloned result
@@ -259,24 +222,24 @@
         }
     }
 
-    /// Clones each of the elements of the vector `v` into the ProgramBuilder
+    /// Clones each of the elements of the vector `v` into the Builder
     /// #dst.
     ///
-    /// All the elements of the vector `v` must be owned by the Program #src.
+    /// All the elements of the vector `v` must be owned by the source program.
     ///
     /// @param v the vector to clone
     /// @return the cloned vector
     ast::FunctionList Clone(const ast::FunctionList& v);
 
     /// ReplaceAll() registers `replacer` to be called whenever the Clone() method
-    /// is called with a Cloneable type that matches (or derives from) the type of
+    /// is called with a Node type that matches (or derives from) the type of
     /// the single parameter of `replacer`.
-    /// The returned Cloneable of `replacer` will be used as the replacement for
-    /// all references to the object that's being cloned. This returned Cloneable
+    /// The returned Node of `replacer` will be used as the replacement for
+    /// all references to the object that's being cloned. This returned Node
     /// must be owned by the Program #dst.
     ///
     /// `replacer` must be function-like with the signature: `T* (T*)`
-    ///  where `T` is a type deriving from Cloneable.
+    ///  where `T` is a type deriving from Node.
     ///
     /// If `replacer` returns a nullptr then Clone() will call `T::Clone()` to
     /// clone the object.
@@ -287,9 +250,9 @@
     ///   // Replace all ast::UintLiteralExpressions with the number 42
     ///   CloneCtx ctx(&out, in);
     ///   ctx.ReplaceAll([&] (ast::UintLiteralExpression* l) {
-    ///       return ctx->dst->create<ast::UintLiteralExpression>(
-    ///           ctx->Clone(l->source),
-    ///           ctx->Clone(l->type),
+    ///       return ctx.dst->create<ast::UintLiteralExpression>(
+    ///           ctx.Clone(l->source),
+    ///           ctx.Clone(l->type),
     ///           42);
     ///     });
     ///   ctx.Clone();
@@ -302,11 +265,11 @@
     /// references of the original object. A type mismatch will result in an
     /// assertion in debug builds, and undefined behavior in release builds.
     /// @param replacer a function or function-like object with the signature
-    ///        `T* (T*)`, where `T` derives from Cloneable
+    ///        `T* (T*)`, where `T` derives from Node
     /// @returns this CloneContext so calls can be chained
     template <typename F>
-    tint::traits::EnableIf<ParamTypeIsPtrOf<F, Cloneable>, CloneContext>& ReplaceAll(F&& replacer) {
-        using TPtr = tint::traits::ParameterType<F, 0>;
+    traits::EnableIf<ParamTypeIsPtrOf<F, ast::Node>, CloneContext>& ReplaceAll(F&& replacer) {
+        using TPtr = traits::ParameterType<F, 0>;
         using T = typename std::remove_pointer<TPtr>::type;
         for (auto& transform : transforms_) {
             bool already_registered = transform.typeinfo->Is(&tint::TypeInfo::Of<T>()) ||
@@ -320,8 +283,8 @@
             }
         }
         CloneableTransform transform;
-        transform.typeinfo = &tint::TypeInfo::Of<T>();
-        transform.function = [=](const Cloneable* in) { return replacer(in->As<T>()); };
+        transform.typeinfo = &TypeInfo::Of<T>();
+        transform.function = [=](const ast::Node* in) { return replacer(in->As<T>()); };
         transforms_.Push(std::move(transform));
         return *this;
     }
@@ -345,11 +308,11 @@
         return *this;
     }
 
-    /// Replace replaces all occurrences of `what` in #src with the pointer `with`
+    /// Replace replaces all occurrences of `what` in the source program with the pointer `with`
     /// in #dst when calling Clone().
     /// [DEPRECATED]: This function cannot handle nested replacements. Use the
     /// overload of Replace() that take a function for the `WITH` argument.
-    /// @param what a pointer to the object in #src that will be replaced with
+    /// @param what a pointer to the object in the source program that will be replaced with
     /// `with`
     /// @param with a pointer to the replacement object owned by #dst that will be
     /// used as a replacement for `what`
@@ -357,21 +320,19 @@
     /// references of the original object. A type mismatch will result in an
     /// assertion in debug builds, and undefined behavior in release builds.
     /// @returns this CloneContext so calls can be chained
-    template <typename WHAT,
-              typename WITH,
-              typename = tint::traits::EnableIfIsType<WITH, Cloneable>>
+    template <typename WHAT, typename WITH, typename = traits::EnableIfIsType<WITH, ast::Node>>
     CloneContext& Replace(const WHAT* what, const WITH* with) {
-        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, what);
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, what);
         TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(dst, with);
-        replacements_.Replace(what, [with]() -> const Cloneable* { return with; });
+        replacements_.Replace(what, [with]() -> const ast::Node* { return with; });
         return *this;
     }
 
-    /// Replace replaces all occurrences of `what` in #src with the result of the
+    /// Replace replaces all occurrences of `what` in the source program with the result of the
     /// function `with` in #dst when calling Clone(). `with` will be called each
     /// time `what` is cloned by this context. If `what` is not cloned, then
     /// `with` may never be called.
-    /// @param what a pointer to the object in #src that will be replaced with
+    /// @param what a pointer to the object in the source program that will be replaced with
     /// `with`
     /// @param with a function that takes no arguments and returns a pointer to
     /// the replacement object owned by #dst. The returned pointer will be used as
@@ -382,19 +343,19 @@
     /// @returns this CloneContext so calls can be chained
     template <typename WHAT, typename WITH, typename = std::invoke_result_t<WITH>>
     CloneContext& Replace(const WHAT* what, WITH&& with) {
-        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, what);
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, what);
         replacements_.Replace(what, with);
         return *this;
     }
 
     /// Removes @p object from the cloned copy of @p vector.
-    /// @param vector the vector in #src
-    /// @param object a pointer to the object in #src that will be omitted from
+    /// @param vector the vector in the source program
+    /// @param object a pointer to the object in the source program that will be omitted from
     /// the cloned vector.
     /// @returns this CloneContext so calls can be chained
     template <typename T, size_t N, typename OBJECT>
     CloneContext& Remove(const Vector<T, N>& vector, OBJECT* object) {
-        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, object);
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, object);
         if (TINT_UNLIKELY((std::find(vector.begin(), vector.end(), object) == vector.end()))) {
             TINT_ICE() << "CloneContext::Remove() vector does not contain object";
             return *this;
@@ -405,7 +366,7 @@
     }
 
     /// Inserts @p object before any other objects of @p vector, when the vector is cloned.
-    /// @param vector the vector in #src
+    /// @param vector the vector in the source program
     /// @param object a pointer to the object in #dst that will be inserted at the
     /// front of the vector
     /// @returns this CloneContext so calls can be chained
@@ -417,7 +378,7 @@
 
     /// Inserts a lazily built object before any other objects of @p vector, when the vector is
     /// cloned.
-    /// @param vector the vector in #src
+    /// @param vector the vector in the source program
     /// @param builder a builder of the object that will be inserted at the front of the vector.
     /// @returns this CloneContext so calls can be chained
     template <typename T, size_t N, typename BUILDER>
@@ -427,7 +388,7 @@
     }
 
     /// Inserts @p object after any other objects of @p vector, when the vector is cloned.
-    /// @param vector the vector in #src
+    /// @param vector the vector in the source program
     /// @param object a pointer to the object in #dst that will be inserted at the
     /// end of the vector
     /// @returns this CloneContext so calls can be chained
@@ -439,7 +400,7 @@
 
     /// Inserts a lazily built object after any other objects of @p vector, when the vector is
     /// cloned.
-    /// @param vector the vector in #src
+    /// @param vector the vector in the source program
     /// @param builder the builder of the object in #dst that will be inserted at the end of the
     /// vector.
     /// @returns this CloneContext so calls can be chained
@@ -450,8 +411,8 @@
     }
 
     /// Inserts @p object before @p before whenever @p vector is cloned.
-    /// @param vector the vector in #src
-    /// @param before a pointer to the object in #src
+    /// @param vector the vector in the source program
+    /// @param before a pointer to the object in the source program
     /// @param object a pointer to the object in #dst that will be inserted before
     /// any occurrence of the clone of @p before
     /// @returns this CloneContext so calls can be chained
@@ -459,7 +420,7 @@
     CloneContext& InsertBefore(const tint::Vector<T, N>& vector,
                                const BEFORE* before,
                                const OBJECT* object) {
-        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, before);
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, before);
         TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(dst, object);
         if (TINT_UNLIKELY((std::find(vector.begin(), vector.end(), before) == vector.end()))) {
             TINT_ICE() << "CloneContext::InsertBefore() vector does not contain before";
@@ -472,8 +433,8 @@
     }
 
     /// Inserts a lazily created object before @p before whenever @p vector is cloned.
-    /// @param vector the vector in #src
-    /// @param before a pointer to the object in #src
+    /// @param vector the vector in the source program
+    /// @param before a pointer to the object in the source program
     /// @param builder the builder of the object in #dst that will be inserted before any occurrence
     /// of the clone of @p before
     /// @returns this CloneContext so calls can be chained
@@ -491,8 +452,8 @@
     }
 
     /// Inserts @p object after @p after whenever @p vector is cloned.
-    /// @param vector the vector in #src
-    /// @param after a pointer to the object in #src
+    /// @param vector the vector in the source program
+    /// @param after a pointer to the object in the source program
     /// @param object a pointer to the object in #dst that will be inserted after
     /// any occurrence of the clone of @p after
     /// @returns this CloneContext so calls can be chained
@@ -500,7 +461,7 @@
     CloneContext& InsertAfter(const tint::Vector<T, N>& vector,
                               const AFTER* after,
                               const OBJECT* object) {
-        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, after);
+        TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, after);
         TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(dst, object);
         if (TINT_UNLIKELY((std::find(vector.begin(), vector.end(), after) == vector.end()))) {
             TINT_ICE() << "CloneContext::InsertAfter() vector does not contain after";
@@ -513,8 +474,8 @@
     }
 
     /// Inserts a lazily created object after @p after whenever @p vector is cloned.
-    /// @param vector the vector in #src
-    /// @param after a pointer to the object in #src
+    /// @param vector the vector in the source program
+    /// @param after a pointer to the object in the source program
     /// @param builder the builder of the object in #dst that will be inserted after any occurrence
     /// of the clone of @p after
     /// @returns this CloneContext so calls can be chained
@@ -531,16 +492,11 @@
         return *this;
     }
 
-    /// Clone performs the clone of the Program's AST nodes, types and symbols
-    /// from #src to #dst. Semantic nodes are not cloned, as these will be rebuilt
-    /// when the ProgramBuilder #dst builds its Program.
-    void Clone();
+    /// The target Builder to clone into.
+    Builder* const dst;
 
-    /// The target ProgramBuilder to clone into.
-    ProgramBuilder* const dst;
-
-    /// The source Program to clone from.
-    Program const* const src;
+    /// The source Program generation identifier.
+    GenerationID const src_id;
 
   private:
     struct CloneableTransform {
@@ -552,34 +508,34 @@
         /// Destructor
         ~CloneableTransform();
 
-        // tint::TypeInfo of the Cloneable that the transform operates on
-        const tint::TypeInfo* typeinfo;
-        std::function<const Cloneable*(const Cloneable*)> function;
+        // TypeInfo of the Node that the transform operates on
+        const TypeInfo* typeinfo;
+        std::function<const ast::Node*(const ast::Node*)> function;
     };
 
-    /// A vector of const Cloneable*
-    using CloneableBuilderList = tint::Vector<std::function<const Cloneable*()>, 4>;
+    /// A vector of const ast::Node*
+    using NodeBuilderList = Vector<std::function<const ast::Node*()>, 4>;
 
     /// Transformations to be applied to a list (vector)
     struct ListTransforms {
-        /// A map of object in #src to omit when cloned into #dst.
-        Hashset<const Cloneable*, 4> remove_;
+        /// A map of object in the source program to omit when cloned into #dst.
+        Hashset<const ast::Node*, 4> remove_;
 
         /// A list of objects in #dst to insert before any others when the vector is cloned.
-        CloneableBuilderList insert_front_;
+        NodeBuilderList insert_front_;
 
         /// A list of objects in #dst to insert after all others when the vector is cloned.
-        CloneableBuilderList insert_back_;
+        NodeBuilderList insert_back_;
 
-        /// A map of object in #src to the list of cloned objects in #dst.
-        /// Clone(const tint::Vector<T*>& v) will use this to insert the map-value
+        /// A map of object in the source program to the list of cloned objects in #dst.
+        /// Clone(const Vector<T*>& v) will use this to insert the map-value
         /// list into the target vector before cloning and inserting the map-key.
-        Hashmap<const Cloneable*, CloneableBuilderList, 4> insert_before_;
+        Hashmap<const ast::Node*, NodeBuilderList, 4> insert_before_;
 
-        /// A map of object in #src to the list of cloned objects in #dst.
-        /// Clone(const tint::Vector<T*>& v) will use this to insert the map-value
+        /// A map of object in the source program to the list of cloned objects in #dst.
+        /// Clone(const Vector<T*>& v) will use this to insert the map-value
         /// list into the target vector after cloning and inserting the map-key.
-        Hashmap<const Cloneable*, CloneableBuilderList, 4> insert_after_;
+        Hashmap<const ast::Node*, NodeBuilderList, 4> insert_after_;
     };
 
     CloneContext(const CloneContext&) = delete;
@@ -600,25 +556,25 @@
         return nullptr;
     }
 
-    /// Clones a Cloneable object, using any replacements or transforms that have
+    /// Clones a Node object, using any replacements or transforms that have
     /// been configured.
-    const Cloneable* CloneCloneable(const Cloneable* object);
+    const ast::Node* CloneNode(const ast::Node* object);
 
     /// Adds an error diagnostic to Diagnostics() that the cloned object was not
     /// of the expected type.
-    void CheckedCastFailure(const Cloneable* got, const tint::TypeInfo& expected);
+    void CheckedCastFailure(const ast::Node* got, const TypeInfo& expected);
 
     /// @returns the diagnostic list of #dst
     diag::List& Diagnostics() const;
 
-    /// A map of object in #src to functions that create their replacement in #dst
-    Hashmap<const Cloneable*, std::function<const Cloneable*()>, 8> replacements_;
+    /// A map of object in the source program to functions that create their replacement in #dst
+    Hashmap<const ast::Node*, std::function<const ast::Node*()>, 8> replacements_;
 
-    /// A map of symbol in #src to their cloned equivalent in #dst
+    /// A map of symbol in the source program to their cloned equivalent in #dst
     Hashmap<Symbol, Symbol, 32> cloned_symbols_;
 
-    /// Cloneable transform functions registered with ReplaceAll()
-    tint::Vector<CloneableTransform, 8> transforms_;
+    /// Node transform functions registered with ReplaceAll()
+    Vector<CloneableTransform, 8> transforms_;
 
     /// Transformations to apply to vectors
     Hashmap<const void*, ListTransforms, 4> list_transforms_;
@@ -627,6 +583,6 @@
     SymbolTransform symbol_transform_;
 };
 
-}  // namespace tint
+}  // namespace tint::ast
 
 #endif  // SRC_TINT_LANG_WGSL_AST_CLONE_CONTEXT_H_
diff --git a/src/tint/lang/wgsl/ast/clone_context_test.cc b/src/tint/lang/wgsl/ast/clone_context_test.cc
index 17d6e5f..9d0e337 100644
--- a/src/tint/lang/wgsl/ast/clone_context_test.cc
+++ b/src/tint/lang/wgsl/ast/clone_context_test.cc
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <string>
 #include <unordered_set>
 
 #include "gtest/gtest-spi.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
-namespace tint {
+namespace tint::ast {
 namespace {
 
 struct Allocator {
@@ -27,86 +29,79 @@
     }
 
   private:
-    BlockAllocator<Cloneable> alloc;
+    BlockAllocator<ast::Node> alloc;
 };
 
-struct Node : public Castable<Node, Cloneable> {
-    Node(Allocator* alloc,
-         Symbol n,
-         const Node* node_a = nullptr,
-         const Node* node_b = nullptr,
-         const Node* node_c = nullptr)
-        : allocator(alloc), name(n), a(node_a), b(node_b), c(node_c) {}
-    Node(Node&&) = delete;
+struct TestNode : public Castable<TestNode, ast::Node> {
+    TestNode(Allocator* alloc,
+             Symbol n,
+             const TestNode* Testnode_a = nullptr,
+             const TestNode* Testnode_b = nullptr,
+             const TestNode* Testnode_c = nullptr)
+        : Base(GenerationID{}, ast::NodeID{}, Source{}),
+          allocator(alloc),
+          name(n),
+          a(Testnode_a),
+          b(Testnode_b),
+          c(Testnode_c) {}
+    TestNode(TestNode&&) = delete;
     Allocator* const allocator;
     Symbol name;
-    const Node* a = nullptr;
-    const Node* b = nullptr;
-    const Node* c = nullptr;
-    tint::Vector<const Node*, 8> vec;
+    const TestNode* a = nullptr;
+    const TestNode* b = nullptr;
+    const TestNode* c = nullptr;
+    Vector<const TestNode*, 8> vec;
 
-    Node* Clone(CloneContext* ctx) const override {
-        auto* out = allocator->Create<Node>(ctx->Clone(name));
-        out->a = ctx->Clone(a);
-        out->b = ctx->Clone(b);
-        out->c = ctx->Clone(c);
-        out->vec = ctx->Clone(vec);
+    TestNode* Clone(ast::CloneContext& ctx) const override {
+        auto* out = allocator->Create<TestNode>(ctx.Clone(name));
+        out->a = ctx.Clone(a);
+        out->b = ctx.Clone(b);
+        out->c = ctx.Clone(c);
+        out->vec = ctx.Clone(vec);
         return out;
     }
 };
 
-struct Replaceable : public Castable<Replaceable, Node> {
+struct Replaceable : public Castable<Replaceable, TestNode> {
     Replaceable(Allocator* alloc,
                 Symbol n,
-                const Node* node_a = nullptr,
-                const Node* node_b = nullptr,
-                const Node* node_c = nullptr)
-        : Base(alloc, n, node_a, node_b, node_c) {}
+                const TestNode* Testnode_a = nullptr,
+                const TestNode* Testnode_b = nullptr,
+                const TestNode* Testnode_c = nullptr)
+        : Base(alloc, n, Testnode_a, Testnode_b, Testnode_c) {}
 };
 
 struct Replacement : public Castable<Replacement, Replaceable> {
     Replacement(Allocator* alloc, Symbol n) : Base(alloc, n) {}
 };
 
-struct NotANode : public Castable<NotANode, Cloneable> {
-    explicit NotANode(Allocator* alloc) : allocator(alloc) {}
+struct IDTestNode : public Castable<IDTestNode, ast::Node> {
+    IDTestNode(Allocator* alloc, GenerationID program_id, GenerationID cid)
+        : Base(program_id, ast::NodeID{}, Source{}), allocator(alloc), cloned_id(cid) {}
 
     Allocator* const allocator;
-    NotANode* Clone(CloneContext*) const override { return allocator->Create<NotANode>(); }
-};
+    const GenerationID cloned_id;
 
-struct ProgramNode : public Castable<ProgramNode, Cloneable> {
-    ProgramNode(Allocator* alloc, GenerationID id, GenerationID cloned_id)
-        : allocator(alloc), generation_id(id), cloned_generation_id(cloned_id) {}
-
-    Allocator* const allocator;
-    const GenerationID generation_id;
-    const GenerationID cloned_generation_id;
-
-    ProgramNode* Clone(CloneContext*) const override {
-        return allocator->Create<ProgramNode>(cloned_generation_id, cloned_generation_id);
+    IDTestNode* Clone(ast::CloneContext&) const override {
+        return allocator->Create<IDTestNode>(cloned_id, cloned_id);
     }
 };
 
-GenerationID GenerationIDOf(const ProgramNode* node) {
-    return node->generation_id;
-}
+using ASTCloneContextTestNodeTest = ::testing::Test;
 
-using CloneContextNodeTest = ::testing::Test;
-
-TEST_F(CloneContextNodeTest, Clone) {
+TEST_F(ASTCloneContextTestNodeTest, Clone) {
     Allocator alloc;
 
     ProgramBuilder builder;
-    Node* original_root;
+    TestNode* original_root;
     {
-        auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
-        auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+        auto* a_b = alloc.Create<TestNode>(builder.Symbols().New("a->b"));
+        auto* a = alloc.Create<TestNode>(builder.Symbols().New("a"), nullptr, a_b);
         auto* b_a = a;  // Aliased
-        auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
-        auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+        auto* b_b = alloc.Create<TestNode>(builder.Symbols().New("b->b"));
+        auto* b = alloc.Create<TestNode>(builder.Symbols().New("b"), b_a, b_b);
         auto* c = b;  // Aliased
-        original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+        original_root = alloc.Create<TestNode>(builder.Symbols().New("root"), a, b, c);
     }
     Program original(std::move(builder));
 
@@ -118,10 +113,10 @@
     //  (a)  (b)  (c)   │  (a)  (b)  (c)
     //        N         └───┘    N
     //
-    // N: Node
+    // N: TestNode
 
     ProgramBuilder cloned;
-    auto* cloned_root = CloneContext(&cloned, &original).Clone(original_root);
+    auto* cloned_root = CloneContext(&cloned, original.ID()).Clone(original_root);
 
     EXPECT_NE(cloned_root->a, nullptr);
     EXPECT_EQ(cloned_root->a->a, nullptr);
@@ -153,18 +148,18 @@
     EXPECT_EQ(cloned_root->c->name, cloned_root->b->name);
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplaceAll_Cloneable) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplaceAll_TestNode) {
     Allocator alloc;
 
     ProgramBuilder builder;
-    Node* original_root;
+    TestNode* original_root;
     {
         auto* a_b = alloc.Create<Replaceable>(builder.Symbols().New("a->b"));
-        auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+        auto* a = alloc.Create<TestNode>(builder.Symbols().New("a"), nullptr, a_b);
         auto* b_a = a;  // Aliased
         auto* b = alloc.Create<Replaceable>(builder.Symbols().New("b"), b_a, nullptr);
         auto* c = b;  // Aliased
-        original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+        original_root = alloc.Create<TestNode>(builder.Symbols().New("root"), a, b, c);
     }
     Program original(std::move(builder));
 
@@ -176,17 +171,17 @@
     //  (a)  (b)  (c)   │  (a)  (b)  (c)
     //        R         └───┘
     //
-    // N: Node
+    // N: TestNode
     // R: Replaceable
 
     ProgramBuilder cloned;
 
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
     ctx.ReplaceAll([&](const Replaceable* in) {
         auto out_name = cloned.Symbols().Register("replacement:" + in->name.Name());
         auto b_name = cloned.Symbols().Register("replacement-child:" + in->name.Name());
         auto* out = alloc.Create<Replacement>(out_name);
-        out->b = alloc.Create<Node>(b_name);
+        out->b = alloc.Create<TestNode>(b_name);
         out->c = ctx.Clone(in->a);
         return out;
     });
@@ -203,7 +198,7 @@
     //  (a)  (b)  (c)
     //        N
     //
-    // N: Node
+    // N: TestNode
     // R: Replacement
 
     EXPECT_NE(cloned_root->a, nullptr);
@@ -245,19 +240,19 @@
     EXPECT_FALSE(Is<Replacement>(cloned_root->b->b));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplaceAll_Symbols) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplaceAll_Symbols) {
     Allocator alloc;
 
     ProgramBuilder builder;
-    Node* original_root;
+    TestNode* original_root;
     {
-        auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
-        auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+        auto* a_b = alloc.Create<TestNode>(builder.Symbols().New("a->b"));
+        auto* a = alloc.Create<TestNode>(builder.Symbols().New("a"), nullptr, a_b);
         auto* b_a = a;  // Aliased
-        auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
-        auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+        auto* b_b = alloc.Create<TestNode>(builder.Symbols().New("b->b"));
+        auto* b = alloc.Create<TestNode>(builder.Symbols().New("b"), b_a, b_b);
         auto* c = b;  // Aliased
-        original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+        original_root = alloc.Create<TestNode>(builder.Symbols().New("root"), a, b, c);
     }
     Program original(std::move(builder));
 
@@ -269,10 +264,10 @@
     //  (a)  (b)  (c)   │  (a)  (b)  (c)
     //        N         └───┘    N
     //
-    // N: Node
+    // N: TestNode
 
     ProgramBuilder cloned;
-    auto* cloned_root = CloneContext(&cloned, &original, false)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .ReplaceAll([&](Symbol sym) {
                                 auto in = sym.Name();
                                 auto out = "transformed<" + in + ">";
@@ -287,32 +282,32 @@
     EXPECT_EQ(cloned_root->b->b->name, cloned.Symbols().Get("transformed<b->b>"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithoutTransform) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithoutTransform) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_node = a.Create<Node>(builder.Symbols().New("root"));
+    auto* original_Testnode = a.Create<TestNode>(builder.Symbols().New("root"));
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original);
-    ctx.ReplaceAll([&](const Node*) {
-        return a.Create<Replacement>(builder.Symbols().New("<unexpected-node>"));
+    CloneContext ctx(&cloned, original.ID());
+    ctx.ReplaceAll([&](const TestNode*) {
+        return a.Create<Replacement>(builder.Symbols().New("<unexpected-Testnode>"));
     });
 
-    auto* cloned_node = ctx.CloneWithoutTransform(original_node);
-    EXPECT_NE(cloned_node, original_node);
-    EXPECT_EQ(cloned_node->name, cloned.Symbols().Get("root"));
+    auto* cloned_Testnode = ctx.CloneWithoutTransform(original_Testnode);
+    EXPECT_NE(cloned_Testnode, original_Testnode);
+    EXPECT_EQ(cloned_Testnode->name, cloned.Symbols().Get("root"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplacePointer) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplacePointer) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
-    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().New("root"));
+    original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
+    original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
+    original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
     Program original(std::move(builder));
 
     //                          root
@@ -321,9 +316,9 @@
     //                        Replaced
 
     ProgramBuilder cloned;
-    auto* replacement = a.Create<Node>(cloned.Symbols().New("replacement"));
+    auto* replacement = a.Create<TestNode>(cloned.Symbols().New("replacement"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .Replace(original_root->b, replacement)
                             .Clone(original_root);
 
@@ -337,33 +332,33 @@
     EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithRepeatedImmediateReplacePointer) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithRepeatedImmediateReplacePointer) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
-    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().New("root"));
+    original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
+    original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
+    original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
 
     // Demonstrate that ctx.Replace() can be called multiple times to update the replacement of a
-    // node.
+    // Testnode.
 
     auto* replacement_x =
-        a.Create<Node>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
+        a.Create<TestNode>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
     ctx.Replace(original_root->b, replacement_x);
 
     auto* replacement_y =
-        a.Create<Node>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
+        a.Create<TestNode>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
     ctx.Replace(original_root->b, replacement_y);
 
     auto* replacement_z =
-        a.Create<Node>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
+        a.Create<TestNode>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
     ctx.Replace(original_root->b, replacement_z);
 
     auto* cloned_root = ctx.Clone(original_root);
@@ -376,14 +371,14 @@
     EXPECT_EQ(replacement_y->a, replacement_x);
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplaceFunction) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplaceFunction) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
-    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().New("root"));
+    original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
+    original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
+    original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
     Program original(std::move(builder));
 
     //                          root
@@ -392,9 +387,9 @@
     //                        Replaced
 
     ProgramBuilder cloned;
-    auto* replacement = a.Create<Node>(cloned.Symbols().New("replacement"));
+    auto* replacement = a.Create<TestNode>(cloned.Symbols().New("replacement"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .Replace(original_root->b, [=] { return replacement; })
                             .Clone(original_root);
 
@@ -408,33 +403,33 @@
     EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithRepeatedImmediateReplaceFunction) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithRepeatedImmediateReplaceFunction) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
-    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
-    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
-    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().New("root"));
+    original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
+    original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
+    original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
 
     // Demonstrate that ctx.Replace() can be called multiple times to update the replacement of a
-    // node.
+    // Testnode.
 
-    Node* replacement_x =
-        a.Create<Node>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
+    TestNode* replacement_x =
+        a.Create<TestNode>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
     ctx.Replace(original_root->b, [&] { return replacement_x; });
 
-    Node* replacement_y =
-        a.Create<Node>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
+    TestNode* replacement_y =
+        a.Create<TestNode>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
     ctx.Replace(original_root->b, [&] { return replacement_y; });
 
-    Node* replacement_z =
-        a.Create<Node>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
+    TestNode* replacement_z =
+        a.Create<TestNode>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
     ctx.Replace(original_root->b, [&] { return replacement_z; });
 
     auto* cloned_root = ctx.Clone(original_root);
@@ -447,20 +442,20 @@
     EXPECT_EQ(replacement_y->a, replacement_x);
 }
 
-TEST_F(CloneContextNodeTest, CloneWithRemove) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithRemove) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .Remove(original_root->vec, original_root->vec[1])
                             .Clone(original_root);
 
@@ -474,22 +469,22 @@
     EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertFront) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertFront) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertFront(original_root->vec, insertion)
                             .Clone(original_root);
 
@@ -506,24 +501,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertFrontFunction) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertFrontFunction) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertFront(original_root->vec,
-                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+                         [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 4u);
@@ -539,18 +534,18 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertFront_Empty) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertFront_Empty) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertFront(original_root->vec, insertion)
                             .Clone(original_root);
 
@@ -560,20 +555,20 @@
     EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertFront_Empty_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertFront_Empty_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertFront(original_root->vec,
-                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+                         [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 1u);
@@ -582,22 +577,22 @@
     EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBack) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBack) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertBack(original_root->vec, insertion)
                             .Clone(original_root);
 
@@ -610,24 +605,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBack_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBack_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertBack(original_root->vec,
-                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+                        [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 4u);
@@ -639,18 +634,18 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBack_Empty) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBack_Empty) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertBack(original_root->vec, insertion)
                             .Clone(original_root);
 
@@ -660,20 +655,20 @@
     EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBack_Empty_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBack_Empty_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertBack(original_root->vec,
-                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+                        [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 1u);
@@ -682,19 +677,19 @@
     EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertFrontAndBack_Empty) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertFrontAndBack_Empty) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion_back = a.Create<Node>(cloned.Symbols().New("insertion_back"));
-    auto* insertion_front = a.Create<Node>(cloned.Symbols().New("insertion_front"));
+    auto* insertion_back = a.Create<TestNode>(cloned.Symbols().New("insertion_back"));
+    auto* insertion_front = a.Create<TestNode>(cloned.Symbols().New("insertion_front"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertBack(original_root->vec, insertion_back)
                             .InsertFront(original_root->vec, insertion_front)
                             .Clone(original_root);
@@ -706,22 +701,23 @@
     EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_back"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertFrontAndBack_Empty_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertFrontAndBack_Empty_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertBack(original_root->vec,
-                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion_back")); })
-            .InsertFront(original_root->vec,
-                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion_front")); })
+                        [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion_back")); })
+            .InsertFront(
+                original_root->vec,
+                [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion_front")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 2u);
@@ -731,22 +727,22 @@
     EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_back"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBefore) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBefore) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertBefore(original_root->vec, original_root->vec[1], insertion)
                             .Clone(original_root);
 
@@ -759,24 +755,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBefore_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBefore_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertBefore(original_root->vec, original_root->vec[1],
-                          [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+                          [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 4u);
@@ -788,22 +784,22 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertAfter) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertAfter) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
 
-    auto* cloned_root = CloneContext(&cloned, &original)
+    auto* cloned_root = CloneContext(&cloned, original.ID())
                             .InsertAfter(original_root->vec, original_root->vec[1], insertion)
                             .Clone(original_root);
 
@@ -816,24 +812,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertAfter_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertAfter_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertAfter(original_root->vec, original_root->vec[1],
-                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+                         [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); })
             .Clone(original_root);
 
     EXPECT_EQ(cloned_root->vec.Length(), 4u);
@@ -845,23 +841,23 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertAfterInVectorNodeClone) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertAfterInVectorNodeClone) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
         a.Create<Replaceable>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
     ctx.ReplaceAll([&](const Replaceable* r) {
-        auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+        auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
         ctx.InsertAfter(original_root->vec, r, insertion);
         return nullptr;
     });
@@ -877,24 +873,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertAfterInVectorNodeClone_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertAfterInVectorNodeClone_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
         a.Create<Replaceable>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
     ctx.ReplaceAll([&](const Replaceable* r) {
         ctx.InsertAfter(original_root->vec, r,
-                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); });
+                        [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); });
         return nullptr;
     });
 
@@ -909,23 +905,23 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBackInVectorNodeClone) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBackInVectorNodeClone) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
         a.Create<Replaceable>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
     ctx.ReplaceAll([&](const Replaceable* /*r*/) {
-        auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+        auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
         ctx.InsertBack(original_root->vec, insertion);
         return nullptr;
     });
@@ -941,24 +937,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBackInVectorNodeClone_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBackInVectorNodeClone_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
         a.Create<Replaceable>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original);
+    CloneContext ctx(&cloned, original.ID());
     ctx.ReplaceAll([&](const Replaceable* /*r*/) {
         ctx.InsertBack(original_root->vec,
-                       [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); });
+                       [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion")); });
         return nullptr;
     });
 
@@ -973,24 +969,24 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBeforeAndAfterRemoved) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBeforeAndAfterRemoved) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    auto* insertion_before = a.Create<Node>(cloned.Symbols().New("insertion_before"));
-    auto* insertion_after = a.Create<Node>(cloned.Symbols().New("insertion_after"));
+    auto* insertion_before = a.Create<TestNode>(cloned.Symbols().New("insertion_before"));
+    auto* insertion_after = a.Create<TestNode>(cloned.Symbols().New("insertion_after"));
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
+        CloneContext(&cloned, original.ID())
             .InsertBefore(original_root->vec, original_root->vec[1], insertion_before)
             .InsertAfter(original_root->vec, original_root->vec[1], insertion_after)
             .Remove(original_root->vec, original_root->vec[1])
@@ -1005,26 +1001,28 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneWithInsertBeforeAndAfterRemoved_Function) {
+TEST_F(ASTCloneContextTestNodeTest, CloneWithInsertBeforeAndAfterRemoved_Function) {
     Allocator a;
 
     ProgramBuilder builder;
-    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec = {
-        a.Create<Node>(builder.Symbols().Register("a")),
-        a.Create<Node>(builder.Symbols().Register("b")),
-        a.Create<Node>(builder.Symbols().Register("c")),
+        a.Create<TestNode>(builder.Symbols().Register("a")),
+        a.Create<TestNode>(builder.Symbols().Register("b")),
+        a.Create<TestNode>(builder.Symbols().Register("c")),
     };
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
 
     auto* cloned_root =
-        CloneContext(&cloned, &original)
-            .InsertBefore(original_root->vec, original_root->vec[1],
-                          [&] { return a.Create<Node>(cloned.Symbols().New("insertion_before")); })
-            .InsertAfter(original_root->vec, original_root->vec[1],
-                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion_after")); })
+        CloneContext(&cloned, original.ID())
+            .InsertBefore(
+                original_root->vec, original_root->vec[1],
+                [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion_before")); })
+            .InsertAfter(
+                original_root->vec, original_root->vec[1],
+                [&] { return a.Create<TestNode>(cloned.Symbols().New("insertion_after")); })
             .Remove(original_root->vec, original_root->vec[1])
             .Clone(original_root);
 
@@ -1037,128 +1035,61 @@
     EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
 }
 
-TEST_F(CloneContextNodeTest, CloneIntoSameBuilder) {
-    ProgramBuilder builder;
-    CloneContext ctx(&builder);
-    Allocator allocator;
-    auto* original = allocator.Create<Node>(builder.Symbols().New());
-    auto* cloned_a = ctx.Clone(original);
-    auto* cloned_b = ctx.Clone(original);
-    EXPECT_NE(original, cloned_a);
-    EXPECT_NE(original, cloned_b);
-
-    EXPECT_NE(cloned_a, cloned_b);
-}
-
-TEST_F(CloneContextNodeTest, CloneWithReplaceAll_SameTypeTwice) {
-    std::string node_name = tint::TypeInfo::Of<Node>().name;
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplaceAll_SameTypeTwice) {
+    std::string Testnode_name = TypeInfo::Of<TestNode>().name;
 
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder cloned;
             Program original;
-            CloneContext ctx(&cloned, &original);
-            ctx.ReplaceAll([](const Node*) { return nullptr; });
-            ctx.ReplaceAll([](const Node*) { return nullptr; });
+            CloneContext ctx(&cloned, original.ID());
+            ctx.ReplaceAll([](const TestNode*) { return nullptr; });
+            ctx.ReplaceAll([](const TestNode*) { return nullptr; });
         },
-        "internal compiler error: ReplaceAll() called with a handler for type " + node_name +
-            " that is already handled by a handler for type " + node_name);
+        "internal compiler error: ReplaceAll() called with a handler for type " + Testnode_name +
+            " that is already handled by a handler for type " + Testnode_name);
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplaceAll_BaseThenDerived) {
-    std::string node_name = tint::TypeInfo::Of<Node>().name;
-    std::string replaceable_name = tint::TypeInfo::Of<Replaceable>().name;
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplaceAll_BaseThenDerived) {
+    std::string Testnode_name = TypeInfo::Of<TestNode>().name;
+    std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
 
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder cloned;
             Program original;
-            CloneContext ctx(&cloned, &original);
-            ctx.ReplaceAll([](const Node*) { return nullptr; });
+            CloneContext ctx(&cloned, original.ID());
+            ctx.ReplaceAll([](const TestNode*) { return nullptr; });
             ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
         },
         "internal compiler error: ReplaceAll() called with a handler for type " + replaceable_name +
-            " that is already handled by a handler for type " + node_name);
+            " that is already handled by a handler for type " + Testnode_name);
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplaceAll_DerivedThenBase) {
-    std::string node_name = tint::TypeInfo::Of<Node>().name;
-    std::string replaceable_name = tint::TypeInfo::Of<Replaceable>().name;
+TEST_F(ASTCloneContextTestNodeTest, CloneWithReplaceAll_DerivedThenBase) {
+    std::string Testnode_name = TypeInfo::Of<TestNode>().name;
+    std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
 
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder cloned;
             Program original;
-            CloneContext ctx(&cloned, &original);
+            CloneContext ctx(&cloned, original.ID());
             ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
-            ctx.ReplaceAll([](const Node*) { return nullptr; });
+            ctx.ReplaceAll([](const TestNode*) { return nullptr; });
         },
-        "internal compiler error: ReplaceAll() called with a handler for type " + node_name +
+        "internal compiler error: ReplaceAll() called with a handler for type " + Testnode_name +
             " that is already handled by a handler for type " + replaceable_name);
 }
 
-TEST_F(CloneContextNodeTest, CloneWithReplacePointer_WithNotANode) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Allocator allocator;
-            ProgramBuilder builder;
-            auto* original_root = allocator.Create<Node>(builder.Symbols().New("root"));
-            original_root->a = allocator.Create<Node>(builder.Symbols().New("a"));
-            original_root->b = allocator.Create<Node>(builder.Symbols().New("b"));
-            original_root->c = allocator.Create<Node>(builder.Symbols().New("c"));
-            Program original(std::move(builder));
+using ASTCloneContextTest = ::testing::Test;
 
-            //                          root
-            //        ╭──────────────────┼──────────────────╮
-            //       (a)                (b)                (c)
-            //                        Replaced
-
-            ProgramBuilder cloned;
-            auto* replacement = allocator.Create<NotANode>();
-
-            CloneContext ctx(&cloned, &original);
-            ctx.Replace(original_root->b, replacement);
-
-            ctx.Clone(original_root);
-        },
-        "internal compiler error");
-}
-
-TEST_F(CloneContextNodeTest, CloneWithReplaceFunction_WithNotANode) {
-    EXPECT_FATAL_FAILURE(
-        {
-            Allocator allocator;
-            ProgramBuilder builder;
-            auto* original_root = allocator.Create<Node>(builder.Symbols().New("root"));
-            original_root->a = allocator.Create<Node>(builder.Symbols().New("a"));
-            original_root->b = allocator.Create<Node>(builder.Symbols().New("b"));
-            original_root->c = allocator.Create<Node>(builder.Symbols().New("c"));
-            Program original(std::move(builder));
-
-            //                          root
-            //        ╭──────────────────┼──────────────────╮
-            //       (a)                (b)                (c)
-            //                        Replaced
-
-            ProgramBuilder cloned;
-            auto* replacement = allocator.Create<NotANode>();
-
-            CloneContext ctx(&cloned, &original);
-            ctx.Replace(original_root->b, [=] { return replacement; });
-
-            ctx.Clone(original_root);
-        },
-        "internal compiler error");
-}
-
-using CloneContextTest = ::testing::Test;
-
-TEST_F(CloneContextTest, CloneWithReplaceAll_SymbolsTwice) {
+TEST_F(ASTCloneContextTest, CloneWithReplaceAll_SymbolsTwice) {
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder cloned;
             Program original;
-            CloneContext ctx(&cloned, &original);
+            CloneContext ctx(&cloned, original.ID());
             ctx.ReplaceAll([](const Symbol s) { return s; });
             ctx.ReplaceAll([](const Symbol s) { return s; });
         },
@@ -1166,7 +1097,7 @@
         "multiple times on the same CloneContext");
 }
 
-TEST_F(CloneContextTest, CloneNewUnnamedSymbols) {
+TEST_F(ASTCloneContextTest, CloneNewUnnamedSymbols) {
     ProgramBuilder builder;
     Symbol old_a = builder.Symbols().New();
     Symbol old_b = builder.Symbols().New();
@@ -1178,7 +1109,7 @@
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original, false);
+    CloneContext ctx(&cloned, original.ID());
     Symbol new_x = cloned.Symbols().New();
     Symbol new_a = ctx.Clone(old_a);
     Symbol new_y = cloned.Symbols().New();
@@ -1194,7 +1125,7 @@
     EXPECT_EQ(new_c.Name(), "tint_symbol_2_1");
 }
 
-TEST_F(CloneContextTest, CloneNewSymbols) {
+TEST_F(ASTCloneContextTest, CloneNewSymbols) {
     ProgramBuilder builder;
     Symbol old_a = builder.Symbols().New("a");
     Symbol old_b = builder.Symbols().New("b");
@@ -1206,7 +1137,7 @@
     Program original(std::move(builder));
 
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original, false);
+    CloneContext ctx(&cloned, original.ID());
     Symbol new_x = cloned.Symbols().New("a");
     Symbol new_a = ctx.Clone(old_a);
     Symbol new_y = cloned.Symbols().New("b");
@@ -1222,73 +1153,43 @@
     EXPECT_EQ(new_c.Name(), "c_1");
 }
 
-TEST_F(CloneContextTest, CloneNewSymbols_AfterCloneSymbols) {
-    ProgramBuilder builder;
-    Symbol old_a = builder.Symbols().New("a");
-    Symbol old_b = builder.Symbols().New("b");
-    Symbol old_c = builder.Symbols().New("c");
-    EXPECT_EQ(old_a.Name(), "a");
-    EXPECT_EQ(old_b.Name(), "b");
-    EXPECT_EQ(old_c.Name(), "c");
-
-    Program original(std::move(builder));
-
-    ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &original);
-    Symbol new_x = cloned.Symbols().New("a");
-    Symbol new_a = ctx.Clone(old_a);
-    Symbol new_y = cloned.Symbols().New("b");
-    Symbol new_b = ctx.Clone(old_b);
-    Symbol new_z = cloned.Symbols().New("c");
-    Symbol new_c = ctx.Clone(old_c);
-
-    EXPECT_EQ(new_x.Name(), "a_1");
-    EXPECT_EQ(new_a.Name(), "a");
-    EXPECT_EQ(new_y.Name(), "b_1");
-    EXPECT_EQ(new_b.Name(), "b");
-    EXPECT_EQ(new_z.Name(), "c_1");
-    EXPECT_EQ(new_c.Name(), "c");
-}
-
-TEST_F(CloneContextTest, GenerationIDs) {
+TEST_F(ASTCloneContextTest, GenerationIDs) {
     ProgramBuilder dst;
     Program src(ProgramBuilder{});
-    CloneContext ctx(&dst, &src);
+    CloneContext ctx(&dst, src.ID());
     Allocator allocator;
-    auto* cloned = ctx.Clone(allocator.Create<ProgramNode>(src.ID(), dst.ID()));
+    auto* cloned = ctx.Clone(allocator.Create<IDTestNode>(src.ID(), dst.ID()));
     EXPECT_EQ(cloned->generation_id, dst.ID());
 }
 
-TEST_F(CloneContextTest, GenerationIDs_Clone_ObjectNotOwnedBySrc) {
+TEST_F(ASTCloneContextTest, GenerationIDs_Clone_ObjectNotOwnedBySrc) {
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder dst;
             Program src(ProgramBuilder{});
-            CloneContext ctx(&dst, &src);
+            CloneContext ctx(&dst, src.ID());
             Allocator allocator;
-            ctx.Clone(allocator.Create<ProgramNode>(GenerationID::New(), dst.ID()));
+            ctx.Clone(allocator.Create<IDTestNode>(GenerationID::New(), dst.ID()));
         },
-        R"(internal compiler error: TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src, object))");
+        R"(internal compiler error: TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, object))");
 }
 
-TEST_F(CloneContextTest, GenerationIDs_Clone_ObjectNotOwnedByDst) {
+TEST_F(ASTCloneContextTest, GenerationIDs_Clone_ObjectNotOwnedByDst) {
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder dst;
             Program src(ProgramBuilder{});
-            CloneContext ctx(&dst, &src);
+            CloneContext ctx(&dst, src.ID());
             Allocator allocator;
-            ctx.Clone(allocator.Create<ProgramNode>(src.ID(), GenerationID::New()));
+            ctx.Clone(allocator.Create<IDTestNode>(src.ID(), GenerationID::New()));
         },
         R"(internal compiler error: TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(dst, out))");
 }
 
 }  // namespace
+}  // namespace tint::ast
 
-TINT_INSTANTIATE_TYPEINFO(Node);
-TINT_INSTANTIATE_TYPEINFO(Replaceable);
-TINT_INSTANTIATE_TYPEINFO(Replacement);
-TINT_INSTANTIATE_TYPEINFO(NotANode);
-TINT_INSTANTIATE_TYPEINFO(ProgramNode);
-
-}  // namespace tint
+TINT_INSTANTIATE_TYPEINFO(tint::ast::TestNode);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Replaceable);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Replacement);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IDTestNode);
diff --git a/src/tint/lang/wgsl/ast/compound_assignment_statement.cc b/src/tint/lang/wgsl/ast/compound_assignment_statement.cc
index 7050a2c..2609bad 100644
--- a/src/tint/lang/wgsl/ast/compound_assignment_statement.cc
+++ b/src/tint/lang/wgsl/ast/compound_assignment_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/compound_assignment_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::CompoundAssignmentStatement);
 
@@ -35,12 +36,12 @@
 
 CompoundAssignmentStatement::~CompoundAssignmentStatement() = default;
 
-const CompoundAssignmentStatement* CompoundAssignmentStatement::Clone(CloneContext* ctx) const {
+const CompoundAssignmentStatement* CompoundAssignmentStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* l = ctx->Clone(lhs);
-    auto* r = ctx->Clone(rhs);
-    return ctx->dst->create<CompoundAssignmentStatement>(src, l, r, op);
+    auto src = ctx.Clone(source);
+    auto* l = ctx.Clone(lhs);
+    auto* r = ctx.Clone(rhs);
+    return ctx.dst->create<CompoundAssignmentStatement>(src, l, r, op);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/compound_assignment_statement.h b/src/tint/lang/wgsl/ast/compound_assignment_statement.h
index 12ac67f..6e70d1c 100644
--- a/src/tint/lang/wgsl/ast/compound_assignment_statement.h
+++ b/src/tint/lang/wgsl/ast/compound_assignment_statement.h
@@ -45,7 +45,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const CompoundAssignmentStatement* Clone(CloneContext* ctx) const override;
+    const CompoundAssignmentStatement* Clone(CloneContext& ctx) const override;
 
     /// left side expression
     const Expression* const lhs;
diff --git a/src/tint/lang/wgsl/ast/const.cc b/src/tint/lang/wgsl/ast/const.cc
index 62ab73b..4de5846 100644
--- a/src/tint/lang/wgsl/ast/const.cc
+++ b/src/tint/lang/wgsl/ast/const.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Const);
 
@@ -39,13 +40,13 @@
     return "const";
 }
 
-const Const* Const::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto n = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    auto* init = ctx->Clone(initializer);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<Const>(src, n, ty, init, std::move(attrs));
+const Const* Const::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto n = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    auto* init = ctx.Clone(initializer);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<Const>(src, n, ty, init, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/const.h b/src/tint/lang/wgsl/ast/const.h
index 751c1c5..20bbf93 100644
--- a/src/tint/lang/wgsl/ast/const.h
+++ b/src/tint/lang/wgsl/ast/const.h
@@ -58,7 +58,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Const* Clone(CloneContext* ctx) const override;
+    const Const* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/const_assert.cc b/src/tint/lang/wgsl/ast/const_assert.cc
index 5775d94..8d7593c 100644
--- a/src/tint/lang/wgsl/ast/const_assert.cc
+++ b/src/tint/lang/wgsl/ast/const_assert.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/const_assert.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::ConstAssert);
 
@@ -28,11 +29,11 @@
 
 ConstAssert::~ConstAssert() = default;
 
-const ConstAssert* ConstAssert::Clone(CloneContext* ctx) const {
+const ConstAssert* ConstAssert::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* cond = ctx->Clone(condition);
-    return ctx->dst->create<ConstAssert>(src, cond);
+    auto src = ctx.Clone(source);
+    auto* cond = ctx.Clone(condition);
+    return ctx.dst->create<ConstAssert>(src, cond);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/const_assert.h b/src/tint/lang/wgsl/ast/const_assert.h
index d2f02f0..ab7b6ca 100644
--- a/src/tint/lang/wgsl/ast/const_assert.h
+++ b/src/tint/lang/wgsl/ast/const_assert.h
@@ -36,7 +36,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const ConstAssert* Clone(CloneContext* ctx) const override;
+    const ConstAssert* Clone(CloneContext& ctx) const override;
 
     /// The assertion condition
     const Expression* const condition;
diff --git a/src/tint/lang/wgsl/ast/continue_statement.cc b/src/tint/lang/wgsl/ast/continue_statement.cc
index 724a083..bbfcbed 100644
--- a/src/tint/lang/wgsl/ast/continue_statement.cc
+++ b/src/tint/lang/wgsl/ast/continue_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/continue_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::ContinueStatement);
 
@@ -25,10 +26,10 @@
 
 ContinueStatement::~ContinueStatement() = default;
 
-const ContinueStatement* ContinueStatement::Clone(CloneContext* ctx) const {
+const ContinueStatement* ContinueStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<ContinueStatement>(src);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<ContinueStatement>(src);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/continue_statement.h b/src/tint/lang/wgsl/ast/continue_statement.h
index 7cce4c3..58332de 100644
--- a/src/tint/lang/wgsl/ast/continue_statement.h
+++ b/src/tint/lang/wgsl/ast/continue_statement.h
@@ -35,7 +35,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const ContinueStatement* Clone(CloneContext* ctx) const override;
+    const ContinueStatement* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/diagnostic_attribute.cc b/src/tint/lang/wgsl/ast/diagnostic_attribute.cc
index d3e5888..966011f 100644
--- a/src/tint/lang/wgsl/ast/diagnostic_attribute.cc
+++ b/src/tint/lang/wgsl/ast/diagnostic_attribute.cc
@@ -17,7 +17,8 @@
 #include <string>
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::DiagnosticAttribute);
 
@@ -35,12 +36,12 @@
     return "diagnostic";
 }
 
-const DiagnosticAttribute* DiagnosticAttribute::Clone(CloneContext* ctx) const {
+const DiagnosticAttribute* DiagnosticAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto rule = ctx->Clone(control.rule_name);
+    auto src = ctx.Clone(source);
+    auto rule = ctx.Clone(control.rule_name);
     DiagnosticControl dc(control.severity, rule);
-    return ctx->dst->create<DiagnosticAttribute>(src, std::move(dc));
+    return ctx.dst->create<DiagnosticAttribute>(src, std::move(dc));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/diagnostic_attribute.h b/src/tint/lang/wgsl/ast/diagnostic_attribute.h
index a5846df..dffb7a3 100644
--- a/src/tint/lang/wgsl/ast/diagnostic_attribute.h
+++ b/src/tint/lang/wgsl/ast/diagnostic_attribute.h
@@ -39,7 +39,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const DiagnosticAttribute* Clone(CloneContext* ctx) const override;
+    const DiagnosticAttribute* Clone(CloneContext& ctx) const override;
 
     /// The diagnostic control.
     const DiagnosticControl control;
diff --git a/src/tint/lang/wgsl/ast/diagnostic_directive.cc b/src/tint/lang/wgsl/ast/diagnostic_directive.cc
index 77ff219..a301029 100644
--- a/src/tint/lang/wgsl/ast/diagnostic_directive.cc
+++ b/src/tint/lang/wgsl/ast/diagnostic_directive.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/diagnostic_directive.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::DiagnosticDirective);
 
@@ -28,11 +29,11 @@
 
 DiagnosticDirective::~DiagnosticDirective() = default;
 
-const DiagnosticDirective* DiagnosticDirective::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto rule = ctx->Clone(control.rule_name);
+const DiagnosticDirective* DiagnosticDirective::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto rule = ctx.Clone(control.rule_name);
     DiagnosticControl dc(control.severity, rule);
-    return ctx->dst->create<DiagnosticDirective>(src, std::move(dc));
+    return ctx.dst->create<DiagnosticDirective>(src, std::move(dc));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/diagnostic_directive.h b/src/tint/lang/wgsl/ast/diagnostic_directive.h
index 802576e..5b73abf 100644
--- a/src/tint/lang/wgsl/ast/diagnostic_directive.h
+++ b/src/tint/lang/wgsl/ast/diagnostic_directive.h
@@ -44,7 +44,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const DiagnosticDirective* Clone(CloneContext* ctx) const override;
+    const DiagnosticDirective* Clone(CloneContext& ctx) const override;
 
     /// The diagnostic control.
     const DiagnosticControl control;
diff --git a/src/tint/lang/wgsl/ast/diagnostic_rule_name.cc b/src/tint/lang/wgsl/ast/diagnostic_rule_name.cc
index 0c3e180..64511b0 100644
--- a/src/tint/lang/wgsl/ast/diagnostic_rule_name.cc
+++ b/src/tint/lang/wgsl/ast/diagnostic_rule_name.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::DiagnosticRuleName);
 
@@ -54,13 +55,13 @@
     }
 }
 
-const DiagnosticRuleName* DiagnosticRuleName::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto n = ctx->Clone(name);
-    if (auto c = ctx->Clone(category)) {
-        return ctx->dst->create<DiagnosticRuleName>(src, c, n);
+const DiagnosticRuleName* DiagnosticRuleName::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto n = ctx.Clone(name);
+    if (auto c = ctx.Clone(category)) {
+        return ctx.dst->create<DiagnosticRuleName>(src, c, n);
     }
-    return ctx->dst->create<DiagnosticRuleName>(src, n);
+    return ctx.dst->create<DiagnosticRuleName>(src, n);
 }
 
 std::string DiagnosticRuleName::String() const {
diff --git a/src/tint/lang/wgsl/ast/diagnostic_rule_name.h b/src/tint/lang/wgsl/ast/diagnostic_rule_name.h
index bad739a..87c257c 100644
--- a/src/tint/lang/wgsl/ast/diagnostic_rule_name.h
+++ b/src/tint/lang/wgsl/ast/diagnostic_rule_name.h
@@ -51,7 +51,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const DiagnosticRuleName* Clone(CloneContext* ctx) const override;
+    const DiagnosticRuleName* Clone(CloneContext& ctx) const override;
 
     /// @return the full name of this diagnostic rule, either as `name` or `category.name`.
     std::string String() const;
diff --git a/src/tint/lang/wgsl/ast/disable_validation_attribute.cc b/src/tint/lang/wgsl/ast/disable_validation_attribute.cc
index 4c908f6..c241a5b 100644
--- a/src/tint/lang/wgsl/ast/disable_validation_attribute.cc
+++ b/src/tint/lang/wgsl/ast/disable_validation_attribute.cc
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/lang/wgsl/ast/clone_context.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::DisableValidationAttribute);
 
@@ -51,9 +51,9 @@
     return "<invalid>";
 }
 
-const DisableValidationAttribute* DisableValidationAttribute::Clone(CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<DisableValidationAttribute>(
-        ctx->dst->ID(), ctx->dst->AllocateNodeID(), validation);
+const DisableValidationAttribute* DisableValidationAttribute::Clone(CloneContext& ctx) const {
+    return ctx.dst->ASTNodes().Create<DisableValidationAttribute>(
+        ctx.dst->ID(), ctx.dst->AllocateNodeID(), validation);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/disable_validation_attribute.h b/src/tint/lang/wgsl/ast/disable_validation_attribute.h
index e523084..1ae28ea 100644
--- a/src/tint/lang/wgsl/ast/disable_validation_attribute.h
+++ b/src/tint/lang/wgsl/ast/disable_validation_attribute.h
@@ -73,7 +73,7 @@
     /// Performs a deep clone of this object using the CloneContext `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned object
-    const DisableValidationAttribute* Clone(CloneContext* ctx) const override;
+    const DisableValidationAttribute* Clone(CloneContext& ctx) const override;
 
     /// The validation that this attribute disables
     const DisabledValidation validation;
diff --git a/src/tint/lang/wgsl/ast/discard_statement.cc b/src/tint/lang/wgsl/ast/discard_statement.cc
index de4d60f..b7bd7e5 100644
--- a/src/tint/lang/wgsl/ast/discard_statement.cc
+++ b/src/tint/lang/wgsl/ast/discard_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/discard_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::DiscardStatement);
 
@@ -25,10 +26,10 @@
 
 DiscardStatement::~DiscardStatement() = default;
 
-const DiscardStatement* DiscardStatement::Clone(CloneContext* ctx) const {
+const DiscardStatement* DiscardStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<DiscardStatement>(src);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<DiscardStatement>(src);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/discard_statement.h b/src/tint/lang/wgsl/ast/discard_statement.h
index 4886db7..1dbea7a 100644
--- a/src/tint/lang/wgsl/ast/discard_statement.h
+++ b/src/tint/lang/wgsl/ast/discard_statement.h
@@ -35,7 +35,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const DiscardStatement* Clone(CloneContext* ctx) const override;
+    const DiscardStatement* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/enable.cc b/src/tint/lang/wgsl/ast/enable.cc
index 8bd4b82..6ff3bcb 100644
--- a/src/tint/lang/wgsl/ast/enable.cc
+++ b/src/tint/lang/wgsl/ast/enable.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/enable.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Enable);
 
@@ -34,10 +35,10 @@
     return false;
 }
 
-const Enable* Enable::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto exts = ctx->Clone(extensions);
-    return ctx->dst->create<Enable>(src, std::move(exts));
+const Enable* Enable::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto exts = ctx.Clone(extensions);
+    return ctx.dst->create<Enable>(src, std::move(exts));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/enable.h b/src/tint/lang/wgsl/ast/enable.h
index 95c6c12..c5b2301 100644
--- a/src/tint/lang/wgsl/ast/enable.h
+++ b/src/tint/lang/wgsl/ast/enable.h
@@ -47,7 +47,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Enable* Clone(CloneContext* ctx) const override;
+    const Enable* Clone(CloneContext& ctx) const override;
 
     /// The extensions being enabled by this directive
     const tint::Vector<const Extension*, 4> extensions;
diff --git a/src/tint/lang/wgsl/ast/extension.cc b/src/tint/lang/wgsl/ast/extension.cc
index ac8a3ad..b23f14b 100644
--- a/src/tint/lang/wgsl/ast/extension.cc
+++ b/src/tint/lang/wgsl/ast/extension.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/extension.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 //! @cond Doxygen_Suppress
 // Doxygen gets confused with tint::ast::Extension and tint::builtin::Extension
@@ -28,9 +29,9 @@
 
 Extension::~Extension() = default;
 
-const Extension* Extension::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<Extension>(src, name);
+const Extension* Extension::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<Extension>(src, name);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/extension.h b/src/tint/lang/wgsl/ast/extension.h
index 65c5bf3..ce126ee 100644
--- a/src/tint/lang/wgsl/ast/extension.h
+++ b/src/tint/lang/wgsl/ast/extension.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Extension* Clone(CloneContext* ctx) const override;
+    const Extension* Clone(CloneContext& ctx) const override;
 
     /// The extension name
     const builtin::Extension name;
diff --git a/src/tint/lang/wgsl/ast/float_literal_expression.cc b/src/tint/lang/wgsl/ast/float_literal_expression.cc
index 9fb7cbf..6e053d4 100644
--- a/src/tint/lang/wgsl/ast/float_literal_expression.cc
+++ b/src/tint/lang/wgsl/ast/float_literal_expression.cc
@@ -16,7 +16,8 @@
 
 #include <limits>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::FloatLiteralExpression);
 
@@ -31,10 +32,10 @@
 
 FloatLiteralExpression::~FloatLiteralExpression() = default;
 
-const FloatLiteralExpression* FloatLiteralExpression::Clone(CloneContext* ctx) const {
+const FloatLiteralExpression* FloatLiteralExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<FloatLiteralExpression>(src, value, suffix);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<FloatLiteralExpression>(src, value, suffix);
 }
 
 std::string_view ToString(FloatLiteralExpression::Suffix suffix) {
diff --git a/src/tint/lang/wgsl/ast/float_literal_expression.h b/src/tint/lang/wgsl/ast/float_literal_expression.h
index 679d3f2..4c723f8 100644
--- a/src/tint/lang/wgsl/ast/float_literal_expression.h
+++ b/src/tint/lang/wgsl/ast/float_literal_expression.h
@@ -47,7 +47,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const FloatLiteralExpression* Clone(CloneContext* ctx) const override;
+    const FloatLiteralExpression* Clone(CloneContext& ctx) const override;
 
     /// The literal value
     const double value;
diff --git a/src/tint/lang/wgsl/ast/for_loop_statement.cc b/src/tint/lang/wgsl/ast/for_loop_statement.cc
index 0f976e9..0df624e 100644
--- a/src/tint/lang/wgsl/ast/for_loop_statement.cc
+++ b/src/tint/lang/wgsl/ast/for_loop_statement.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::ForLoopStatement);
 
@@ -50,16 +51,16 @@
 
 ForLoopStatement::~ForLoopStatement() = default;
 
-const ForLoopStatement* ForLoopStatement::Clone(CloneContext* ctx) const {
+const ForLoopStatement* ForLoopStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
+    auto src = ctx.Clone(source);
 
-    auto* init = ctx->Clone(initializer);
-    auto* cond = ctx->Clone(condition);
-    auto* cont = ctx->Clone(continuing);
-    auto* b = ctx->Clone(body);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<ForLoopStatement>(src, init, cond, cont, b, std::move(attrs));
+    auto* init = ctx.Clone(initializer);
+    auto* cond = ctx.Clone(condition);
+    auto* cont = ctx.Clone(continuing);
+    auto* b = ctx.Clone(body);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<ForLoopStatement>(src, init, cond, cont, b, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/for_loop_statement.h b/src/tint/lang/wgsl/ast/for_loop_statement.h
index 6135b90..bc93a45 100644
--- a/src/tint/lang/wgsl/ast/for_loop_statement.h
+++ b/src/tint/lang/wgsl/ast/for_loop_statement.h
@@ -49,7 +49,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const ForLoopStatement* Clone(CloneContext* ctx) const override;
+    const ForLoopStatement* Clone(CloneContext& ctx) const override;
 
     /// The initializer statement
     const Statement* const initializer;
diff --git a/src/tint/lang/wgsl/ast/function.cc b/src/tint/lang/wgsl/ast/function.cc
index 78ed12c..f92eb8f 100644
--- a/src/tint/lang/wgsl/ast/function.cc
+++ b/src/tint/lang/wgsl/ast/function.cc
@@ -14,10 +14,11 @@
 
 #include "src/tint/lang/wgsl/ast/function.h"
 
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/ast/stage_attribute.h"
 #include "src/tint/lang/wgsl/ast/type.h"
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Function);
 
@@ -67,16 +68,16 @@
     return PipelineStage::kNone;
 }
 
-const Function* Function::Clone(CloneContext* ctx) const {
+const Function* Function::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto n = ctx->Clone(name);
-    auto p = ctx->Clone(params);
-    auto ret = ctx->Clone(return_type);
-    auto* b = ctx->Clone(body);
-    auto attrs = ctx->Clone(attributes);
-    auto ret_attrs = ctx->Clone(return_type_attributes);
-    return ctx->dst->create<Function>(src, n, p, ret, b, attrs, ret_attrs);
+    auto src = ctx.Clone(source);
+    auto n = ctx.Clone(name);
+    auto p = ctx.Clone(params);
+    auto ret = ctx.Clone(return_type);
+    auto* b = ctx.Clone(body);
+    auto attrs = ctx.Clone(attributes);
+    auto ret_attrs = ctx.Clone(return_type_attributes);
+    return ctx.dst->create<Function>(src, n, p, ret, b, attrs, ret_attrs);
 }
 
 const Function* FunctionList::Find(Symbol sym) const {
diff --git a/src/tint/lang/wgsl/ast/function.h b/src/tint/lang/wgsl/ast/function.h
index 42489ed..b766397 100644
--- a/src/tint/lang/wgsl/ast/function.h
+++ b/src/tint/lang/wgsl/ast/function.h
@@ -28,6 +28,7 @@
 #include "src/tint/lang/wgsl/ast/location_attribute.h"
 #include "src/tint/lang/wgsl/ast/parameter.h"
 #include "src/tint/lang/wgsl/ast/pipeline_stage.h"
+#include "src/tint/utils/text/symbol.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -73,7 +74,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Function* Clone(CloneContext* ctx) const override;
+    const Function* Clone(CloneContext& ctx) const override;
 
     /// The function name
     const Identifier* const name;
diff --git a/src/tint/lang/wgsl/ast/group_attribute.cc b/src/tint/lang/wgsl/ast/group_attribute.cc
index 30da424..6afb6f8 100644
--- a/src/tint/lang/wgsl/ast/group_attribute.cc
+++ b/src/tint/lang/wgsl/ast/group_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::GroupAttribute);
 
@@ -34,11 +35,11 @@
     return "group";
 }
 
-const GroupAttribute* GroupAttribute::Clone(CloneContext* ctx) const {
+const GroupAttribute* GroupAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* expr_ = ctx->Clone(expr);
-    return ctx->dst->create<GroupAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto* expr_ = ctx.Clone(expr);
+    return ctx.dst->create<GroupAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/group_attribute.h b/src/tint/lang/wgsl/ast/group_attribute.h
index f8e6ebf..2998ca5 100644
--- a/src/tint/lang/wgsl/ast/group_attribute.h
+++ b/src/tint/lang/wgsl/ast/group_attribute.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const GroupAttribute* Clone(CloneContext* ctx) const override;
+    const GroupAttribute* Clone(CloneContext& ctx) const override;
 
     /// The group expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/id_attribute.cc b/src/tint/lang/wgsl/ast/id_attribute.cc
index 007b92b..cc58803 100644
--- a/src/tint/lang/wgsl/ast/id_attribute.cc
+++ b/src/tint/lang/wgsl/ast/id_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IdAttribute);
 
@@ -31,11 +32,11 @@
     return "id";
 }
 
-const IdAttribute* IdAttribute::Clone(CloneContext* ctx) const {
+const IdAttribute* IdAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* expr_ = ctx->Clone(expr);
-    return ctx->dst->create<IdAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto* expr_ = ctx.Clone(expr);
+    return ctx.dst->create<IdAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/id_attribute.h b/src/tint/lang/wgsl/ast/id_attribute.h
index cc593aa..197c09d 100644
--- a/src/tint/lang/wgsl/ast/id_attribute.h
+++ b/src/tint/lang/wgsl/ast/id_attribute.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IdAttribute* Clone(CloneContext* ctx) const override;
+    const IdAttribute* Clone(CloneContext& ctx) const override;
 
     /// The id expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/identifier.cc b/src/tint/lang/wgsl/ast/identifier.cc
index 827487b..ac0e265 100644
--- a/src/tint/lang/wgsl/ast/identifier.cc
+++ b/src/tint/lang/wgsl/ast/identifier.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/identifier.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Identifier);
 
@@ -28,11 +29,11 @@
 
 Identifier::~Identifier() = default;
 
-const Identifier* Identifier::Clone(CloneContext* ctx) const {
+const Identifier* Identifier::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto sym = ctx->Clone(symbol);
-    return ctx->dst->create<Identifier>(src, sym);
+    auto src = ctx.Clone(source);
+    auto sym = ctx.Clone(symbol);
+    return ctx.dst->create<Identifier>(src, sym);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/identifier.h b/src/tint/lang/wgsl/ast/identifier.h
index 995c6f2..bf0400f 100644
--- a/src/tint/lang/wgsl/ast/identifier.h
+++ b/src/tint/lang/wgsl/ast/identifier.h
@@ -16,6 +16,7 @@
 #define SRC_TINT_LANG_WGSL_AST_IDENTIFIER_H_
 
 #include "src/tint/lang/wgsl/ast/node.h"
+#include "src/tint/utils/text/symbol.h"
 
 namespace tint::ast {
 
@@ -36,7 +37,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Identifier* Clone(CloneContext* ctx) const override;
+    const Identifier* Clone(CloneContext& ctx) const override;
 
     /// The symbol for the identifier
     const Symbol symbol;
diff --git a/src/tint/lang/wgsl/ast/identifier_expression.cc b/src/tint/lang/wgsl/ast/identifier_expression.cc
index 028600e..efd503e 100644
--- a/src/tint/lang/wgsl/ast/identifier_expression.cc
+++ b/src/tint/lang/wgsl/ast/identifier_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/identifier_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IdentifierExpression);
 
@@ -31,11 +32,11 @@
 
 IdentifierExpression::~IdentifierExpression() = default;
 
-const IdentifierExpression* IdentifierExpression::Clone(CloneContext* ctx) const {
+const IdentifierExpression* IdentifierExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto ident = ctx->Clone(identifier);
-    return ctx->dst->create<IdentifierExpression>(src, ident);
+    auto src = ctx.Clone(source);
+    auto ident = ctx.Clone(identifier);
+    return ctx.dst->create<IdentifierExpression>(src, ident);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/identifier_expression.h b/src/tint/lang/wgsl/ast/identifier_expression.h
index f7b4385..6a84906 100644
--- a/src/tint/lang/wgsl/ast/identifier_expression.h
+++ b/src/tint/lang/wgsl/ast/identifier_expression.h
@@ -44,7 +44,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IdentifierExpression* Clone(CloneContext* ctx) const override;
+    const IdentifierExpression* Clone(CloneContext& ctx) const override;
 
     /// The identifier for the expression
     Identifier const* const identifier;
diff --git a/src/tint/lang/wgsl/ast/if_statement.cc b/src/tint/lang/wgsl/ast/if_statement.cc
index 4168b63..5c4c636 100644
--- a/src/tint/lang/wgsl/ast/if_statement.cc
+++ b/src/tint/lang/wgsl/ast/if_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/if_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IfStatement);
 
@@ -48,14 +49,14 @@
 
 IfStatement::~IfStatement() = default;
 
-const IfStatement* IfStatement::Clone(CloneContext* ctx) const {
+const IfStatement* IfStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* cond = ctx->Clone(condition);
-    auto* b = ctx->Clone(body);
-    auto* el = ctx->Clone(else_statement);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<IfStatement>(src, cond, b, el, std::move(attrs));
+    auto src = ctx.Clone(source);
+    auto* cond = ctx.Clone(condition);
+    auto* b = ctx.Clone(body);
+    auto* el = ctx.Clone(else_statement);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<IfStatement>(src, cond, b, el, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/if_statement.h b/src/tint/lang/wgsl/ast/if_statement.h
index 27cd333..c61ee3e 100644
--- a/src/tint/lang/wgsl/ast/if_statement.h
+++ b/src/tint/lang/wgsl/ast/if_statement.h
@@ -48,7 +48,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IfStatement* Clone(CloneContext* ctx) const override;
+    const IfStatement* Clone(CloneContext& ctx) const override;
 
     /// The if condition or nullptr if none set
     const Expression* const condition;
diff --git a/src/tint/lang/wgsl/ast/increment_decrement_statement.cc b/src/tint/lang/wgsl/ast/increment_decrement_statement.cc
index 4e90671..5307e2e 100644
--- a/src/tint/lang/wgsl/ast/increment_decrement_statement.cc
+++ b/src/tint/lang/wgsl/ast/increment_decrement_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/increment_decrement_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IncrementDecrementStatement);
 
@@ -31,11 +32,11 @@
 
 IncrementDecrementStatement::~IncrementDecrementStatement() = default;
 
-const IncrementDecrementStatement* IncrementDecrementStatement::Clone(CloneContext* ctx) const {
+const IncrementDecrementStatement* IncrementDecrementStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* l = ctx->Clone(lhs);
-    return ctx->dst->create<IncrementDecrementStatement>(src, l, increment);
+    auto src = ctx.Clone(source);
+    auto* l = ctx.Clone(lhs);
+    return ctx.dst->create<IncrementDecrementStatement>(src, l, increment);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/increment_decrement_statement.h b/src/tint/lang/wgsl/ast/increment_decrement_statement.h
index 3b5f6cb..1c317b9 100644
--- a/src/tint/lang/wgsl/ast/increment_decrement_statement.h
+++ b/src/tint/lang/wgsl/ast/increment_decrement_statement.h
@@ -42,7 +42,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IncrementDecrementStatement* Clone(CloneContext* ctx) const override;
+    const IncrementDecrementStatement* Clone(CloneContext& ctx) const override;
 
     /// The LHS expression.
     const Expression* const lhs;
diff --git a/src/tint/lang/wgsl/ast/index_accessor_expression.cc b/src/tint/lang/wgsl/ast/index_accessor_expression.cc
index 1419497..1e8426d 100644
--- a/src/tint/lang/wgsl/ast/index_accessor_expression.cc
+++ b/src/tint/lang/wgsl/ast/index_accessor_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/index_accessor_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IndexAccessorExpression);
 
@@ -32,12 +33,12 @@
 
 IndexAccessorExpression::~IndexAccessorExpression() = default;
 
-const IndexAccessorExpression* IndexAccessorExpression::Clone(CloneContext* ctx) const {
+const IndexAccessorExpression* IndexAccessorExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* obj = ctx->Clone(object);
-    auto* idx = ctx->Clone(index);
-    return ctx->dst->create<IndexAccessorExpression>(src, obj, idx);
+    auto src = ctx.Clone(source);
+    auto* obj = ctx.Clone(object);
+    auto* idx = ctx.Clone(index);
+    return ctx.dst->create<IndexAccessorExpression>(src, obj, idx);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/index_accessor_expression.h b/src/tint/lang/wgsl/ast/index_accessor_expression.h
index 7ee9ac4..a61153d 100644
--- a/src/tint/lang/wgsl/ast/index_accessor_expression.h
+++ b/src/tint/lang/wgsl/ast/index_accessor_expression.h
@@ -41,7 +41,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IndexAccessorExpression* Clone(CloneContext* ctx) const override;
+    const IndexAccessorExpression* Clone(CloneContext& ctx) const override;
 
     /// the index expression
     const Expression* const index;
diff --git a/src/tint/lang/wgsl/ast/index_attribute.cc b/src/tint/lang/wgsl/ast/index_attribute.cc
index 1d45c6e..0833bfa 100644
--- a/src/tint/lang/wgsl/ast/index_attribute.cc
+++ b/src/tint/lang/wgsl/ast/index_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IndexAttribute);
 
@@ -34,11 +35,11 @@
     return "index";
 }
 
-const IndexAttribute* IndexAttribute::Clone(CloneContext* ctx) const {
+const IndexAttribute* IndexAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* expr_ = ctx->Clone(expr);
-    return ctx->dst->create<IndexAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto* expr_ = ctx.Clone(expr);
+    return ctx.dst->create<IndexAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/index_attribute.h b/src/tint/lang/wgsl/ast/index_attribute.h
index 42d5b4e..cd2a890 100644
--- a/src/tint/lang/wgsl/ast/index_attribute.h
+++ b/src/tint/lang/wgsl/ast/index_attribute.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IndexAttribute* Clone(CloneContext* ctx) const override;
+    const IndexAttribute* Clone(CloneContext& ctx) const override;
 
     /// The id expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/int_literal_expression.cc b/src/tint/lang/wgsl/ast/int_literal_expression.cc
index 6858bf7..2661aec 100644
--- a/src/tint/lang/wgsl/ast/int_literal_expression.cc
+++ b/src/tint/lang/wgsl/ast/int_literal_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/int_literal_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::IntLiteralExpression);
 
@@ -29,10 +30,10 @@
 
 IntLiteralExpression::~IntLiteralExpression() = default;
 
-const IntLiteralExpression* IntLiteralExpression::Clone(CloneContext* ctx) const {
+const IntLiteralExpression* IntLiteralExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<IntLiteralExpression>(src, value, suffix);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<IntLiteralExpression>(src, value, suffix);
 }
 
 std::string_view ToString(IntLiteralExpression::Suffix suffix) {
diff --git a/src/tint/lang/wgsl/ast/int_literal_expression.h b/src/tint/lang/wgsl/ast/int_literal_expression.h
index f6399db..10548c1 100644
--- a/src/tint/lang/wgsl/ast/int_literal_expression.h
+++ b/src/tint/lang/wgsl/ast/int_literal_expression.h
@@ -46,7 +46,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IntLiteralExpression* Clone(CloneContext* ctx) const override;
+    const IntLiteralExpression* Clone(CloneContext& ctx) const override;
 
     /// The literal value
     const int64_t value;
diff --git a/src/tint/lang/wgsl/ast/interpolate_attribute.cc b/src/tint/lang/wgsl/ast/interpolate_attribute.cc
index e927fa6..0219abb 100644
--- a/src/tint/lang/wgsl/ast/interpolate_attribute.cc
+++ b/src/tint/lang/wgsl/ast/interpolate_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::InterpolateAttribute);
 
@@ -35,12 +36,12 @@
     return "interpolate";
 }
 
-const InterpolateAttribute* InterpolateAttribute::Clone(CloneContext* ctx) const {
+const InterpolateAttribute* InterpolateAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* ty = ctx->Clone(type);
-    auto* smpl = ctx->Clone(sampling);
-    return ctx->dst->create<InterpolateAttribute>(src, ty, smpl);
+    auto src = ctx.Clone(source);
+    auto* ty = ctx.Clone(type);
+    auto* smpl = ctx.Clone(sampling);
+    return ctx.dst->create<InterpolateAttribute>(src, ty, smpl);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/interpolate_attribute.h b/src/tint/lang/wgsl/ast/interpolate_attribute.h
index 290c078..e27fa7e 100644
--- a/src/tint/lang/wgsl/ast/interpolate_attribute.h
+++ b/src/tint/lang/wgsl/ast/interpolate_attribute.h
@@ -49,7 +49,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const InterpolateAttribute* Clone(CloneContext* ctx) const override;
+    const InterpolateAttribute* Clone(CloneContext& ctx) const override;
 
     /// The interpolation type
     const Expression* const type;
diff --git a/src/tint/lang/wgsl/ast/invariant_attribute.cc b/src/tint/lang/wgsl/ast/invariant_attribute.cc
index 6581fed..8d8b3cd 100644
--- a/src/tint/lang/wgsl/ast/invariant_attribute.cc
+++ b/src/tint/lang/wgsl/ast/invariant_attribute.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/invariant_attribute.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::InvariantAttribute);
 
@@ -29,10 +30,10 @@
     return "invariant";
 }
 
-const InvariantAttribute* InvariantAttribute::Clone(CloneContext* ctx) const {
+const InvariantAttribute* InvariantAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<InvariantAttribute>(src);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<InvariantAttribute>(src);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/invariant_attribute.h b/src/tint/lang/wgsl/ast/invariant_attribute.h
index 97e5798..d335ee20 100644
--- a/src/tint/lang/wgsl/ast/invariant_attribute.h
+++ b/src/tint/lang/wgsl/ast/invariant_attribute.h
@@ -38,7 +38,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const InvariantAttribute* Clone(CloneContext* ctx) const override;
+    const InvariantAttribute* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/let.cc b/src/tint/lang/wgsl/ast/let.cc
index 5f9d90b..970f122 100644
--- a/src/tint/lang/wgsl/ast/let.cc
+++ b/src/tint/lang/wgsl/ast/let.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Let);
 
@@ -39,13 +40,13 @@
     return "let";
 }
 
-const Let* Let::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto* n = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    auto* init = ctx->Clone(initializer);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<Let>(src, n, ty, init, std::move(attrs));
+const Let* Let::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto* n = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    auto* init = ctx.Clone(initializer);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<Let>(src, n, ty, init, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/let.h b/src/tint/lang/wgsl/ast/let.h
index bcf0901..c0e7190 100644
--- a/src/tint/lang/wgsl/ast/let.h
+++ b/src/tint/lang/wgsl/ast/let.h
@@ -55,7 +55,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Let* Clone(CloneContext* ctx) const override;
+    const Let* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/location_attribute.cc b/src/tint/lang/wgsl/ast/location_attribute.cc
index 708884f..3849800 100644
--- a/src/tint/lang/wgsl/ast/location_attribute.cc
+++ b/src/tint/lang/wgsl/ast/location_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::LocationAttribute);
 
@@ -34,11 +35,11 @@
     return "location";
 }
 
-const LocationAttribute* LocationAttribute::Clone(CloneContext* ctx) const {
+const LocationAttribute* LocationAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto expr_ = ctx->Clone(expr);
-    return ctx->dst->create<LocationAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto expr_ = ctx.Clone(expr);
+    return ctx.dst->create<LocationAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/location_attribute.h b/src/tint/lang/wgsl/ast/location_attribute.h
index d08f823..b10b7e4 100644
--- a/src/tint/lang/wgsl/ast/location_attribute.h
+++ b/src/tint/lang/wgsl/ast/location_attribute.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const LocationAttribute* Clone(CloneContext* ctx) const override;
+    const LocationAttribute* Clone(CloneContext& ctx) const override;
 
     /// The location expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/loop_statement.cc b/src/tint/lang/wgsl/ast/loop_statement.cc
index 0da9ad5..a61ec80 100644
--- a/src/tint/lang/wgsl/ast/loop_statement.cc
+++ b/src/tint/lang/wgsl/ast/loop_statement.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::LoopStatement);
 
@@ -40,13 +41,13 @@
 
 LoopStatement::~LoopStatement() = default;
 
-const LoopStatement* LoopStatement::Clone(CloneContext* ctx) const {
+const LoopStatement* LoopStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* b = ctx->Clone(body);
-    auto* cont = ctx->Clone(continuing);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<LoopStatement>(src, b, cont, std::move(attrs));
+    auto src = ctx.Clone(source);
+    auto* b = ctx.Clone(body);
+    auto* cont = ctx.Clone(continuing);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<LoopStatement>(src, b, cont, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/loop_statement.h b/src/tint/lang/wgsl/ast/loop_statement.h
index 32aab61..0bf4289 100644
--- a/src/tint/lang/wgsl/ast/loop_statement.h
+++ b/src/tint/lang/wgsl/ast/loop_statement.h
@@ -42,7 +42,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const LoopStatement* Clone(CloneContext* ctx) const override;
+    const LoopStatement* Clone(CloneContext& ctx) const override;
 
     /// The loop body
     const BlockStatement* const body;
diff --git a/src/tint/lang/wgsl/ast/member_accessor_expression.cc b/src/tint/lang/wgsl/ast/member_accessor_expression.cc
index d9ec1a4..635c094 100644
--- a/src/tint/lang/wgsl/ast/member_accessor_expression.cc
+++ b/src/tint/lang/wgsl/ast/member_accessor_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/member_accessor_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::MemberAccessorExpression);
 
@@ -37,12 +38,12 @@
 
 MemberAccessorExpression::~MemberAccessorExpression() = default;
 
-const MemberAccessorExpression* MemberAccessorExpression::Clone(CloneContext* ctx) const {
+const MemberAccessorExpression* MemberAccessorExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* obj = ctx->Clone(object);
-    auto* mem = ctx->Clone(member);
-    return ctx->dst->create<MemberAccessorExpression>(src, obj, mem);
+    auto src = ctx.Clone(source);
+    auto* obj = ctx.Clone(object);
+    auto* mem = ctx.Clone(member);
+    return ctx.dst->create<MemberAccessorExpression>(src, obj, mem);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/member_accessor_expression.h b/src/tint/lang/wgsl/ast/member_accessor_expression.h
index 8c14b48..ba580f2 100644
--- a/src/tint/lang/wgsl/ast/member_accessor_expression.h
+++ b/src/tint/lang/wgsl/ast/member_accessor_expression.h
@@ -43,7 +43,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const MemberAccessorExpression* Clone(CloneContext* ctx) const override;
+    const MemberAccessorExpression* Clone(CloneContext& ctx) const override;
 
     /// The member expression
     const Identifier* const member;
diff --git a/src/tint/lang/wgsl/ast/module.cc b/src/tint/lang/wgsl/ast/module.cc
index a5b2797..b9d0186 100644
--- a/src/tint/lang/wgsl/ast/module.cc
+++ b/src/tint/lang/wgsl/ast/module.cc
@@ -16,8 +16,9 @@
 
 #include <utility>
 
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/ast/type_decl.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/utils/rtti/switch.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Module);
@@ -124,14 +125,14 @@
     global_declarations_.Push(func);
 }
 
-const Module* Module::Clone(CloneContext* ctx) const {
-    auto* out = ctx->dst->create<Module>();
+const Module* Module::Clone(CloneContext& ctx) const {
+    auto* out = ctx.dst->create<Module>();
     out->Copy(ctx, this);
     return out;
 }
 
-void Module::Copy(CloneContext* ctx, const Module* src) {
-    ctx->Clone(global_declarations_, src->global_declarations_);
+void Module::Copy(CloneContext& ctx, const Module* src) {
+    ctx.Clone(global_declarations_, src->global_declarations_);
 
     // During the clone, declarations may have been placed into the module.
     // Clear everything out, as we're about to re-bin the declarations.
diff --git a/src/tint/lang/wgsl/ast/module.h b/src/tint/lang/wgsl/ast/module.h
index b0516bd..9dce51d 100644
--- a/src/tint/lang/wgsl/ast/module.h
+++ b/src/tint/lang/wgsl/ast/module.h
@@ -132,12 +132,12 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Module* Clone(CloneContext* ctx) const override;
+    const Module* Clone(CloneContext& ctx) const override;
 
     /// Copy copies the content of the Module src into this module.
     /// @param ctx the clone context
     /// @param src the module to copy into this module
-    void Copy(CloneContext* ctx, const Module* src);
+    void Copy(CloneContext& ctx, const Module* src);
 
   private:
     /// Adds `decl` to either:
diff --git a/src/tint/lang/wgsl/ast/module_test.cc b/src/tint/lang/wgsl/ast/module_test.cc
index 85f7c78..e896f7c 100644
--- a/src/tint/lang/wgsl/ast/module_test.cc
+++ b/src/tint/lang/wgsl/ast/module_test.cc
@@ -14,8 +14,8 @@
 
 #include "gmock/gmock.h"
 #include "gtest/gtest-spi.h"
-#include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/ast/test_helper.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 
 namespace tint::ast {
 namespace {
@@ -100,7 +100,7 @@
     // declarations. We want to test that these are added just before the
     // declaration that triggered the ReplaceAll().
     ProgramBuilder cloned;
-    CloneContext ctx(&cloned, &p);
+    program::CloneContext ctx(&cloned, &p);
     ctx.ReplaceAll([&](const Function*) -> const Function* {
         ctx.dst->Alias("inserted_before_F", cloned.ty.u32());
         return nullptr;
diff --git a/src/tint/lang/wgsl/ast/must_use_attribute.cc b/src/tint/lang/wgsl/ast/must_use_attribute.cc
index 35b4fdb..24d3a9b 100644
--- a/src/tint/lang/wgsl/ast/must_use_attribute.cc
+++ b/src/tint/lang/wgsl/ast/must_use_attribute.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/must_use_attribute.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::MustUseAttribute);
 
@@ -29,10 +30,10 @@
     return "must_use";
 }
 
-const MustUseAttribute* MustUseAttribute::Clone(CloneContext* ctx) const {
+const MustUseAttribute* MustUseAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<MustUseAttribute>(src);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<MustUseAttribute>(src);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/must_use_attribute.h b/src/tint/lang/wgsl/ast/must_use_attribute.h
index e882bd6..14de7c6 100644
--- a/src/tint/lang/wgsl/ast/must_use_attribute.h
+++ b/src/tint/lang/wgsl/ast/must_use_attribute.h
@@ -38,7 +38,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const MustUseAttribute* Clone(CloneContext* ctx) const override;
+    const MustUseAttribute* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/node.h b/src/tint/lang/wgsl/ast/node.h
index 75f84bf..b94aa8e 100644
--- a/src/tint/lang/wgsl/ast/node.h
+++ b/src/tint/lang/wgsl/ast/node.h
@@ -17,16 +17,28 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/ast/node_id.h"
+#include "src/tint/utils/diagnostic/source.h"
+#include "src/tint/utils/generation_id.h"
+#include "src/tint/utils/rtti/castable.h"
+
+// Forward declarations
+namespace tint::ast {
+class CloneContext;
+}
 
 namespace tint::ast {
 
 /// AST base class node
-class Node : public Castable<Node, Cloneable> {
+class Node : public Castable<Node> {
   public:
     ~Node() override;
 
+    /// Performs a deep clone of this object using the CloneContext `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned object
+    virtual const Node* Clone(CloneContext& ctx) const = 0;
+
     /// The identifier of the program that owns this node
     const GenerationID generation_id;
 
diff --git a/src/tint/lang/wgsl/ast/override.cc b/src/tint/lang/wgsl/ast/override.cc
index f7d8291..bdf8135 100644
--- a/src/tint/lang/wgsl/ast/override.cc
+++ b/src/tint/lang/wgsl/ast/override.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Override);
 
@@ -37,13 +38,13 @@
     return "override";
 }
 
-const Override* Override::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto* n = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    auto* init = ctx->Clone(initializer);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<Override>(src, n, ty, init, std::move(attrs));
+const Override* Override::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto* n = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    auto* init = ctx.Clone(initializer);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<Override>(src, n, ty, init, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/override.h b/src/tint/lang/wgsl/ast/override.h
index c7fe24c..c3bb0ea 100644
--- a/src/tint/lang/wgsl/ast/override.h
+++ b/src/tint/lang/wgsl/ast/override.h
@@ -58,7 +58,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Override* Clone(CloneContext* ctx) const override;
+    const Override* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/parameter.cc b/src/tint/lang/wgsl/ast/parameter.cc
index d366835..b9b36a8 100644
--- a/src/tint/lang/wgsl/ast/parameter.cc
+++ b/src/tint/lang/wgsl/ast/parameter.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Parameter);
 
@@ -36,12 +37,12 @@
     return "parameter";
 }
 
-const Parameter* Parameter::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto* n = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<Parameter>(src, n, ty, std::move(attrs));
+const Parameter* Parameter::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto* n = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<Parameter>(src, n, ty, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/parameter.h b/src/tint/lang/wgsl/ast/parameter.h
index e6618db..f5f6bd6 100644
--- a/src/tint/lang/wgsl/ast/parameter.h
+++ b/src/tint/lang/wgsl/ast/parameter.h
@@ -57,7 +57,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Parameter* Clone(CloneContext* ctx) const override;
+    const Parameter* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/phony_expression.cc b/src/tint/lang/wgsl/ast/phony_expression.cc
index 13205fc..8e3485f 100644
--- a/src/tint/lang/wgsl/ast/phony_expression.cc
+++ b/src/tint/lang/wgsl/ast/phony_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/phony_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::PhonyExpression);
 
@@ -25,10 +26,10 @@
 
 PhonyExpression::~PhonyExpression() = default;
 
-const PhonyExpression* PhonyExpression::Clone(CloneContext* ctx) const {
+const PhonyExpression* PhonyExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<PhonyExpression>(src);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<PhonyExpression>(src);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/phony_expression.h b/src/tint/lang/wgsl/ast/phony_expression.h
index e03ca34..e96ff78 100644
--- a/src/tint/lang/wgsl/ast/phony_expression.h
+++ b/src/tint/lang/wgsl/ast/phony_expression.h
@@ -36,7 +36,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const PhonyExpression* Clone(CloneContext* ctx) const override;
+    const PhonyExpression* Clone(CloneContext& ctx) const override;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/return_statement.cc b/src/tint/lang/wgsl/ast/return_statement.cc
index b3ae820..0bdd09d 100644
--- a/src/tint/lang/wgsl/ast/return_statement.cc
+++ b/src/tint/lang/wgsl/ast/return_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/return_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::ReturnStatement);
 
@@ -33,11 +34,11 @@
 
 ReturnStatement::~ReturnStatement() = default;
 
-const ReturnStatement* ReturnStatement::Clone(CloneContext* ctx) const {
+const ReturnStatement* ReturnStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* ret = ctx->Clone(value);
-    return ctx->dst->create<ReturnStatement>(src, ret);
+    auto src = ctx.Clone(source);
+    auto* ret = ctx.Clone(value);
+    return ctx.dst->create<ReturnStatement>(src, ret);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/return_statement.h b/src/tint/lang/wgsl/ast/return_statement.h
index 3f68526..82a9d7d 100644
--- a/src/tint/lang/wgsl/ast/return_statement.h
+++ b/src/tint/lang/wgsl/ast/return_statement.h
@@ -43,7 +43,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const ReturnStatement* Clone(CloneContext* ctx) const override;
+    const ReturnStatement* Clone(CloneContext& ctx) const override;
 
     /// The value returned. May be null.
     const Expression* const value;
diff --git a/src/tint/lang/wgsl/ast/stage_attribute.cc b/src/tint/lang/wgsl/ast/stage_attribute.cc
index fbec3f1..87457e7 100644
--- a/src/tint/lang/wgsl/ast/stage_attribute.cc
+++ b/src/tint/lang/wgsl/ast/stage_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StageAttribute);
 
@@ -31,10 +32,10 @@
     return "stage";
 }
 
-const StageAttribute* StageAttribute::Clone(CloneContext* ctx) const {
+const StageAttribute* StageAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<StageAttribute>(src, stage);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<StageAttribute>(src, stage);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/stage_attribute.h b/src/tint/lang/wgsl/ast/stage_attribute.h
index c1385a9..d72e6ef 100644
--- a/src/tint/lang/wgsl/ast/stage_attribute.h
+++ b/src/tint/lang/wgsl/ast/stage_attribute.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StageAttribute* Clone(CloneContext* ctx) const override;
+    const StageAttribute* Clone(CloneContext& ctx) const override;
 
     /// The pipeline stage
     const PipelineStage stage;
diff --git a/src/tint/lang/wgsl/ast/stride_attribute.cc b/src/tint/lang/wgsl/ast/stride_attribute.cc
index 1414bc9..f7981c5 100644
--- a/src/tint/lang/wgsl/ast/stride_attribute.cc
+++ b/src/tint/lang/wgsl/ast/stride_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StrideAttribute);
 
@@ -31,10 +32,10 @@
     return "stride";
 }
 
-const StrideAttribute* StrideAttribute::Clone(CloneContext* ctx) const {
+const StrideAttribute* StrideAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    return ctx->dst->create<StrideAttribute>(src, stride);
+    auto src = ctx.Clone(source);
+    return ctx.dst->create<StrideAttribute>(src, stride);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/stride_attribute.h b/src/tint/lang/wgsl/ast/stride_attribute.h
index 8a51961..bdf559c 100644
--- a/src/tint/lang/wgsl/ast/stride_attribute.h
+++ b/src/tint/lang/wgsl/ast/stride_attribute.h
@@ -41,7 +41,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StrideAttribute* Clone(CloneContext* ctx) const override;
+    const StrideAttribute* Clone(CloneContext& ctx) const override;
 
     /// The stride value
     const uint32_t stride;
diff --git a/src/tint/lang/wgsl/ast/struct.cc b/src/tint/lang/wgsl/ast/struct.cc
index 266b3a4..2ae18e7 100644
--- a/src/tint/lang/wgsl/ast/struct.cc
+++ b/src/tint/lang/wgsl/ast/struct.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Struct);
 
@@ -41,13 +42,13 @@
 
 Struct::~Struct() = default;
 
-const Struct* Struct::Clone(CloneContext* ctx) const {
+const Struct* Struct::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto n = ctx->Clone(name);
-    auto mem = ctx->Clone(members);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<Struct>(src, n, std::move(mem), std::move(attrs));
+    auto src = ctx.Clone(source);
+    auto n = ctx.Clone(name);
+    auto mem = ctx.Clone(members);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<Struct>(src, n, std::move(mem), std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/struct.h b/src/tint/lang/wgsl/ast/struct.h
index c91be45..a01ae0e 100644
--- a/src/tint/lang/wgsl/ast/struct.h
+++ b/src/tint/lang/wgsl/ast/struct.h
@@ -49,7 +49,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Struct* Clone(CloneContext* ctx) const override;
+    const Struct* Clone(CloneContext& ctx) const override;
 
     /// The members
     const tint::Vector<const StructMember*, 8> members;
diff --git a/src/tint/lang/wgsl/ast/struct_member.cc b/src/tint/lang/wgsl/ast/struct_member.cc
index 4ec5e7d..46ef545 100644
--- a/src/tint/lang/wgsl/ast/struct_member.cc
+++ b/src/tint/lang/wgsl/ast/struct_member.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/struct_member.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMember);
 
@@ -41,13 +42,13 @@
 
 StructMember::~StructMember() = default;
 
-const StructMember* StructMember::Clone(CloneContext* ctx) const {
+const StructMember* StructMember::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto n = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<StructMember>(src, n, ty, std::move(attrs));
+    auto src = ctx.Clone(source);
+    auto n = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<StructMember>(src, n, ty, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/struct_member.h b/src/tint/lang/wgsl/ast/struct_member.h
index f7cf496..756d05e 100644
--- a/src/tint/lang/wgsl/ast/struct_member.h
+++ b/src/tint/lang/wgsl/ast/struct_member.h
@@ -51,7 +51,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StructMember* Clone(CloneContext* ctx) const override;
+    const StructMember* Clone(CloneContext& ctx) const override;
 
     /// The member name
     const Identifier* const name;
diff --git a/src/tint/lang/wgsl/ast/struct_member_align_attribute.cc b/src/tint/lang/wgsl/ast/struct_member_align_attribute.cc
index 0d29db1..63e0a9d 100644
--- a/src/tint/lang/wgsl/ast/struct_member_align_attribute.cc
+++ b/src/tint/lang/wgsl/ast/struct_member_align_attribute.cc
@@ -16,8 +16,8 @@
 
 #include <string>
 
+#include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/lang/wgsl/ast/clone_context.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberAlignAttribute);
 
@@ -35,11 +35,11 @@
     return "align";
 }
 
-const StructMemberAlignAttribute* StructMemberAlignAttribute::Clone(CloneContext* ctx) const {
+const StructMemberAlignAttribute* StructMemberAlignAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* expr_ = ctx->Clone(expr);
-    return ctx->dst->create<StructMemberAlignAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto* expr_ = ctx.Clone(expr);
+    return ctx.dst->create<StructMemberAlignAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/struct_member_align_attribute.h b/src/tint/lang/wgsl/ast/struct_member_align_attribute.h
index 33931bb..6503764 100644
--- a/src/tint/lang/wgsl/ast/struct_member_align_attribute.h
+++ b/src/tint/lang/wgsl/ast/struct_member_align_attribute.h
@@ -44,7 +44,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StructMemberAlignAttribute* Clone(CloneContext* ctx) const override;
+    const StructMemberAlignAttribute* Clone(CloneContext& ctx) const override;
 
     /// The align expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/struct_member_offset_attribute.cc b/src/tint/lang/wgsl/ast/struct_member_offset_attribute.cc
index 1317cfc..7d8ebca 100644
--- a/src/tint/lang/wgsl/ast/struct_member_offset_attribute.cc
+++ b/src/tint/lang/wgsl/ast/struct_member_offset_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberOffsetAttribute);
 
@@ -34,11 +35,11 @@
     return "offset";
 }
 
-const StructMemberOffsetAttribute* StructMemberOffsetAttribute::Clone(CloneContext* ctx) const {
+const StructMemberOffsetAttribute* StructMemberOffsetAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto expr_ = ctx->Clone(expr);
-    return ctx->dst->create<StructMemberOffsetAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto expr_ = ctx.Clone(expr);
+    return ctx.dst->create<StructMemberOffsetAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/struct_member_offset_attribute.h b/src/tint/lang/wgsl/ast/struct_member_offset_attribute.h
index f69a838..7fb4844 100644
--- a/src/tint/lang/wgsl/ast/struct_member_offset_attribute.h
+++ b/src/tint/lang/wgsl/ast/struct_member_offset_attribute.h
@@ -52,7 +52,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StructMemberOffsetAttribute* Clone(CloneContext* ctx) const override;
+    const StructMemberOffsetAttribute* Clone(CloneContext& ctx) const override;
 
     /// The offset expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/struct_member_size_attribute.cc b/src/tint/lang/wgsl/ast/struct_member_size_attribute.cc
index 154270c..9dd54c3 100644
--- a/src/tint/lang/wgsl/ast/struct_member_size_attribute.cc
+++ b/src/tint/lang/wgsl/ast/struct_member_size_attribute.cc
@@ -16,8 +16,8 @@
 
 #include <string>
 
+#include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/lang/wgsl/ast/clone_context.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberSizeAttribute);
 
@@ -35,11 +35,11 @@
     return "size";
 }
 
-const StructMemberSizeAttribute* StructMemberSizeAttribute::Clone(CloneContext* ctx) const {
+const StructMemberSizeAttribute* StructMemberSizeAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto expr_ = ctx->Clone(expr);
-    return ctx->dst->create<StructMemberSizeAttribute>(src, expr_);
+    auto src = ctx.Clone(source);
+    auto expr_ = ctx.Clone(expr);
+    return ctx.dst->create<StructMemberSizeAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/struct_member_size_attribute.h b/src/tint/lang/wgsl/ast/struct_member_size_attribute.h
index 7f10f3a..f1cb2f0 100644
--- a/src/tint/lang/wgsl/ast/struct_member_size_attribute.h
+++ b/src/tint/lang/wgsl/ast/struct_member_size_attribute.h
@@ -44,7 +44,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const StructMemberSizeAttribute* Clone(CloneContext* ctx) const override;
+    const StructMemberSizeAttribute* Clone(CloneContext& ctx) const override;
 
     /// The size expression
     const Expression* const expr;
diff --git a/src/tint/lang/wgsl/ast/switch_statement.cc b/src/tint/lang/wgsl/ast/switch_statement.cc
index a830f3e..feb2f78 100644
--- a/src/tint/lang/wgsl/ast/switch_statement.cc
+++ b/src/tint/lang/wgsl/ast/switch_statement.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::SwitchStatement);
 
@@ -52,15 +53,15 @@
 
 SwitchStatement::~SwitchStatement() = default;
 
-const SwitchStatement* SwitchStatement::Clone(CloneContext* ctx) const {
+const SwitchStatement* SwitchStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* cond = ctx->Clone(condition);
-    auto b = ctx->Clone(body);
-    auto attrs = ctx->Clone(attributes);
-    auto body_attrs = ctx->Clone(body_attributes);
-    return ctx->dst->create<SwitchStatement>(src, cond, std::move(b), std::move(attrs),
-                                             std::move(body_attrs));
+    auto src = ctx.Clone(source);
+    auto* cond = ctx.Clone(condition);
+    auto b = ctx.Clone(body);
+    auto attrs = ctx.Clone(attributes);
+    auto body_attrs = ctx.Clone(body_attributes);
+    return ctx.dst->create<SwitchStatement>(src, cond, std::move(b), std::move(attrs),
+                                            std::move(body_attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/switch_statement.h b/src/tint/lang/wgsl/ast/switch_statement.h
index 5eba1b7..76b921c 100644
--- a/src/tint/lang/wgsl/ast/switch_statement.h
+++ b/src/tint/lang/wgsl/ast/switch_statement.h
@@ -46,7 +46,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const SwitchStatement* Clone(CloneContext* ctx) const override;
+    const SwitchStatement* Clone(CloneContext& ctx) const override;
 
     /// The switch condition or nullptr if none set
     const Expression* const condition;
diff --git a/src/tint/lang/wgsl/ast/templated_identifier.cc b/src/tint/lang/wgsl/ast/templated_identifier.cc
index 72d54f0..79c178e 100644
--- a/src/tint/lang/wgsl/ast/templated_identifier.cc
+++ b/src/tint/lang/wgsl/ast/templated_identifier.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::TemplatedIdentifier);
 
@@ -40,13 +41,13 @@
 
 TemplatedIdentifier::~TemplatedIdentifier() = default;
 
-const TemplatedIdentifier* TemplatedIdentifier::Clone(CloneContext* ctx) const {
+const TemplatedIdentifier* TemplatedIdentifier::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto sym = ctx->Clone(symbol);
-    auto args = ctx->Clone(arguments);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<TemplatedIdentifier>(src, sym, std::move(args), std::move(attrs));
+    auto src = ctx.Clone(source);
+    auto sym = ctx.Clone(symbol);
+    auto args = ctx.Clone(arguments);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<TemplatedIdentifier>(src, sym, std::move(args), std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/templated_identifier.h b/src/tint/lang/wgsl/ast/templated_identifier.h
index 34d96fe..28dc874 100644
--- a/src/tint/lang/wgsl/ast/templated_identifier.h
+++ b/src/tint/lang/wgsl/ast/templated_identifier.h
@@ -16,6 +16,7 @@
 #define SRC_TINT_LANG_WGSL_AST_TEMPLATED_IDENTIFIER_H_
 
 #include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/utils/containers/vector.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -48,7 +49,7 @@
     /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const TemplatedIdentifier* Clone(CloneContext* ctx) const override;
+    const TemplatedIdentifier* Clone(CloneContext& ctx) const override;
 
     /// The templated arguments
     const tint::Vector<const Expression*, 3> arguments;
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
index 4c8cef0..72394b4 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
@@ -17,6 +17,7 @@
 #include <unordered_set>
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/containers/hashmap.h"
@@ -35,7 +36,7 @@
                                                 const DataMap&,
                                                 DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto& sem = src->Sem();
 
@@ -109,9 +110,9 @@
 }
 
 const AddBlockAttribute::BlockAttribute* AddBlockAttribute::BlockAttribute::Clone(
-    CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<AddBlockAttribute::BlockAttribute>(
-        ctx->dst->ID(), ctx->dst->AllocateNodeID());
+    ast::CloneContext& ctx) const {
+    return ctx.dst->ASTNodes().Create<AddBlockAttribute::BlockAttribute>(ctx.dst->ID(),
+                                                                         ctx.dst->AllocateNodeID());
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
index dc981ef..cd17977 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
@@ -44,7 +44,7 @@
         /// Performs a deep clone of this object using the CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const BlockAttribute* Clone(CloneContext* ctx) const override;
+        const BlockAttribute* Clone(CloneContext& ctx) const override;
     };
 
     /// Constructor
diff --git a/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc b/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc
index 4231905..5894052 100644
--- a/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc
@@ -16,6 +16,7 @@
 
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddEmptyEntryPoint);
@@ -48,7 +49,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     b.Func(b.Symbols().New("unused_entry_point"), {}, b.ty.void_(), {},
            tint::Vector{
diff --git a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
index cb44940..8d1c234 100644
--- a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
+++ b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -181,7 +182,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
 
     /// Iterate over all arrayLength() builtins that operate on
     /// storage buffer variables.
diff --git a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h
index e4b1698..413d89d 100644
--- a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h
+++ b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h
@@ -21,11 +21,6 @@
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 #include "tint/binding_point.h"
 
-// Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
-
 namespace tint::ast::transform {
 
 /// ArrayLengthFromUniform is a transform that implements calls to arrayLength()
diff --git a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
index dd9e643..f63c433 100644
--- a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
+++ b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -44,7 +45,7 @@
                                               const DataMap& inputs,
                                               DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto* remappings = inputs.Get<Remappings>();
     if (!remappings) {
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
index 8bf324b..45047ba 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
@@ -21,6 +21,7 @@
 
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/builtin.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -146,7 +147,7 @@
     /// The destination program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx{&b, src};
+    program::CloneContext ctx{&b, src};
     /// The source clone context
     const sem::Info& sem = src->Sem();
     /// Polyfill functions for binary operators.
diff --git a/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc b/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
index abbe9a5..21a1059 100644
--- a/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
+++ b/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -77,9 +78,9 @@
 }
 
 const CalculateArrayLength::BufferSizeIntrinsic* CalculateArrayLength::BufferSizeIntrinsic::Clone(
-    CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<CalculateArrayLength::BufferSizeIntrinsic>(
-        ctx->dst->ID(), ctx->dst->AllocateNodeID());
+    ast::CloneContext& ctx) const {
+    return ctx.dst->ASTNodes().Create<CalculateArrayLength::BufferSizeIntrinsic>(
+        ctx.dst->ID(), ctx.dst->AllocateNodeID());
 }
 
 CalculateArrayLength::CalculateArrayLength() = default;
@@ -93,7 +94,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     auto& sem = src->Sem();
 
     // get_buffer_size_intrinsic() emits the function decorated with
diff --git a/src/tint/lang/wgsl/ast/transform/calculate_array_length.h b/src/tint/lang/wgsl/ast/transform/calculate_array_length.h
index d9af32d..f492e90 100644
--- a/src/tint/lang/wgsl/ast/transform/calculate_array_length.h
+++ b/src/tint/lang/wgsl/ast/transform/calculate_array_length.h
@@ -20,11 +20,6 @@
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-// Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
-
 namespace tint::ast::transform {
 
 /// CalculateArrayLength is a transform used to replace calls to arrayLength()
@@ -48,10 +43,10 @@
         /// @return "buffer_size"
         std::string InternalName() const override;
 
-        /// Performs a deep clone of this object using the CloneContext `ctx`.
+        /// Performs a deep clone of this object using the program::CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const BufferSizeIntrinsic* Clone(CloneContext* ctx) const override;
+        const BufferSizeIntrinsic* Clone(CloneContext& ctx) const override;
     };
 
     /// Constructor
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index 4dfce6a..4cc81fe 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/builtin/builtin_value.h"
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 
@@ -111,7 +112,9 @@
     };
 
     /// The clone context.
-    CloneContext& ctx;
+    program::CloneContext& ctx;
+    /// The program builder
+    ProgramBuilder& b;
     /// The transform config.
     CanonicalizeEntryPointIO::Config const cfg;
     /// The entry point function (AST).
@@ -141,12 +144,18 @@
 
     /// Constructor
     /// @param context the clone context
+    /// @param builder the program builder
     /// @param config the transform config
     /// @param function the entry point function
-    State(CloneContext& context,
+    State(program::CloneContext& context,
+          ProgramBuilder& builder,
           const CanonicalizeEntryPointIO::Config& config,
           const Function* function)
-        : ctx(context), cfg(config), func_ast(function), func_sem(ctx.src->Sem().Get(function)) {}
+        : ctx(context),
+          b(builder),
+          cfg(config),
+          func_ast(function),
+          func_sem(ctx.src->Sem().Get(function)) {}
 
     /// Clones the attributes from @p in and adds it to @p out. If @p in is a builtin attribute,
     /// then builtin_attrs is updated with the builtin information.
@@ -180,11 +189,11 @@
     /// @param attr the input attribute
     /// @returns the builtin value of the attribute
     builtin::BuiltinValue BuiltinOf(const BuiltinAttribute* attr) {
-        if (attr->generation_id == ctx.dst->ID()) {
+        if (attr->generation_id == b.ID()) {
             // attr belongs to the target program.
             // Obtain the builtin value from #builtin_attrs.
-            if (auto b = builtin_attrs.Get(attr)) {
-                return *b;
+            if (auto builtin = builtin_attrs.Get(attr)) {
+                return *builtin;
             }
         } else {
             // attr belongs to the source program.
@@ -209,7 +218,7 @@
     /// @returns the symbol for the struct parameter
     Symbol InputStructSymbol() {
         if (!wrapper_struct_param_name.IsValid()) {
-            wrapper_struct_param_name = ctx.dst->Sym();
+            wrapper_struct_param_name = b.Sym();
         }
         return wrapper_struct_param_name;
     }
@@ -238,12 +247,12 @@
                 type->is_integer_scalar_or_vector() && !HasAttribute<InterpolateAttribute>(attrs) &&
                 (HasAttribute<LocationAttribute>(attrs) ||
                  cfg.shader_style == ShaderStyle::kSpirv)) {
-                attrs.Push(ctx.dst->Interpolate(builtin::InterpolationType::kFlat,
-                                                builtin::InterpolationSampling::kUndefined));
+                attrs.Push(b.Interpolate(builtin::InterpolationType::kFlat,
+                                         builtin::InterpolationSampling::kUndefined));
             }
 
             // Disable validation for use of the `input` address space.
-            attrs.Push(ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace));
+            attrs.Push(b.Disable(DisabledValidation::kIgnoreAddressSpace));
 
             // In GLSL, if it's a builtin, override the name with the
             // corresponding gl_ builtin name
@@ -252,10 +261,10 @@
                 name = GLSLBuiltinToString(builtin_attr, func_ast->PipelineStage(),
                                            builtin::AddressSpace::kIn);
             }
-            auto symbol = ctx.dst->Symbols().New(name);
+            auto symbol = b.Symbols().New(name);
 
             // Create the global variable and use its value for the shader input.
-            const Expression* value = ctx.dst->Expr(symbol);
+            const Expression* value = b.Expr(symbol);
 
             if (builtin_attr != builtin::BuiltinValue::kUndefined) {
                 if (cfg.shader_style == ShaderStyle::kGlsl) {
@@ -263,27 +272,27 @@
                 } else if (builtin_attr == builtin::BuiltinValue::kSampleMask) {
                     // Vulkan requires the type of a SampleMask builtin to be an array.
                     // Declare it as array<u32, 1> and then load the first element.
-                    ast_type = ctx.dst->ty.array(ast_type, 1_u);
-                    value = ctx.dst->IndexAccessor(value, 0_i);
+                    ast_type = b.ty.array(ast_type, 1_u);
+                    value = b.IndexAccessor(value, 0_i);
                 }
             }
-            ctx.dst->GlobalVar(symbol, ast_type, builtin::AddressSpace::kIn, std::move(attrs));
+            b.GlobalVar(symbol, ast_type, builtin::AddressSpace::kIn, std::move(attrs));
             return value;
         } else if (cfg.shader_style == ShaderStyle::kMsl &&
                    builtin_attr != builtin::BuiltinValue::kUndefined) {
             // If this input is a builtin and we are targeting MSL, then add it to the
             // parameter list and pass it directly to the inner function.
-            Symbol symbol = input_names.emplace(name).second ? ctx.dst->Symbols().Register(name)
-                                                             : ctx.dst->Symbols().New(name);
-            wrapper_ep_parameters.Push(ctx.dst->Param(symbol, ast_type, std::move(attrs)));
-            return ctx.dst->Expr(symbol);
+            Symbol symbol = input_names.emplace(name).second ? b.Symbols().Register(name)
+                                                             : b.Symbols().New(name);
+            wrapper_ep_parameters.Push(b.Param(symbol, ast_type, std::move(attrs)));
+            return b.Expr(symbol);
         } else {
             // Otherwise, move it to the new structure member list.
-            Symbol symbol = input_names.emplace(name).second ? ctx.dst->Symbols().Register(name)
-                                                             : ctx.dst->Symbols().New(name);
+            Symbol symbol = input_names.emplace(name).second ? b.Symbols().Register(name)
+                                                             : b.Symbols().New(name);
             wrapper_struct_param_members.Push(
-                {ctx.dst->Member(symbol, ast_type, std::move(attrs)), location, std::nullopt});
-            return ctx.dst->MemberAccessor(InputStructSymbol(), symbol);
+                {b.Member(symbol, ast_type, std::move(attrs)), location, std::nullopt});
+            return b.MemberAccessor(InputStructSymbol(), symbol);
         }
     }
 
@@ -309,8 +318,8 @@
             func_ast->PipelineStage() == PipelineStage::kVertex &&
             type->is_integer_scalar_or_vector() && HasAttribute<LocationAttribute>(attrs) &&
             !HasAttribute<InterpolateAttribute>(attrs)) {
-            attrs.Push(ctx.dst->Interpolate(builtin::InterpolationType::kFlat,
-                                            builtin::InterpolationSampling::kUndefined));
+            attrs.Push(b.Interpolate(builtin::InterpolationType::kFlat,
+                                     builtin::InterpolationSampling::kUndefined));
         }
 
         // In GLSL, if it's a builtin, override the name with the
@@ -389,7 +398,7 @@
 
         // Construct the original structure using the new shader input objects.
         inner_call_parameters.Push(
-            ctx.dst->Call(ctx.Clone(param->Declaration()->type), inner_struct_values));
+            b.Call(ctx.Clone(param->Declaration()->type), inner_struct_values));
     }
 
     /// Process the entry point return type.
@@ -414,7 +423,7 @@
                 // Extract the original structure member.
                 AddOutput(name, member->Type(), member->Attributes().location,
                           member->Attributes().index, std::move(attributes),
-                          ctx.dst->MemberAccessor(original_result, name));
+                          b.MemberAccessor(original_result, name));
             }
         } else if (!inner_ret_type->Is<type::Void>()) {
             auto attributes =
@@ -422,8 +431,7 @@
 
             // Propagate the non-struct return value as is.
             AddOutput("value", func_sem->ReturnType(), func_sem->ReturnLocation(),
-                      func_sem->ReturnIndex(), std::move(attributes),
-                      ctx.dst->Expr(original_result));
+                      func_sem->ReturnIndex(), std::move(attributes), b.Expr(original_result));
         }
     }
 
@@ -435,63 +443,63 @@
         for (auto& outval : wrapper_output_values) {
             if (BuiltinOf(outval.attributes) == builtin::BuiltinValue::kSampleMask) {
                 // Combine the authored sample mask with the fixed mask.
-                outval.value = ctx.dst->And(outval.value, u32(cfg.fixed_sample_mask));
+                outval.value = b.And(outval.value, u32(cfg.fixed_sample_mask));
                 return;
             }
         }
 
         // No existing sample mask builtin was found, so create a new output value using the fixed
         // sample mask.
-        auto* builtin = ctx.dst->Builtin(builtin::BuiltinValue::kSampleMask);
+        auto* builtin = b.Builtin(builtin::BuiltinValue::kSampleMask);
         builtin_attrs.Add(builtin, builtin::BuiltinValue::kSampleMask);
-        AddOutput("fixed_sample_mask", ctx.dst->create<type::U32>(), std::nullopt, std::nullopt,
-                  {builtin}, ctx.dst->Expr(u32(cfg.fixed_sample_mask)));
+        AddOutput("fixed_sample_mask", b.create<type::U32>(), std::nullopt, std::nullopt, {builtin},
+                  b.Expr(u32(cfg.fixed_sample_mask)));
     }
 
     /// Add a point size builtin to the wrapper function output.
     void AddVertexPointSize() {
         // Create a new output value and assign it a literal 1.0 value.
-        auto* builtin = ctx.dst->Builtin(builtin::BuiltinValue::kPointSize);
+        auto* builtin = b.Builtin(builtin::BuiltinValue::kPointSize);
         builtin_attrs.Add(builtin, builtin::BuiltinValue::kPointSize);
-        AddOutput("vertex_point_size", ctx.dst->create<type::F32>(), std::nullopt, std::nullopt,
-                  {builtin}, ctx.dst->Expr(1_f));
+        AddOutput("vertex_point_size", b.create<type::F32>(), std::nullopt, std::nullopt, {builtin},
+                  b.Expr(1_f));
     }
 
     /// Create an expression for gl_Position.[component]
     /// @param component the component of gl_Position to access
     /// @returns the new expression
     const Expression* GLPosition(const char* component) {
-        Symbol pos = ctx.dst->Symbols().Register("gl_Position");
-        Symbol c = ctx.dst->Symbols().Register(component);
-        return ctx.dst->MemberAccessor(ctx.dst->Expr(pos), c);
+        Symbol pos = b.Symbols().Register("gl_Position");
+        Symbol c = b.Symbols().Register(component);
+        return b.MemberAccessor(b.Expr(pos), c);
     }
 
     /// Comparison function used to reorder struct members such that all members with
     /// location attributes appear first (ordered by location slot), followed by
     /// those with builtin attributes.
-    /// @param a a struct member
-    /// @param b another struct member
+    /// @param x a struct member
+    /// @param y another struct member
     /// @returns true if a comes before b
-    bool StructMemberComparator(const MemberInfo& a, const MemberInfo& b) {
-        auto* a_loc = GetAttribute<LocationAttribute>(a.member->attributes);
-        auto* b_loc = GetAttribute<LocationAttribute>(b.member->attributes);
-        auto* a_blt = GetAttribute<BuiltinAttribute>(a.member->attributes);
-        auto* b_blt = GetAttribute<BuiltinAttribute>(b.member->attributes);
-        if (a_loc) {
-            if (!b_loc) {
+    bool StructMemberComparator(const MemberInfo& x, const MemberInfo& y) {
+        auto* x_loc = GetAttribute<LocationAttribute>(x.member->attributes);
+        auto* y_loc = GetAttribute<LocationAttribute>(y.member->attributes);
+        auto* x_blt = GetAttribute<BuiltinAttribute>(x.member->attributes);
+        auto* y_blt = GetAttribute<BuiltinAttribute>(y.member->attributes);
+        if (x_loc) {
+            if (!y_loc) {
                 // `a` has location attribute and `b` does not: `a` goes first.
                 return true;
             }
             // Both have location attributes: smallest goes first.
-            return a.location < b.location;
+            return x.location < y.location;
         } else {
-            if (b_loc) {
+            if (y_loc) {
                 // `b` has location attribute and `a` does not: `b` goes first.
                 return false;
             }
             // Both are builtins: order matters for FXC.
-            auto builtin_a = BuiltinOf(a_blt);
-            auto builtin_b = BuiltinOf(b_blt);
+            auto builtin_a = BuiltinOf(x_blt);
+            auto builtin_b = BuiltinOf(y_blt);
             return BuiltinOrder(builtin_a) < BuiltinOrder(builtin_b);
         }
     }
@@ -499,7 +507,7 @@
     void CreateInputStruct() {
         // Sort the struct members to satisfy HLSL interfacing matching rules.
         std::sort(wrapper_struct_param_members.begin(), wrapper_struct_param_members.end(),
-                  [&](auto& a, auto& b) { return StructMemberComparator(a, b); });
+                  [&](auto& x, auto& y) { return StructMemberComparator(x, y); });
 
         tint::Vector<const StructMember*, 8> members;
         for (auto& mem : wrapper_struct_param_members) {
@@ -507,13 +515,12 @@
         }
 
         // Create the new struct type.
-        auto struct_name = ctx.dst->Sym();
-        auto* in_struct =
-            ctx.dst->create<Struct>(ctx.dst->Ident(struct_name), std::move(members), tint::Empty);
+        auto struct_name = b.Sym();
+        auto* in_struct = b.create<Struct>(b.Ident(struct_name), std::move(members), Empty);
         ctx.InsertBefore(ctx.src->AST().GlobalDeclarations(), func_ast, in_struct);
 
         // Create a new function parameter using this struct type.
-        auto* param = ctx.dst->Param(InputStructSymbol(), ctx.dst->ty(struct_name));
+        auto* param = b.Param(InputStructSymbol(), b.ty(struct_name));
         wrapper_ep_parameters.Push(param);
     }
 
@@ -522,7 +529,7 @@
     Struct* CreateOutputStruct() {
         tint::Vector<const Statement*, 8> assignments;
 
-        auto wrapper_result = ctx.dst->Symbols().New("wrapper_result");
+        auto wrapper_result = b.Symbols().New("wrapper_result");
 
         // Create the struct members and their corresponding assignment statements.
         std::unordered_set<std::string> member_names;
@@ -530,22 +537,21 @@
             // Use the original output name, unless that is already taken.
             Symbol name;
             if (member_names.count(outval.name)) {
-                name = ctx.dst->Symbols().New(outval.name);
+                name = b.Symbols().New(outval.name);
             } else {
-                name = ctx.dst->Symbols().Register(outval.name);
+                name = b.Symbols().Register(outval.name);
             }
             member_names.insert(name.Name());
 
             wrapper_struct_output_members.Push(
-                {ctx.dst->Member(name, outval.type, std::move(outval.attributes)), outval.location,
+                {b.Member(name, outval.type, std::move(outval.attributes)), outval.location,
                  std::nullopt});
-            assignments.Push(
-                ctx.dst->Assign(ctx.dst->MemberAccessor(wrapper_result, name), outval.value));
+            assignments.Push(b.Assign(b.MemberAccessor(wrapper_result, name), outval.value));
         }
 
         // Sort the struct members to satisfy HLSL interfacing matching rules.
         std::sort(wrapper_struct_output_members.begin(), wrapper_struct_output_members.end(),
-                  [&](auto& a, auto& b) { return StructMemberComparator(a, b); });
+                  [&](auto& x, auto& y) { return StructMemberComparator(x, y); });
 
         tint::Vector<const StructMember*, 8> members;
         for (auto& mem : wrapper_struct_output_members) {
@@ -553,17 +559,16 @@
         }
 
         // Create the new struct type.
-        auto* out_struct = ctx.dst->create<Struct>(ctx.dst->Ident(ctx.dst->Sym()),
-                                                   std::move(members), tint::Empty);
+        auto* out_struct = b.create<Struct>(b.Ident(b.Sym()), std::move(members), Empty);
         ctx.InsertBefore(ctx.src->AST().GlobalDeclarations(), func_ast, out_struct);
 
         // Create the output struct object, assign its members, and return it.
-        auto* result_object = ctx.dst->Var(wrapper_result, ctx.dst->ty(out_struct->name->symbol));
-        wrapper_body.Push(ctx.dst->Decl(result_object));
+        auto* result_object = b.Var(wrapper_result, b.ty(out_struct->name->symbol));
+        wrapper_body.Push(b.Decl(result_object));
         for (auto* assignment : assignments) {
             wrapper_body.Push(assignment);
         }
-        wrapper_body.Push(ctx.dst->Return(wrapper_result));
+        wrapper_body.Push(b.Return(wrapper_result));
 
         return out_struct;
     }
@@ -573,20 +578,20 @@
         for (auto& outval : wrapper_output_values) {
             // Disable validation for use of the `output` address space.
             auto attributes = std::move(outval.attributes);
-            attributes.Push(ctx.dst->Disable(DisabledValidation::kIgnoreAddressSpace));
+            attributes.Push(b.Disable(DisabledValidation::kIgnoreAddressSpace));
 
             // Create the global variable and assign it the output value.
-            auto name = ctx.dst->Symbols().New(outval.name);
+            auto name = b.Symbols().New(outval.name);
             Type type = outval.type;
-            const Expression* lhs = ctx.dst->Expr(name);
+            const Expression* lhs = b.Expr(name);
             if (BuiltinOf(attributes) == builtin::BuiltinValue::kSampleMask) {
                 // Vulkan requires the type of a SampleMask builtin to be an array.
                 // Declare it as array<u32, 1> and then store to the first element.
-                type = ctx.dst->ty.array(type, 1_u);
-                lhs = ctx.dst->IndexAccessor(lhs, 0_i);
+                type = b.ty.array(type, 1_u);
+                lhs = b.IndexAccessor(lhs, 0_i);
             }
-            ctx.dst->GlobalVar(name, type, builtin::AddressSpace::kOut, std::move(attributes));
-            wrapper_body.Push(ctx.dst->Assign(lhs, outval.value));
+            b.GlobalVar(name, type, builtin::AddressSpace::kOut, std::move(attributes));
+            wrapper_body.Push(b.Assign(lhs, outval.value));
         }
     }
 
@@ -602,19 +607,19 @@
             // Add a suffix to the function name, as the wrapper function will take
             // the original entry point name.
             auto ep_name = func_ast->name->symbol.Name();
-            inner_name = ctx.dst->Symbols().New(ep_name + "_inner");
+            inner_name = b.Symbols().New(ep_name + "_inner");
         }
 
         // Clone everything, dropping the function and return type attributes.
         // The parameter attributes will have already been stripped during
         // processing.
-        auto* inner_function = ctx.dst->create<Function>(
-            ctx.dst->Ident(inner_name), ctx.Clone(func_ast->params),
-            ctx.Clone(func_ast->return_type), ctx.Clone(func_ast->body), tint::Empty, tint::Empty);
+        auto* inner_function = b.create<Function>(b.Ident(inner_name), ctx.Clone(func_ast->params),
+                                                  ctx.Clone(func_ast->return_type),
+                                                  ctx.Clone(func_ast->body), Empty, Empty);
         ctx.Replace(func_ast, inner_function);
 
         // Call the function.
-        return ctx.dst->Call(inner_function->name->symbol, inner_call_parameters);
+        return b.Call(inner_function->name->symbol, inner_call_parameters);
     }
 
     /// Process the entry point function.
@@ -657,14 +662,14 @@
         auto* call_inner = CallInnerFunction();
 
         // Process the return type, and start building the wrapper function body.
-        std::function<Type()> wrapper_ret_type = [&] { return ctx.dst->ty.void_(); };
+        std::function<Type()> wrapper_ret_type = [&] { return b.ty.void_(); };
         if (func_sem->ReturnType()->Is<type::Void>()) {
             // The function call is just a statement with no result.
-            wrapper_body.Push(ctx.dst->CallStmt(call_inner));
+            wrapper_body.Push(b.CallStmt(call_inner));
         } else {
             // Capture the result of calling the original function.
-            auto* inner_result = ctx.dst->Let(ctx.dst->Symbols().New("inner_result"), call_inner);
-            wrapper_body.Push(ctx.dst->Decl(inner_result));
+            auto* inner_result = b.Let(b.Symbols().New("inner_result"), call_inner);
+            wrapper_body.Push(b.Decl(inner_result));
 
             // Process the original return type to determine the outputs that the
             // outer function needs to produce.
@@ -687,22 +692,19 @@
                 CreateGlobalOutputVariables();
             } else {
                 auto* output_struct = CreateOutputStruct();
-                wrapper_ret_type = [&, output_struct] {
-                    return ctx.dst->ty(output_struct->name->symbol);
-                };
+                wrapper_ret_type = [&, output_struct] { return b.ty(output_struct->name->symbol); };
             }
         }
 
         if (cfg.shader_style == ShaderStyle::kGlsl &&
             func_ast->PipelineStage() == PipelineStage::kVertex) {
             auto* pos_y = GLPosition("y");
-            auto* negate_pos_y =
-                ctx.dst->create<UnaryOpExpression>(UnaryOp::kNegation, GLPosition("y"));
-            wrapper_body.Push(ctx.dst->Assign(pos_y, negate_pos_y));
+            auto* negate_pos_y = b.create<UnaryOpExpression>(UnaryOp::kNegation, GLPosition("y"));
+            wrapper_body.Push(b.Assign(pos_y, negate_pos_y));
 
-            auto* two_z = ctx.dst->Mul(ctx.dst->Expr(2_f), GLPosition("z"));
-            auto* fixed_z = ctx.dst->Sub(two_z, GLPosition("w"));
-            wrapper_body.Push(ctx.dst->Assign(GLPosition("z"), fixed_z));
+            auto* two_z = b.Mul(b.Expr(2_f), GLPosition("z"));
+            auto* fixed_z = b.Sub(two_z, GLPosition("w"));
+            wrapper_body.Push(b.Assign(GLPosition("z"), fixed_z));
         }
 
         // Create the wrapper entry point function.
@@ -710,14 +712,14 @@
         // entry point function.
         Symbol name;
         if (cfg.shader_style == ShaderStyle::kGlsl) {
-            name = ctx.dst->Symbols().New("main");
+            name = b.Symbols().New("main");
         } else {
             name = ctx.Clone(func_ast->name->symbol);
         }
 
-        auto* wrapper_func = ctx.dst->create<Function>(
-            ctx.dst->Ident(name), wrapper_ep_parameters, ctx.dst->ty(wrapper_ret_type()),
-            ctx.dst->Block(wrapper_body), ctx.Clone(func_ast->attributes), tint::Empty);
+        auto* wrapper_func =
+            b.create<Function>(b.Ident(name), wrapper_ep_parameters, b.ty(wrapper_ret_type()),
+                               b.Block(wrapper_body), ctx.Clone(func_ast->attributes), Empty);
         ctx.InsertAfter(ctx.src->AST().GlobalDeclarations(), func_ast, wrapper_func);
     }
 
@@ -784,15 +786,15 @@
             case builtin::BuiltinValue::kInstanceIndex:
             case builtin::BuiltinValue::kSampleIndex:
                 // GLSL uses i32 for these, so bitcast to u32.
-                value = ctx.dst->Bitcast(ast_type, value);
-                ast_type = ctx.dst->ty.i32();
+                value = b.Bitcast(ast_type, value);
+                ast_type = b.ty.i32();
                 break;
             case builtin::BuiltinValue::kSampleMask:
                 // gl_SampleMask is an array of i32. Retrieve the first element and
                 // bitcast it to u32.
-                value = ctx.dst->IndexAccessor(value, 0_i);
-                value = ctx.dst->Bitcast(ast_type, value);
-                ast_type = ctx.dst->ty.array(ctx.dst->ty.i32(), 1_u);
+                value = b.IndexAccessor(value, 0_i);
+                value = b.Bitcast(ast_type, value);
+                ast_type = b.ty.array(b.ty.i32(), 1_u);
                 break;
             default:
                 break;
@@ -814,8 +816,8 @@
             case builtin::BuiltinValue::kInstanceIndex:
             case builtin::BuiltinValue::kSampleIndex:
             case builtin::BuiltinValue::kSampleMask:
-                type = ctx.dst->create<type::I32>();
-                value = ctx.dst->Bitcast(CreateASTTypeFor(ctx, type), value);
+                type = b.create<type::I32>();
+                value = b.Bitcast(CreateASTTypeFor(ctx, type), value);
                 break;
             default:
                 break;
@@ -828,7 +830,7 @@
                                                        const DataMap& inputs,
                                                        DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
@@ -856,7 +858,7 @@
             continue;
         }
 
-        State state(ctx, *cfg, func_ast);
+        State state(ctx, b, *cfg, func_ast);
         state.Process();
     }
 
diff --git a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
index ccdd747..0964a81 100644
--- a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
+++ b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/function.h"
 #include "src/tint/lang/wgsl/ast/module.h"
 #include "src/tint/lang/wgsl/ast/struct.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -40,7 +41,7 @@
     /// The target program builder
     ProgramBuilder b{};
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// The sem::Info of the program
     const sem::Info& sem = src->Sem();
     /// The symbols of the program
diff --git a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
index 710c72b..464a608 100644
--- a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
+++ b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
@@ -17,11 +17,6 @@
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-// Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
-
 namespace tint::ast::transform {
 
 /// Add clamping of the `@builtin(frag_depth)` output of fragment shaders using two push constants
diff --git a/src/tint/lang/wgsl/ast/transform/combine_samplers.cc b/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
index f4b9fa3..5b8c7da 100644
--- a/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
+++ b/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
@@ -19,6 +19,7 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -53,7 +54,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
 
     /// The binding info
     const BindingInfo* binding_info;
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc b/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
index fd5aa24..18267a1 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
@@ -27,6 +27,7 @@
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/ast/unary_op.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -64,7 +65,7 @@
 /// offsets for storage and uniform buffer accesses.
 struct Offset : Castable<Offset> {
     /// @returns builds and returns the Expression in `ctx.dst`
-    virtual const Expression* Build(CloneContext& ctx) const = 0;
+    virtual const Expression* Build(program::CloneContext& ctx) const = 0;
 };
 
 /// OffsetExpr is an implementation of Offset that clones and casts the given
@@ -74,7 +75,7 @@
 
     explicit OffsetExpr(const Expression* e) : expr(e) {}
 
-    const Expression* Build(CloneContext& ctx) const override {
+    const Expression* Build(program::CloneContext& ctx) const override {
         auto* type = ctx.src->Sem().GetVal(expr)->Type()->UnwrapRef();
         auto* res = ctx.Clone(expr);
         if (!type->Is<type::U32>()) {
@@ -91,7 +92,7 @@
 
     explicit OffsetLiteral(uint32_t lit) : literal(lit) {}
 
-    const Expression* Build(CloneContext& ctx) const override {
+    const Expression* Build(program::CloneContext& ctx) const override {
         return ctx.dst->Expr(u32(literal));
     }
 };
@@ -103,7 +104,7 @@
     Offset const* lhs = nullptr;
     Offset const* rhs = nullptr;
 
-    const Expression* Build(CloneContext& ctx) const override {
+    const Expression* Build(program::CloneContext& ctx) const override {
         return ctx.dst->create<BinaryExpression>(op, lhs->Build(ctx), rhs->Build(ctx));
     }
 };
@@ -219,7 +220,7 @@
 
 /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied to a stub function to
 /// load the type @p ty from the uniform or storage buffer with name @p buffer.
-DecomposeMemoryAccess::Intrinsic* IntrinsicLoadFor(ProgramBuilder* builder,
+DecomposeMemoryAccess::Intrinsic* IntrinsicLoadFor(ast::Builder* builder,
                                                    const type::Type* ty,
                                                    builtin::AddressSpace address_space,
                                                    const Symbol& buffer) {
@@ -234,7 +235,7 @@
 
 /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied to a stub function to
 /// store the type @p ty to the storage buffer with name @p buffer.
-DecomposeMemoryAccess::Intrinsic* IntrinsicStoreFor(ProgramBuilder* builder,
+DecomposeMemoryAccess::Intrinsic* IntrinsicStoreFor(ast::Builder* builder,
                                                     const type::Type* ty,
                                                     const Symbol& buffer) {
     DecomposeMemoryAccess::Intrinsic::DataType type;
@@ -248,7 +249,7 @@
 
 /// @returns a DecomposeMemoryAccess::Intrinsic attribute that can be applied to a stub function for
 /// the atomic op and the type @p ty.
-DecomposeMemoryAccess::Intrinsic* IntrinsicAtomicFor(ProgramBuilder* builder,
+DecomposeMemoryAccess::Intrinsic* IntrinsicAtomicFor(ast::Builder* builder,
                                                      builtin::Function ity,
                                                      const type::Type* ty,
                                                      const Symbol& buffer) {
@@ -321,9 +322,9 @@
 /// PIMPL state for the transform
 struct DecomposeMemoryAccess::State {
     /// The clone context
-    CloneContext& ctx;
+    program::CloneContext& ctx;
     /// Alias to `*ctx.dst`
-    ProgramBuilder& b;
+    ast::Builder& b;
     /// Map of AST expression to storage or uniform buffer access
     /// This map has entries added when encountered, and removed when outer
     /// expressions chain the access.
@@ -344,8 +345,8 @@
     BlockAllocator<Offset> offsets_;
 
     /// Constructor
-    /// @param context the CloneContext
-    explicit State(CloneContext& context) : ctx(context), b(*ctx.dst) {}
+    /// @param context the program::CloneContext
+    explicit State(program::CloneContext& context) : ctx(context), b(*ctx.dst) {}
 
     /// @param offset the offset value to wrap in an Offset
     /// @returns an Offset for the given literal value
@@ -772,10 +773,10 @@
 }
 
 const DecomposeMemoryAccess::Intrinsic* DecomposeMemoryAccess::Intrinsic::Clone(
-    CloneContext* ctx) const {
-    auto buf = ctx->Clone(Buffer());
-    return ctx->dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
-        ctx->dst->ID(), ctx->dst->AllocateNodeID(), op, type, address_space, buf);
+    ast::CloneContext& ctx) const {
+    auto buf = ctx.Clone(Buffer());
+    return ctx.dst->ASTNodes().Create<DecomposeMemoryAccess::Intrinsic>(
+        ctx.dst->ID(), ctx.dst->AllocateNodeID(), op, type, address_space, buf);
 }
 
 bool DecomposeMemoryAccess::Intrinsic::IsAtomic() const {
@@ -798,7 +799,7 @@
 
     auto& sem = src->Sem();
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     State state(ctx);
 
     // Scan the AST nodes for storage and uniform buffer accesses. Complex
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.h b/src/tint/lang/wgsl/ast/transform/decompose_memory_access.h
index bc9f39c..0c87d77 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.h
+++ b/src/tint/lang/wgsl/ast/transform/decompose_memory_access.h
@@ -20,11 +20,6 @@
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-// Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
-
 namespace tint::ast::transform {
 
 /// DecomposeMemoryAccess is a transform used to replace storage and uniform buffer accesses with a
@@ -94,10 +89,10 @@
         /// displayed as `@internal(<name>)`
         std::string InternalName() const override;
 
-        /// Performs a deep clone of this object using the CloneContext `ctx`.
+        /// Performs a deep clone of this object using the program::CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const Intrinsic* Clone(CloneContext* ctx) const override;
+        const Intrinsic* Clone(CloneContext& ctx) const override;
 
         /// @return true if op is atomic
         bool IsAtomic() const;
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
index b477646..8982cf0 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
@@ -19,6 +19,7 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -60,7 +61,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     const auto& sem = src->Sem();
 
     static constexpr const char* kMemberName = "el";
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
index e6f129e..79778db 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/test_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 using namespace tint::number_suffixes;  // NOLINT
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
index b17c0ee..e3124fe 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
@@ -19,6 +19,7 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
@@ -38,7 +39,7 @@
     const type::Matrix* matrix = nullptr;
 
     /// @returns the identifier of an array that holds an vector column for each row of the matrix.
-    Type array(ProgramBuilder* b) const {
+    Type array(ast::Builder* b) const {
         return b->ty.array(b->ty.vec<f32>(matrix->rows()), u32(matrix->columns()),
                            tint::Vector{
                                b->Stride(stride),
@@ -65,7 +66,7 @@
                                                      const DataMap&,
                                                      DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     // Scan the program for all storage and uniform structure matrix members with
     // a custom stride attribute. Replace these matrices with an equivalent array,
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
index d939a8e..2bdc058 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/transform/test_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 namespace tint::ast::transform {
diff --git a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
index 2e950d9..045d306 100644
--- a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
+++ b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -78,7 +79,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     // Create a module-scope flag that indicates whether the current invocation has been discarded.
     auto flag = b.Symbols().New("tint_discarded");
diff --git a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
index 7cfc434..5d7543f 100644
--- a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
+++ b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/core/type/abstract_int.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -254,7 +255,7 @@
         CloneState state;
         clone_state = &state;
         ctx.Clone();
-        return Program(std::move(*ctx.dst));
+        return Program(std::move(b));
     }
 
   private:
@@ -379,7 +380,7 @@
     /// The program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx;
+    program::CloneContext ctx;
     /// The transform options
     const Options& opts;
     /// Alias to the semantic info in ctx.src
@@ -414,7 +415,7 @@
     };
 
     /// The clone state.
-    /// Only valid during the lifetime of the CloneContext::Clone().
+    /// Only valid during the lifetime of the program::CloneContext::Clone().
     CloneState* clone_state = nullptr;
 
     /// AppendAccessChain creates or extends an existing AccessChain for the given expression,
diff --git a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
index 658b96a..8946945 100644
--- a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
+++ b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
@@ -16,6 +16,7 @@
 
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/module.h"
 
@@ -36,7 +37,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     b.Enable(builtin::Extension::kChromiumDisableUniformityAnalysis);
 
     ctx.Clone();
diff --git a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
index 668d9ed..958a4c2 100644
--- a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
+++ b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/compound_assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/increment_decrement_statement.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -48,7 +49,8 @@
 struct ExpandCompoundAssignment::State {
     /// Constructor
     /// @param context the clone context
-    explicit State(CloneContext& context) : ctx(context), b(*ctx.dst), hoist_to_decl_before(ctx) {}
+    explicit State(program::CloneContext& context)
+        : ctx(context), b(*ctx.dst), hoist_to_decl_before(ctx) {}
 
     /// Replace `stmt` with a regular assignment statement of the form:
     ///     lhs = lhs op rhs
@@ -147,10 +149,10 @@
 
   private:
     /// The clone context.
-    CloneContext& ctx;
+    program::CloneContext& ctx;
 
-    /// The program builder.
-    ProgramBuilder& b;
+    /// The AST builder.
+    ast::Builder& b;
 
     /// The HoistToDeclBefore helper instance.
     HoistToDeclBefore hoist_to_decl_before;
@@ -168,7 +170,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     State state(ctx);
     for (auto* node : src->ASTNodes().Objects()) {
         if (auto* assign = node->As<CompoundAssignmentStatement>()) {
diff --git a/src/tint/lang/wgsl/ast/transform/first_index_offset.cc b/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
index 541f26f..221a0d0 100644
--- a/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
+++ b/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/builtin/builtin_value.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -67,7 +68,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     // Get the uniform buffer binding point
     uint32_t ub_binding = binding_;
diff --git a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc b/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
index 2d81ceb..110263e 100644
--- a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
+++ b/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/utils/containers/hashmap.h"
@@ -32,7 +33,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// The semantic info.
     const sem::Info& sem = {ctx.src->Sem()};
 
diff --git a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc b/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
index aac4039..caecbe6 100644
--- a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
+++ b/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/break_statement.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ForLoopToLoop);
@@ -45,7 +46,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     ctx.ReplaceAll([&](const ForLoopStatement* for_loop) -> const Statement* {
         tint::Vector<const Statement*, 8> stmts;
diff --git a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc b/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
index 1e617bf..387135f 100644
--- a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
+++ b/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -155,7 +156,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
 
     /// Returns true if `expr` contains an index accessor expression to a
     /// structure member of array type.
diff --git a/src/tint/lang/wgsl/ast/transform/manager.cc b/src/tint/lang/wgsl/ast/transform/manager.cc
index 3dc7b77..7a12a43 100644
--- a/src/tint/lang/wgsl/ast/transform/manager.cc
+++ b/src/tint/lang/wgsl/ast/transform/manager.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 /// If set to 1 then the transform::Manager will dump the WGSL of the program
@@ -74,7 +75,7 @@
 
     if (!output) {
         ProgramBuilder b;
-        CloneContext ctx{&b, program, /* auto_clone_symbols */ true};
+        program::CloneContext ctx{&b, program, /* auto_clone_symbols */ true};
         ctx.Clone();
         output = Program(std::move(b));
     }
diff --git a/src/tint/lang/wgsl/ast/transform/manager_test.cc b/src/tint/lang/wgsl/ast/transform/manager_test.cc
index f082eb8..3215305 100644
--- a/src/tint/lang/wgsl/ast/transform/manager_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/manager_test.cc
@@ -18,6 +18,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 namespace tint::ast::transform {
@@ -34,7 +35,7 @@
 class AST_AddFunction final : public ast::transform::Transform {
     ApplyResult Apply(const Program* src, const DataMap&, DataMap&) const override {
         ProgramBuilder b;
-        CloneContext ctx{&b, src};
+        program::CloneContext ctx{&b, src};
         b.Func(b.Sym("ast_func"), {}, b.ty.void_(), {});
         ctx.Clone();
         return Program(std::move(b));
diff --git a/src/tint/lang/wgsl/ast/transform/merge_return.cc b/src/tint/lang/wgsl/ast/transform/merge_return.cc
index ec25627..d3a1ede 100644
--- a/src/tint/lang/wgsl/ast/transform/merge_return.cc
+++ b/src/tint/lang/wgsl/ast/transform/merge_return.cc
@@ -16,6 +16,7 @@
 
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
@@ -72,10 +73,10 @@
 class State {
   private:
     /// The clone context.
-    CloneContext& ctx;
+    program::CloneContext& ctx;
 
-    /// The program builder.
-    ProgramBuilder& b;
+    /// Alias to `*ctx.dst`
+    ast::Builder& b;
 
     /// The function.
     const Function* function;
@@ -92,7 +93,7 @@
   public:
     /// Constructor
     /// @param context the clone context
-    State(CloneContext& context, const Function* func)
+    State(program::CloneContext& context, const Function* func)
         : ctx(context), b(*ctx.dst), function(func) {}
 
     /// Process a statement (recursively).
@@ -216,7 +217,7 @@
 
 Transform::ApplyResult MergeReturn::Apply(const Program* src, const DataMap&, DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     bool made_changes = false;
 
diff --git a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc b/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
index b29ed3f..27748f0 100644
--- a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
@@ -20,11 +20,13 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/module.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/text/string.h"
 
@@ -70,11 +72,11 @@
 /// PIMPL state for the transform
 struct ModuleScopeVarToEntryPointParam::State {
     /// The clone context.
-    CloneContext& ctx;
+    program::CloneContext& ctx;
 
     /// Constructor
     /// @param context the clone context
-    explicit State(CloneContext& context) : ctx(context) {}
+    explicit State(program::CloneContext& context) : ctx(context) {}
 
     /// Clone any struct types that are contained in `ty` (including `ty` itself),
     /// and add it to the global declarations now, so that they precede new global
@@ -593,7 +595,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     State state{ctx};
     state.Process();
 
diff --git a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
index 6689242..9aaa641 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/core/type/texture_dimension.h"
 #include "src/tint/lang/wgsl/ast/function.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -50,10 +51,10 @@
 /// PIMPL state for the transform
 struct MultiplanarExternalTexture::State {
     /// The clone context.
-    CloneContext& ctx;
+    program::CloneContext& ctx;
 
-    /// ProgramBuilder for the context
-    ProgramBuilder& b;
+    /// Alias to `*ctx.dst`
+    ast::Builder& b;
 
     /// Destination binding locations for the expanded texture_external provided
     /// as input into the transform.
@@ -85,7 +86,7 @@
     /// @param context the clone
     /// @param newBindingPoints the input destination binding locations for the
     /// expanded texture_external
-    State(CloneContext& context, const NewBindingPoints* newBindingPoints)
+    State(program::CloneContext& context, const NewBindingPoints* newBindingPoints)
         : ctx(context), b(*context.dst), new_binding_points(newBindingPoints) {}
 
     /// Processes the module
@@ -510,7 +511,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     if (!new_binding_points) {
         b.Diagnostics().add_error(diag::System::Transform, "missing new binding point data for " +
                                                                std::string(TypeInfo().name));
diff --git a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
index f6d1766..3e4563a 100644
--- a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
+++ b/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
@@ -21,6 +21,7 @@
 
 #include "src/tint/lang/core/builtin/builtin_value.h"
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/utils/math/hash.h"
@@ -67,7 +68,7 @@
                                                        const DataMap& inputs,
                                                        DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
diff --git a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h b/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h
index bb86bfc..c05e773 100644
--- a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h
+++ b/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.h
@@ -20,11 +20,6 @@
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 #include "tint/binding_point.h"
 
-// Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
-
 namespace tint::ast::transform {
 
 /// NumWorkgroupsFromUniform is a transform that implements the `num_workgroups`
diff --git a/src/tint/lang/wgsl/ast/transform/packed_vec3.cc b/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
index 688a778..dbc0270 100644
--- a/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
+++ b/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/core/type/vector.h"
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/array_count.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
@@ -505,7 +506,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// Alias to the semantic info in ctx.src
     const sem::Info& sem = ctx.src->Sem();
 };
diff --git a/src/tint/lang/wgsl/ast/transform/pad_structs.cc b/src/tint/lang/wgsl/ast/transform/pad_structs.cc
index 4f2f94f..8403708 100644
--- a/src/tint/lang/wgsl/ast/transform/pad_structs.cc
+++ b/src/tint/lang/wgsl/ast/transform/pad_structs.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/ast/parameter.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/module.h"
@@ -33,9 +34,9 @@
 
 namespace {
 
-void CreatePadding(tint::Vector<const StructMember*, 8>* new_members,
+void CreatePadding(Vector<const StructMember*, 8>* new_members,
                    Hashset<const StructMember*, 8>* padding_members,
-                   ProgramBuilder* b,
+                   ast::Builder* b,
                    uint32_t bytes) {
     const size_t count = bytes / 4u;
     padding_members->Reserve(count);
@@ -56,7 +57,7 @@
 
 Transform::ApplyResult PadStructs::Apply(const Program* src, const DataMap&, DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
     auto& sem = src->Sem();
 
     std::unordered_map<const Struct*, const Struct*> replaced_structs;
diff --git a/src/tint/lang/wgsl/ast/transform/preserve_padding.cc b/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
index 4dda687..2d92ee8 100644
--- a/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
+++ b/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
@@ -18,6 +18,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/utils/containers/map.h"
@@ -227,7 +228,7 @@
     /// The program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx;
+    program::CloneContext ctx;
     /// Alias to the semantic info in ctx.src
     const sem::Info& sem = ctx.src->Sem();
     /// Alias to the symbols in ctx.src
diff --git a/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc b/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc
index 4bb5bee..f4ac2e0 100644
--- a/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc
+++ b/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/core/type/struct.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -37,7 +38,7 @@
                                                        const DataMap&,
                                                        DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     // Returns true if the expression should be hoisted to a new let statement before the
     // expression's statement.
diff --git a/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc b/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
index 1e989f9..0b367f0 100644
--- a/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
+++ b/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
@@ -24,6 +24,8 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -41,11 +43,11 @@
 // Base state class for common members
 class StateBase {
   protected:
-    CloneContext& ctx;
-    ProgramBuilder& b;
+    program::CloneContext& ctx;
+    ast::Builder& b;
     const sem::Info& sem;
 
-    explicit StateBase(CloneContext& ctx_in)
+    explicit StateBase(program::CloneContext& ctx_in)
         : ctx(ctx_in), b(*ctx_in.dst), sem(ctx_in.src->Sem()) {}
 };
 
@@ -60,7 +62,7 @@
                                                            const DataMap&,
                                                            DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     bool made_changes = false;
 
@@ -344,7 +346,7 @@
     }
 
   public:
-    explicit CollectHoistsState(CloneContext& ctx_in) : StateBase(ctx_in) {}
+    explicit CollectHoistsState(program::CloneContext& ctx_in) : StateBase(ctx_in) {}
 
     ToHoistSet Run() {
         // Traverse all statements, recursively processing their expression tree(s)
@@ -615,7 +617,7 @@
     }
 
   public:
-    explicit DecomposeState(CloneContext& ctx_in, ToHoistSet to_hoist_in)
+    explicit DecomposeState(program::CloneContext& ctx_in, ToHoistSet to_hoist_in)
         : StateBase(ctx_in), to_hoist(std::move(to_hoist_in)) {}
 
     void Run() {
@@ -647,7 +649,7 @@
                                                    const DataMap&,
                                                    DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     // First collect side-effecting expressions to hoist
     CollectHoistsState collect_hoists_state{ctx};
diff --git a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc b/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
index 65791eb..8532759 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/continue_statement.h"
 #include "src/tint/lang/wgsl/ast/switch_statement.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -97,7 +98,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// Alias to src->sem
     const sem::Info& sem = src->Sem();
 
diff --git a/src/tint/lang/wgsl/ast/transform/remove_phonies.cc b/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
index 2f51084..15b84ca 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -43,7 +44,7 @@
 
 Transform::ApplyResult RemovePhonies::Apply(const Program* src, const DataMap&, DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto& sem = src->Sem();
 
diff --git a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
index 3828f63..dc9c8ee 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -40,7 +41,7 @@
                                                           const DataMap&,
                                                           DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     bool made_changes = false;
     for (auto* node : src->ASTNodes().Objects()) {
diff --git a/src/tint/lang/wgsl/ast/transform/renamer.cc b/src/tint/lang/wgsl/ast/transform/renamer.cc
index 98375df..c981130 100644
--- a/src/tint/lang/wgsl/ast/transform/renamer.cc
+++ b/src/tint/lang/wgsl/ast/transform/renamer.cc
@@ -17,6 +17,7 @@
 #include <memory>
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -1370,7 +1371,7 @@
     Hashmap<Symbol, Symbol, 32> remappings;
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ false};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ false};
 
     ctx.ReplaceAll([&](const Identifier* ident) -> const Identifier* {
         const auto symbol = ident->symbol;
diff --git a/src/tint/lang/wgsl/ast/transform/robustness.cc b/src/tint/lang/wgsl/ast/transform/robustness.cc
index 3f3e93f..1a8165e 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/builtin.h"
@@ -208,7 +209,7 @@
     /// The target program builder
     ProgramBuilder b{};
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// Helper for hoisting declarations
     HoistToDeclBefore hoist{ctx};
     /// Alias to the source program's semantic info
diff --git a/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc b/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
index 9cf933b..9deb98c 100644
--- a/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
+++ b/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -53,7 +54,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
 
     /// Constructor
     /// @param program the source program
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
index 4019eb7..58135f1 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -17,6 +17,7 @@
 #include <unordered_set>
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -35,7 +36,7 @@
                                                const DataMap& inputs,
                                                DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc b/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
index 96d5525..5507da9 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
+++ b/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "src/tint/lang/core/type/reference.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -52,7 +53,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     std::unordered_map<const type::Struct*, ForkedStruct> forked_structs;
     std::unordered_set<const sem::Variable*> atomic_variables;
     UniqueVector<const sem::ValueExpression*, 8> atomic_expressions;
@@ -300,9 +301,9 @@
     return "@internal(spirv-atomic " + std::string(builtin::str(builtin)) + ")";
 }
 
-const SpirvAtomic::Stub* SpirvAtomic::Stub::Clone(CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<SpirvAtomic::Stub>(ctx->dst->ID(),
-                                                          ctx->dst->AllocateNodeID(), builtin);
+const SpirvAtomic::Stub* SpirvAtomic::Stub::Clone(ast::CloneContext& ctx) const {
+    return ctx.dst->ASTNodes().Create<SpirvAtomic::Stub>(ctx.dst->ID(), ctx.dst->AllocateNodeID(),
+                                                         builtin);
 }
 
 Transform::ApplyResult SpirvAtomic::Apply(const Program* src, const DataMap&, DataMap&) const {
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic.h b/src/tint/lang/wgsl/ast/transform/spirv_atomic.h
index 7a9ccca..8e5cc39 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic.h
+++ b/src/tint/lang/wgsl/ast/transform/spirv_atomic.h
@@ -21,11 +21,6 @@
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-// Forward declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
-
 namespace tint::ast::transform {
 
 /// SpirvAtomic is a transform that replaces calls to stub functions created by the SPIR-V reader
@@ -54,10 +49,10 @@
         /// displayed as `@internal(<name>)`
         std::string InternalName() const override;
 
-        /// Performs a deep clone of this object using the CloneContext `ctx`.
+        /// Performs a deep clone of this object using the program::CloneContext `ctx`.
         /// @param ctx the clone context
         /// @return the newly cloned object
-        const Stub* Clone(CloneContext* ctx) const override;
+        const Stub* Clone(ast::CloneContext& ctx) const override;
 
         /// The type of the intrinsic
         const builtin::Function builtin;
diff --git a/src/tint/lang/wgsl/ast/transform/std140.cc b/src/tint/lang/wgsl/ast/transform/std140.cc
index 0a18620..dfe519a 100644
--- a/src/tint/lang/wgsl/ast/transform/std140.cc
+++ b/src/tint/lang/wgsl/ast/transform/std140.cc
@@ -19,6 +19,7 @@
 #include <utility>
 #include <variant>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -210,7 +211,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// Alias to the semantic info in src
     const sem::Info& sem = src->Sem();
     /// Alias to the symbols in src
diff --git a/src/tint/lang/wgsl/ast/transform/substitute_override.cc b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
index 95362ba..7e9103e 100644
--- a/src/tint/lang/wgsl/ast/transform/substitute_override.cc
+++ b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
@@ -18,6 +18,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/builtin/function.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/builtin.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
@@ -49,7 +50,7 @@
                                                  const DataMap& config,
                                                  DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     const auto* data = config.Get<Config>();
     if (!data) {
diff --git a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc b/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
index a8ed07d..7605cb4 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
+++ b/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -70,7 +71,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
 
     /// Constructor
     /// @param program the source program
diff --git a/src/tint/lang/wgsl/ast/transform/transform.cc b/src/tint/lang/wgsl/ast/transform/transform.cc
index 5b561bb..34cc03b 100644
--- a/src/tint/lang/wgsl/ast/transform/transform.cc
+++ b/src/tint/lang/wgsl/ast/transform/transform.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/core/type/depth_multisampled_texture.h"
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/core/type/sampler.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -42,14 +43,14 @@
         output.program = std::move(program.value());
     } else {
         ProgramBuilder b;
-        CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+        program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
         ctx.Clone();
         output.program = Program(std::move(b));
     }
     return output;
 }
 
-void Transform::RemoveStatement(CloneContext& ctx, const Statement* stmt) {
+void Transform::RemoveStatement(program::CloneContext& ctx, const Statement* stmt) {
     auto* sem = ctx.src->Sem().Get(stmt);
     if (auto* block = tint::As<sem::BlockStatement>(sem->Parent())) {
         ctx.Remove(block->Declaration()->statements, stmt);
@@ -62,7 +63,7 @@
     TINT_ICE() << "unable to remove statement from parent of type " << sem->TypeInfo().name;
 }
 
-Type Transform::CreateASTTypeFor(CloneContext& ctx, const type::Type* ty) {
+Type Transform::CreateASTTypeFor(program::CloneContext& ctx, const type::Type* ty) {
     if (ty->Is<type::Void>()) {
         return Type{};
     }
diff --git a/src/tint/lang/wgsl/ast/transform/transform.h b/src/tint/lang/wgsl/ast/transform/transform.h
index 38a544f..b5a461a 100644
--- a/src/tint/lang/wgsl/ast/transform/transform.h
+++ b/src/tint/lang/wgsl/ast/transform/transform.h
@@ -21,6 +21,11 @@
 #include "src/tint/lang/wgsl/program/program.h"
 #include "src/tint/utils/rtti/castable.h"
 
+// Forward declarations
+namespace tint::program {
+class CloneContext;
+}
+
 namespace tint::ast::transform {
 
 /// The return type of Run()
@@ -83,7 +88,7 @@
     /// @param ctx the clone context
     /// @param ty the semantic type to reconstruct
     /// @returns an Type that when resolved, will produce the semantic type `ty`.
-    static Type CreateASTTypeFor(CloneContext& ctx, const type::Type* ty);
+    static Type CreateASTTypeFor(program::CloneContext& ctx, const type::Type* ty);
 
   protected:
     /// Removes the statement `stmt` from the transformed program.
@@ -91,7 +96,7 @@
     /// continuing of for-loops.
     /// @param ctx the clone context
     /// @param stmt the statement to remove when the program is cloned
-    static void RemoveStatement(CloneContext& ctx, const Statement* stmt);
+    static void RemoveStatement(program::CloneContext& ctx, const Statement* stmt);
 };
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/transform_test.cc b/src/tint/lang/wgsl/ast/transform/transform_test.cc
index 93d190a..09d1321 100644
--- a/src/tint/lang/wgsl/ast/transform/transform_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/transform_test.cc
@@ -14,9 +14,9 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/ast/test_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 #include "gtest/gtest.h"
@@ -36,7 +36,7 @@
         ProgramBuilder sem_type_builder;
         auto* sem_type = create_sem_type(sem_type_builder);
         Program program(std::move(sem_type_builder));
-        CloneContext ctx(&ast_type_builder, &program, false);
+        program::CloneContext ctx(&ast_type_builder, &program, false);
         return CreateASTTypeFor(ctx, sem_type);
     }
 
@@ -110,7 +110,7 @@
 
     auto* arr_ty = program.Sem().Get(alias);
 
-    CloneContext ctx(&ast_type_builder, &program, false);
+    program::CloneContext ctx(&ast_type_builder, &program, false);
     auto ast_ty = CreateASTTypeFor(ctx, arr_ty);
     CheckIdentifier(ast_ty, "A");
 }
diff --git a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc b/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
index 1e6f3e4..06aa9bf 100644
--- a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
+++ b/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
@@ -18,6 +18,7 @@
 #include <string>
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -50,7 +51,7 @@
                                                           const DataMap& config,
                                                           DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     const auto* data = config.Get<Config>();
     if (data == nullptr) {
diff --git a/src/tint/lang/wgsl/ast/transform/unshadow.cc b/src/tint/lang/wgsl/ast/transform/unshadow.cc
index 17da2d1..a2c6a97 100644
--- a/src/tint/lang/wgsl/ast/transform/unshadow.cc
+++ b/src/tint/lang/wgsl/ast/transform/unshadow.cc
@@ -18,6 +18,7 @@
 #include <unordered_map>
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -36,7 +37,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
 
     /// Constructor
     /// @param program the source program
diff --git a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.cc b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.cc
index 641028d..bb0163c 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.cc
@@ -13,6 +13,8 @@
 // limitations under the License.
 
 #include "src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/ice/ice.h"
@@ -20,7 +22,7 @@
 
 namespace tint::ast::transform::utils {
 
-InsertionPoint GetInsertionPoint(CloneContext& ctx, const Statement* stmt) {
+InsertionPoint GetInsertionPoint(program::CloneContext& ctx, const Statement* stmt) {
     auto& sem = ctx.src->Sem();
     using RetType = std::pair<const sem::BlockStatement*, const Statement*>;
 
diff --git a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h
index 1614854..0396e78 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h
+++ b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h
@@ -17,9 +17,14 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 
+// Forward declarations
+namespace tint::program {
+class CloneContext;
+}
+
 namespace tint::ast::transform::utils {
 
 /// InsertionPoint is a pair of the block (`first`) within which, and the
@@ -32,7 +37,7 @@
 /// @param ctx the clone context
 /// @param stmt the statement to insert before or after
 /// @return the insertion point
-InsertionPoint GetInsertionPoint(CloneContext& ctx, const Statement* stmt);
+InsertionPoint GetInsertionPoint(program::CloneContext& ctx, const Statement* stmt);
 
 }  // namespace tint::ast::transform::utils
 
diff --git a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc
index 8a779ce..f690c27 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc
@@ -17,6 +17,7 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/lang/wgsl/ast/transform/test_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/utils/ice/ice.h"
 
@@ -39,7 +40,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     // Can insert in block containing the variable, above or below the input statement.
     auto ip = utils::GetInsertionPoint(ctx, var);
@@ -61,7 +62,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     // Can insert in block containing for-loop above the for-loop itself.
     auto ip = utils::GetInsertionPoint(ctx, var);
@@ -82,7 +83,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     // Can't insert before/after for loop continue statement (would ned to be converted to loop).
     auto ip = utils::GetInsertionPoint(ctx, var);
diff --git a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.cc b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.cc
index 49a2950..5e1b28e 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.cc
+++ b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.cc
@@ -17,7 +17,8 @@
 #include <utility>
 
 #include "src/tint/lang/core/type/reference.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
 #include "src/tint/lang/wgsl/sem/if_statement.h"
@@ -33,7 +34,7 @@
 struct HoistToDeclBefore::State {
     /// Constructor
     /// @param ctx_in the clone context
-    explicit State(CloneContext& ctx_in) : ctx(ctx_in), b(*ctx_in.dst) {}
+    explicit State(program::CloneContext& ctx_in) : ctx(ctx_in), b(*ctx_in.dst) {}
 
     /// @copydoc HoistToDeclBefore::Add()
     bool Add(const sem::ValueExpression* before_expr,
@@ -120,8 +121,8 @@
     }
 
   private:
-    CloneContext& ctx;
-    ProgramBuilder& b;
+    program::CloneContext& ctx;
+    ast::Builder& b;
 
     /// Holds information about a for-loop that needs to be decomposed into a
     /// loop, so that declaration statements can be inserted before the
@@ -406,7 +407,8 @@
     }
 };
 
-HoistToDeclBefore::HoistToDeclBefore(CloneContext& ctx) : state_(std::make_unique<State>(ctx)) {}
+HoistToDeclBefore::HoistToDeclBefore(program::CloneContext& ctx)
+    : state_(std::make_unique<State>(ctx)) {}
 
 HoistToDeclBefore::~HoistToDeclBefore() {}
 
diff --git a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h
index 237860a..adc99ba 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h
+++ b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h
@@ -30,7 +30,7 @@
   public:
     /// Constructor
     /// @param ctx the clone context
-    explicit HoistToDeclBefore(CloneContext& ctx);
+    explicit HoistToDeclBefore(program::CloneContext& ctx);
 
     /// Destructor
     ~HoistToDeclBefore();
diff --git a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc
index 50e8f76..0adc716 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc
@@ -17,6 +17,7 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/lang/wgsl/ast/transform/test_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/if_statement.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
@@ -40,7 +41,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -71,7 +72,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -113,7 +114,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().GetVal(expr);
@@ -151,7 +152,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -194,7 +195,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().GetVal(expr);
@@ -238,7 +239,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().GetVal(expr);
@@ -276,7 +277,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -310,7 +311,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -344,7 +345,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().GetVal(expr);
@@ -381,7 +382,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -427,7 +428,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().GetVal(expr);
@@ -464,7 +465,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(var);
@@ -500,7 +501,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(var);
@@ -538,7 +539,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(var);
@@ -585,7 +586,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(var);
@@ -634,7 +635,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(cont->As<Statement>());
@@ -685,7 +686,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(cont->As<Statement>());
@@ -739,7 +740,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(elseif);
@@ -788,7 +789,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* before_stmt = ctx.src->Sem().Get(elseif);
@@ -828,7 +829,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -858,7 +859,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* sem_expr = ctx.src->Sem().Get(expr);
@@ -890,7 +891,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* target_stmt = ctx.src->Sem().Get(var);
@@ -925,7 +926,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* target_stmt = ctx.src->Sem().Get(var);
@@ -961,7 +962,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* target_stmt = ctx.src->Sem().Get(var);
@@ -1007,7 +1008,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* target_stmt = ctx.src->Sem().Get(var);
@@ -1054,7 +1055,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* target_stmt = ctx.src->Sem().Get(cont->As<Statement>());
@@ -1104,7 +1105,7 @@
 
     Program original(std::move(b));
     ProgramBuilder cloned_b;
-    CloneContext ctx(&cloned_b, &original);
+    program::CloneContext ctx(&cloned_b, &original);
 
     HoistToDeclBefore hoistToDeclBefore(ctx);
     auto* target_stmt = ctx.src->Sem().Get(cont->As<Statement>());
diff --git a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc b/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
index e985fcd..994f9a1 100644
--- a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
+++ b/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::VarForDynamicIndex);
@@ -31,7 +32,7 @@
                                                  const DataMap&,
                                                  DataMap&) const {
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     HoistToDeclBefore hoist_to_decl_before(ctx);
 
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc b/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
index 05d89a8..489311f 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
+++ b/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/type/abstract_numeric.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/value_conversion.h"
@@ -63,7 +64,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     using HelperFunctionKey =
         tint::UnorderedKeyWrapper<std::tuple<const type::Matrix*, const type::Matrix*>>;
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc b/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
index cbdcfc3..8432dab 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
+++ b/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
@@ -18,6 +18,7 @@
 #include <utility>
 
 #include "src/tint/lang/core/type/abstract_numeric.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/value_constructor.h"
@@ -57,7 +58,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     std::unordered_map<const type::Matrix*, Symbol> scalar_inits;
 
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index 40630d3..693f923 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/containers/map.h"
@@ -288,7 +289,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     std::unordered_map<uint32_t, LocationInfo> location_info;
     std::function<const Expression*()> vertex_index_expr = nullptr;
     std::function<const Expression*()> instance_index_expr = nullptr;
diff --git a/src/tint/lang/wgsl/ast/transform/while_to_loop.cc b/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
index 9e815ee..5d0191a 100644
--- a/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
+++ b/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
@@ -17,6 +17,7 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/ast/break_statement.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::WhileToLoop);
@@ -45,7 +46,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     ctx.ReplaceAll([&](const WhileStatement* w) -> const Statement* {
         tint::Vector<const Statement*, 16> stmts;
diff --git a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
index eec3828..5609227 100644
--- a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
+++ b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/builtin/builtin_value.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -53,10 +54,10 @@
 /// PIMPL state for the transform
 struct ZeroInitWorkgroupMemory::State {
     /// The clone context
-    CloneContext& ctx;
+    program::CloneContext& ctx;
 
     /// An alias to *ctx.dst
-    ProgramBuilder& b = *ctx.dst;
+    ast::Builder& b = *ctx.dst;
 
     /// The constant size of the workgroup. If 0, then #workgroup_size_expr should
     /// be used instead.
@@ -124,8 +125,8 @@
     std::unordered_map<ArrayIndex, Symbol, ArrayIndex::Hasher> array_index_names;
 
     /// Constructor
-    /// @param c the CloneContext used for the transform
-    explicit State(CloneContext& c) : ctx(c) {}
+    /// @param c the program::CloneContext used for the transform
+    explicit State(program::CloneContext& c) : ctx(c) {}
 
     /// Run inserts the workgroup memory zero-initialization logic at the top of
     /// the given function
@@ -465,7 +466,7 @@
     }
 
     ProgramBuilder b;
-    CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
     for (auto* fn : src->AST().Functions()) {
         if (fn->PipelineStage() == PipelineStage::kCompute) {
diff --git a/src/tint/lang/wgsl/ast/type.cc b/src/tint/lang/wgsl/ast/type.cc
index b493ab6..95e896a 100644
--- a/src/tint/lang/wgsl/ast/type.cc
+++ b/src/tint/lang/wgsl/ast/type.cc
@@ -15,10 +15,10 @@
 #include "src/tint/lang/wgsl/ast/type.h"
 #include "src/tint/lang/wgsl/ast/identifier_expression.h"
 
-namespace tint::ast {
+namespace tint {
 
 GenerationID GenerationIDOf(ast::Type type) {
     return GenerationIDOf(type.expr);
 }
 
-}  // namespace tint::ast
+}  // namespace tint
diff --git a/src/tint/lang/wgsl/ast/type.h b/src/tint/lang/wgsl/ast/type.h
index a2ca4d3..bb6705e 100644
--- a/src/tint/lang/wgsl/ast/type.h
+++ b/src/tint/lang/wgsl/ast/type.h
@@ -39,10 +39,14 @@
     operator const IdentifierExpression*() const { return expr; }
 };
 
+}  // namespace tint::ast
+
+namespace tint {
+
 /// @param type an AST type
 /// @returns the GenerationID of the given AST type.
 GenerationID GenerationIDOf(ast::Type type);
 
-}  // namespace tint::ast
+}  // namespace tint
 
 #endif  // SRC_TINT_LANG_WGSL_AST_TYPE_H_
diff --git a/src/tint/lang/wgsl/ast/unary_op_expression.cc b/src/tint/lang/wgsl/ast/unary_op_expression.cc
index 6ed63c0..713e79c 100644
--- a/src/tint/lang/wgsl/ast/unary_op_expression.cc
+++ b/src/tint/lang/wgsl/ast/unary_op_expression.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::UnaryOpExpression);
 
@@ -32,11 +33,11 @@
 
 UnaryOpExpression::~UnaryOpExpression() = default;
 
-const UnaryOpExpression* UnaryOpExpression::Clone(CloneContext* ctx) const {
+const UnaryOpExpression* UnaryOpExpression::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* e = ctx->Clone(expr);
-    return ctx->dst->create<UnaryOpExpression>(src, op, e);
+    auto src = ctx.Clone(source);
+    auto* e = ctx.Clone(expr);
+    return ctx.dst->create<UnaryOpExpression>(src, op, e);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/unary_op_expression.h b/src/tint/lang/wgsl/ast/unary_op_expression.h
index 7cc3378..c7bd8b6 100644
--- a/src/tint/lang/wgsl/ast/unary_op_expression.h
+++ b/src/tint/lang/wgsl/ast/unary_op_expression.h
@@ -42,7 +42,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const UnaryOpExpression* Clone(CloneContext* ctx) const override;
+    const UnaryOpExpression* Clone(CloneContext& ctx) const override;
 
     /// The op
     const UnaryOp op;
diff --git a/src/tint/lang/wgsl/ast/var.cc b/src/tint/lang/wgsl/ast/var.cc
index 9b05102..21dda18 100644
--- a/src/tint/lang/wgsl/ast/var.cc
+++ b/src/tint/lang/wgsl/ast/var.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/var.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::Var);
 
@@ -39,15 +40,15 @@
     return "var";
 }
 
-const Var* Var::Clone(CloneContext* ctx) const {
-    auto src = ctx->Clone(source);
-    auto* n = ctx->Clone(name);
-    auto ty = ctx->Clone(type);
-    auto* address_space = ctx->Clone(declared_address_space);
-    auto* access = ctx->Clone(declared_access);
-    auto* init = ctx->Clone(initializer);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<Var>(src, n, ty, address_space, access, init, std::move(attrs));
+const Var* Var::Clone(CloneContext& ctx) const {
+    auto src = ctx.Clone(source);
+    auto* n = ctx.Clone(name);
+    auto ty = ctx.Clone(type);
+    auto* address_space = ctx.Clone(declared_address_space);
+    auto* access = ctx.Clone(declared_access);
+    auto* init = ctx.Clone(initializer);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<Var>(src, n, ty, address_space, access, init, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/var.h b/src/tint/lang/wgsl/ast/var.h
index e34f5ce..5043e77 100644
--- a/src/tint/lang/wgsl/ast/var.h
+++ b/src/tint/lang/wgsl/ast/var.h
@@ -71,7 +71,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const Var* Clone(CloneContext* ctx) const override;
+    const Var* Clone(CloneContext& ctx) const override;
 
     /// The declared address space
     const Expression* const declared_address_space = nullptr;
diff --git a/src/tint/lang/wgsl/ast/variable.h b/src/tint/lang/wgsl/ast/variable.h
index 58a214e..938ccf9 100644
--- a/src/tint/lang/wgsl/ast/variable.h
+++ b/src/tint/lang/wgsl/ast/variable.h
@@ -24,6 +24,7 @@
 #include "src/tint/lang/wgsl/ast/binding_attribute.h"
 #include "src/tint/lang/wgsl/ast/expression.h"
 #include "src/tint/lang/wgsl/ast/group_attribute.h"
+#include "src/tint/lang/wgsl/ast/node.h"
 #include "src/tint/lang/wgsl/ast/type.h"
 
 // Forward declarations
diff --git a/src/tint/lang/wgsl/ast/variable_decl_statement.cc b/src/tint/lang/wgsl/ast/variable_decl_statement.cc
index b9717a9..8c7566b 100644
--- a/src/tint/lang/wgsl/ast/variable_decl_statement.cc
+++ b/src/tint/lang/wgsl/ast/variable_decl_statement.cc
@@ -14,7 +14,8 @@
 
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::VariableDeclStatement);
 
@@ -31,11 +32,11 @@
 
 VariableDeclStatement::~VariableDeclStatement() = default;
 
-const VariableDeclStatement* VariableDeclStatement::Clone(CloneContext* ctx) const {
+const VariableDeclStatement* VariableDeclStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* var = ctx->Clone(variable);
-    return ctx->dst->create<VariableDeclStatement>(src, var);
+    auto src = ctx.Clone(source);
+    auto* var = ctx.Clone(variable);
+    return ctx.dst->create<VariableDeclStatement>(src, var);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/variable_decl_statement.h b/src/tint/lang/wgsl/ast/variable_decl_statement.h
index 92bd4ec..7329f2f 100644
--- a/src/tint/lang/wgsl/ast/variable_decl_statement.h
+++ b/src/tint/lang/wgsl/ast/variable_decl_statement.h
@@ -40,7 +40,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const VariableDeclStatement* Clone(CloneContext* ctx) const override;
+    const VariableDeclStatement* Clone(CloneContext& ctx) const override;
 
     /// The variable
     const Variable* const variable;
diff --git a/src/tint/lang/wgsl/ast/while_statement.cc b/src/tint/lang/wgsl/ast/while_statement.cc
index 3b29852..ff7982b 100644
--- a/src/tint/lang/wgsl/ast/while_statement.cc
+++ b/src/tint/lang/wgsl/ast/while_statement.cc
@@ -16,7 +16,8 @@
 
 #include <utility>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::WhileStatement);
 
@@ -42,14 +43,14 @@
 
 WhileStatement::~WhileStatement() = default;
 
-const WhileStatement* WhileStatement::Clone(CloneContext* ctx) const {
+const WhileStatement* WhileStatement::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
+    auto src = ctx.Clone(source);
 
-    auto* cond = ctx->Clone(condition);
-    auto* b = ctx->Clone(body);
-    auto attrs = ctx->Clone(attributes);
-    return ctx->dst->create<WhileStatement>(src, cond, b, std::move(attrs));
+    auto* cond = ctx.Clone(condition);
+    auto* b = ctx.Clone(body);
+    auto attrs = ctx.Clone(attributes);
+    return ctx.dst->create<WhileStatement>(src, cond, b, std::move(attrs));
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/while_statement.h b/src/tint/lang/wgsl/ast/while_statement.h
index d3eaa0a..64d94a6 100644
--- a/src/tint/lang/wgsl/ast/while_statement.h
+++ b/src/tint/lang/wgsl/ast/while_statement.h
@@ -45,7 +45,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const WhileStatement* Clone(CloneContext* ctx) const override;
+    const WhileStatement* Clone(CloneContext& ctx) const override;
 
     /// The condition expression
     const Expression* const condition;
diff --git a/src/tint/lang/wgsl/ast/workgroup_attribute.cc b/src/tint/lang/wgsl/ast/workgroup_attribute.cc
index dbbc45a..1a6c587 100644
--- a/src/tint/lang/wgsl/ast/workgroup_attribute.cc
+++ b/src/tint/lang/wgsl/ast/workgroup_attribute.cc
@@ -16,7 +16,8 @@
 
 #include <string>
 
-#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/lang/wgsl/ast/clone_context.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::WorkgroupAttribute);
 
@@ -36,13 +37,13 @@
     return "workgroup_size";
 }
 
-const WorkgroupAttribute* WorkgroupAttribute::Clone(CloneContext* ctx) const {
+const WorkgroupAttribute* WorkgroupAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
-    auto src = ctx->Clone(source);
-    auto* x_ = ctx->Clone(x);
-    auto* y_ = ctx->Clone(y);
-    auto* z_ = ctx->Clone(z);
-    return ctx->dst->create<WorkgroupAttribute>(src, x_, y_, z_);
+    auto src = ctx.Clone(source);
+    auto* x_ = ctx.Clone(x);
+    auto* y_ = ctx.Clone(y);
+    auto* z_ = ctx.Clone(z);
+    return ctx.dst->create<WorkgroupAttribute>(src, x_, y_, z_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/workgroup_attribute.h b/src/tint/lang/wgsl/ast/workgroup_attribute.h
index 2494da8..6036285 100644
--- a/src/tint/lang/wgsl/ast/workgroup_attribute.h
+++ b/src/tint/lang/wgsl/ast/workgroup_attribute.h
@@ -56,7 +56,7 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const WorkgroupAttribute* Clone(CloneContext* ctx) const override;
+    const WorkgroupAttribute* Clone(CloneContext& ctx) const override;
 
     /// The workgroup x dimension.
     const Expression* const x;
diff --git a/src/tint/lang/wgsl/program/clone_context.cc b/src/tint/lang/wgsl/program/clone_context.cc
new file mode 100644
index 0000000..f45a407
--- /dev/null
+++ b/src/tint/lang/wgsl/program/clone_context.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 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/wgsl/program/clone_context.h"
+
+#include <string>
+
+#include "src/tint/lang/wgsl/ast/builder.h"
+#include "src/tint/utils/containers/map.h"
+
+namespace tint::program {
+
+CloneContext::CloneContext(ProgramBuilder* to, Program const* from, bool auto_clone_symbols)
+    : dst(to), src(from), ctx_(to, from->ID()) {
+    if (auto_clone_symbols) {
+        // Almost all transforms will want to clone all symbols before doing any
+        // work, to avoid any newly created symbols clashing with existing symbols
+        // in the source program and causing them to be renamed.
+        from->Symbols().Foreach([&](Symbol s) { Clone(s); });
+    }
+}
+
+CloneContext::~CloneContext() = default;
+
+void CloneContext::Clone() {
+    dst->AST().Copy(ctx_, &src->AST());
+}
+
+}  // namespace tint::program
diff --git a/src/tint/lang/wgsl/program/clone_context.h b/src/tint/lang/wgsl/program/clone_context.h
new file mode 100644
index 0000000..9b80385
--- /dev/null
+++ b/src/tint/lang/wgsl/program/clone_context.h
@@ -0,0 +1,147 @@
+// 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_WGSL_PROGRAM_CLONE_CONTEXT_H_
+#define SRC_TINT_LANG_WGSL_PROGRAM_CLONE_CONTEXT_H_
+
+#include <utility>
+
+#include "src/tint/lang/wgsl/ast/clone_context.h"
+#include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+
+// Forward declarations
+namespace tint::program {
+
+/// CloneContext holds the state used while cloning Programs.
+class CloneContext {
+  public:
+    /// SymbolTransform is a function that takes a symbol and returns a new
+    /// symbol.
+    using SymbolTransform = std::function<Symbol(Symbol)>;
+
+    /// Constructor for cloning objects from `from` into `to`.
+    /// @param to the target Builder to clone into
+    /// @param from the source Program to clone from
+    /// @param auto_clone_symbols clone all symbols in `from` before returning
+    CloneContext(ProgramBuilder* to, Program const* from, bool auto_clone_symbols = true);
+
+    /// Destructor
+    ~CloneContext();
+
+    /// @copybrief ast::CloneContext::Clone
+    /// @param args the arguments to forward to ast::CloneContext::Clone
+    /// @return the cloned node
+    template <typename... ARGS>
+    auto Clone(ARGS&&... args) {
+        return ctx_.Clone(std::forward<ARGS>(args)...);
+    }
+
+    /// @copybrief ast::CloneContext::CloneWithoutTransform
+    /// @param args the arguments to forward to ast::CloneContext::CloneWithoutTransform
+    /// @return the cloned node
+    template <typename... ARGS>
+    auto CloneWithoutTransform(ARGS&&... args) {
+        return ctx_.CloneWithoutTransform(std::forward<ARGS>(args)...);
+    }
+
+    /// @copybrief ast::CloneContext::ReplaceAll
+    /// @param args the arguments to forward to ast::CloneContext::ReplaceAll
+    /// @return this CloneContext so calls can be chained
+    template <typename... ARGS>
+    CloneContext& ReplaceAll(ARGS&&... args) {
+        ctx_.ReplaceAll(std::forward<ARGS>(args)...);
+        return *this;
+    }
+
+    /// @copydoc ast::CloneContext::Replace
+    template <typename WHAT, typename WITH, typename = traits::EnableIfIsType<WITH, ast::Node>>
+    CloneContext& Replace(const WHAT* what, const WITH* with) {
+        ctx_.Replace<WHAT, WITH>(what, with);
+        return *this;
+    }
+
+    /// @copydoc ast::CloneContext::Replace
+    template <typename WHAT, typename WITH, typename = std::invoke_result_t<WITH>>
+    CloneContext& Replace(const WHAT* what, WITH&& with) {
+        ctx_.Replace<WHAT, WITH>(what, std::forward<WITH>(with));
+        return *this;
+    }
+
+    /// @copybrief ast::CloneContext::Remove
+    /// @param args the arguments to forward to ast::CloneContext::Remove
+    /// @return this CloneContext so calls can be chained
+    template <typename... ARGS>
+    CloneContext& Remove(ARGS&&... args) {
+        ctx_.Remove(std::forward<ARGS>(args)...);
+        return *this;
+    }
+
+    /// @copybrief ast::CloneContext::InsertFront
+    /// @param args the arguments to forward to ast::CloneContext::InsertFront
+    /// @return this CloneContext so calls can be chained
+    template <typename... ARGS>
+    CloneContext& InsertFront(ARGS&&... args) {
+        ctx_.InsertFront(std::forward<ARGS>(args)...);
+        return *this;
+    }
+
+    /// @copybrief ast::CloneContext::InsertBack
+    /// @param args the arguments to forward to ast::CloneContext::InsertBack
+    /// @return this CloneContext so calls can be chained
+    template <typename... ARGS>
+    CloneContext& InsertBack(ARGS&&... args) {
+        ctx_.InsertBack(std::forward<ARGS>(args)...);
+        return *this;
+    }
+
+    /// @copybrief ast::CloneContext::InsertBefore
+    /// @param args the arguments to forward to ast::CloneContext::InsertBefore
+    /// @return this CloneContext so calls can be chained
+    template <typename... ARGS>
+    CloneContext& InsertBefore(ARGS&&... args) {
+        ctx_.InsertBefore(std::forward<ARGS>(args)...);
+        return *this;
+    }
+
+    /// @copybrief ast::CloneContext::InsertAfter
+    /// @param args the arguments to forward to ast::CloneContext::InsertAfter
+    /// @return this CloneContext so calls can be chained
+    template <typename... ARGS>
+    CloneContext& InsertAfter(ARGS&&... args) {
+        ctx_.InsertAfter(std::forward<ARGS>(args)...);
+        return *this;
+    }
+
+    /// Clone performs the clone of the Program's AST nodes, types and symbols
+    /// from #src to #dst. Semantic nodes are not cloned, as these will be rebuilt
+    /// when the Builder #dst builds its Program.
+    void Clone();
+
+    /// @returns the ast::CloneContext
+    inline operator ast::CloneContext&() { return ctx_; }
+
+    /// The target Builder to clone into.
+    ProgramBuilder* const dst;
+
+    /// The source Program to clone from.
+    Program const* const src;
+
+  private:
+    ast::CloneContext ctx_;
+};
+
+}  // namespace tint::program
+
+#endif  // SRC_TINT_LANG_WGSL_PROGRAM_CLONE_CONTEXT_H_
diff --git a/src/tint/lang/wgsl/program/clone_context_test.cc b/src/tint/lang/wgsl/program/clone_context_test.cc
new file mode 100644
index 0000000..228802e
--- /dev/null
+++ b/src/tint/lang/wgsl/program/clone_context_test.cc
@@ -0,0 +1,1221 @@
+// Copyright 2020 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 <string>
+#include <unordered_set>
+#include <utility>
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+
+namespace tint::program {
+namespace {
+
+struct Allocator {
+    template <typename T, typename... ARGS>
+    T* Create(ARGS&&... args) {
+        return alloc.Create<T>(this, std::forward<ARGS>(args)...);
+    }
+
+  private:
+    BlockAllocator<ast::Node> alloc;
+};
+
+struct Node : public Castable<Node, ast::Node> {
+    Node(Allocator* alloc,
+         Symbol n,
+         const Node* node_a = nullptr,
+         const Node* node_b = nullptr,
+         const Node* node_c = nullptr)
+        : Base(GenerationID{}, ast::NodeID{}, Source{}),
+          allocator(alloc),
+          name(n),
+          a(node_a),
+          b(node_b),
+          c(node_c) {}
+    Node(Node&&) = delete;
+    Allocator* const allocator;
+    Symbol name;
+    const Node* a = nullptr;
+    const Node* b = nullptr;
+    const Node* c = nullptr;
+    Vector<const Node*, 8> vec;
+
+    Node* Clone(ast::CloneContext& ctx) const override {
+        auto* out = allocator->Create<Node>(ctx.Clone(name));
+        out->a = ctx.Clone(a);
+        out->b = ctx.Clone(b);
+        out->c = ctx.Clone(c);
+        out->vec = ctx.Clone(vec);
+        return out;
+    }
+};
+
+struct Replaceable : public Castable<Replaceable, Node> {
+    Replaceable(Allocator* alloc,
+                Symbol n,
+                const Node* node_a = nullptr,
+                const Node* node_b = nullptr,
+                const Node* node_c = nullptr)
+        : Base(alloc, n, node_a, node_b, node_c) {}
+};
+
+struct Replacement : public Castable<Replacement, Replaceable> {
+    Replacement(Allocator* alloc, Symbol n) : Base(alloc, n) {}
+};
+
+struct IDNode : public Castable<IDNode, ast::Node> {
+    IDNode(Allocator* alloc, GenerationID program_id, GenerationID cid)
+        : Base(program_id, ast::NodeID{}, Source{}), allocator(alloc), cloned_id(cid) {}
+
+    Allocator* const allocator;
+    const GenerationID cloned_id;
+
+    IDNode* Clone(ast::CloneContext&) const override {
+        return allocator->Create<IDNode>(cloned_id, cloned_id);
+    }
+};
+
+using ProgramCloneContextNodeTest = ::testing::Test;
+
+TEST_F(ProgramCloneContextNodeTest, Clone) {
+    Allocator alloc;
+
+    ProgramBuilder builder;
+    Node* original_root;
+    {
+        auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
+        auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+        auto* b_a = a;  // Aliased
+        auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
+        auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+        auto* c = b;  // Aliased
+        original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+    }
+    Program original(std::move(builder));
+
+    //                          root
+    //        ╭──────────────────┼──────────────────╮
+    //       (a)                (b)                (c)
+    //        N  <──────┐        N  <───────────────┘
+    //   ╭────┼────╮    │   ╭────┼────╮
+    //  (a)  (b)  (c)   │  (a)  (b)  (c)
+    //        N         └───┘    N
+    //
+    // N: Node
+
+    ProgramBuilder cloned;
+    auto* cloned_root = CloneContext(&cloned, &original).Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, nullptr);
+    EXPECT_EQ(cloned_root->a->a, nullptr);
+    EXPECT_NE(cloned_root->a->b, nullptr);
+    EXPECT_EQ(cloned_root->a->c, nullptr);
+    EXPECT_NE(cloned_root->b, nullptr);
+    EXPECT_NE(cloned_root->b->a, nullptr);
+    EXPECT_NE(cloned_root->b->b, nullptr);
+    EXPECT_EQ(cloned_root->b->c, nullptr);
+    EXPECT_NE(cloned_root->c, nullptr);
+
+    EXPECT_NE(cloned_root->a, original_root->a);
+    EXPECT_NE(cloned_root->a->b, original_root->a->b);
+    EXPECT_NE(cloned_root->b, original_root->b);
+    EXPECT_NE(cloned_root->b->a, original_root->b->a);
+    EXPECT_NE(cloned_root->b->b, original_root->b->b);
+    EXPECT_NE(cloned_root->c, original_root->c);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->a->b->name, cloned.Symbols().Get("a->b"));
+    EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->b->b->name, cloned.Symbols().Get("b->b"));
+
+    EXPECT_NE(cloned_root->b->a, cloned_root->a);  // De-aliased
+    EXPECT_NE(cloned_root->c, cloned_root->b);     // De-aliased
+
+    EXPECT_EQ(cloned_root->b->a->name, cloned_root->a->name);
+    EXPECT_EQ(cloned_root->c->name, cloned_root->b->name);
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplaceAll_Node) {
+    Allocator alloc;
+
+    ProgramBuilder builder;
+    Node* original_root;
+    {
+        auto* a_b = alloc.Create<Replaceable>(builder.Symbols().New("a->b"));
+        auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+        auto* b_a = a;  // Aliased
+        auto* b = alloc.Create<Replaceable>(builder.Symbols().New("b"), b_a, nullptr);
+        auto* c = b;  // Aliased
+        original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+    }
+    Program original(std::move(builder));
+
+    //                          root
+    //        ╭──────────────────┼──────────────────╮
+    //       (a)                (b)                (c)
+    //        N  <──────┐        R  <───────────────┘
+    //   ╭────┼────╮    │   ╭────┼────╮
+    //  (a)  (b)  (c)   │  (a)  (b)  (c)
+    //        R         └───┘
+    //
+    // N: Node
+    // R: Replaceable
+
+    ProgramBuilder cloned;
+
+    CloneContext ctx(&cloned, &original);
+    ctx.ReplaceAll([&](const Replaceable* in) {
+        auto out_name = cloned.Symbols().Register("replacement:" + in->name.Name());
+        auto b_name = cloned.Symbols().Register("replacement-child:" + in->name.Name());
+        auto* out = alloc.Create<Replacement>(out_name);
+        out->b = alloc.Create<Node>(b_name);
+        out->c = ctx.Clone(in->a);
+        return out;
+    });
+    auto* cloned_root = ctx.Clone(original_root);
+
+    //                         root
+    //        ╭─────────────────┼──────────────────╮
+    //       (a)               (b)                (c)
+    //        N  <──────┐       R  <───────────────┘
+    //   ╭────┼────╮    │  ╭────┼────╮
+    //  (a)  (b)  (c)   │ (a)  (b)  (c)
+    //        R         │       N    |
+    //   ╭────┼────╮    └────────────┘
+    //  (a)  (b)  (c)
+    //        N
+    //
+    // N: Node
+    // R: Replacement
+
+    EXPECT_NE(cloned_root->a, nullptr);
+    EXPECT_EQ(cloned_root->a->a, nullptr);
+    EXPECT_NE(cloned_root->a->b, nullptr);     // Replaced
+    EXPECT_EQ(cloned_root->a->b->a, nullptr);  // From replacement
+    EXPECT_NE(cloned_root->a->b->b, nullptr);  // From replacement
+    EXPECT_EQ(cloned_root->a->b->c, nullptr);  // From replacement
+    EXPECT_EQ(cloned_root->a->c, nullptr);
+    EXPECT_NE(cloned_root->b, nullptr);
+    EXPECT_EQ(cloned_root->b->a, nullptr);  // From replacement
+    EXPECT_NE(cloned_root->b->b, nullptr);  // From replacement
+    EXPECT_NE(cloned_root->b->c, nullptr);  // From replacement
+    EXPECT_NE(cloned_root->c, nullptr);
+
+    EXPECT_NE(cloned_root->a, original_root->a);
+    EXPECT_NE(cloned_root->a->b, original_root->a->b);
+    EXPECT_NE(cloned_root->b, original_root->b);
+    EXPECT_NE(cloned_root->b->a, original_root->b->a);
+    EXPECT_NE(cloned_root->c, original_root->c);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->a->b->name, cloned.Symbols().Get("replacement:a->b"));
+    EXPECT_EQ(cloned_root->a->b->b->name, cloned.Symbols().Get("replacement-child:a->b"));
+    EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("replacement:b"));
+    EXPECT_EQ(cloned_root->b->b->name, cloned.Symbols().Get("replacement-child:b"));
+
+    EXPECT_NE(cloned_root->b->c, cloned_root->a);  // De-aliased
+    EXPECT_NE(cloned_root->c, cloned_root->b);     // De-aliased
+
+    EXPECT_EQ(cloned_root->b->c->name, cloned_root->a->name);
+    EXPECT_EQ(cloned_root->c->name, cloned_root->b->name);
+
+    EXPECT_FALSE(Is<Replacement>(cloned_root->a));
+    EXPECT_TRUE(Is<Replacement>(cloned_root->a->b));
+    EXPECT_FALSE(Is<Replacement>(cloned_root->a->b->b));
+    EXPECT_TRUE(Is<Replacement>(cloned_root->b));
+    EXPECT_FALSE(Is<Replacement>(cloned_root->b->b));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplaceAll_Symbols) {
+    Allocator alloc;
+
+    ProgramBuilder builder;
+    Node* original_root;
+    {
+        auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
+        auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+        auto* b_a = a;  // Aliased
+        auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
+        auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+        auto* c = b;  // Aliased
+        original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+    }
+    Program original(std::move(builder));
+
+    //                          root
+    //        ╭──────────────────┼──────────────────╮
+    //       (a)                (b)                (c)
+    //        N  <──────┐        N  <───────────────┘
+    //   ╭────┼────╮    │   ╭────┼────╮
+    //  (a)  (b)  (c)   │  (a)  (b)  (c)
+    //        N         └───┘    N
+    //
+    // N: Node
+
+    ProgramBuilder cloned;
+    auto* cloned_root = CloneContext(&cloned, &original, false)
+                            .ReplaceAll([&](Symbol sym) {
+                                auto in = sym.Name();
+                                auto out = "transformed<" + in + ">";
+                                return cloned.Symbols().New(out);
+                            })
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("transformed<root>"));
+    EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("transformed<a>"));
+    EXPECT_EQ(cloned_root->a->b->name, cloned.Symbols().Get("transformed<a->b>"));
+    EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("transformed<b>"));
+    EXPECT_EQ(cloned_root->b->b->name, cloned.Symbols().Get("transformed<b->b>"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithoutTransform) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_node = a.Create<Node>(builder.Symbols().New("root"));
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original);
+    ctx.ReplaceAll([&](const Node*) {
+        return a.Create<Replacement>(builder.Symbols().New("<unexpected-node>"));
+    });
+
+    auto* cloned_node = ctx.CloneWithoutTransform(original_node);
+    EXPECT_NE(cloned_node, original_node);
+    EXPECT_EQ(cloned_node->name, cloned.Symbols().Get("root"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplacePointer) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    Program original(std::move(builder));
+
+    //                          root
+    //        ╭──────────────────┼──────────────────╮
+    //       (a)                (b)                (c)
+    //                        Replaced
+
+    ProgramBuilder cloned;
+    auto* replacement = a.Create<Node>(cloned.Symbols().New("replacement"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .Replace(original_root->b, replacement)
+                            .Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, replacement);
+    EXPECT_EQ(cloned_root->b, replacement);
+    EXPECT_NE(cloned_root->c, replacement);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("replacement"));
+    EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithRepeatedImmediateReplacePointer) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    CloneContext ctx(&cloned, &original);
+
+    // Demonstrate that ctx.Replace() can be called multiple times to update the replacement of a
+    // node.
+
+    auto* replacement_x =
+        a.Create<Node>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, replacement_x);
+
+    auto* replacement_y =
+        a.Create<Node>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, replacement_y);
+
+    auto* replacement_z =
+        a.Create<Node>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, replacement_z);
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, replacement_z);
+    EXPECT_EQ(cloned_root->b, replacement_z);
+    EXPECT_NE(cloned_root->c, replacement_z);
+
+    EXPECT_EQ(replacement_z->a, replacement_y);
+    EXPECT_EQ(replacement_y->a, replacement_x);
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplaceFunction) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    Program original(std::move(builder));
+
+    //                          root
+    //        ╭──────────────────┼──────────────────╮
+    //       (a)                (b)                (c)
+    //                        Replaced
+
+    ProgramBuilder cloned;
+    auto* replacement = a.Create<Node>(cloned.Symbols().New("replacement"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .Replace(original_root->b, [=] { return replacement; })
+                            .Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, replacement);
+    EXPECT_EQ(cloned_root->b, replacement);
+    EXPECT_NE(cloned_root->c, replacement);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("replacement"));
+    EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithRepeatedImmediateReplaceFunction) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    CloneContext ctx(&cloned, &original);
+
+    // Demonstrate that ctx.Replace() can be called multiple times to update the replacement of a
+    // node.
+
+    Node* replacement_x =
+        a.Create<Node>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, [&] { return replacement_x; });
+
+    Node* replacement_y =
+        a.Create<Node>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, [&] { return replacement_y; });
+
+    Node* replacement_z =
+        a.Create<Node>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, [&] { return replacement_z; });
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, replacement_z);
+    EXPECT_EQ(cloned_root->b, replacement_z);
+    EXPECT_NE(cloned_root->c, replacement_z);
+
+    EXPECT_EQ(replacement_z->a, replacement_y);
+    EXPECT_EQ(replacement_y->a, replacement_x);
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithRemove) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .Remove(original_root->vec, original_root->vec[1])
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 2u);
+
+    EXPECT_NE(cloned_root->vec[0], cloned_root->a);
+    EXPECT_NE(cloned_root->vec[1], cloned_root->c);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertFront) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertFront(original_root->vec, insertion)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_NE(cloned_root->vec[0], cloned_root->a);
+    EXPECT_NE(cloned_root->vec[1], cloned_root->b);
+    EXPECT_NE(cloned_root->vec[2], cloned_root->c);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertFrontFunction) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertFront(original_root->vec,
+                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_NE(cloned_root->vec[0], cloned_root->a);
+    EXPECT_NE(cloned_root->vec[1], cloned_root->b);
+    EXPECT_NE(cloned_root->vec[2], cloned_root->c);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertFront_Empty) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec.Clear();
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertFront(original_root->vec, insertion)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 1u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertFront_Empty_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec.Clear();
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertFront(original_root->vec,
+                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 1u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBack) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertBack(original_root->vec, insertion)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("c"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBack_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertBack(original_root->vec,
+                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("c"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBack_Empty) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec.Clear();
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertBack(original_root->vec, insertion)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 1u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBack_Empty_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec.Clear();
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertBack(original_root->vec,
+                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 1u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertFrontAndBack_Empty) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec.Clear();
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion_back = a.Create<Node>(cloned.Symbols().New("insertion_back"));
+    auto* insertion_front = a.Create<Node>(cloned.Symbols().New("insertion_front"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertBack(original_root->vec, insertion_back)
+                            .InsertFront(original_root->vec, insertion_front)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 2u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion_front"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_back"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertFrontAndBack_Empty_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec.Clear();
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertBack(original_root->vec,
+                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion_back")); })
+            .InsertFront(original_root->vec,
+                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion_front")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 2u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion_front"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_back"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBefore) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertBefore(original_root->vec, original_root->vec[1], insertion)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBefore_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertBefore(original_root->vec, original_root->vec[1],
+                          [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertAfter) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+    auto* cloned_root = CloneContext(&cloned, &original)
+                            .InsertAfter(original_root->vec, original_root->vec[1], insertion)
+                            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertAfter_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertAfter(original_root->vec, original_root->vec[1],
+                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); })
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertAfterInVectorNodeClone) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Replaceable>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original);
+    ctx.ReplaceAll([&](const Replaceable* r) {
+        auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+        ctx.InsertAfter(original_root->vec, r, insertion);
+        return nullptr;
+    });
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertAfterInVectorNodeClone_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Replaceable>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original);
+    ctx.ReplaceAll([&](const Replaceable* r) {
+        ctx.InsertAfter(original_root->vec, r,
+                        [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); });
+        return nullptr;
+    });
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBackInVectorNodeClone) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Replaceable>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original);
+    ctx.ReplaceAll([&](const Replaceable* /*r*/) {
+        auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+        ctx.InsertBack(original_root->vec, insertion);
+        return nullptr;
+    });
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("c"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBackInVectorNodeClone_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Replaceable>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original);
+    ctx.ReplaceAll([&](const Replaceable* /*r*/) {
+        ctx.InsertBack(original_root->vec,
+                       [&] { return a.Create<Node>(cloned.Symbols().New("insertion")); });
+        return nullptr;
+    });
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("c"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBeforeAndAfterRemoved) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    auto* insertion_before = a.Create<Node>(cloned.Symbols().New("insertion_before"));
+    auto* insertion_after = a.Create<Node>(cloned.Symbols().New("insertion_after"));
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertBefore(original_root->vec, original_root->vec[1], insertion_before)
+            .InsertAfter(original_root->vec, original_root->vec[1], insertion_after)
+            .Remove(original_root->vec, original_root->vec[1])
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_before"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion_after"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithInsertBeforeAndAfterRemoved_Function) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+    original_root->vec = {
+        a.Create<Node>(builder.Symbols().Register("a")),
+        a.Create<Node>(builder.Symbols().Register("b")),
+        a.Create<Node>(builder.Symbols().Register("c")),
+    };
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    auto* cloned_root =
+        CloneContext(&cloned, &original)
+            .InsertBefore(original_root->vec, original_root->vec[1],
+                          [&] { return a.Create<Node>(cloned.Symbols().New("insertion_before")); })
+            .InsertAfter(original_root->vec, original_root->vec[1],
+                         [&] { return a.Create<Node>(cloned.Symbols().New("insertion_after")); })
+            .Remove(original_root->vec, original_root->vec[1])
+            .Clone(original_root);
+
+    EXPECT_EQ(cloned_root->vec.Length(), 4u);
+
+    EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+    EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+    EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_before"));
+    EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion_after"));
+    EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplaceAll_SameTypeTwice) {
+    std::string node_name = TypeInfo::Of<Node>().name;
+
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder cloned;
+            Program original;
+            CloneContext ctx(&cloned, &original);
+            ctx.ReplaceAll([](const Node*) { return nullptr; });
+            ctx.ReplaceAll([](const Node*) { return nullptr; });
+        },
+        "internal compiler error: ReplaceAll() called with a handler for type " + node_name +
+            " that is already handled by a handler for type " + node_name);
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplaceAll_BaseThenDerived) {
+    std::string node_name = TypeInfo::Of<Node>().name;
+    std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
+
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder cloned;
+            Program original;
+            CloneContext ctx(&cloned, &original);
+            ctx.ReplaceAll([](const Node*) { return nullptr; });
+            ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
+        },
+        "internal compiler error: ReplaceAll() called with a handler for type " + replaceable_name +
+            " that is already handled by a handler for type " + node_name);
+}
+
+TEST_F(ProgramCloneContextNodeTest, CloneWithReplaceAll_DerivedThenBase) {
+    std::string node_name = TypeInfo::Of<Node>().name;
+    std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
+
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder cloned;
+            Program original;
+            CloneContext ctx(&cloned, &original);
+            ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
+            ctx.ReplaceAll([](const Node*) { return nullptr; });
+        },
+        "internal compiler error: ReplaceAll() called with a handler for type " + node_name +
+            " that is already handled by a handler for type " + replaceable_name);
+}
+
+using ProgramCloneContextTest = ::testing::Test;
+
+TEST_F(ProgramCloneContextTest, CloneWithReplaceAll_SymbolsTwice) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder cloned;
+            Program original;
+            CloneContext ctx(&cloned, &original);
+            ctx.ReplaceAll([](const Symbol s) { return s; });
+            ctx.ReplaceAll([](const Symbol s) { return s; });
+        },
+        "internal compiler error: ReplaceAll(const SymbolTransform&) called "
+        "multiple times on the same CloneContext");
+}
+
+TEST_F(ProgramCloneContextTest, CloneNewUnnamedSymbols) {
+    ProgramBuilder builder;
+    Symbol old_a = builder.Symbols().New();
+    Symbol old_b = builder.Symbols().New();
+    Symbol old_c = builder.Symbols().New();
+    EXPECT_EQ(old_a.Name(), "tint_symbol");
+    EXPECT_EQ(old_b.Name(), "tint_symbol_1");
+    EXPECT_EQ(old_c.Name(), "tint_symbol_2");
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original, false);
+    Symbol new_x = cloned.Symbols().New();
+    Symbol new_a = ctx.Clone(old_a);
+    Symbol new_y = cloned.Symbols().New();
+    Symbol new_b = ctx.Clone(old_b);
+    Symbol new_z = cloned.Symbols().New();
+    Symbol new_c = ctx.Clone(old_c);
+
+    EXPECT_EQ(new_x.Name(), "tint_symbol");
+    EXPECT_EQ(new_a.Name(), "tint_symbol_1");
+    EXPECT_EQ(new_y.Name(), "tint_symbol_2");
+    EXPECT_EQ(new_b.Name(), "tint_symbol_1_1");
+    EXPECT_EQ(new_z.Name(), "tint_symbol_3");
+    EXPECT_EQ(new_c.Name(), "tint_symbol_2_1");
+}
+
+TEST_F(ProgramCloneContextTest, CloneNewSymbols) {
+    ProgramBuilder builder;
+    Symbol old_a = builder.Symbols().New("a");
+    Symbol old_b = builder.Symbols().New("b");
+    Symbol old_c = builder.Symbols().New("c");
+    EXPECT_EQ(old_a.Name(), "a");
+    EXPECT_EQ(old_b.Name(), "b");
+    EXPECT_EQ(old_c.Name(), "c");
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original, false);
+    Symbol new_x = cloned.Symbols().New("a");
+    Symbol new_a = ctx.Clone(old_a);
+    Symbol new_y = cloned.Symbols().New("b");
+    Symbol new_b = ctx.Clone(old_b);
+    Symbol new_z = cloned.Symbols().New("c");
+    Symbol new_c = ctx.Clone(old_c);
+
+    EXPECT_EQ(new_x.Name(), "a");
+    EXPECT_EQ(new_a.Name(), "a_1");
+    EXPECT_EQ(new_y.Name(), "b");
+    EXPECT_EQ(new_b.Name(), "b_1");
+    EXPECT_EQ(new_z.Name(), "c");
+    EXPECT_EQ(new_c.Name(), "c_1");
+}
+
+TEST_F(ProgramCloneContextTest, CloneNewSymbols_AfterCloneSymbols) {
+    ProgramBuilder builder;
+    Symbol old_a = builder.Symbols().New("a");
+    Symbol old_b = builder.Symbols().New("b");
+    Symbol old_c = builder.Symbols().New("c");
+    EXPECT_EQ(old_a.Name(), "a");
+    EXPECT_EQ(old_b.Name(), "b");
+    EXPECT_EQ(old_c.Name(), "c");
+
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+    CloneContext ctx(&cloned, &original);
+    Symbol new_x = cloned.Symbols().New("a");
+    Symbol new_a = ctx.Clone(old_a);
+    Symbol new_y = cloned.Symbols().New("b");
+    Symbol new_b = ctx.Clone(old_b);
+    Symbol new_z = cloned.Symbols().New("c");
+    Symbol new_c = ctx.Clone(old_c);
+
+    EXPECT_EQ(new_x.Name(), "a_1");
+    EXPECT_EQ(new_a.Name(), "a");
+    EXPECT_EQ(new_y.Name(), "b_1");
+    EXPECT_EQ(new_b.Name(), "b");
+    EXPECT_EQ(new_z.Name(), "c_1");
+    EXPECT_EQ(new_c.Name(), "c");
+}
+
+TEST_F(ProgramCloneContextTest, GenerationIDs) {
+    ProgramBuilder dst;
+    Program src(ProgramBuilder{});
+    CloneContext ctx(&dst, &src);
+    Allocator allocator;
+    auto* cloned = ctx.Clone(allocator.Create<IDNode>(src.ID(), dst.ID()));
+    EXPECT_EQ(cloned->generation_id, dst.ID());
+}
+
+TEST_F(ProgramCloneContextTest, GenerationIDs_Clone_ObjectNotOwnedBySrc) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder dst;
+            Program src(ProgramBuilder{});
+            CloneContext ctx(&dst, &src);
+            Allocator allocator;
+            ctx.Clone(allocator.Create<IDNode>(GenerationID::New(), dst.ID()));
+        },
+        R"(internal compiler error: TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(src_id, object))");
+}
+
+TEST_F(ProgramCloneContextTest, GenerationIDs_Clone_ObjectNotOwnedByDst) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder dst;
+            Program src(ProgramBuilder{});
+            CloneContext ctx(&dst, &src);
+            Allocator allocator;
+            ctx.Clone(allocator.Create<IDNode>(src.ID(), GenerationID::New()));
+        },
+        R"(internal compiler error: TINT_ASSERT_GENERATION_IDS_EQUAL_IF_VALID(dst, out))");
+}
+
+}  // namespace
+}  // namespace tint::program
+
+TINT_INSTANTIATE_TYPEINFO(tint::program::Node);
+TINT_INSTANTIATE_TYPEINFO(tint::program::Replaceable);
+TINT_INSTANTIATE_TYPEINFO(tint::program::Replacement);
+TINT_INSTANTIATE_TYPEINFO(tint::program::IDNode);
diff --git a/src/tint/lang/wgsl/program/program.cc b/src/tint/lang/wgsl/program/program.cc
index 45900e5..b9cdc75 100644
--- a/src/tint/lang/wgsl/program/program.cc
+++ b/src/tint/lang/wgsl/program/program.cc
@@ -16,6 +16,7 @@
 
 #include <utility>
 
+#include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/resolver/resolver.h"
 #include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
@@ -106,7 +107,7 @@
 ProgramBuilder Program::CloneAsBuilder() const {
     AssertNotMoved();
     ProgramBuilder out;
-    CloneContext(&out, this).Clone();
+    program::CloneContext(&out, this).Clone();
     return out;
 }
 
diff --git a/src/tint/lang/wgsl/program/program.h b/src/tint/lang/wgsl/program/program.h
index 6ae49fc..7e887e7 100644
--- a/src/tint/lang/wgsl/program/program.h
+++ b/src/tint/lang/wgsl/program/program.h
@@ -26,9 +26,6 @@
 #include "src/tint/utils/text/symbol_table.h"
 
 // Forward Declarations
-namespace tint {
-class CloneContext;
-}  // namespace tint
 namespace tint::ast {
 class Module;
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
index 884d235..f14107b 100644
--- a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
@@ -2299,7 +2299,7 @@
     TestAttribute(GenerationID pid, ast::NodeID nid, const ast::IdentifierExpression* dep)
         : Base(pid, nid, Vector{dep}) {}
     std::string InternalName() const override { return "test_attribute"; }
-    const Cloneable* Clone(CloneContext*) const override { return nullptr; }
+    const Node* Clone(ast::CloneContext&) const override { return nullptr; }
 };
 
 using InternalAttributeDepsTest = ResolverTest;
diff --git a/src/tint/lang/wgsl/resolver/validation_test.cc b/src/tint/lang/wgsl/resolver/validation_test.cc
index 8a23f2d..874d23d 100644
--- a/src/tint/lang/wgsl/resolver/validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/validation_test.cc
@@ -53,13 +53,13 @@
 class FakeStmt final : public Castable<FakeStmt, ast::Statement> {
   public:
     FakeStmt(GenerationID pid, ast::NodeID nid, Source src) : Base(pid, nid, src) {}
-    FakeStmt* Clone(CloneContext*) const override { return nullptr; }
+    FakeStmt* Clone(ast::CloneContext&) const override { return nullptr; }
 };
 
 class FakeExpr final : public Castable<FakeExpr, ast::Expression> {
   public:
     FakeExpr(GenerationID pid, ast::NodeID nid, Source src) : Base(pid, nid, src) {}
-    FakeExpr* Clone(CloneContext*) const override { return nullptr; }
+    FakeExpr* Clone(ast::CloneContext&) const override { return nullptr; }
 };
 
 TEST_F(ResolverValidationTest, WorkgroupMemoryUsedInVertexStage) {
diff --git a/src/tint/lang/wgsl/sem/function.h b/src/tint/lang/wgsl/sem/function.h
index cddfdcd..8adb916 100644
--- a/src/tint/lang/wgsl/sem/function.h
+++ b/src/tint/lang/wgsl/sem/function.h
@@ -25,6 +25,7 @@
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/utils/containers/unique_vector.h"
 #include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/text/symbol.h"
 
 // Forward declarations
 namespace tint::ast {
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
index 10fc7ab..a189ff8 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
@@ -22,6 +22,9 @@
 #include "src/tint/utils/text/text_generator.h"
 
 // Forward declarations
+namespace tint {
+class Program;
+}
 namespace tint::ast {
 class AssignmentStatement;
 class Attribute;